Skip to content

Commit

Permalink
Nillable Identifier (#69)
Browse files Browse the repository at this point in the history
* Configurable representation of nil identifier.
EncodeInt64 and EncodeBytes functions now strong typed.

* Eliminate EncodeInt64 and EncodeBytes

* atlas.rpc.Identifier implements encoding.TextMarshaler interface.
  • Loading branch information
amaskalenka authored Jul 11, 2018
1 parent 6ec9bf2 commit d7c924e
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 145 deletions.
14 changes: 11 additions & 3 deletions gorm/resource/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ You could register `resource.Codec` for you PB type to be used to convert `atlas
By default if PB resource is undefined (`nil`) the `atlas.rpc.Identifier` is converted to a string in fully qualified format specified for
Atlas References, otherwise the Resource ID part is returned as string value.

If `driver.Value` is `nil` default codecs returns `nil` `atlas.rpc.Identifier`, if you want to override such behavior in order to return empty
`atlas.rpc.Identifier` that could be rendered to `null` string in JSON -

If `resource.Codec` is not registered for a PB type the value of identifier is converted from `driver.Value` to a string.
If Resource Type is not found it is populated from the name of PB type,
the Application Name is populated if was registered. (see `RegisterApplication`).
Expand All @@ -25,6 +28,9 @@ The only numeric and text formats are supported. If type is not set it will be g
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
parent's primary key type.

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
and external references `not_null: true`.

The Postgres types from tags are converted as follows:

```go
Expand Down Expand Up @@ -57,7 +63,7 @@ option go_package = "github.com/yourapp/pb;pb";
message A {
option (gorm.opts).ormable = true;

atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer"}];
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer" primary_key: true}];
string value = 2;
repeated B b_list = 3; // has many
atlas.rpc.Identifier external = 4 [(gorm.field).tag = {type: "text"}];
Expand All @@ -66,10 +72,11 @@ message A {
message B {
option (gorm.opts).ormable = true;

atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer"}];
atlas.rpc.Identifier id = 1 [(gorm.field).tag = {type: "integer" primary_key: true}];
string value = 2;
// foreign key to A parent. !!! Will be set to the type of A.id
atlas.rpc.Identifier a_id = 3;
atlas.rpc.Identifier external_not_null = 4 [(gorm.field).tag = {type: "text" not_null: true}];
}
```

Expand All @@ -96,13 +103,14 @@ type AORM struct {
Id int64
Value string
BList []*BORM
External string
External *string
}

type BORM struct {
Id int64
Value string
AId *int64
ExternalNotNull string
}
```

Expand Down
2 changes: 1 addition & 1 deletion gorm/resource/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func Example() {
pb.VarName = v.VarName

// convert internal id to RPC representation using registered UUID codec
if id, err := EncodeInt64(pb, v.ID); err != nil {
if id, err := Encode(pb, v.ID); err != nil {
return nil, err
} else {
pb.Id = id
Expand Down
73 changes: 37 additions & 36 deletions gorm/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
mu sync.RWMutex
registry = make(map[string]Codec)
appname string
asEmpty bool
)

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

// SetReturnEmpty sets package flag that indicates all nil values of driver.Value
// type in codecs must be converted to empty instance of Identifier.
// Default value is false.
func SetReturnEmpty() {
mu.Lock()
defer mu.Unlock()
asEmpty = true
}

// ReturnEmpty returns flag that indicates all nil values of driver.Value type
// in codecs must be converted to empty instance of Identifier.
func ReturnEmpty() bool {
mu.RLock()
defer mu.RUnlock()
return asEmpty
}

// RegisterCodec registers codec for a given pb.
// If pb is nil the codec is registered as default.
// If codec is nil or registered twice for the same resource
Expand Down Expand Up @@ -157,23 +175,34 @@ func DecodeBytes(pb proto.Message, id *resourcepb.Identifier) ([]byte, error) {
// are populated by ApplicationName and Name functions accordingly, otherwise
// the empty identifier is returned.
func Encode(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
var id resourcepb.Identifier

if c, ok := lookupCodec(pb); ok {
return c.Encode(value)
}
if value == nil {
if ReturnEmpty() {
return &id, nil
}
return nil, nil
}

var id resourcepb.Identifier
s, ok := value.(string)
if !ok {
return nil, fmt.Errorf("resource: invalid value type %T, expected string", value)
var sval string
switch v := value.(type) {
case []byte:
sval = string(v)
case int64:
sval = fmt.Sprintf("%d", v)
case string:
sval = v
default:
return nil, fmt.Errorf("resource: unsupported value type %T", value)
}
if s == "" {

if sval == "" {
return &id, nil
}
if pb == nil {
id.ApplicationName, id.ResourceType, id.ResourceId = resourcepb.ParseString(s)
id.ApplicationName, id.ResourceType, id.ResourceId = resourcepb.ParseString(sval)
}

if id.ApplicationName == "" {
Expand All @@ -183,40 +212,12 @@ func Encode(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error
id.ResourceType = Name(pb)
}
if id.ResourceId == "" {
id.ResourceId = s
id.ResourceId = sval
}

return &id, nil
}

// EncodeInt64 converts value to string and forwards call to Encode.
// Returns an error if value is not of int64 type.
func EncodeInt64(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
if c, ok := lookupCodec(pb); ok {
return c.Encode(value)
}

v, ok := value.(int64)
if !ok {
return nil, fmt.Errorf("resource: invalid value type %T, expected int64", value)
}
return Encode(pb, fmt.Sprintf("%d", v))
}

// EncodeBytes converts value to string and forwards call to Encode.
// Returns an error if value is not of []byte type.
func EncodeBytes(pb proto.Message, value driver.Value) (*resourcepb.Identifier, error) {
if c, ok := lookupCodec(pb); ok {
return c.Encode(value)
}

v, ok := value.([]byte)
if !ok {
return nil, fmt.Errorf("resource: invalid value type %T, expected []byte", value)
}
return Encode(pb, string(v))
}

// Name returns name of pb.
// If pb implements XXX_MessageName then it is used to return name, otherwise
// proto.MessageName is used and "s" symbol is added at the end of the message name.
Expand Down
142 changes: 37 additions & 105 deletions gorm/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,33 @@ import (
type TestCodec struct{}

func (TestCodec) Decode(id *resourcepb.Identifier) (driver.Value, error) {
if id.GetResourceId() == "err" {
return nil, errors.New("test error")
}
if id.GetResourceId() == "invalid" {
return true, nil
}
if id.GetResourceId() == "str" {
return "", nil
}
if id.GetResourceId() == "strempty" {
return "", nil
}
if id.GetResourceId() == "12" {
return strconv.ParseInt(id.GetResourceId(), 10, 64)
}
return id.ResourceId, nil
}
func (TestCodec) Encode(value driver.Value) (*resourcepb.Identifier, error) {
return &resourcepb.Identifier{ResourceId: value.(string)}, nil

switch value.(type) {
case string:
return &resourcepb.Identifier{ResourceId: value.(string)}, nil
case int64:
return &resourcepb.Identifier{ResourceId: strconv.FormatInt(value.(int64), 10)}, nil
case []byte:
return &resourcepb.Identifier{ResourceId: string(value.([]byte))}, nil
}
return nil, nil
}

type TestInt64Codec struct{}
Expand Down Expand Up @@ -395,66 +417,12 @@ func TestEncode(t *testing.T) {
},
},
{
Identifier: &resourcepb.Identifier{
ApplicationName: "",
ResourceType: "",
ResourceId: "",
},
Message: nil,
Value: 12,
ExpectedError: "resource: invalid value type int, expected string",
},
{
Value: nil,
Message: nil,
Identifier: nil,
},
{
Identifier: &resourcepb.Identifier{
ApplicationName: "",
ResourceType: "",
ResourceId: "",
},
Message: nil,
Value: "",
},
}

for n, tc := range tcases {
id, err := Encode(tc.Message, tc.Value)
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
}
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.ApplicationName)
}
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
t.Errorf("tc %d: nvalid resource type %s, expected %s", n, v, tc.Identifier.ResourceType)
}
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.ResourceId)
}
}
}

func TestEncodeInt64(t *testing.T) {
RegisterCodec(&TestInt64Codec{}, &TestProtoMessage{})
RegisterApplication("app")
defer Cleanup(t)

tcases := []struct {
Value driver.Value
Message proto.Message
Identifier *resourcepb.Identifier
ExpectedError string
}{
{
Value: int64(1),
Value: int64(12),
Message: &TestProtoMessage{},
Identifier: &resourcepb.Identifier{
ApplicationName: "",
ResourceType: "",
ResourceId: "1",
ResourceId: "12",
},
},
{
Expand All @@ -466,46 +434,6 @@ func TestEncodeInt64(t *testing.T) {
ResourceId: "1",
},
},
{
Value: "1",
Message: nil,
Identifier: &resourcepb.Identifier{
ApplicationName: "",
ResourceType: "",
ResourceId: "",
},
ExpectedError: "resource: invalid value type string, expected int64",
},
}

for n, tc := range tcases {
id, err := EncodeInt64(tc.Message, tc.Value)
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
}
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.GetApplicationName())
}
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
t.Errorf("tc %d: invalid resource type %s, expected %s", n, v, tc.Identifier.GetResourceType())
}
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.GetResourceId())
}
}
}

