Skip to content

Commit fec7838

Browse files
authored
Merge pull request #83 from imulab/features/sdk
Features/sdk
2 parents 9b2d7b3 + 464dd06 commit fec7838

File tree

8 files changed

+669
-433
lines changed

8 files changed

+669
-433
lines changed

pkg/v2/facade/doc.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// This package serves as a frontend of custom structures that are mappable to SCIM schemas.
2+
//
3+
// Export and Import are the two main entrypoints. For structures to be recognized by these entrypoints, the intended
4+
// fields must be tagged with "scim", whose content is a comma delimited list of SCIM paths. Apart from having to be a
5+
// legal path backed by the resource type, a filtered path may be allowed, provided that only the "and" and "eq" predicate
6+
// is used inside the filter. A filtered path is essential in mapping one or more fields into a multi-valued complex
7+
// property. The following is an example of legal paths under the User resource type with User schema and the Enterprise
8+
// User schema extension:
9+
//
10+
// 1. id
11+
// 2. meta.created
12+
// 3. name.formatted
13+
// 4. emails[type eq "work"].value
14+
// 5. addresses[type eq "office" and primary eq true].value
15+
// 6. urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value
16+
//
17+
// In addition to the "scim" tag definition, the types of tagging fields must also conform to the following rules:
18+
//
19+
// 1. SCIM String: string or *string
20+
// 2. SCIM Integer: int64 or *int64
21+
// 3. SCIM Decimal: float64 or *float64
22+
// 4. SCIM Boolean: bool or *bool
23+
// 5. SCIM DateTime: int64 or *int64, which contains a UNIX timestamp.
24+
// 6. SCIM Reference: string or *string
25+
// 7. SCIM Binary: string or *string, which contains the Base64 encoded data
26+
//
27+
// For multi-valued properties, the struct field can use the slice of the above non-pointer types. For instance, for a
28+
// multi-valued string property, the corresponding type is []string. Nil slices and nil pointers are interpreted as
29+
// "unassigned" and skipped. Because Facade is intended for traditional flat domain objects like SQL table domains, there
30+
// is no type mapping for complex objects. Complex objects will be constructed by mapping a field to a nested SCIM path,
31+
// hence creating the intended hierarchy.
32+
//
33+
// In addition to the user defined fields, some internal properties will be automatically assigned. The "schemas" property
34+
// always reflects the schemas used in the "scim" tags. The "meta.resourceType" is always assigned to the name of the
35+
// spec.ResourceType defined in the Facade.
36+
//
37+
// The following is a complete example of an object that can be converted to prop.Resource.
38+
//
39+
// type User struct {
40+
// Id string `scim:"id"`
41+
// Email string `scim:"userName,emails[type eq \"work\" and primary eq true].value"`
42+
// BackupEmail *string `scim:"emails[type eq \"work\" and primary eq false].value"`
43+
// Name string `scim:"name.formatted"`
44+
// NickName *string `scim:"nickName"`
45+
// CreatedAt int64 `scim:"meta.created"`
46+
// UpdatedAt int64 `scim:"meta.lastModified"`
47+
// Active bool `scim:"active"`
48+
// Manager *string `scim:"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value"`
49+
// }
50+
//
51+
// // ref is a pseudo function that returns reference to a string
52+
// var user = &User{
53+
// Id: "test",
54+
// Email: "[email protected]",
55+
// BackupEmail: ref("[email protected]"),
56+
// Name: "John Doe",
57+
// NickName: nil,
58+
// CreatedAt: 1608795238,
59+
// UpdatedAt: 1608795238,
60+
// Active: false,
61+
// Manager: ref("tom"),
62+
// }
63+
//
64+
// // The above object can be converted to prop.Resource, which will in turn produce the following JSON when rendered:
65+
// {
66+
// "schemas": [
67+
// "urn:ietf:params:scim:schemas:core:2.0:User",
68+
// "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
69+
// ],
70+
// "id": "test",
71+
// "meta": {
72+
// "resourceType": "User",
73+
// "created": "2020-12-24T07:33:58",
74+
// "lastModified": "2020-12-24T07:33:58"
75+
// },
76+
// "name": {
77+
// "formatted": "John Doe"
78+
// },
79+
// "emails": [{
80+
// "value": "[email protected]",
81+
// "type": "work",
82+
// "primary": true
83+
// }, {
84+
// "value": "[email protected]",
85+
// "type": "work",
86+
// "primary": false
87+
// }],
88+
// "active": false,
89+
// "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
90+
// "manager": {
91+
// "value": "tom"
92+
// }
93+
// }
94+
// }
95+
//
96+
// Some tips for designing the domain object structure. First, use concrete types when the data is known to be not nil,
97+
// and use pointer types when data is nullable. Second, when adding two fields to distinct complex objects inside a
98+
// multi-valued property, do not use overlapping filters. For example, [type eq "work" and primary eq true] overlaps
99+
// with [type eq "work"], but it does not overlap with [type eq "work" and primary eq false]. If overlapping cannot be
100+
// avoided, place the fields with the more general filter in front.
101+
package facade

