diff --git a/pkg/v2/facade/facade.go b/pkg/v2/facade/facade.go index 8360ae74..36f939d7 100644 --- a/pkg/v2/facade/facade.go +++ b/pkg/v2/facade/facade.go @@ -73,10 +73,10 @@ type Facade struct { // Id string `scim:"id"` // Email string `scim:"userName,emails[type eq \"work\" and primary eq true].value"` // BackupEmail *string `scim:"emails[type eq \"work\" and primary eq false].value"` -// Name string `scim:"name.formatted" +// Name string `scim:"name.formatted"` // NickName *string `scim:"nickName"` // CreatedAt int64 `scim:"meta.created"` -// UpdatedAt int64 `scim:"meta.lastModified" +// UpdatedAt int64 `scim:"meta.lastModified"` // Active bool `scim:"active"` // Manager *string `scim:"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value"` // } diff --git a/pkg/v2/facade/facade_test.go b/pkg/v2/facade/facade_test.go new file mode 100644 index 00000000..8bb7f974 --- /dev/null +++ b/pkg/v2/facade/facade_test.go @@ -0,0 +1,152 @@ +package facade_test + +import ( + "encoding/json" + "github.com/imulab/go-scim/pkg/v2/crud/expr" + "github.com/imulab/go-scim/pkg/v2/facade" + scimjson "github.com/imulab/go-scim/pkg/v2/json" + "github.com/imulab/go-scim/pkg/v2/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "io/ioutil" + "os" + "testing" +) + +func TestFacade_Export(t *testing.T) { + suite.Run(t, new(facadeTestSuite)) +} + +type facadeTestSuite struct { + suite.Suite + rt *spec.ResourceType +} + +func (s *facadeTestSuite) TestExport() { + type User struct { + Id string `scim:"id"` + Email string `scim:"userName,emails[type eq \"work\" and primary eq true].value"` + BackupEmail *string `scim:"emails[type eq \"work\" and primary eq false].value"` + Name string `scim:"name.formatted"` + NickName *string `scim:"nickName"` + CreatedAt int64 `scim:"meta.created"` + UpdatedAt int64 `scim:"meta.lastModified"` + Active bool `scim:"active"` + Manager *string `scim:"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value"` + } + + var user = &User{ + Id: "test", + Email: "john@gmail.com", + BackupEmail: ref("john@outlook.com"), + Name: "John Doe", + NickName: nil, + CreatedAt: 1608795238, + UpdatedAt: 1608795238, + Active: false, + Manager: ref("tom"), + } + + f := facade.New(s.rt) + + res, err := f.Export(user) + assert.NoError(s.T(), err) + + raw, err := scimjson.Serialize(res) + assert.NoError(s.T(), err) + + expected := ` +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" + ], + "id": "test", + "meta": { + "resourceType": "User", + "created": "2020-12-24T15:33:58", + "lastModified": "2020-12-24T15:33:58" + }, + "userName": "john@gmail.com", + "name": { + "formatted": "John Doe" + }, + "active": false, + "emails": [ + { + "value": "john@gmail.com", + "type": "work", + "primary": true + }, + { + "value": "john@outlook.com", + "type": "work", + "primary": false + } + ], + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { + "manager": { + "value": "tom" + } + } +} +` + assert.JSONEq(s.T(), expected, string(raw)) +} + +func (s *facadeTestSuite) SetupSuite() { + for _, each := range []struct { + filepath string + structure interface{} + post func(parsed interface{}) + }{ + { + filepath: "../../../public/schemas/core_schema.json", + structure: new(spec.Schema), + post: func(parsed interface{}) { + spec.Schemas().Register(parsed.(*spec.Schema)) + }, + }, + { + filepath: "../../../public/schemas/user_schema.json", + structure: new(spec.Schema), + post: func(parsed interface{}) { + spec.Schemas().Register(parsed.(*spec.Schema)) + }, + }, + { + filepath: "../../../public/schemas/user_enterprise_extension_schema.json", + structure: new(spec.Schema), + post: func(parsed interface{}) { + spec.Schemas().Register(parsed.(*spec.Schema)) + }, + }, + { + filepath: "../../../public/resource_types/user_resource_type.json", + structure: new(spec.ResourceType), + post: func(parsed interface{}) { + s.rt = parsed.(*spec.ResourceType) + }, + }, + } { + f, err := os.Open(each.filepath) + require.NoError(s.T(), err) + + raw, err := ioutil.ReadAll(f) + require.NoError(s.T(), err) + + err = json.Unmarshal(raw, each.structure) + require.NoError(s.T(), err) + + if each.post != nil { + each.post(each.structure) + } + } + + expr.RegisterURN("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User") +} + +func ref(v string) *string { + return &v +} diff --git a/pkg/v2/facade/temp.go b/pkg/v2/facade/temp.go deleted file mode 100644 index 6c7f058d..00000000 --- a/pkg/v2/facade/temp.go +++ /dev/null @@ -1,205 +0,0 @@ -package facade - -import ( - "errors" - "github.com/imulab/go-scim/pkg/v2/crud" - "github.com/imulab/go-scim/pkg/v2/crud/expr" - "github.com/imulab/go-scim/pkg/v2/prop" - "github.com/imulab/go-scim/pkg/v2/spec" - "reflect" - "strconv" - "strings" - "time" -) - -// ToResource converts the target structure to a prop.Resource. -// -// To become legible of being converted to a prop.Resource, the target must fulfill the following: -// 1. It must be a struct. -// 2. The fields intended to be transferred must have a "scim" tag. -// 3. The fields tagging the "scim" tag must be of type string, int64, float64, bool, their respective pointer types, -// and their respective slice types. -// 4. The type of the tagged fields must match the type required by the corresponding attributes specified in the tag. -func ToResource(target interface{}, resourceType *spec.ResourceType) (*prop.Resource, error) { - return toResource(reflect.ValueOf(target), resourceType) -} - -func toResource(target reflect.Value, resourceType *spec.ResourceType) (*prop.Resource, error) { - if target.Kind() == reflect.Ptr { - return toResource(target.Elem(), resourceType) - } - - if target.Kind() != reflect.Struct { - return nil, errors.New("target is not of type struct") - } - - resource := prop.NewResource(resourceType) - - for i := 0; i < target.NumField(); i++ { - tag, ok := target.Type().Field(i).Tag.Lookup("scim") - if !ok { - continue - } - - paths := strings.FieldsFunc(tag, func(r rune) bool { return r == ',' }) - for _, path := range paths { - if err := assign(target.Field(i), path, resource); err != nil { - return nil, err - } - } - } - - crud.Add(resource, "meta.resourceType", resourceType.Name()) - - return resource, nil -} - -func assign(field reflect.Value, path string, resource *prop.Resource) error { - if field.Kind() == reflect.Ptr { - if field.IsNil() { - return nil - } - return assign(field.Elem(), path, resource) - } - - // _ = typeCheck(field) - - nav := resource.Navigator() - - head, err := expr.CompilePath(path) - if err != nil { - return err - } - - for cur := head; cur != nil; cur = cur.Next() { - if cur.IsPath() { - attr := nav.Current().Attribute().SubAttributeForName(cur.Token()) - nav.Add(map[string]interface{}{ - cur.Token(): nil, - }) - nav.Dot(cur.Token()) - - if cur.Next() == nil { - switch field.Kind() { - case reflect.String: - switch attr.Type() { - case spec.TypeString, spec.TypeReference, spec.TypeBinary: - nav.Replace(field.String()) - default: - panic("incompatible type") - } - case reflect.Int64: - switch attr.Type() { - case spec.TypeInteger: - nav.Replace(field.Int()) - case spec.TypeDateTime: - nav.Replace(time.Unix(field.Int(), 0).Format(spec.ISO8601)) - default: - panic("incompatible type") - } - case reflect.Float64: - switch attr.Type() { - case spec.TypeDecimal: - nav.Replace(field.Float()) - default: - panic("incompatible type") - } - case reflect.Bool: - switch attr.Type() { - case spec.TypeBoolean: - nav.Replace(field.Bool()) - default: - panic("incompatible type") - } - case reflect.Slice: - panic("not implemented") - default: - panic("incompatible type") - } - } - } - - if cur.IsRootOfFilter() { - nav.Where(func(child prop.Property) bool { - ok, _ := crud.EvaluateExpressionOnProperty(child, cur) - return ok - }) - if nav.HasError() { - nav.ClearError() - - kv := map[string]string{} - if err = collectKv(cur, kv); err != nil { - return err - } - - data := map[string]interface{}{} - - for k, v := range kv { - kattr := nav.Current().Attribute().DeriveElementAttribute().SubAttributeForName(k) - if kattr == nil { - continue - } - - switch kattr.Type() { - case spec.TypeString, spec.TypeReference, spec.TypeDateTime, spec.TypeBinary: - data[k] = v - case spec.TypeInteger: - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - data[k] = i - case spec.TypeDecimal: - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return err - } - data[k] = f - case spec.TypeBoolean: - b, err := strconv.ParseBool(v) - if err != nil { - return err - } - data[k] = b - default: - panic("unexpected types") - } - } - - nav.Add(data) - nav.Where(func(child prop.Property) bool { - ok, _ := crud.EvaluateExpressionOnProperty(child, cur) - return ok - }) - if nav.HasError() { - return nav.Error() - } - } - } - } - - return nil -} - -func collectKv(root *expr.Expression, collector map[string]string) error { - if root.IsOperator() && (root.Token() != expr.And && root.Token() != expr.Eq) { - return errors.New(`currently, only "and" and "eq" is supported`) - } - - if root.IsLogicalOperator() { - if err := collectKv(root.Left(), collector); err != nil { - return err - } - if err := collectKv(root.Right(), collector); err != nil { - return err - } - return nil - } - - if root.IsRelationalOperator() { - collector[root.Left().Token()] = strings.Trim(root.Right().Token(), "\"") - return nil - } - - panic("unreachable code") -} diff --git a/pkg/v2/facade/temp_test.go b/pkg/v2/facade/temp_test.go deleted file mode 100644 index 7cfe802f..00000000 --- a/pkg/v2/facade/temp_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package facade_test - -import ( - "encoding/json" - "github.com/imulab/go-scim/pkg/v2/crud/expr" - "github.com/imulab/go-scim/pkg/v2/facade" - scimjson "github.com/imulab/go-scim/pkg/v2/json" - "github.com/imulab/go-scim/pkg/v2/spec" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "io/ioutil" - "os" - "testing" - "time" -) - -func TestToResource(t *testing.T) { - var resourceType *spec.ResourceType - { - for _, each := range []struct { - filepath string - structure interface{} - post func(parsed interface{}) - }{ - { - filepath: "../../../public/schemas/core_schema.json", - structure: new(spec.Schema), - post: func(parsed interface{}) { - spec.Schemas().Register(parsed.(*spec.Schema)) - }, - }, - { - filepath: "../../../public/schemas/user_schema.json", - structure: new(spec.Schema), - post: func(parsed interface{}) { - spec.Schemas().Register(parsed.(*spec.Schema)) - }, - }, - { - filepath: "../../../public/schemas/user_enterprise_extension_schema.json", - structure: new(spec.Schema), - post: func(parsed interface{}) { - spec.Schemas().Register(parsed.(*spec.Schema)) - }, - }, - { - filepath: "../../../public/resource_types/user_resource_type.json", - structure: new(spec.ResourceType), - post: func(parsed interface{}) { - resourceType = parsed.(*spec.ResourceType) - }, - }, - } { - f, err := os.Open(each.filepath) - require.Nil(t, err) - - raw, err := ioutil.ReadAll(f) - require.Nil(t, err) - - err = json.Unmarshal(raw, each.structure) - require.Nil(t, err) - - if each.post != nil { - each.post(each.structure) - } - } - } - - expr.RegisterURN("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User") - - u := &User{ - Id: "testId", - CreatedAt: time.Now().Unix(), - Email: "foo@bar.com", - BackupEmail: "foo2@bar.com", - Name: "foo", - NickName: func() *string { v := "foobar"; return &v }(), - Manager: "1003", - Active: true, - } - - res, err := facade.ToResource(u, resourceType) - assert.NoError(t, err) - - raw, err := scimjson.Serialize(res) - assert.NoError(t, err) - - println(string(raw)) -} - -type User struct { - Id string `scim:"id"` - CreatedAt int64 `scim:"meta.created"` - Email string `scim:"emails[type eq \"work\" and primary eq true].value"` - BackupEmail string `scim:"emails[type eq \"home\"].value"` - NickName *string `scim:"nickName"` - Name string `scim:"name.formatted"` - Manager string `scim:"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value"` - Active bool `scim:"active"` -}