From 098e14c7131fe72c3082c22c3a4ae811a40c0d60 Mon Sep 17 00:00:00 2001 From: Weinan Qiu Date: Fri, 31 Jan 2020 18:53:47 -0500 Subject: [PATCH 1/4] - introduce Serializable interface to allow multiple things to be serialized, not just spec.Resource. Update API to accept Serializable - spec.Resource now implements json.Serializable - introduce json.SchemaToSerializable to adapt schema to Serializable. The adaptation takes control of how to visit the Visitor. - introduce MetaAttributes() to access baked-in attributes for rendering schemas as resource - Add test to ensure Schema can be serialized as a Serializable --- pkg/v2/json/adapt.go | 11 + pkg/v2/json/adapt_test.go | 778 +++++++++++++++++++++++++++++++++ pkg/v2/json/internal/schema.go | 207 +++++++++ pkg/v2/json/options.go | 11 +- pkg/v2/json/serialize.go | 14 +- pkg/v2/prop/complex.go | 6 +- pkg/v2/prop/resource.go | 5 + pkg/v2/spec/attribute.go | 43 +- pkg/v2/spec/internal/schema.go | 5 - pkg/v2/spec/meta.go | 355 +++++++++++++++ pkg/v2/spec/resource_type.go | 2 +- pkg/v2/spec/schema.go | 21 +- 12 files changed, 1437 insertions(+), 21 deletions(-) create mode 100644 pkg/v2/json/adapt.go create mode 100644 pkg/v2/json/adapt_test.go create mode 100644 pkg/v2/json/internal/schema.go delete mode 100644 pkg/v2/spec/internal/schema.go create mode 100644 pkg/v2/spec/meta.go diff --git a/pkg/v2/json/adapt.go b/pkg/v2/json/adapt.go new file mode 100644 index 00000000..167ed400 --- /dev/null +++ b/pkg/v2/json/adapt.go @@ -0,0 +1,11 @@ +package json + +import ( + "github.com/imulab/go-scim/pkg/v2/json/internal" + "github.com/imulab/go-scim/pkg/v2/spec" +) + +// SchemaToSerializable returns a Serializable wrapper for a schema so it can be used to call json.Serialize +func SchemaToSerializable(sch *spec.Schema) Serializable { + return &internal.SerializableSchema{Sch: sch} +} diff --git a/pkg/v2/json/adapt_test.go b/pkg/v2/json/adapt_test.go new file mode 100644 index 00000000..c27cb108 --- /dev/null +++ b/pkg/v2/json/adapt_test.go @@ -0,0 +1,778 @@ +package json + +import ( + "encoding/json" + "github.com/imulab/go-scim/pkg/v2/spec" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestSchemaToSerializable(t *testing.T) { + f, err := os.Open("../../../public/schemas/user_schema.json") + assert.Nil(t, err) + + sch := new(spec.Schema) + err = json.NewDecoder(f).Decode(sch) + assert.Nil(t, err) + + raw, err := Serialize(SchemaToSerializable(sch)) + assert.Nil(t, err) + + expect := ` +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Schema" + ], + "id": "urn:ietf:params:scim:schemas:core:2.0:User", + "meta": { + "resourceType": "Schema", + "location": "/Schemas/urn:ietf:params:scim:schemas:core:2.0:User" + }, + "name": "User", + "description": "Defined attributes for the user schema", + "attributes": [ + { + "name": "userName", + "type": "string", + "multiValued": false, + "required": true, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "server" + }, + { + "name": "name", + "type": "complex", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "formatted", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "familyName", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "givenName", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "middleName", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "honorificPrefix", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "honorificSuffix", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "displayName", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "nickName", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "profileUrl", + "type": "reference", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "referenceTypes": [ + "external" + ] + }, + { + "name": "title", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "userType", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "Employee", + "Intern" + ] + }, + { + "name": "preferredLanguage", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "zh_CN", + "en_US" + ] + }, + { + "name": "locale", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "en_US", + "zh_CN" + ] + }, + { + "name": "timezone", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "Asia/Shanghai", + "Asia/Beijing", + "America/New_York", + "America/Toronto" + ] + }, + { + "name": "active", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "password", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "writeOnly", + "returned": "never", + "uniqueness": "none" + }, + { + "name": "emails", + "type": "complex", + "multiValued": true, + "required": true, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "work", + "home", + "other" + ] + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "phoneNumbers", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "work", + "home", + "mobile", + "fax", + "other" + ] + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "ims", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "skype", + "qq", + "wechat", + "weibo", + "other" + ] + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "photos", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "reference", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "referenceTypes": [ + "external" + ] + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "photo", + "thumbnail" + ] + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "addresses", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "formatted", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "streetAddress", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "locality", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "region", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "postalCode", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "country", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "work", + "home", + "id", + "driver", + "other" + ] + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "groups", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "$ref", + "type": "reference", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "direct", + "indirect" + ] + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "entitlements", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "roles", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "x509Certificates", + "type": "complex", + "multiValued": true, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { + "name": "value", + "type": "binary", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ] + } + ] +} +` + + assert.JSONEq(t, expect, string(raw)) +} diff --git a/pkg/v2/json/internal/schema.go b/pkg/v2/json/internal/schema.go new file mode 100644 index 00000000..8cc833e6 --- /dev/null +++ b/pkg/v2/json/internal/schema.go @@ -0,0 +1,207 @@ +package internal + +import ( + "github.com/imulab/go-scim/pkg/v2/prop" + "github.com/imulab/go-scim/pkg/v2/spec" +) + +// SerializableSchema is the json.Serializable wrapper for spec.Schema. +type SerializableSchema struct { + Sch *spec.Schema +} + +// MainSchemaId returns the main schema id for Schema as a resource. +func (s *SerializableSchema) MainSchemaId() string { + return "urn:ietf:params:scim:schemas:core:2.0:Schema" +} + +// Visit takes the visitor on a DFS tour of the structure of the Schema resource. This method takes control of what +// to visit and will not consult the ShouldVisit method of visitor. +func (s *SerializableSchema) Visit(visitor prop.Visitor) error { + dummyContainer := prop.NewComplex(spec.MetaAttributes().SchemaAttributeNoSub()) + visitor.BeginChildren(dummyContainer) + if err := s.visitCore(visitor); err != nil { + return err + } + if err := s.visitSchema(visitor); err != nil { + return err + } + visitor.EndChildren(dummyContainer) + return nil +} + +func (s *SerializableSchema) visitCore(visitor prop.Visitor) error { + // schemas + schemas := prop.NewMultiOf(spec.MetaAttributes().CoreSchemasAttribute(), []interface{}{s.MainSchemaId()}) + if err := visitor.Visit(schemas); err != nil { + return err + } + visitor.BeginChildren(schemas) + if err := schemas.ForEachChild(func(index int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(schemas) + + // id + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().CoreIdAttribute(), s.Sch.ID())); err != nil { + return err + } + + // meta + meta := prop.NewComplexOf(spec.MetaAttributes().CoreMetaPartialAttribute(), map[string]interface{}{ + "resourceType": s.Sch.ResourceTypeName(), + "location": s.Sch.ResourceLocation(), + }) + if err := visitor.Visit(meta); err != nil { + return err + } + visitor.BeginChildren(meta) + if err := meta.ForEachChild(func(_ int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(meta) + + return nil +} + +func (s *SerializableSchema) visitSchema(visitor prop.Visitor) error { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().SchemaNameAttribute(), s.Sch.Name())); err != nil { + return err + } + + if description := s.Sch.Description(); len(description) > 0 { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().SchemaDescriptionAttribute(), description)); err != nil { + return err + } + } + + dummyMulti := prop.NewMulti(spec.MetaAttributes().SchemaAttributesAttributeNoSub()) + if err := visitor.Visit(dummyMulti); err != nil { + return err + } + visitor.BeginChildren(dummyMulti) + if err := s.Sch.ForEachAttribute(func(attr *spec.Attribute) error { + dummyComplex := prop.NewComplex(spec.MetaAttributes().SchemaAttributesAttributeNoSub().DeriveElementAttribute()) + if err := visitor.Visit(dummyComplex); err != nil { + return err + } + visitor.BeginChildren(dummyComplex) + if err := s.visitAttribute(attr, visitor); err != nil { + return err + } + visitor.EndChildren(dummyComplex) + return nil + }); err != nil { + return err + } + visitor.EndChildren(dummyMulti) + + return nil +} + +func (s *SerializableSchema) visitAttribute(attr *spec.Attribute, visitor prop.Visitor) error { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeNameAttribute(), attr.Name())); err != nil { + return err + } + + if description := attr.Description(); len(description) > 0 { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeDescriptionAttribute(), description)); err != nil { + return err + } + } + + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeTypeAttribute(), attr.Type().String())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewBooleanOf(spec.MetaAttributes().AttributeMultiValuedAttribute(), attr.MultiValued())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewBooleanOf(spec.MetaAttributes().AttributeRequiredAttribute(), attr.Required())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewBooleanOf(spec.MetaAttributes().AttributeCaseExactAttribute(), attr.CaseExact())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeMutabilityAttribute(), attr.Mutability().String())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeReturnedAttribute(), attr.Returned().String())); err != nil { + return err + } + + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().AttributeUniquenessAttribute(), attr.Uniqueness().String())); err != nil { + return err + } + + if attr.CountCanonicalValues() > 0 { + var canonicalValues []interface{} + attr.ForEachCanonicalValues(func(canonicalValue string) { + canonicalValues = append(canonicalValues, canonicalValue) + }) + + cvp := prop.NewMultiOf(spec.MetaAttributes().AttributeCanonicalValuesAttribute(), canonicalValues) + if err := visitor.Visit(cvp); err != nil { + return err + } + visitor.BeginChildren(cvp) + if err := cvp.ForEachChild(func(_ int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(cvp) + } + + if attr.CountReferenceTypes() > 0 { + var referenceTypes []interface{} + attr.ForEachReferenceTypes(func(referenceType string) { + referenceTypes = append(referenceTypes, referenceType) + }) + + rtp := prop.NewMultiOf(spec.MetaAttributes().AttributeReferenceTypesAttribute(), referenceTypes) + if err := visitor.Visit(rtp); err != nil { + return err + } + visitor.BeginChildren(rtp) + if err := rtp.ForEachChild(func(_ int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(rtp) + } + + if attr.CountSubAttributes() > 0 { + dummyMulti := prop.NewMulti(spec.MetaAttributes().AttributeSubAttributesAttributeNoSub()) + if err := visitor.Visit(dummyMulti); err != nil { + return err + } + visitor.BeginChildren(dummyMulti) + if err := attr.ForEachSubAttribute(func(subAttribute *spec.Attribute) error { + dummyComplex := prop.NewComplex(spec.MetaAttributes().AttributeSubAttributesAttributeNoSub().DeriveElementAttribute()) + if err := visitor.Visit(dummyComplex); err != nil { + return err + } + visitor.BeginChildren(dummyComplex) + if err := s.visitAttribute(subAttribute, visitor); err != nil { + return err + } + visitor.EndChildren(dummyComplex) + return nil + }); err != nil { + return err + } + visitor.EndChildren(dummyMulti) + } + + return nil +} diff --git a/pkg/v2/json/options.go b/pkg/v2/json/options.go index a4a383d6..2da34308 100644 --- a/pkg/v2/json/options.go +++ b/pkg/v2/json/options.go @@ -1,7 +1,6 @@ package json import ( - "github.com/imulab/go-scim/pkg/v2/prop" "strings" ) @@ -19,14 +18,14 @@ func Exclude(attributes ...string) Options { // JSON serialization options. type Options interface { - apply(s *serializer, resource *prop.Resource) + apply(s *serializer, serializable Serializable) } type include struct { attributes []string } -func (i include) apply(s *serializer, resource *prop.Resource) { +func (i include) apply(s *serializer, serializable Serializable) { if s.includes == nil { s.includes = []string{} } @@ -34,7 +33,7 @@ func (i include) apply(s *serializer, resource *prop.Resource) { if len(path) > 0 { s.includes = append(s.includes, strings.TrimPrefix( strings.ToLower(path), - strings.ToLower(resource.ResourceType().Schema().ID()+":")), + strings.ToLower(serializable.MainSchemaId()+":")), ) } } @@ -44,7 +43,7 @@ type exclude struct { attributes []string } -func (e exclude) apply(s *serializer, resource *prop.Resource) { +func (e exclude) apply(s *serializer, serializable Serializable) { if s.excludes == nil { s.excludes = []string{} } @@ -52,7 +51,7 @@ func (e exclude) apply(s *serializer, resource *prop.Resource) { if len(path) > 0 { s.excludes = append(s.excludes, strings.TrimPrefix( strings.ToLower(path), - strings.ToLower(resource.ResourceType().Schema().ID()+":")), + strings.ToLower(serializable.MainSchemaId()+":")), ) } } diff --git a/pkg/v2/json/serialize.go b/pkg/v2/json/serialize.go index 0eaeb7fb..fbdcb197 100644 --- a/pkg/v2/json/serialize.go +++ b/pkg/v2/json/serialize.go @@ -11,9 +11,17 @@ import ( "unicode/utf8" ) +// Interface to implement to be able to serialize to JSON. +type Serializable interface { + // MainSchemaId returns the id of the resource type's main schema that describes the target + MainSchemaId() string + // Visit implements the order for the visitor + Visit(visitor prop.Visitor) error +} + // Serialize the given resource to JSON bytes. The serialization process subjects to the request attributes and // excludedAttributes from options, and the SCIM return-ability rules. -func Serialize(resource *prop.Resource, options ...Options) ([]byte, error) { +func Serialize(serializable Serializable, options ...Options) ([]byte, error) { s := serializer{ Buffer: bytes.Buffer{}, includes: []string{}, @@ -22,14 +30,14 @@ func Serialize(resource *prop.Resource, options ...Options) ([]byte, error) { scratch: [64]byte{}, } for _, opt := range options { - opt.apply(&s, resource) + opt.apply(&s, serializable) } if len(s.includes) > 0 && len(s.excludes) > 0 { return nil, fmt.Errorf("%w: attributes and excludedAttributes are mutually exclusive", spec.ErrInvalidValue) } - if err := resource.Visit(&s); err != nil { + if err := serializable.Visit(&s); err != nil { return nil, err } diff --git a/pkg/v2/prop/complex.go b/pkg/v2/prop/complex.go index e53d2e24..eab3f0b0 100644 --- a/pkg/v2/prop/complex.go +++ b/pkg/v2/prop/complex.go @@ -23,9 +23,10 @@ func NewComplex(attr *spec.Attribute) Property { p.subscribers = append(p.subscribers, subscriber) } }) - attr.ForEachSubAttribute(func(subAttribute *spec.Attribute) { + _ = attr.ForEachSubAttribute(func(subAttribute *spec.Attribute) error { p.subProps = append(p.subProps, NewProperty(subAttribute)) p.nameIndex[strings.ToLower(subAttribute.Name())] = len(p.subProps) - 1 + return nil }) return &p } @@ -87,10 +88,11 @@ func (p *complexProperty) Dirty() bool { func (p *complexProperty) identitySubAttributes() map[*spec.Attribute]struct{} { idSubAttr := map[*spec.Attribute]struct{}{} - p.attr.ForEachSubAttribute(func(subAttribute *spec.Attribute) { + _ = p.attr.ForEachSubAttribute(func(subAttribute *spec.Attribute) error { if _, ok := subAttribute.Annotation(annotation.Identity); ok { idSubAttr[subAttribute] = struct{}{} } + return nil }) return idSubAttr } diff --git a/pkg/v2/prop/resource.go b/pkg/v2/prop/resource.go index 24f2308b..1ba6135d 100644 --- a/pkg/v2/prop/resource.go +++ b/pkg/v2/prop/resource.go @@ -51,6 +51,11 @@ func (r *Resource) Navigator() Navigator { return Navigate(r.data) } +// MainSchemaId returns the id of the resource type's main schema. +func (r *Resource) MainSchemaId() string { + return r.resourceType.Schema().ID() +} + // Visit starts a DFS visit on the root property of the resource. func (r *Resource) Visit(visitor Visitor) error { visitor.BeginChildren(r.data) diff --git a/pkg/v2/spec/attribute.go b/pkg/v2/spec/attribute.go index 059c28c5..f776a785 100644 --- a/pkg/v2/spec/attribute.go +++ b/pkg/v2/spec/attribute.go @@ -176,11 +176,19 @@ func (attr *Attribute) ExistsReferenceType(criteria func(referenceType string) b return false } +// CountReferenceTypes returns the total number of reference types. +func (attr *Attribute) CountReferenceTypes() int { + return len(attr.referenceTypes) +} + // ForEachSubAttribute invokes callback function on each sub attribute. -func (attr *Attribute) ForEachSubAttribute(callback func(subAttribute *Attribute)) { +func (attr *Attribute) ForEachSubAttribute(callback func(subAttribute *Attribute) error) error { for _, eachSubAttribute := range attr.subAttributes { - callback(eachSubAttribute) + if err := callback(eachSubAttribute); err != nil { + return err + } } + return nil } // FindSubAttribute returns the sub attribute that matches the criteria, or returns nil if no sub attribute meets criteria. @@ -203,6 +211,11 @@ func (attr *Attribute) SubAttributeForName(name string) *Attribute { return nil } +// Return the total number of sub attributes. +func (attr *Attribute) CountSubAttributes() int { + return len(attr.subAttributes) +} + // GoesBy returns true if this attribute can be addressed by the given name. func (attr *Attribute) GoesBy(name string) bool { switch strings.ToLower(name) { @@ -309,6 +322,32 @@ func (attr *Attribute) DeriveElementAttribute() *Attribute { return &elemAttr } +// PublicValues returns a representation of this attribute's public values (non-extension) in a data structure +// that conforms to the definition of prop.Property#Raw. However, this method does not implement it. +func (attr *Attribute) PublicValues() interface{} { + data := map[string]interface{}{ + "name": attr.name, + "description": attr.description, + "type": attr.typ.String(), + "multiValued": attr.multiValued, + "required": attr.required, + "caseExact": attr.caseExact, + "mutability": attr.mutability.String(), + "returned": attr.returned.String(), + "uniqueness": attr.uniqueness.String(), + "canonicalValues": attr.canonicalValues, + "referenceTypes": attr.referenceTypes, + } + if len(attr.subAttributes) > 0 { + var subPV []interface{} + for _, subAttr := range attr.subAttributes { + subPV = append(subPV, subAttr.PublicValues()) + } + data["subAttributes"] = subPV + } + return data +} + // Equals returns true if the two attributes are considered equal. func (attr *Attribute) Equals(other *Attribute) bool { return (attr == other) || attr.id == other.id diff --git a/pkg/v2/spec/internal/schema.go b/pkg/v2/spec/internal/schema.go deleted file mode 100644 index 613cc646..00000000 --- a/pkg/v2/spec/internal/schema.go +++ /dev/null @@ -1,5 +0,0 @@ -package internal - -const ( - CoreSchemaId = "core" -) diff --git a/pkg/v2/spec/meta.go b/pkg/v2/spec/meta.go new file mode 100644 index 00000000..89925218 --- /dev/null +++ b/pkg/v2/spec/meta.go @@ -0,0 +1,355 @@ +package spec + +var ( + metaAttributes = &metaAttr{} +) + +// MetaAttributes returns a structure to access individual attributes about +// fields in Schema, Attribute and ResourceType. These attributes are known +// as meta attributes, because they describe things that are used to describe +// other resources. +func MetaAttributes() *metaAttr { + return metaAttributes +} + +type metaAttr struct { + coreSchemas *Attribute + coreId *Attribute + coreMeta *Attribute + + schema *Attribute + schemaName *Attribute + schemaDescription *Attribute + schemaAttributes *Attribute + + attrName *Attribute + attrDescription *Attribute + attrType *Attribute + attrMultiValued *Attribute + attrRequired *Attribute + attrCaseExact *Attribute + attrMutability *Attribute + attrReturned *Attribute + attrUniqueness *Attribute + attrCanonicalValues *Attribute + attrReferenceTypes *Attribute + attrSubAttributes *Attribute +} + +// CoreSchemasAttribute returns an attribute to describe the "schemas" field. +func (m *metaAttr) CoreSchemasAttribute() *Attribute { + if m.coreSchemas == nil { + m.coreSchemas = &Attribute{ + id: "schemas", + name: "schemas", + typ: TypeString, + multiValued: true, + index: 0, + path: "schemas", + } + } + return m.coreSchemas +} + +// CoreIdAttribute returns an attribute to describe the "id" field. +func (m *metaAttr) CoreIdAttribute() *Attribute { + if m.coreId == nil { + m.coreId = &Attribute{ + id: "id", + name: "id", + typ: TypeString, + index: 1, + path: "id", + } + } + return m.coreId +} + +// CoreMetaPartialAttribute returns an attribute to describe the "meta" field. Only the "resourceType" and "location" +// sub attributes are included as subAttributes. +func (m *metaAttr) CoreMetaPartialAttribute() *Attribute { + if m.coreMeta == nil { + m.coreMeta = &Attribute{ + id: "meta", + name: "meta", + typ: TypeComplex, + index: 2, + path: "meta", + subAttributes: []*Attribute{ + { + id: "meta.resourceType", + name: "resourceType", + typ: TypeString, + index: 0, + path: "meta.resourceType", + }, + { + id: "meta.location", + name: "location", + typ: TypeString, + index: 1, + path: "meta.location", + }, + }, + } + } + return m.coreMeta +} + +// SchemaAttributeNoSub returns an attribute to act as the container attribute for schema resources, but it does not +// actually contain any sub attributes. +func (m *metaAttr) SchemaAttributeNoSub() *Attribute { + if m.schema == nil { + m.schema = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema", + name: "", + typ: TypeComplex, + index: 0, + path: "", + } + } + return m.schema +} + +// SchemaNameAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:name" field. +func (m *metaAttr) SchemaNameAttribute() *Attribute { + if m.schemaName == nil { + m.schemaName = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:name", + name: "name", + typ: TypeString, + index: 100, + path: "name", + } + } + return m.schemaName +} + +// SchemaDescriptionAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:description" field. +func (m *metaAttr) SchemaDescriptionAttribute() *Attribute { + if m.schemaDescription == nil { + m.schemaDescription = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:description", + name: "description", + typ: TypeString, + index: 101, + path: "description", + } + } + return m.schemaDescription +} + +// SchemaAttributesAttributeNoSub returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes". +// However, this attribute does not contain the subAttributes definition. +func (m *metaAttr) SchemaAttributesAttributeNoSub() *Attribute { + if m.schemaAttributes == nil { + m.schemaAttributes = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes", + name: "attributes", + typ: TypeComplex, + multiValued: true, + index: 102, + path: "attributes", + } + } + return m.schemaAttributes +} + +// AttributeNameAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.name". +func (m *metaAttr) AttributeNameAttribute() *Attribute { + if m.attrName == nil { + m.attrName = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.name", + name: "name", + typ: TypeString, + index: 0, + path: "attributes.name", + } + } + return m.attrName +} + +// AttributeDescriptionAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.description". +func (m *metaAttr) AttributeDescriptionAttribute() *Attribute { + if m.attrDescription == nil { + m.attrDescription = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.description", + name: "description", + typ: TypeString, + index: 1, + path: "attributes.description", + } + } + return m.attrDescription +} + +// AttributeTypeAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.type". +func (m *metaAttr) AttributeTypeAttribute() *Attribute { + if m.attrType == nil { + m.attrType = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.type", + name: "type", + typ: TypeString, + index: 2, + path: "attributes.type", + canonicalValues: []string{ + TypeString.String(), + TypeInteger.String(), + TypeDecimal.String(), + TypeBoolean.String(), + TypeDateTime.String(), + TypeBinary.String(), + TypeReference.String(), + TypeComplex.String(), + }, + } + } + return m.attrType +} + +// AttributeMultiValuedAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.multiValued". +func (m *metaAttr) AttributeMultiValuedAttribute() *Attribute { + if m.attrMultiValued == nil { + m.attrMultiValued = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.multiValued", + name: "multiValued", + typ: TypeBoolean, + index: 3, + path: "attributes.multiValued", + } + } + return m.attrMultiValued +} + +// AttributeRequiredAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.required". +func (m *metaAttr) AttributeRequiredAttribute() *Attribute { + if m.attrRequired == nil { + m.attrRequired = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.required", + name: "required", + typ: TypeBoolean, + index: 4, + path: "attributes.required", + } + } + return m.attrRequired +} + +// AttributeCaseExactAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.caseExact". +func (m *metaAttr) AttributeCaseExactAttribute() *Attribute { + if m.attrCaseExact == nil { + m.attrCaseExact = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.caseExact", + name: "caseExact", + typ: TypeBoolean, + index: 5, + path: "attributes.caseExact", + } + } + return m.attrCaseExact +} + +// AttributeMutabilityAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.mutability". +func (m *metaAttr) AttributeMutabilityAttribute() *Attribute { + if m.attrMutability == nil { + m.attrMutability = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.mutability", + name: "mutability", + typ: TypeString, + index: 6, + path: "attributes.mutability", + canonicalValues: []string{ + MutabilityReadWrite.String(), + MutabilityReadOnly.String(), + MutabilityImmutable.String(), + MutabilityWriteOnly.String(), + }, + } + } + return m.attrMutability +} + +// AttributeReturnedAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.returned". +func (m *metaAttr) AttributeReturnedAttribute() *Attribute { + if m.attrReturned == nil { + m.attrReturned = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.returned", + name: "returned", + typ: TypeString, + index: 7, + path: "attributes.returned", + canonicalValues: []string{ + ReturnedDefault.String(), + ReturnedAlways.String(), + ReturnedNever.String(), + ReturnedRequest.String(), + }, + } + } + return m.attrReturned +} + +// AttributeUniquenessAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.uniqueness". +func (m *metaAttr) AttributeUniquenessAttribute() *Attribute { + if m.attrUniqueness == nil { + m.attrUniqueness = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.uniqueness", + name: "uniqueness", + typ: TypeString, + index: 8, + path: "attributes.uniqueness", + canonicalValues: []string{ + UniquenessNone.String(), + UniquenessServer.String(), + UniquenessGlobal.String(), + }, + } + } + return m.attrUniqueness +} + +// AttributeCanonicalValuesAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.canonicalValues". +func (m *metaAttr) AttributeCanonicalValuesAttribute() *Attribute { + if m.attrCanonicalValues == nil { + m.attrCanonicalValues = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.canonicalValues", + name: "canonicalValues", + typ: TypeString, + multiValued: true, + index: 9, + path: "attributes.canonicalValues", + } + } + return m.attrCanonicalValues +} + +// AttributeReferenceTypesAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.referenceTypes". +func (m *metaAttr) AttributeReferenceTypesAttribute() *Attribute { + if m.attrReferenceTypes == nil { + m.attrReferenceTypes = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.referenceTypes", + name: "referenceTypes", + typ: TypeReference, + multiValued: true, + index: 10, + path: "attributes.referenceTypes", + } + } + return m.attrReferenceTypes +} + +// AttributeSubAttributesAttributeNoSub returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.subAttributes". +// However, this attribute does not contain the subAttributes definition. +func (m *metaAttr) AttributeSubAttributesAttributeNoSub() *Attribute { + if m.attrSubAttributes == nil { + m.attrSubAttributes = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:Schema:attributes.subAttributes", + name: "subAttributes", + typ: TypeComplex, + multiValued: true, + index: 11, + path: "attributes.subAttributes", + } + } + return m.attrSubAttributes +} diff --git a/pkg/v2/spec/resource_type.go b/pkg/v2/spec/resource_type.go index ac0284ef..989d7da4 100644 --- a/pkg/v2/spec/resource_type.go +++ b/pkg/v2/spec/resource_type.go @@ -136,7 +136,7 @@ func (t *ResourceType) SuperAttribute(includeCore bool) *Attribute { } if includeCore { - super.subAttributes = append(super.subAttributes, Schemas().mustGet(internal.CoreSchemaId).attributes...) + super.subAttributes = append(super.subAttributes, Schemas().mustGet(CoreSchemaId).attributes...) super.annotations[annotation.SyncSchema] = map[string]interface{}{} } diff --git a/pkg/v2/spec/schema.go b/pkg/v2/spec/schema.go index 291648cd..c1a1dee6 100644 --- a/pkg/v2/spec/schema.go +++ b/pkg/v2/spec/schema.go @@ -5,6 +5,9 @@ import ( "sync" ) +// Reserved Id for core schema +const CoreSchemaId = "core" + // Schema models a SCIM schema. It is the collection of one or more attributes. Schema structure is read only // after construction. Schema can be identified by its id, and can be cached in a schema registry. // @@ -38,10 +41,24 @@ func (s *Schema) Description() string { } // ForEachAttribute iterate all attributes in this schema and invoke callback function. -func (s *Schema) ForEachAttribute(callback func(attr *Attribute)) { +func (s *Schema) ForEachAttribute(callback func(attr *Attribute) error) error { for _, attr := range s.attributes { - callback(attr) + if err := callback(attr); err != nil { + return err + } } + return nil +} + +// ResourceTypeName returns the resource type of the Schema resource. This value is formally defined and hence fixed. +func (s *Schema) ResourceTypeName() string { + return "Schema" +} + +// ResourceLocation returns the relative URI at which this Schema resource can be accessed. This value is formally +// defined in the specification and hence fixed. +func (s *Schema) ResourceLocation() string { + return "/Schemas/" + s.ID() } func (s *Schema) MarshalJSON() ([]byte, error) { From 670652b2ba85d4b8e56e3c55da1eb13b11ea0d44 Mon Sep 17 00:00:00 2001 From: Weinan Qiu Date: Fri, 31 Jan 2020 21:00:15 -0500 Subject: [PATCH 2/4] - introduce json.ResourceTypeToSerializable to adapt resource type to Serializable. The adaptation takes control of how to visit the Visitor. - introduce resource type related attributes in MetaAttributes() - Add test to ensure ResourceType can be serialized as a Serializable --- pkg/v2/crud/register.go | 3 +- pkg/v2/json/adapt.go | 5 + pkg/v2/json/adapt_test.go | 39 ++++++++ pkg/v2/json/internal/resource_type.go | 119 ++++++++++++++++++++++++ pkg/v2/spec/meta.go | 127 ++++++++++++++++++++++++++ pkg/v2/spec/resource_type.go | 26 +++++- 6 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 pkg/v2/json/internal/resource_type.go diff --git a/pkg/v2/crud/register.go b/pkg/v2/crud/register.go index b159f2f6..adecbefa 100644 --- a/pkg/v2/crud/register.go +++ b/pkg/v2/crud/register.go @@ -8,7 +8,8 @@ import ( // Register calls expr.RegisterURN for the main schema ids and all schema extension ids in the resource type. func Register(resourceType *spec.ResourceType) { expr.RegisterURN(resourceType.Schema().ID()) - resourceType.ForEachExtension(func(extension *spec.Schema, required bool) { + _ = resourceType.ForEachExtension(func(extension *spec.Schema, required bool) error { expr.RegisterURN(extension.ID()) + return nil }) } diff --git a/pkg/v2/json/adapt.go b/pkg/v2/json/adapt.go index 167ed400..559521a8 100644 --- a/pkg/v2/json/adapt.go +++ b/pkg/v2/json/adapt.go @@ -9,3 +9,8 @@ import ( func SchemaToSerializable(sch *spec.Schema) Serializable { return &internal.SerializableSchema{Sch: sch} } + +// ResourceTypeToSerializable returns a Serializable wrapper for a resource type so it can be used to call json.Serialize +func ResourceTypeToSerializable(resourceType *spec.ResourceType) Serializable { + return &internal.SerializableResourceType{ResourceType: resourceType} +} diff --git a/pkg/v2/json/adapt_test.go b/pkg/v2/json/adapt_test.go index c27cb108..80405172 100644 --- a/pkg/v2/json/adapt_test.go +++ b/pkg/v2/json/adapt_test.go @@ -8,6 +8,45 @@ import ( "testing" ) +func TestResourceTypeToSerializable(t *testing.T) { + f, err := os.Open("../../../public/schemas/user_schema.json") + assert.Nil(t, err) + + sch := new(spec.Schema) + err = json.NewDecoder(f).Decode(sch) + assert.Nil(t, err) + spec.Schemas().Register(sch) + + f, err = os.Open("../../../public/resource_types/user_resource_type.json") + assert.Nil(t, err) + + rt := new(spec.ResourceType) + err = json.NewDecoder(f).Decode(rt) + assert.Nil(t, err) + + raw, err := Serialize(ResourceTypeToSerializable(rt)) + assert.Nil(t, err) + + expect := ` +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:ResourceType" + ], + "id": "User", + "meta": { + "resourceType": "ResourceType", + "location": "/ResourceTypes/User" + }, + "name": "User", + "endpoint": "/Users", + "schema": "urn:ietf:params:scim:schemas:core:2.0:User" +} + +` + + assert.JSONEq(t, expect, string(raw)) +} + func TestSchemaToSerializable(t *testing.T) { f, err := os.Open("../../../public/schemas/user_schema.json") assert.Nil(t, err) diff --git a/pkg/v2/json/internal/resource_type.go b/pkg/v2/json/internal/resource_type.go new file mode 100644 index 00000000..17bb8a70 --- /dev/null +++ b/pkg/v2/json/internal/resource_type.go @@ -0,0 +1,119 @@ +package internal + +import ( + "github.com/imulab/go-scim/pkg/v2/prop" + "github.com/imulab/go-scim/pkg/v2/spec" +) + +// SerializableResourceType is the json.Serializable wrapper for spec.ResourceType. +type SerializableResourceType struct { + ResourceType *spec.ResourceType +} + +// MainSchemaId returns the main schema id for ResourceType as a resource. +func (s *SerializableResourceType) MainSchemaId() string { + return "urn:ietf:params:scim:schemas:core:2.0:ResourceType" +} + +// Visit takes the visitor on a DFS tour of the structure of the ResourceType resource. This method takes control of what +// to visit and will not consult the ShouldVisit method of visitor. +func (s *SerializableResourceType) Visit(visitor prop.Visitor) error { + dummyContainer := prop.NewComplex(spec.MetaAttributes().ResourceTypeAttributeNoSub()) + visitor.BeginChildren(dummyContainer) + if err := s.visitCore(visitor); err != nil { + return err + } + if err := s.visitResourceType(visitor); err != nil { + return err + } + visitor.EndChildren(dummyContainer) + return nil +} + +func (s *SerializableResourceType) visitCore(visitor prop.Visitor) error { + // schemas + schemas := prop.NewMultiOf(spec.MetaAttributes().CoreSchemasAttribute(), []interface{}{s.MainSchemaId()}) + if err := visitor.Visit(schemas); err != nil { + return err + } + visitor.BeginChildren(schemas) + if err := schemas.ForEachChild(func(index int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(schemas) + + // id + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().CoreIdAttribute(), s.ResourceType.ID())); err != nil { + return err + } + + // meta + meta := prop.NewComplexOf(spec.MetaAttributes().CoreMetaPartialAttribute(), map[string]interface{}{ + "resourceType": s.ResourceType.ResourceTypeName(), + "location": s.ResourceType.ResourceLocation(), + }) + if err := visitor.Visit(meta); err != nil { + return err + } + visitor.BeginChildren(meta) + if err := meta.ForEachChild(func(_ int, child prop.Property) error { + return visitor.Visit(child) + }); err != nil { + return err + } + visitor.EndChildren(meta) + + return nil +} + +func (s *SerializableResourceType) visitResourceType(visitor prop.Visitor) error { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().ResourceTypeNameAttribute(), s.ResourceType.Name())); err != nil { + return err + } + + if description := s.ResourceType.Description(); len(description) > 0 { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().ResourceTypeDescriptionAttribute(), description)); err != nil { + return err + } + } + + if endpoint := s.ResourceType.Endpoint(); len(endpoint) > 0 { + if err := visitor.Visit(prop.NewStringOf(spec.MetaAttributes().ResourceTypeEndpointAttribute(), endpoint)); err != nil { + return err + } + } + + if err := visitor.Visit(prop.NewReferenceOf(spec.MetaAttributes().ResourceTypeSchemaAttribute(), s.ResourceType.Schema().ID())); err != nil { + return err + } + + if s.ResourceType.CountExtensions() > 0 { + dummyMulti := prop.NewMulti(spec.MetaAttributes().ResourceTypeSchemaExtensionsAttributeNoSub()) + if err := visitor.Visit(dummyMulti); err != nil { + return err + } + visitor.BeginChildren(dummyMulti) + if err := s.ResourceType.ForEachExtension(func(extension *spec.Schema, required bool) error { + dummyComplex := prop.NewComplex(spec.MetaAttributes().ResourceTypeSchemaExtensionsAttributeNoSub().DeriveElementAttribute()) + if err := visitor.Visit(dummyComplex); err != nil { + return err + } + visitor.BeginChildren(dummyComplex) + if err := visitor.Visit(prop.NewReferenceOf(spec.MetaAttributes().ResourceTypeSchemaExtensionSchemaAttribute(), extension.ID())); err != nil { + return err + } + if err := visitor.Visit(prop.NewBooleanOf(spec.MetaAttributes().ResourceTypeSchemaExtensionRequiredAttribute(), required)); err != nil { + return err + } + visitor.EndChildren(dummyComplex) + return nil + }); err != nil { + return err + } + visitor.EndChildren(dummyMulti) + } + + return nil +} diff --git a/pkg/v2/spec/meta.go b/pkg/v2/spec/meta.go index 89925218..0bdf149a 100644 --- a/pkg/v2/spec/meta.go +++ b/pkg/v2/spec/meta.go @@ -34,6 +34,15 @@ type metaAttr struct { attrCanonicalValues *Attribute attrReferenceTypes *Attribute attrSubAttributes *Attribute + + resourceType *Attribute + resourceTypeName *Attribute + resourceTypeDescription *Attribute + resourceTypeEndpoint *Attribute + resourceTypeSchema *Attribute + resourceTypeSchemaExtensions *Attribute + resourceTypeSchemaExtensionSchema *Attribute + resourceTypeSchemaExtensionRequired *Attribute } // CoreSchemasAttribute returns an attribute to describe the "schemas" field. @@ -353,3 +362,121 @@ func (m *metaAttr) AttributeSubAttributesAttributeNoSub() *Attribute { } return m.attrSubAttributes } + +// ResourceTypeAttributeNoSub returns an attribute to act as the container attribute for ResourceType resources, but it does not +// actually contain any sub attributes. +func (m *metaAttr) ResourceTypeAttributeNoSub() *Attribute { + if m.resourceType == nil { + m.resourceType = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType", + name: "", + typ: TypeComplex, + index: 0, + path: "", + } + } + return m.resourceType +} + +// ResourceTypeNameAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:ResourceType:name" +func (m *metaAttr) ResourceTypeNameAttribute() *Attribute { + if m.resourceTypeName == nil { + m.resourceTypeName = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:name", + name: "name", + typ: TypeString, + index: 100, + path: "name", + } + } + return m.resourceTypeName +} + +// ResourceTypeDescriptionAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:ResourceType:description". +func (m *metaAttr) ResourceTypeDescriptionAttribute() *Attribute { + if m.resourceTypeDescription == nil { + m.resourceTypeDescription = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:description", + name: "description", + typ: TypeString, + index: 101, + path: "description", + } + } + return m.resourceTypeDescription +} + +// ResourceTypeEndpointAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:ResourceType:endpoint". +func (m *metaAttr) ResourceTypeEndpointAttribute() *Attribute { + if m.resourceTypeEndpoint == nil { + m.resourceTypeEndpoint = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:endpoint", + name: "endpoint", + typ: TypeString, + index: 102, + path: "endpoint", + } + } + return m.resourceTypeEndpoint +} + +// ResourceTypeSchemaAttribute returns an attribute to describe "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schema". +func (m *metaAttr) ResourceTypeSchemaAttribute() *Attribute { + if m.resourceTypeSchema == nil { + m.resourceTypeSchema = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schema", + name: "schema", + typ: TypeReference, + index: 103, + path: "schema", + } + } + return m.resourceTypeSchema +} + +// ResourceTypeSchemaExtensionsAttributeNoSub returns an attribute to describe +// "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions", but the returned attribute +// does not have any subAttributes defined. +func (m *metaAttr) ResourceTypeSchemaExtensionsAttributeNoSub() *Attribute { + if m.resourceTypeSchemaExtensions == nil { + m.resourceTypeSchemaExtensions = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions", + name: "schemaExtensions", + typ: TypeComplex, + multiValued: true, + index: 104, + path: "schemaExtensions", + } + } + return m.resourceTypeSchemaExtensions +} + +// ResourceTypeSchemaExtensionSchemaAttribute returns an attribute to describe +// "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions.schema". +func (m *metaAttr) ResourceTypeSchemaExtensionSchemaAttribute() *Attribute { + if m.resourceTypeSchemaExtensionSchema == nil { + m.resourceTypeSchemaExtensionSchema = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions.schema", + name: "schema", + typ: TypeReference, + index: 0, + path: "schemaExtensions.schema", + } + } + return m.resourceTypeSchemaExtensionSchema +} + +// ResourceTypeSchemaExtensionRequiredAttribute returns an attribute to describe +// "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions.required". +func (m *metaAttr) ResourceTypeSchemaExtensionRequiredAttribute() *Attribute { + if m.resourceTypeSchemaExtensionRequired == nil { + m.resourceTypeSchemaExtensionRequired = &Attribute{ + id: "urn:ietf:params:scim:schemas:core:2.0:ResourceType:schemaExtensions.required", + name: "required", + typ: TypeBoolean, + index: 1, + path: "schemaExtensions.required", + } + } + return m.resourceTypeSchemaExtensionRequired +} diff --git a/pkg/v2/spec/resource_type.go b/pkg/v2/spec/resource_type.go index 989d7da4..86b58b72 100644 --- a/pkg/v2/spec/resource_type.go +++ b/pkg/v2/spec/resource_type.go @@ -71,10 +71,29 @@ func (t *ResourceType) Schema() *Schema { } // ForEachExtension iterates through all schema extensions and invoke the callback. -func (t *ResourceType) ForEachExtension(callback func(extension *Schema, required bool)) { +func (t *ResourceType) ForEachExtension(callback func(extension *Schema, required bool) error) error { for _, ext := range t.extensions { - callback(ext, t.required[ext.id]) + if err := callback(ext, t.required[ext.id]); err != nil { + return err + } } + return nil +} + +// CountExtensions returns the total number of extensions +func (t *ResourceType) CountExtensions() int { + return len(t.extensions) +} + +// ResourceTypeName returns the resource type of the ResourceType resource. This value is formally defined and hence fixed. +func (t *ResourceType) ResourceTypeName() string { + return "ResourceType" +} + +// ResourceLocation returns the relative URI at which this ResourceType resource can be accessed. This value is formally +// defined in the specification and hence fixed. +func (t *ResourceType) ResourceLocation() string { + return "/ResourceTypes/" + t.ID() } func (t *ResourceType) MarshalJSON() ([]byte, error) { @@ -143,7 +162,7 @@ func (t *ResourceType) SuperAttribute(includeCore bool) *Attribute { super.subAttributes = append(super.subAttributes, t.schema.attributes...) var i = len(super.subAttributes) - t.ForEachExtension(func(extension *Schema, required bool) { + _ = t.ForEachExtension(func(extension *Schema, required bool) error { super.subAttributes = append(super.subAttributes, &Attribute{ id: extension.id, name: extension.id, @@ -162,6 +181,7 @@ func (t *ResourceType) SuperAttribute(includeCore bool) *Attribute { }, }) i++ + return nil }) return &super From 8ed9b4ea7c5b33d9a465cb350fd14ae637aea7aa Mon Sep 17 00:00:00 2001 From: Weinan Qiu Date: Fri, 31 Jan 2020 21:54:36 -0500 Subject: [PATCH 3/4] add handlers for resource types and schemas --- cmd/api/cmd.go | 6 ++ cmd/api/handler.go | 110 +++++++++++++++++++++++++++++++++++ go.mod | 4 ++ go.sum | 9 +++ pkg/v2/service/query.go | 10 +++- pkg/v2/service/query_test.go | 6 +- pkg/v2/spec/schema.go | 7 ++- 7 files changed, 144 insertions(+), 8 deletions(-) diff --git a/cmd/api/cmd.go b/cmd/api/cmd.go index c482eca2..2149d502 100644 --- a/cmd/api/cmd.go +++ b/cmd/api/cmd.go @@ -18,9 +18,15 @@ func Command() *cli.Command { app := args.Initialize() defer app.Close() + app.ensureSchemaRegistered() + var router = httprouter.New() { router.GET("/ServiceProviderConfig", ServiceProviderConfigHandler(app.ServiceProviderConfig())) + router.GET("/Schemas", SchemasHandler()) + router.GET("/Schemas/:id", SchemaByIdHandler()) + router.GET("/ResourceTypes", ResourceTypesHandler(app.UserResourceType(), app.GroupResourceType())) + router.GET("/ResourceTypes/:id", ResourceTypeByIdHandler(app.userResourceType, app.GroupResourceType())) router.GET("/Users/:id", GetHandler(app.UserGetService(), app.Logger())) router.GET("/Users", SearchHandler(app.UserQueryService(), app.Logger())) diff --git a/cmd/api/handler.go b/cmd/api/handler.go index ab950777..e97f64f7 100644 --- a/cmd/api/handler.go +++ b/cmd/api/handler.go @@ -14,6 +14,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" "net/http" + "net/http/httptest" ) // CreateHandler returns a route handler function for creating SCIM resources. @@ -247,6 +248,115 @@ func ServiceProviderConfigHandler(config *spec.ServiceProviderConfig) func(rw ht } } +// ResourceTypesHandler returns a route handler function for getting all defined ResourceType. +func ResourceTypesHandler(resourceTypes ...*spec.ResourceType) func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + result := &service.QueryResponse{ + TotalResults: len(resourceTypes), + StartIndex: 1, + ItemsPerPage: len(resourceTypes), + Resources: []json.Serializable{}, + } + for _, resourceType := range resourceTypes { + result.Resources = append(result.Resources, json.ResourceTypeToSerializable(resourceType)) + } + + // use recorder to cache render result + recorder := httptest.NewRecorder() + if err := handlerutil.WriteSearchResultToResponse(recorder, result); err != nil { + panic(err) + } + + return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + rw.WriteHeader(200) + rw.Header().Set("Content-Type", recorder.Header().Get("Content-Type")) + _, _ = rw.Write(recorder.Body.Bytes()) + } +} + +// ResourceTypeByIdHandler returns a route handler function get ResourceType by its id. +func ResourceTypeByIdHandler(resourceTypes ...*spec.ResourceType) func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + cache := map[string]gojson.RawMessage{} + for _, resourceType := range resourceTypes { + raw, err := json.Serialize(json.ResourceTypeToSerializable(resourceType)) + if err != nil { + panic(err) + } + cache[resourceType.ID()] = raw + } + + return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + raw, ok := cache[params.ByName("id")] + if !ok { + _ = handlerutil.WriteError(rw, fmt.Errorf("%w: resource type is not found", spec.ErrNotFound)) + return + } + + rw.WriteHeader(200) + rw.Header().Set("Content-Type", "application/json+scim") + _, _ = rw.Write(raw) + } +} + +// SchemasHandler returns a route handler function for getting all defined Schema. +func SchemasHandler() func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + result := &service.QueryResponse{StartIndex: 1, Resources: []json.Serializable{}} + if err := spec.Schemas().ForEachSchema(func(schema *spec.Schema) error { + if schema.ID() == spec.CoreSchemaId { + return nil + } + result.Resources = append(result.Resources, json.SchemaToSerializable(schema)) + return nil + }); err != nil { + panic(err) + } + result.TotalResults = len(result.Resources) + result.ItemsPerPage = len(result.Resources) + + // use recorder to cache render result + recorder := httptest.NewRecorder() + if err := handlerutil.WriteSearchResultToResponse(recorder, result); err != nil { + panic(err) + } + + return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + rw.WriteHeader(200) + rw.Header().Set("Content-Type", recorder.Header().Get("Content-Type")) + _, _ = rw.Write(recorder.Body.Bytes()) + } +} + +// SchemaByIdHandler returns a route handler function get Schema by its id. +func SchemaByIdHandler() func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + cache := map[string]gojson.RawMessage{} + if err := spec.Schemas().ForEachSchema(func(schema *spec.Schema) error { + if schema.ID() == spec.CoreSchemaId { + return nil + } + + raw, err := json.Serialize(json.SchemaToSerializable(schema)) + if err != nil { + return err + } + cache[schema.ID()] = raw + + return nil + }); err != nil { + panic(err) + } + + return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { + raw, ok := cache[params.ByName("id")] + if !ok { + _ = handlerutil.WriteError(rw, fmt.Errorf("%w: schema is not found", spec.ErrNotFound)) + return + } + + rw.WriteHeader(200) + rw.Header().Set("Content-Type", "application/json+scim") + _, _ = rw.Write(raw) + } +} + // HealthHandler returns a http handler to report service health status. func HealthHandler(mongoClient *mongo.Client, rabbitConn *amqp.Connection) func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { diff --git a/go.mod b/go.mod index 94472ca9..453aeff7 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,18 @@ require ( github.com/imulab/go-scim/mongo/v2 v2.0.0 github.com/imulab/go-scim/pkg/v2 v2.0.0 github.com/julienschmidt/httprouter v1.3.0 + github.com/opencontainers/runc v1.0.0-rc9 // indirect github.com/ory/dockertest v3.3.5+incompatible github.com/rs/zerolog v1.17.2 github.com/satori/go.uuid v1.2.0 github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 + github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.4.0 github.com/urfave/cli/v2 v2.1.1 go.mongodb.org/mongo-driver v1.2.1 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e + golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 // indirect + gopkg.in/yaml.v2 v2.2.7 // indirect ) replace github.com/imulab/go-scim/mongo/v2 => ./mongo/v2 diff --git a/go.sum b/go.sum index 4d9743cb..70a56eba 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVo github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v1.0.0-rc5 h1:rYjdzMDXVly2Av0RLs3nf/iVkaWh2UrDhuTdTT2KggQ= github.com/opencontainers/runc v1.0.0-rc5/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU4LguQVtc= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -69,7 +71,10 @@ github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -103,6 +108,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50= +golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -116,3 +123,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/v2/service/query.go b/pkg/v2/service/query.go index 1878d657..e3f6a936 100644 --- a/pkg/v2/service/query.go +++ b/pkg/v2/service/query.go @@ -6,7 +6,7 @@ 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/db" - "github.com/imulab/go-scim/pkg/v2/prop" + "github.com/imulab/go-scim/pkg/v2/json" "github.com/imulab/go-scim/pkg/v2/spec" ) @@ -36,7 +36,7 @@ type ( TotalResults int StartIndex int ItemsPerPage int - Resources []*prop.Resource + Resources []json.Serializable Projection *crud.Projection // included so that caller may render properly } ) @@ -77,9 +77,13 @@ func (s *queryService) Do(ctx context.Context, req *QueryRequest) (resp *QueryRe } } - if resp.Resources, err = s.database.Query(ctx, req.Filter, req.Sort, req.Pagination, req.Projection); err != nil { + resources, err := s.database.Query(ctx, req.Filter, req.Sort, req.Pagination, req.Projection) + if err != nil { return } + for _, r := range resources { + resp.Resources = append(resp.Resources, r) + } resp.ItemsPerPage = len(resp.Resources) return diff --git a/pkg/v2/service/query_test.go b/pkg/v2/service/query_test.go index 591d68da..7b83d20e 100644 --- a/pkg/v2/service/query_test.go +++ b/pkg/v2/service/query_test.go @@ -87,7 +87,7 @@ func (s *QueryServiceTestSuite) TestDo() { assert.Nil(t, err) assert.Equal(t, 1, resp.TotalResults) assert.Len(t, resp.Resources, 1) - assert.Equal(t, "user003", resp.Resources[0].Navigator().Dot("id").Current().Raw()) + assert.Equal(t, "user003", resp.Resources[0].(*prop.Resource).Navigator().Dot("id").Current().Raw()) }, }, { @@ -119,7 +119,7 @@ func (s *QueryServiceTestSuite) TestDo() { assert.Equal(t, 5, resp.TotalResults) assert.Len(t, resp.Resources, 5) for i, expected := range []string{"user005", "user004", "user003", "user002", "user001"} { - assert.Equal(t, expected, resp.Resources[i].Navigator().Dot("id").Current().Raw()) + assert.Equal(t, expected, resp.Resources[i].(*prop.Resource).Navigator().Dot("id").Current().Raw()) } }, }, @@ -156,7 +156,7 @@ func (s *QueryServiceTestSuite) TestDo() { assert.Equal(t, 5, resp.TotalResults) assert.Len(t, resp.Resources, 2) for i, expected := range []string{"user002", "user003"} { - assert.Equal(t, expected, resp.Resources[i].Navigator().Dot("id").Current().Raw()) + assert.Equal(t, expected, resp.Resources[i].(*prop.Resource).Navigator().Dot("id").Current().Raw()) } }, }, diff --git a/pkg/v2/spec/schema.go b/pkg/v2/spec/schema.go index c1a1dee6..e49a24a0 100644 --- a/pkg/v2/spec/schema.go +++ b/pkg/v2/spec/schema.go @@ -112,10 +112,13 @@ func (r *schemaRegistry) Get(schemaId string) (schema *Schema, ok bool) { } // ForEachSchema invokes the callback function on each registered schema. -func (r *schemaRegistry) ForEachSchema(callback func(schema *Schema)) { +func (r *schemaRegistry) ForEachSchema(callback func(schema *Schema) error) error { for _, schema := range r.db { - callback(schema) + if err := callback(schema); err != nil { + return err + } } + return nil } func (r *schemaRegistry) mustGet(schemaId string) *Schema { From 962b42d0ef94fc9a31cd92d0255740e14d8fd7c7 Mon Sep 17 00:00:00 2001 From: Weinan Qiu Date: Fri, 31 Jan 2020 22:02:06 -0500 Subject: [PATCH 4/4] fix bug where header was wrote after status code --- cmd/api/handler.go | 5 ----- pkg/v2/handlerutil/response.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cmd/api/handler.go b/cmd/api/handler.go index e97f64f7..6b3da69d 100644 --- a/cmd/api/handler.go +++ b/cmd/api/handler.go @@ -242,7 +242,6 @@ func ServiceProviderConfigHandler(config *spec.ServiceProviderConfig) func(rw ht } return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { - rw.WriteHeader(200) rw.Header().Set("Content-Type", "application/json+scim") _, _ = rw.Write(raw) } @@ -267,7 +266,6 @@ func ResourceTypesHandler(resourceTypes ...*spec.ResourceType) func(rw http.Resp } return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { - rw.WriteHeader(200) rw.Header().Set("Content-Type", recorder.Header().Get("Content-Type")) _, _ = rw.Write(recorder.Body.Bytes()) } @@ -291,7 +289,6 @@ func ResourceTypeByIdHandler(resourceTypes ...*spec.ResourceType) func(rw http.R return } - rw.WriteHeader(200) rw.Header().Set("Content-Type", "application/json+scim") _, _ = rw.Write(raw) } @@ -319,7 +316,6 @@ func SchemasHandler() func(rw http.ResponseWriter, r *http.Request, params httpr } return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) { - rw.WriteHeader(200) rw.Header().Set("Content-Type", recorder.Header().Get("Content-Type")) _, _ = rw.Write(recorder.Body.Bytes()) } @@ -351,7 +347,6 @@ func SchemaByIdHandler() func(rw http.ResponseWriter, r *http.Request, params ht return } - rw.WriteHeader(200) rw.Header().Set("Content-Type", "application/json+scim") _, _ = rw.Write(raw) } diff --git a/pkg/v2/handlerutil/response.go b/pkg/v2/handlerutil/response.go index 024efe49..6711ffbb 100644 --- a/pkg/v2/handlerutil/response.go +++ b/pkg/v2/handlerutil/response.go @@ -83,8 +83,8 @@ func WriteError(rw http.ResponseWriter, err error) error { errMsg.ScimType = spec.ErrInternal.Type } - rw.WriteHeader(errMsg.Status) rw.Header().Set("Content-Type", "application/json+scim") + rw.WriteHeader(errMsg.Status) raw, jsonErr := json.Marshal(errMsg) if jsonErr != nil {