pkg/v2/facade/export.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package facade
2+
3+
import (
4+
"github.com/imulab/go-scim/pkg/v2/crud"
5+
"github.com/imulab/go-scim/pkg/v2/crud/expr"
6+
"github.com/imulab/go-scim/pkg/v2/prop"
7+
"github.com/imulab/go-scim/pkg/v2/spec"
8+
"reflect"
9+
"strconv"
10+
"strings"
11+
"time"
12+
)
13+
14+
// Export exports the object as a prop.Resource. For each field and the corresponding path specified in the "scim" tag,
15+
// it creates a property with the field value at the specified path.
16+
func Export(obj interface{}, resourceType *spec.ResourceType) (*prop.Resource, error) {
17+
r := prop.NewResource(resourceType)
18+
if err := crud.Add(r, "schemas", resourceType.Schema().ID()); err != nil {
19+
return nil, err
20+
}
21+
if err := crud.Add(r, "meta.resourceType", resourceType.Name()); err != nil {
22+
return nil, err
23+
}
24+
25+
exp := exporter{}
26+
forEachMapping(reflect.ValueOf(obj), func(field reflect.Value, path string) error {
27+
return exp.assign(r, field, path)
28+
})
29+
30+
return r, nil
31+
}
32+
33+
type exporter struct{}
34+
35+
func (f exporter) assign(r *prop.Resource, field reflect.Value, path string) error {
36+
if field.Kind() == reflect.Ptr {
37+
if field.IsNil() {
38+
return nil
39+
}
40+
return f.assign(r, field.Elem(), path)
41+
}
42+
43+
head, err := expr.CompilePath(path)
44+
if err != nil {
45+
return err
46+
}
47+
48+
nav := r.Navigator()
49+
50+
for cur := head; cur != nil; cur = cur.Next() {
51+
switch {
52+
case cur.IsPath():
53+
if err := f.stepIn(nav, cur.Token()); err != nil {
54+
return err
55+
}
56+
if cur.Next() == nil {
57+
if err := f.set(nav, field); err != nil {
58+
return err
59+
}
60+
}
61+
case cur.IsRootOfFilter():
62+
if err := f.selectElem(nav, cur); err != nil {
63+
return err
64+
}
65+
default:
66+
return ErrSCIMPath
67+
}
68+
}
69+
70+
return nil
71+
}
72+
73+
func (f exporter) stepIn(nav prop.Navigator, path string) error {
74+
nav.Add(map[string]interface{}{path: nil})
75+
nav.Dot(path)
76+
return nav.Error()
77+
}
78+
79+
func (f exporter) selectElem(nav prop.Navigator, filter *expr.Expression) error {
80+
nav.Where(func(child prop.Property) bool {
81+
ok, _ := crud.EvaluateExpressionOnProperty(child, filter)
82+
return ok
83+
})
84+
if !nav.HasError() {
85+
return nil
86+
}
87+
88+
// Navigator errors because it didn't find such element, clear the
89+
// error and create it!
90+
nav.ClearError()
91+
92+
filterPropValues := map[string]string{}
93+
if err := f.collectLeafProps(filter, filterPropValues); err != nil {
94+
return err
95+
}
96+
97+
complexData := map[string]interface{}{}
98+
for k, v := range filterPropValues {
99+
attr := nav.Current().Attribute().DeriveElementAttribute().SubAttributeForName(k)
100+
switch attr.Type() {
101+
case spec.TypeString, spec.TypeReference, spec.TypeDateTime, spec.TypeBinary:
102+
complexData[k] = v
103+
case spec.TypeInteger:
104+
i, err := strconv.ParseInt(v, 10, 64)
105+
if err != nil {
106+
return err
107+
}
108+
complexData[k] = i
109+
case spec.TypeDecimal:
110+
f, err := strconv.ParseFloat(v, 64)
111+
if err != nil {
112+
return err
113+
}
114+
complexData[k] = f
115+
case spec.TypeBoolean:
116+
b, err := strconv.ParseBool(v)
117+
if err != nil {
118+
return err
119+
}
120+
complexData[k] = b
121+
default:
122+
panic("unexpected type")
123+
}
124+
}
125+
126+
nav.Add(complexData)
127+
if nav.HasError() {
128+
return nav.Error()
129+
}
130+
131+
nav.Where(func(child prop.Property) bool {
132+
ok, _ := crud.EvaluateExpressionOnProperty(child, filter)
133+
return ok
134+
})
135+
return nav.Error()
136+
}
137+
138+
func (f exporter) collectLeafProps(root *expr.Expression, collector map[string]string) error {
139+
if root.IsOperator() {
140+
if root.Token() != expr.And && root.Token() != expr.Eq {
141+
return ErrDisallowedOperator
142+
}
143+
}
144+
145+
if root.IsLogicalOperator() {
146+
if err := f.collectLeafProps(root.Left(), collector); err != nil {
147+
return err
148+
}
149+
return f.collectLeafProps(root.Right(), collector)
150+
}
151+
152+
if root.IsRelationalOperator() {
153+
k := root.Left().Token()
154+
v := strings.Trim(root.Right().Token(), "\"")
155+
collector[k] = v
156+
return nil
157+
}
158+
159+
panic("unreachable code")
160+
}
161+
162+
func (f exporter) set(nav prop.Navigator, field reflect.Value) error {
163+
attr := nav.Current().Attribute()
164+
165+
if err := typeCheck(attr, field.Type()); err != nil {
166+
return err
167+
}
168+
169+
switch field.Kind() {
170+
case reflect.String:
171+
nav.Replace(field.String())
172+
return nav.Error()
173+
case reflect.Int64:
174+
switch attr.Type() {
175+
case spec.TypeInteger:
176+
nav.Replace(field.Int())
177+
return nav.Error()
178+
case spec.TypeDateTime:
179+
nav.Replace(time.Unix(field.Int(), 0).UTC().Format(spec.ISO8601))
180+
return nav.Error()
181+
}
182+
case reflect.Float64:
183+
nav.Replace(field.Float())
184+
return nav.Error()
185+
case reflect.Bool:
186+
nav.Replace(field.Bool())
187+
return nav.Error()
188+
case reflect.Slice:
189+
if attr.MultiValued() {
190+
var list []interface{}
191+
for i := 0; i < field.Len(); i++ {
192+
list = append(list, field.Index(i).Interface())
193+
}
194+
nav.Replace(list)
195+
return nav.Error()
196+
}
197+
}
198+
199+
return ErrInputType
200+
}

0 commit comments

Comments
 (0)