Skip to content

Commit

Permalink
Merge pull request #83 from imulab/features/sdk
Browse files Browse the repository at this point in the history
Features/sdk
  • Loading branch information
imulab authored Dec 25, 2020
2 parents 9b2d7b3 + 464dd06 commit fec7838
Show file tree
Hide file tree
Showing 8 changed files with 669 additions and 433 deletions.
101 changes: 101 additions & 0 deletions pkg/v2/facade/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// This package serves as a frontend of custom structures that are mappable to SCIM schemas.
//
// Export and Import are the two main entrypoints. For structures to be recognized by these entrypoints, the intended
// fields must be tagged with "scim", whose content is a comma delimited list of SCIM paths. Apart from having to be a
// legal path backed by the resource type, a filtered path may be allowed, provided that only the "and" and "eq" predicate
// is used inside the filter. A filtered path is essential in mapping one or more fields into a multi-valued complex
// property. The following is an example of legal paths under the User resource type with User schema and the Enterprise
// User schema extension:
//
// 1. id
// 2. meta.created
// 3. name.formatted
// 4. emails[type eq "work"].value
// 5. addresses[type eq "office" and primary eq true].value
// 6. urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value
//
// In addition to the "scim" tag definition, the types of tagging fields must also conform to the following rules:
//
// 1. SCIM String: string or *string
// 2. SCIM Integer: int64 or *int64
// 3. SCIM Decimal: float64 or *float64
// 4. SCIM Boolean: bool or *bool
// 5. SCIM DateTime: int64 or *int64, which contains a UNIX timestamp.
// 6. SCIM Reference: string or *string
// 7. SCIM Binary: string or *string, which contains the Base64 encoded data
//
// For multi-valued properties, the struct field can use the slice of the above non-pointer types. For instance, for a
// multi-valued string property, the corresponding type is []string. Nil slices and nil pointers are interpreted as
// "unassigned" and skipped. Because Facade is intended for traditional flat domain objects like SQL table domains, there
// is no type mapping for complex objects. Complex objects will be constructed by mapping a field to a nested SCIM path,
// hence creating the intended hierarchy.
//
// In addition to the user defined fields, some internal properties will be automatically assigned. The "schemas" property
// always reflects the schemas used in the "scim" tags. The "meta.resourceType" is always assigned to the name of the
// spec.ResourceType defined in the Facade.
//
// The following is a complete example of an object that can be converted to prop.Resource.
//
// 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"`
// }
//
// // ref is a pseudo function that returns reference to a string
// var user = &User{
// Id: "test",
// Email: "[email protected]",
// BackupEmail: ref("[email protected]"),
// Name: "John Doe",
// NickName: nil,
// CreatedAt: 1608795238,
// UpdatedAt: 1608795238,
// Active: false,
// Manager: ref("tom"),
// }
//
// // The above object can be converted to prop.Resource, which will in turn produce the following JSON when rendered:
// {
// "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-24T07:33:58",
// "lastModified": "2020-12-24T07:33:58"
// },
// "name": {
// "formatted": "John Doe"
// },
// "emails": [{
// "value": "[email protected]",
// "type": "work",
// "primary": true
// }, {
// "value": "[email protected]",
// "type": "work",
// "primary": false
// }],
// "active": false,
// "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
// "manager": {
// "value": "tom"
// }
// }
// }
//
// Some tips for designing the domain object structure. First, use concrete types when the data is known to be not nil,
// and use pointer types when data is nullable. Second, when adding two fields to distinct complex objects inside a
// multi-valued property, do not use overlapping filters. For example, [type eq "work" and primary eq true] overlaps
// with [type eq "work"], but it does not overlap with [type eq "work" and primary eq false]. If overlapping cannot be
// avoided, place the fields with the more general filter in front.
package facade
200 changes: 200 additions & 0 deletions pkg/v2/facade/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package facade

import (
"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"
)

// Export exports the object as a prop.Resource. For each field and the corresponding path specified in the "scim" tag,
// it creates a property with the field value at the specified path.
func Export(obj interface{}, resourceType *spec.ResourceType) (*prop.Resource, error) {
r := prop.NewResource(resourceType)
if err := crud.Add(r, "schemas", resourceType.Schema().ID()); err != nil {
return nil, err
}
if err := crud.Add(r, "meta.resourceType", resourceType.Name()); err != nil {
return nil, err
}

exp := exporter{}
forEachMapping(reflect.ValueOf(obj), func(field reflect.Value, path string) error {
return exp.assign(r, field, path)
})

return r, nil
}

