Skip to content

Commit d7c924e

Browse files
authored
Nillable Identifier (#69)
* Configurable representation of nil identifier. EncodeInt64 and EncodeBytes functions now strong typed. * Eliminate EncodeInt64 and EncodeBytes * atlas.rpc.Identifier implements encoding.TextMarshaler interface.
1 parent 6ec9bf2 commit d7c924e

File tree

6 files changed

+134
-145
lines changed

6 files changed

+134
-145
lines changed

gorm/resource/README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ You could register `resource.Codec` for you PB type to be used to convert `atlas
1010
By default if PB resource is undefined (`nil`) the `atlas.rpc.Identifier` is converted to a string in fully qualified format specified for
1111
Atlas References, otherwise the Resource ID part is returned as string value.
1212

13+
If `driver.Value` is `nil` default codecs returns `nil` `atlas.rpc.Identifier`, if you want to override such behavior in order to return empty
14+
`atlas.rpc.Identifier` that could be rendered to `null` string in JSON -
15+
1316
If `resource.Codec` is not registered for a PB type the value of identifier is converted from `driver.Value` to a string.
1417
If Resource Type is not found it is populated from the name of PB type,
1518
the Application Name is populated if was registered. (see `RegisterApplication`).
@@ -25,6 +28,9 @@ The only numeric and text formats are supported. If type is not set it will be g
2528
If you want to expose foreign keys on API just leave them with empty type in `gorm.field.tag` and it will be calculated based on the
2629
parent's primary key type.
2730

31+
By default `Identifier`s are nillable, it means that for primary keys you need to set corresponding tag `primary_key: true` and for foreign keys
32+
and external references `not_null: true`.
33+
2834
The Postgres types from tags are converted as follows:
2935

3036
```go
@@ -57,7 +63,7 @@ option go_package = "github.com/yourapp/pb;pb";
5763
message A {
5864
option (gorm.opts).ormable = true;
5965
60-
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer"}];
66+
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer" primary_key: true}];
6167
string value = 2;
6268
repeated B b_list = 3; // has many
6369
atlas.rpc.Identifier external = 4 [(gorm.field).tag = {type: "text"}];
@@ -66,10 +72,11 @@ message A {
6672
message B {
6773
option (gorm.opts).ormable = true;
6874
69-
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer"}];
75+
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer" primary_key: true}];
7076
string value = 2;
7177
// foreign key to A parent. !!! Will be set to the type of A.id
7278
atlas.rpc.Identifier a_id = 3;
79+
atlas.rpc.Identifier external_not_null = 4 [(gorm.field).tag = {type: "text" not_null: true}];
7380
}
7481
```
7582

@@ -96,13 +103,14 @@ type AORM struct {
96103
Id int64
97104
Value string
98105
BList []*BORM
99-
External string
106+
External *string
100107
}
101108

102109
type BORM struct {
103110
Id int64
104111
Value string
105112
AId *int64
113+
ExternalNotNull string
106114
}
107115
```
108116

gorm/resource/example_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func Example() {
8282
pb.VarName = v.VarName
8383

8484
// convert internal id to RPC representation using registered UUID codec
85-
if id, err := EncodeInt64(pb, v.ID); err != nil {
85+
if id, err := Encode(pb, v.ID); err != nil {
8686
return nil, err
8787
} else {
8888
pb.Id = id

gorm/resource/resource.go

+37-36
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
mu sync.RWMutex
1919
registry = make(map[string]Codec)
2020
appname string
21+
asEmpty bool
2122
)
2223

2324
// Codec defines the interface package uses to encode and decode Protocol Buffer
@@ -42,6 +43,23 @@ func RegisterApplication(name string) {
4243
appname = name
4344
}
4445

46+
// SetReturnEmpty sets package flag that indicates all nil values of driver.Value
47+
// type in codecs must be converted to empty instance of Identifier.
48+
// Default value is false.
49+
func SetReturnEmpty() {
50+
mu.Lock()
51+
defer mu.Unlock()
52+
asEmpty = true
53+
}
54+
55+
// ReturnEmpty returns flag that indicates all nil values of driver.Value type
56+
// in codecs must be converted to empty instance of Identifier.
57+
func ReturnEmpty() bool {
58+
mu.RLock()
59+
defer mu.RUnlock()
60+
return asEmpty
61+
}
62+
4563
// RegisterCodec registers codec for a given pb.
4664
// If pb is nil the codec is registered as default.
4765
// If codec is nil or registered twice for the same resource
@@ -157,23 +175,34 @@ func DecodeBytes(pb proto.Message, id *resourcepb.Identifier) ([]byte, error) {
157175
// are populated by ApplicationName and Name functions accordingly, otherwise
158176
// the empty identifier is returned.
159177
func Encode(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
178+
var id resourcepb.Identifier
179+
160180
if c, ok := lookupCodec(pb); ok {
161181
return c.Encode(value)
162182
}
163183
if value == nil {
184+
if ReturnEmpty() {
185+
return &id, nil
186+
}
164187
return nil, nil
165188
}
166-
167-
var id resourcepb.Identifier
168-
s, ok := value.(string)
169-
if !ok {
170-
return nil, fmt.Errorf("resource: invalid value type %T, expected string", value)
189+
var sval string
190+
switch v := value.(type) {
191+
case []byte:
192+
sval = string(v)
193+
case int64:
194+
sval = fmt.Sprintf("%d", v)
195+
case string:
196+
sval = v
197+
default:
198+
return nil, fmt.Errorf("resource: unsupported value type %T", value)
171199
}
172-
if s == "" {
200+
201+
if sval == "" {
173202
return &id, nil
174203
}
175204
if pb == nil {
176-
id.ApplicationName, id.ResourceType, id.ResourceId = resourcepb.ParseString(s)
205+
id.ApplicationName, id.ResourceType, id.ResourceId = resourcepb.ParseString(sval)
177206
}
178207

179208
if id.ApplicationName == "" {
@@ -183,40 +212,12 @@ func Encode(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error
183212
id.ResourceType = Name(pb)
184213
}
185214
if id.ResourceId == "" {
186-
id.ResourceId = s
215+
id.ResourceId = sval
187216
}
188217

189218
return &id, nil
190219
}
191220

192-
// EncodeInt64 converts value to string and forwards call to Encode.
193-
// Returns an error if value is not of int64 type.
194-
func EncodeInt64(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
195-
if c, ok := lookupCodec(pb); ok {
196-
return c.Encode(value)
197-
}
198-
199-
v, ok := value.(int64)
200-
if !ok {
201-
return nil, fmt.Errorf("resource: invalid value type %T, expected int64", value)
202-
}
203-
return Encode(pb, fmt.Sprintf("%d", v))
204-
}
205-
206-
// EncodeBytes converts value to string and forwards call to Encode.
207-
// Returns an error if value is not of []byte type.
208-
func EncodeBytes(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
209-
if c, ok := lookupCodec(pb); ok {
210-
return c.Encode(value)
211-
}
212-
213-
v, ok := value.([]byte)
214-
if !ok {
215-
return nil, fmt.Errorf("resource: invalid value type %T, expected []byte", value)
216-
}
217-
return Encode(pb, string(v))
218-
}
219-
220221
// Name returns name of pb.
221222
// If pb implements XXX_MessageName then it is used to return name, otherwise
222223
// proto.MessageName is used and "s" symbol is added at the end of the message name.

gorm/resource/resource_test.go

+37-105
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,33 @@ import (
1717
type TestCodec struct{}
1818

1919
func (TestCodec) Decode(id *resourcepb.Identifier) (driver.Value, error) {
20+
if id.GetResourceId() == "err" {
21+
return nil, errors.New("test error")
22+
}
23+
if id.GetResourceId() == "invalid" {
24+
return true, nil
25+
}
26+
if id.GetResourceId() == "str" {
27+
return "", nil
28+
}
29+
if id.GetResourceId() == "strempty" {
30+
return "", nil
31+
}
32+
if id.GetResourceId() == "12" {
33+
return strconv.ParseInt(id.GetResourceId(), 10, 64)
34+
}
2035
return id.ResourceId, nil
2136
}
2237
func (TestCodec) Encode(value driver.Value) (*resourcepb.Identifier, error) {
23-
return &resourcepb.Identifier{ResourceId: value.(string)}, nil
24-
38+
switch value.(type) {
39+
case string:
40+
return &resourcepb.Identifier{ResourceId: value.(string)}, nil
41+
case int64:
42+
return &resourcepb.Identifier{ResourceId: strconv.FormatInt(value.(int64), 10)}, nil
43+
case []byte:
44+
return &resourcepb.Identifier{ResourceId: string(value.([]byte))}, nil
45+
}
46+
return nil, nil
2547
}
2648

2749
type TestInt64Codec struct{}
@@ -395,66 +417,12 @@ func TestEncode(t *testing.T) {
395417
},
396418
},
397419
{
398-
Identifier: &resourcepb.Identifier{
399-
ApplicationName: "",
400-
ResourceType: "",
401-
ResourceId: "",
402-
},
403-
Message: nil,
404-
Value: 12,
405-
ExpectedError: "resource: invalid value type int, expected string",
406-
},
407-
{
408-
Value: nil,
409-
Message: nil,
410-
Identifier: nil,
411-
},
412-
{
413-
Identifier: &resourcepb.Identifier{
414-
ApplicationName: "",
415-
ResourceType: "",
416-
ResourceId: "",
417-
},
418-
Message: nil,
419-
Value: "",
420-
},
421-
}
422-
423-
for n, tc := range tcases {
424-
id, err := Encode(tc.Message, tc.Value)
425-
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
426-
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
427-
}
428-
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
429-
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.ApplicationName)
430-
}
431-
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
432-
t.Errorf("tc %d: nvalid resource type %s, expected %s", n, v, tc.Identifier.ResourceType)
433-
}
434-
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
435-
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.ResourceId)
436-
}
437-
}
438-
}
439-
440-
func TestEncodeInt64(t *testing.T) {
441-
RegisterCodec(&TestInt64Codec{}, &TestProtoMessage{})
442-
RegisterApplication("app")
443-
defer Cleanup(t)
444-
445-
tcases := []struct {
446-
Value driver.Value
447-
Message proto.Message
448-
Identifier *resourcepb.Identifier
449-
ExpectedError string
450-
}{
451-
{
452-
Value: int64(1),
420+
Value: int64(12),
453421
Message: &TestProtoMessage{},
454422
Identifier: &resourcepb.Identifier{
455423
ApplicationName: "",
456424
ResourceType: "",
457-
ResourceId: "1",
425+
ResourceId: "12",
458426
},
459427
},
460428
{
@@ -466,46 +434,6 @@ func TestEncodeInt64(t *testing.T) {
466434
ResourceId: "1",
467435
},
468436
},
469-
{
470-
Value: "1",
471-
Message: nil,
472-
Identifier: &resourcepb.Identifier{
473-
ApplicationName: "",
474-
ResourceType: "",
475-
ResourceId: "",
476-
},
477-
ExpectedError: "resource: invalid value type string, expected int64",
478-
},
479-
}
480-
481-
for n, tc := range tcases {
482-
id, err := EncodeInt64(tc.Message, tc.Value)
483-
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
484-
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
485-
}
486-
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
487-
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.GetApplicationName())
488-
}
489-
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
490-
t.Errorf("tc %d: invalid resource type %s, expected %s", n, v, tc.Identifier.GetResourceType())
491-
}
492-
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
493-
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.GetResourceId())
494-
}
495-
}
496-
}
497-
498-
func TestEncodeBytes(t *testing.T) {
499-
RegisterCodec(&TestBytesCodec{}, &TestProtoMessage{})
500-
RegisterApplication("app")
501-
defer Cleanup(t)
502-
503-
tcases := []struct {
504-
Value driver.Value
505-
Message proto.Message
506-
Identifier *resourcepb.Identifier
507-
ExpectedError string
508-
}{
509437
{
510438
Value: []byte("1"),
511439
Message: &TestProtoMessage{},
@@ -525,30 +453,34 @@ func TestEncodeBytes(t *testing.T) {
525453
},
526454
},
527455
{
528-
Value: "1",
529-
Message: nil,
456+
Value: nil,
457+
Message: nil,
458+
Identifier: nil,
459+
},
460+
{
530461
Identifier: &resourcepb.Identifier{
531462
ApplicationName: "",
532463
ResourceType: "",
533464
ResourceId: "",
534465
},
535-
ExpectedError: "resource: invalid value type string, expected []byte",
466+
Message: nil,
467+
Value: "",
536468
},
537469
}
538470

539471
for n, tc := range tcases {
540-
id, err := EncodeBytes(tc.Message, tc.Value)
472+
id, err := Encode(tc.Message, tc.Value)
541473
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
542474
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
543475
}
544476
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
545-
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.GetApplicationName())
477+
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.ApplicationName)
546478
}
547479
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
548-
t.Errorf("tc %d: invalid resource type %s, expected %s", n, v, tc.Identifier.GetResourceType())
480+
t.Errorf("tc %d: nvalid resource type %s, expected %s", n, v, tc.Identifier.ResourceType)
549481
}
550482
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
551-
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.GetResourceId())
483+
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.ResourceId)
552484
}
553485
}
554486
}

rpc/resource/string.go

+5
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ func ParseString(id string) (aname, rtype, rid string) {
4343
}
4444
return
4545
}
46+
47+
func (m Identifier) MarshalText() (text []byte, err error) {
48+
text = []byte(BuildString(m.GetApplicationName(), m.GetResourceType(), m.GetResourceId()))
49+
return
50+
}

0 commit comments

Comments
 (0)