func TestEncodeBytes(t *testing.T) {
RegisterCodec(&TestBytesCodec{}, &TestProtoMessage{})
RegisterApplication("app")
defer Cleanup(t)

tcases := []struct {
Value driver.Value
Message proto.Message
Identifier *resourcepb.Identifier
ExpectedError string
}{
{
Value: []byte("1"),
Message: &TestProtoMessage{},
Expand All @@ -525,30 +453,34 @@ func TestEncodeBytes(t *testing.T) {
},
},
{
Value: "1",
Message: nil,
Value: nil,
Message: nil,
Identifier: nil,
},
{
Identifier: &resourcepb.Identifier{
ApplicationName: "",
ResourceType: "",
ResourceId: "",
},
ExpectedError: "resource: invalid value type string, expected []byte",
Message: nil,
Value: "",
},
}

for n, tc := range tcases {
id, err := EncodeBytes(tc.Message, tc.Value)
id, err := Encode(tc.Message, tc.Value)
if (err != nil && tc.ExpectedError != err.Error()) || (err == nil && tc.ExpectedError != "") {
t.Fatalf("tc %d: invalid error %s, expected %s", n, err, tc.ExpectedError)
}
if v := id.GetApplicationName(); v != tc.Identifier.GetApplicationName() {
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.GetApplicationName())
t.Errorf("tc %d: invalid application name %s, expected %s", n, v, tc.Identifier.ApplicationName)
}
if v := id.GetResourceType(); v != tc.Identifier.GetResourceType() {
t.Errorf("tc %d: invalid resource type %s, expected %s", n, v, tc.Identifier.GetResourceType())
t.Errorf("tc %d: nvalid resource type %s, expected %s", n, v, tc.Identifier.ResourceType)
}
if v := id.GetResourceId(); v != tc.Identifier.GetResourceId() {
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.GetResourceId())
t.Errorf("tc %d: invalid resource id %s, expected %s", n, v, tc.Identifier.ResourceId)
}
}
}
5 changes: 5 additions & 0 deletions rpc/resource/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ func ParseString(id string) (aname, rtype, rid string) {
}
return
}

func (m Identifier) MarshalText() (text []byte, err error) {
text = []byte(BuildString(m.GetApplicationName(), m.GetResourceType(), m.GetResourceId()))
return
}
Loading

0 comments on commit d7c924e

Please sign in to comment.