type exporter struct{}

func (f exporter) assign(r *prop.Resource, field reflect.Value, path string) error {
if field.Kind() == reflect.Ptr {
if field.IsNil() {
return nil
}
return f.assign(r, field.Elem(), path)
}

head, err := expr.CompilePath(path)
if err != nil {
return err
}

nav := r.Navigator()

for cur := head; cur != nil; cur = cur.Next() {
switch {
case cur.IsPath():
if err := f.stepIn(nav, cur.Token()); err != nil {
return err
}
if cur.Next() == nil {
if err := f.set(nav, field); err != nil {
return err
}
}
case cur.IsRootOfFilter():
if err := f.selectElem(nav, cur); err != nil {
return err
}
default:
return ErrSCIMPath
}
}

return nil
}

func (f exporter) stepIn(nav prop.Navigator, path string) error {
nav.Add(map[string]interface{}{path: nil})
nav.Dot(path)
return nav.Error()
}

func (f exporter) selectElem(nav prop.Navigator, filter *expr.Expression) error {
nav.Where(func(child prop.Property) bool {
ok, _ := crud.EvaluateExpressionOnProperty(child, filter)
return ok
})
if !nav.HasError() {
return nil
}

// Navigator errors because it didn't find such element, clear the
// error and create it!
nav.ClearError()

filterPropValues := map[string]string{}
if err := f.collectLeafProps(filter, filterPropValues); err != nil {
return err
}

complexData := map[string]interface{}{}
for k, v := range filterPropValues {
attr := nav.Current().Attribute().DeriveElementAttribute().SubAttributeForName(k)
switch attr.Type() {
case spec.TypeString, spec.TypeReference, spec.TypeDateTime, spec.TypeBinary:
complexData[k] = v
case spec.TypeInteger:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
complexData[k] = i
case spec.TypeDecimal:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
complexData[k] = f
case spec.TypeBoolean:
b, err := strconv.ParseBool(v)
if err != nil {
return err
}
complexData[k] = b
default:
panic("unexpected type")
}
}

nav.Add(complexData)
if nav.HasError() {
return nav.Error()
}

nav.Where(func(child prop.Property) bool {
ok, _ := crud.EvaluateExpressionOnProperty(child, filter)
return ok
})
return nav.Error()
}

func (f exporter) collectLeafProps(root *expr.Expression, collector map[string]string) error {
if root.IsOperator() {
if root.Token() != expr.And && root.Token() != expr.Eq {
return ErrDisallowedOperator
}
}

if root.IsLogicalOperator() {
if err := f.collectLeafProps(root.Left(), collector); err != nil {
return err
}
return f.collectLeafProps(root.Right(), collector)
}

if root.IsRelationalOperator() {
k := root.Left().Token()
v := strings.Trim(root.Right().Token(), "\"")
collector[k] = v
return nil
}

panic("unreachable code")
}

func (f exporter) set(nav prop.Navigator, field reflect.Value) error {
attr := nav.Current().Attribute()

if err := typeCheck(attr, field.Type()); err != nil {
return err
}

switch field.Kind() {
case reflect.String:
nav.Replace(field.String())
return nav.Error()
case reflect.Int64:
switch attr.Type() {
case spec.TypeInteger:
nav.Replace(field.Int())
return nav.Error()
case spec.TypeDateTime:
nav.Replace(time.Unix(field.Int(), 0).UTC().Format(spec.ISO8601))
return nav.Error()
}
case reflect.Float64:
nav.Replace(field.Float())
return nav.Error()
case reflect.Bool:
nav.Replace(field.Bool())
return nav.Error()
case reflect.Slice:
if attr.MultiValued() {
var list []interface{}
for i := 0; i < field.Len(); i++ {
list = append(list, field.Index(i).Interface())
}
nav.Replace(list)
return nav.Error()
}
}

return ErrInputType
}
Loading

0 comments on commit fec7838

Please sign in to comment.