diff --git a/types/entity_uid.go b/types/entity_uid.go index d7ec5d3a..c9501009 100644 --- a/types/entity_uid.go +++ b/types/entity_uid.go @@ -14,9 +14,45 @@ import ( // Path is a series of idents separated by :: type Path string +// IsQualified returns whether a Path has any qualifiers (i.e. at least one ::) +func (p Path) IsQualified() bool { + return strings.Contains(string(p), "::") +} + +// Qualifier returns a Path with everything but the last element in the original Path or "" if there is only one element. +func (p Path) Qualifier() Path { + idx := strings.LastIndex(string(p), "::") + if idx == -1 { + return "" + } + return p[:idx] +} + +// Basename returns the last element in the Path +func (p Path) Basename() string { + idx := strings.LastIndex(string(p), "::") + if idx == -1 { + return string(p) + } + return string(p[idx+2:]) +} + +// Namespace is a type of Path whose basename does not refer to a type +type Namespace Path + // EntityType is the type portion of an EntityUID type EntityType Path +// Namespace returns the namespace for the EntityType or "" if the type has no namespace. +func (e EntityType) Namespace() Namespace { + return Namespace(Path(e).Qualifier()) +} + +// Basename returns the unqualified entity type name. +func (e EntityType) Basename() string { + return Path(e).Basename() +} + // An EntityUID is the identifier for a principal, action, or resource. type EntityUID struct { Type EntityType diff --git a/types/entity_uid_test.go b/types/entity_uid_test.go index b52d9400..ad4d2e9f 100644 --- a/types/entity_uid_test.go +++ b/types/entity_uid_test.go @@ -113,6 +113,46 @@ func TestEntity(t *testing.T) { }) } +func TestPathQualification(t *testing.T) { + t.Parallel() + + tests := []struct { + path types.Path + qualified bool + qualifier types.Path + basename string + }{ + {"NS::User", true, "NS", "User"}, + {"A::B::C", true, "A::B", "C"}, + {"User", false, "", "User"}, + {"", false, "", ""}, + } + for _, tt := range tests { + testutil.Equals(t, tt.path.IsQualified(), tt.qualified) + testutil.Equals(t, tt.path.Qualifier(), tt.qualifier) + testutil.Equals(t, tt.path.Basename(), tt.basename) + } +} + +func TestEntityTypeQualification(t *testing.T) { + t.Parallel() + + tests := []struct { + typ types.EntityType + qualified bool + namespace types.Namespace + basename string + }{ + {"NS::User", true, "NS", "User"}, + {"A::B::C", true, "A::B", "C"}, + {"User", false, "", "User"}, + } + for _, tt := range tests { + testutil.Equals(t, tt.typ.Namespace(), tt.namespace) + testutil.Equals(t, tt.typ.Basename(), tt.basename) + } +} + func TestEntityUIDSet(t *testing.T) { t.Parallel() diff --git a/x/exp/schema/ast/ast.go b/x/exp/schema/ast/ast.go index fb3303b4..6048c92f 100644 --- a/x/exp/schema/ast/ast.go +++ b/x/exp/schema/ast/ast.go @@ -21,7 +21,7 @@ type Actions map[types.String]Action type CommonTypes map[types.Ident]CommonType // Namespaces maps namespace paths to their definitions. -type Namespaces map[types.Path]Namespace +type Namespaces map[types.Namespace]Namespace // Schema is the top-level Cedar schema AST. // The Entities, Enums, Actions, and CommonTypes are for the top-level namespace. diff --git a/x/exp/schema/ast/ast_test.go b/x/exp/schema/ast/ast_test.go index b6ce5eb5..a32152ef 100644 --- a/x/exp/schema/ast/ast_test.go +++ b/x/exp/schema/ast/ast_test.go @@ -21,6 +21,30 @@ func TestConstructors(t *testing.T) { testutil.Equals(t, ast.Type("MyType"), ast.TypeRef("MyType")) } +func TestEntityTypeRefQualification(t *testing.T) { + qualified := ast.EntityTypeRef("NS::User") + testutil.Equals(t, qualified.IsQualified(), true) + testutil.Equals(t, qualified.Namespace(), types.Namespace("NS")) + testutil.Equals(t, qualified.Basename(), "User") + + unqualified := ast.EntityTypeRef("User") + testutil.Equals(t, unqualified.IsQualified(), false) + testutil.Equals(t, unqualified.Namespace(), types.Namespace("")) + testutil.Equals(t, unqualified.Basename(), "User") +} + +func TestTypeRefQualification(t *testing.T) { + qualified := ast.TypeRef("NS::MyType") + testutil.Equals(t, qualified.IsQualified(), true) + testutil.Equals(t, qualified.Namespace(), types.Namespace("NS")) + testutil.Equals(t, qualified.Basename(), "MyType") + + unqualified := ast.TypeRef("MyType") + testutil.Equals(t, unqualified.IsQualified(), false) + testutil.Equals(t, unqualified.Namespace(), types.Namespace("")) + testutil.Equals(t, unqualified.Basename(), "MyType") +} + func TestParentRefFromID(t *testing.T) { ref := ast.ParentRefFromID("view") testutil.Equals(t, ref.ID, types.String("view")) diff --git a/x/exp/schema/ast/types.go b/x/exp/schema/ast/types.go index 0d0a96d1..14b40708 100644 --- a/x/exp/schema/ast/types.go +++ b/x/exp/schema/ast/types.go @@ -81,6 +81,21 @@ type EntityTypeRef types.EntityType func (EntityTypeRef) isType() { _ = 0 } +// IsQualified reports whether the entity type reference contains a namespace qualifier. +func (e EntityTypeRef) IsQualified() bool { + return types.EntityType(e).Namespace() != "" +} + +// Namespace returns the namespace portion of a qualified entity type reference, or "" if unqualified. +func (e EntityTypeRef) Namespace() types.Namespace { + return types.Namespace(types.Path(e).Qualifier()) +} + +// Basename returns the unqualified entity type name. +func (e EntityTypeRef) Basename() string { + return types.EntityType(e).Basename() +} + // EntityType returns an EntityTypeRef for the given entity type name. func EntityType(name types.EntityType) EntityTypeRef { return EntityTypeRef(name) @@ -91,6 +106,21 @@ type TypeRef types.Path func (TypeRef) isType() { _ = 0 } +// IsQualified reports whether the type reference contains a namespace qualifier. +func (t TypeRef) IsQualified() bool { + return types.Path(t).IsQualified() +} + +// Namespace returns the namespace portion of a qualified type reference, or "" if unqualified. +func (t TypeRef) Namespace() types.Namespace { + return types.Namespace(types.Path(t).Qualifier()) +} + +// Basename returns the unqualified type name. +func (t TypeRef) Basename() string { + return types.Path(t).Basename() +} + // Type returns a TypeRef for the given path. func Type(name types.Path) TypeRef { return TypeRef(name) diff --git a/x/exp/schema/internal/json/json.go b/x/exp/schema/internal/json/json.go index 4c1740dd..a6e64ec5 100644 --- a/x/exp/schema/internal/json/json.go +++ b/x/exp/schema/internal/json/json.go @@ -19,7 +19,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) { // Bare declarations go under the empty string key. if hasBareDecls((*ast.Schema)(s)) { - ns, err := marshalNamespace("", ast.Namespace{ + ns, err := marshalNamespace(ast.Namespace{ Entities: s.Entities, Enums: s.Enums, Actions: s.Actions, @@ -32,7 +32,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) { } for name, ns := range s.Namespaces { - jns, err := marshalNamespace(name, ns) + jns, err := marshalNamespace(ns) if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error { if result.Namespaces == nil { result.Namespaces = ast.Namespaces{} } - result.Namespaces[types.Path(name)] = ns + result.Namespaces[types.Namespace(name)] = ns } } *s = Schema(result) @@ -131,7 +131,7 @@ type jsonAttr struct { Annotations map[string]string `json:"annotations,omitempty"` } -func marshalNamespace(name types.Path, ns ast.Namespace) (jsonNamespace, error) { +func marshalNamespace(ns ast.Namespace) (jsonNamespace, error) { jns := jsonNamespace{ EntityTypes: make(map[string]jsonEntityType), Actions: make(map[string]jsonAction), diff --git a/x/exp/schema/internal/json/json_internal_test.go b/x/exp/schema/internal/json/json_internal_test.go index 6fc6edcb..95796195 100644 --- a/x/exp/schema/internal/json/json_internal_test.go +++ b/x/exp/schema/internal/json/json_internal_test.go @@ -26,7 +26,7 @@ func TestMarshalRecordTypeError(t *testing.T) { } func TestMarshalNamespaceCommonTypeError(t *testing.T) { - _, err := marshalNamespace("", ast.Namespace{ + _, err := marshalNamespace(ast.Namespace{ CommonTypes: ast.CommonTypes{ "Bad": ast.CommonType{Type: nil}, }, @@ -35,7 +35,7 @@ func TestMarshalNamespaceCommonTypeError(t *testing.T) { } func TestMarshalNamespaceEntityShapeError(t *testing.T) { - _, err := marshalNamespace("", ast.Namespace{ + _, err := marshalNamespace(ast.Namespace{ Entities: ast.Entities{ "Foo": ast.Entity{ Shape: ast.RecordType{ @@ -50,7 +50,7 @@ func TestMarshalNamespaceEntityShapeError(t *testing.T) { func TestMarshalNamespaceEntityTagsError(t *testing.T) { // Tags is nil, but the code checks `entity.Tags != nil` first // So we need a non-nil tags that fails. Use SetType{Element: nil}. - _, err := marshalNamespace("", ast.Namespace{ + _, err := marshalNamespace(ast.Namespace{ Entities: ast.Entities{ "Foo": ast.Entity{Tags: nil}, }, @@ -59,7 +59,7 @@ func TestMarshalNamespaceEntityTagsError(t *testing.T) { } func TestMarshalNamespaceEntityTagsError2(t *testing.T) { - _, err := marshalNamespace("", ast.Namespace{ + _, err := marshalNamespace(ast.Namespace{ Entities: ast.Entities{ "Foo": ast.Entity{Tags: ast.SetType{Element: nil}}, }, @@ -68,7 +68,7 @@ func TestMarshalNamespaceEntityTagsError2(t *testing.T) { } func TestMarshalNamespaceActionAnnotations(t *testing.T) { - ns, err := marshalNamespace("", ast.Namespace{ + ns, err := marshalNamespace(ast.Namespace{ Actions: ast.Actions{ "view": ast.Action{ Annotations: ast.Annotations{"doc": "test"}, @@ -80,7 +80,7 @@ func TestMarshalNamespaceActionAnnotations(t *testing.T) { } func TestMarshalNamespaceContextError(t *testing.T) { - _, err := marshalNamespace("", ast.Namespace{ + _, err := marshalNamespace(ast.Namespace{ Actions: ast.Actions{ "view": ast.Action{ AppliesTo: &ast.AppliesTo{ diff --git a/x/exp/schema/internal/parser/parser.go b/x/exp/schema/internal/parser/parser.go index 87588afc..d6bd5710 100644 --- a/x/exp/schema/internal/parser/parser.go +++ b/x/exp/schema/internal/parser/parser.go @@ -141,7 +141,7 @@ func (p *parser) parseSchema() (*ast.Schema, error) { } type parsedNamespace struct { - name types.Path + name types.Namespace ns ast.Namespace } @@ -150,7 +150,8 @@ func (p *parser) parseNamespace(annotations ast.Annotations) (parsedNamespace, e if err != nil { return parsedNamespace{}, err } - if slices.Contains(strings.Split(string(path), "::"), "__cedar") { + nsName := types.Namespace(path) + if slices.Contains(strings.Split(string(nsName), "::"), "__cedar") { return parsedNamespace{}, fmt.Errorf("%s: the name %q contains \"__cedar\", which is reserved", p.tok.Pos, path) } if err := p.expect(tokenLBrace); err != nil { @@ -177,7 +178,7 @@ func (p *parser) parseNamespace(annotations ast.Annotations) (parsedNamespace, e ns.Enums = innerSchema.Enums ns.Actions = innerSchema.Actions ns.CommonTypes = innerSchema.CommonTypes - return parsedNamespace{name: path, ns: ns}, nil + return parsedNamespace{name: nsName, ns: ns}, nil } func (p *parser) parseDecl(annotations ast.Annotations, schema *ast.Schema) error { diff --git a/x/exp/schema/resolved/resolve.go b/x/exp/schema/resolved/resolve.go index 82e9af2f..a7880672 100644 --- a/x/exp/schema/resolved/resolve.go +++ b/x/exp/schema/resolved/resolve.go @@ -12,7 +12,7 @@ import ( // Schema is a Cedar schema with resolved types and indexed declarations. type Schema struct { - Namespaces map[types.Path]Namespace + Namespaces map[types.Namespace]Namespace Entities map[types.EntityType]Entity Enums map[types.EntityType]Enum Actions map[types.EntityUID]Action @@ -20,7 +20,7 @@ type Schema struct { // Namespace represents a resolved namespace. type Namespace struct { - Name types.Path + Name types.Namespace Annotations Annotations } @@ -55,12 +55,19 @@ type Action struct { AppliesTo *AppliesTo } +type commonType types.Path + +// Namespace returns the namespace for the commonType or "" if the type is un-namespaced +func (c commonType) Namespace() types.Namespace { + return types.Namespace(types.Path(c).Qualifier()) +} + // Resolve transforms an AST schema into a fully resolved schema. func Resolve(s *ast.Schema) (*Schema, error) { r := &resolverState{ entityTypes: make(map[types.EntityType]bool), enumTypes: make(map[types.EntityType]bool), - commonTypes: make(map[types.Path]ast.IsType), + commonTypes: make(map[commonType]ast.IsType), } // Phase 1: Register all declarations @@ -85,7 +92,7 @@ func Resolve(s *ast.Schema) (*Schema, error) { // Phase 4: Resolve everything result := &Schema{ - Namespaces: make(map[types.Path]Namespace), + Namespaces: make(map[types.Namespace]Namespace), Entities: make(map[types.EntityType]Entity), Enums: make(map[types.EntityType]Enum), Actions: make(map[types.EntityUID]Action), @@ -126,10 +133,10 @@ func Resolve(s *ast.Schema) (*Schema, error) { type resolverState struct { entityTypes map[types.EntityType]bool enumTypes map[types.EntityType]bool - commonTypes map[types.Path]ast.IsType + commonTypes map[commonType]ast.IsType } -func (r *resolverState) registerDecls(nsName types.Path, entities ast.Entities, enums ast.Enums, commonTypes ast.CommonTypes) error { +func (r *resolverState) registerDecls(nsName types.Namespace, entities ast.Entities, enums ast.Enums, commonTypes ast.CommonTypes) error { for name := range entities { if _, ok := enums[name]; ok { return fmt.Errorf("%q is declared twice", qualifyEntityType(nsName, name)) @@ -140,7 +147,7 @@ func (r *resolverState) registerDecls(nsName types.Path, entities ast.Entities, r.enumTypes[qualifyEntityType(nsName, name)] = true } for name, ct := range commonTypes { - r.commonTypes[qualifyPath(nsName, name)] = ct.Type + r.commonTypes[qualifyCommonType(nsName, name)] = ct.Type } return nil } @@ -200,12 +207,11 @@ func checkShadowing(s *ast.Schema) error { func (r *resolverState) detectCommonTypeCycles() error { // Build dependency graph - deps := make(map[types.Path][]types.Path) + deps := make(map[commonType][]commonType) for name, typ := range r.commonTypes { - ns := extractNamespace(name) refs := collectTypeRefs(typ) for _, ref := range refs { - resolved := r.resolveTypeRefPath(ns, ref) + resolved := r.resolveCommonTypeRefPath(name.Namespace(), ref) if _, ok := r.commonTypes[resolved]; ok { deps[name] = append(deps[name], resolved) } @@ -213,7 +219,7 @@ func (r *resolverState) detectCommonTypeCycles() error { } // Kahn's algorithm for topological sort / cycle detection - inDegree := make(map[types.Path]int) + inDegree := make(map[commonType]int) for name := range r.commonTypes { inDegree[name] = 0 } @@ -223,7 +229,7 @@ func (r *resolverState) detectCommonTypeCycles() error { } } - var queue []types.Path + var queue []commonType for name, degree := range inDegree { if degree == 0 { queue = append(queue, name) @@ -255,7 +261,7 @@ func (r *resolverState) detectCommonTypeCycles() error { return nil } -func (r *resolverState) resolveEntities(nsName types.Path, entities ast.Entities, result *Schema) error { +func (r *resolverState) resolveEntities(nsName types.Namespace, entities ast.Entities, result *Schema) error { for name, entity := range entities { qualName := qualifyEntityType(nsName, name) resolved := Entity{ @@ -288,7 +294,7 @@ func (r *resolverState) resolveEntities(nsName types.Path, entities ast.Entities return nil } -func (r *resolverState) resolveEnums(nsName types.Path, enums ast.Enums, result *Schema) { +func (r *resolverState) resolveEnums(nsName types.Namespace, enums ast.Enums, result *Schema) { for name, enum := range enums { qualName := qualifyEntityType(nsName, name) values := make([]types.EntityUID, len(enum.Values)) @@ -303,7 +309,7 @@ func (r *resolverState) resolveEnums(nsName types.Path, enums ast.Enums, result } } -func (r *resolverState) resolveActions(nsName types.Path, actions ast.Actions, result *Schema) error { +func (r *resolverState) resolveActions(nsName types.Namespace, actions ast.Actions, result *Schema) error { for name, action := range actions { actionTypeName := qualifyActionType(nsName) uid := types.NewEntityUID(actionTypeName, types.String(name)) @@ -350,7 +356,7 @@ func (r *resolverState) resolveActions(nsName types.Path, actions ast.Actions, r return nil } -func (r *resolverState) resolveType(ns types.Path, t ast.IsType) (IsType, error) { +func (r *resolverState) resolveType(ns types.Namespace, t ast.IsType) (IsType, error) { switch t := t.(type) { case ast.StringType: return StringType{}, nil @@ -381,7 +387,7 @@ func (r *resolverState) resolveType(ns types.Path, t ast.IsType) (IsType, error) } } -func (r *resolverState) resolveRecordType(ns types.Path, rec ast.RecordType) (RecordType, error) { +func (r *resolverState) resolveRecordType(ns types.Namespace, rec ast.RecordType) (RecordType, error) { result := make(RecordType, len(rec)) for name, attr := range rec { t, err := r.resolveType(ns, attr.Type) @@ -397,28 +403,27 @@ func (r *resolverState) resolveRecordType(ns types.Path, rec ast.RecordType) (Re return result, nil } -func (r *resolverState) resolveEntityTypeRef(ns types.Path, ref ast.EntityTypeRef) (types.EntityType, error) { - path := types.Path(ref) +func (r *resolverState) resolveEntityTypeRef(ns types.Namespace, ref ast.EntityTypeRef) (types.EntityType, error) { // If it's already a qualified path (contains ::), resolve directly - if strings.Contains(string(path), "::") { - et := types.EntityType(path) + if ref.IsQualified() { + et := types.EntityType(ref) if r.entityTypes[et] || r.enumTypes[et] { return et, nil } - return "", fmt.Errorf("undefined entity type %q", path) + return "", fmt.Errorf("undefined entity type %q", ref) } // Unqualified: try NS::Name first, then bare Name if ns != "" { - qualified := types.EntityType(string(ns) + "::" + string(path)) + qualified := types.EntityType(string(ns) + "::" + string(ref)) if r.entityTypes[qualified] || r.enumTypes[qualified] { return qualified, nil } } - bare := types.EntityType(path) + bare := types.EntityType(ref) if r.entityTypes[bare] || r.enumTypes[bare] { return bare, nil } - return "", fmt.Errorf("undefined entity type %q", path) + return "", fmt.Errorf("undefined entity type %q", ref) } // resolveTypeRef resolves a type reference (TypeRef) following the Cedar disambiguation rules: @@ -428,9 +433,9 @@ func (r *resolverState) resolveEntityTypeRef(ns types.Path, ref ast.EntityTypeRe // 4. Check if N (empty namespace) is declared as an entity type // 5. Check if N is a built-in type // 6. Error -func (r *resolverState) resolveTypeRef(ns types.Path, ref ast.TypeRef) (IsType, error) { +func (r *resolverState) resolveTypeRef(ns types.Namespace, ref ast.TypeRef) (IsType, error) { // Qualified: resolve directly - if strings.Contains(string(ref), "::") { + if ref.IsQualified() { return r.resolveQualifiedTypeRef(ref) } @@ -438,7 +443,8 @@ func (r *resolverState) resolveTypeRef(ns types.Path, ref ast.TypeRef) (IsType, if ns != "" { qualifiedPath := types.Path(string(ns) + "::" + string(ref)) // 1. Check NS::N as common type - if ct, ok := r.commonTypes[qualifiedPath]; ok { + qualifiedCT := commonType(qualifiedPath) + if ct, ok := r.commonTypes[qualifiedCT]; ok { return r.resolveType(ns, ct) } // 2. Check NS::N as entity type @@ -449,8 +455,8 @@ func (r *resolverState) resolveTypeRef(ns types.Path, ref ast.TypeRef) (IsType, } // 3. Check N as common type in empty namespace - path := types.Path(ref) - if ct, ok := r.commonTypes[path]; ok { + ct := commonType(ref) + if ct, ok := r.commonTypes[ct]; ok { return r.resolveType("", ct) } @@ -461,7 +467,7 @@ func (r *resolverState) resolveTypeRef(ns types.Path, ref ast.TypeRef) (IsType, } // 5. Check built-in types - if t := lookupBuiltin(path); t != nil { + if t := lookupBuiltin(ref); t != nil { return t, nil } @@ -472,17 +478,16 @@ func (r *resolverState) resolveQualifiedTypeRef(ref ast.TypeRef) (IsType, error) // Check for __cedar:: prefix first if strings.HasPrefix(string(ref), "__cedar::") { builtinName := ref[len("__cedar::"):] - if t := lookupBuiltin(types.Path(builtinName)); t != nil { + if t := lookupBuiltin(builtinName); t != nil { return t, nil } return nil, fmt.Errorf("undefined built-in type %q", ref) } // Try as common type first - path := types.Path(ref) - if ct, ok := r.commonTypes[path]; ok { - ns := extractNamespace(path) - return r.resolveType(ns, ct) + ctName := commonType(ref) + if ct, ok := r.commonTypes[ctName]; ok { + return r.resolveType(ctName.Namespace(), ct) } // Try as entity type et := types.EntityType(ref) @@ -492,20 +497,20 @@ func (r *resolverState) resolveQualifiedTypeRef(ref ast.TypeRef) (IsType, error) return nil, fmt.Errorf("undefined type %q", ref) } -func (r *resolverState) resolveTypeRefPath(ns types.Path, ref ast.TypeRef) types.Path { - if strings.Contains(string(ref), "::") { - return types.Path(ref) +func (r *resolverState) resolveCommonTypeRefPath(ns types.Namespace, ref ast.TypeRef) commonType { + if ref.IsQualified() { + return commonType(ref) } if ns != "" { - qualifiedPath := types.Path(string(ns) + "::" + string(ref)) + qualifiedPath := commonType(string(ns) + "::" + string(ref)) if _, ok := r.commonTypes[qualifiedPath]; ok { return qualifiedPath } } - return types.Path(ref) + return commonType(ref) } -func resolveActionParentRef(ns types.Path, ref ast.ParentRef) types.EntityUID { +func resolveActionParentRef(ns types.Namespace, ref ast.ParentRef) types.EntityUID { if types.EntityType(ref.Type) == "" { // Bare reference: action in same namespace actionType := qualifyActionType(ns) @@ -560,7 +565,7 @@ func (r *resolverState) validateActionMembership(result *Schema) error { return nil } -func lookupBuiltin(path types.Path) IsType { +func lookupBuiltin(path ast.TypeRef) IsType { switch path { case "String": return StringType{} @@ -600,31 +605,20 @@ func collectTypeRefs(t ast.IsType) []ast.TypeRef { } } -func qualifyEntityType(ns types.Path, name types.Ident) types.EntityType { +func qualifyEntityType(ns types.Namespace, name types.Ident) types.EntityType { if ns != "" { return types.EntityType(string(ns) + "::" + string(name)) } return types.EntityType(name) } -func qualifyPath(ns types.Path, name types.Ident) types.Path { +func qualifyCommonType(ns types.Namespace, name types.Ident) commonType { if ns != "" { - return types.Path(string(ns) + "::" + string(name)) + return commonType(string(ns) + "::" + string(name)) } - return types.Path(name) + return commonType(name) } -func qualifyActionType(ns types.Path) types.EntityType { - if ns != "" { - return types.EntityType(string(ns) + "::Action") - } - return types.EntityType("Action") -} - -func extractNamespace(path types.Path) types.Path { - s := string(path) - if idx := strings.LastIndex(s, "::"); idx >= 0 { - return types.Path(s[:idx]) - } - return "" +func qualifyActionType(ns types.Namespace) types.EntityType { + return qualifyEntityType(ns, "Action") } diff --git a/x/exp/schema/resolved/resolve_internal_test.go b/x/exp/schema/resolved/resolve_internal_test.go index 9ab91bbf..def16a02 100644 --- a/x/exp/schema/resolved/resolve_internal_test.go +++ b/x/exp/schema/resolved/resolve_internal_test.go @@ -13,7 +13,7 @@ func TestResolveTypeDefault(t *testing.T) { r := &resolverState{ entityTypes: make(map[types.EntityType]bool), enumTypes: make(map[types.EntityType]bool), - commonTypes: make(map[types.Path]ast.IsType), + commonTypes: make(map[commonType]ast.IsType), } testutil.Panic(t, func() { _, _ = r.resolveType("", nil) @@ -22,7 +22,7 @@ func TestResolveTypeDefault(t *testing.T) { func TestResolveTypePath(t *testing.T) { r := &resolverState{ - commonTypes: map[types.Path]ast.IsType{ + commonTypes: map[commonType]ast.IsType{ "NS::A": ast.StringType{}, "B": ast.LongType{}, }, @@ -31,16 +31,16 @@ func TestResolveTypePath(t *testing.T) { } // __cedar:: prefix returns path unchanged - p := r.resolveTypeRefPath("NS", "__cedar::String") - testutil.Equals(t, p, types.Path("__cedar::String")) + p := r.resolveCommonTypeRefPath("NS", "__cedar::String") + testutil.Equals(t, p, "__cedar::String") // Already qualified (contains ::) returns path unchanged - p = r.resolveTypeRefPath("NS", "Other::Foo") - testutil.Equals(t, p, types.Path("Other::Foo")) + p = r.resolveCommonTypeRefPath("NS", "Other::Foo") + testutil.Equals(t, p, "Other::Foo") // Unqualified in namespace resolves to NS::A - p = r.resolveTypeRefPath("NS", "A") - testutil.Equals(t, p, types.Path("NS::A")) + p = r.resolveCommonTypeRefPath("NS", "A") + testutil.Equals(t, p, "NS::A") } func TestResolveActionParentRef(t *testing.T) { @@ -69,7 +69,7 @@ func TestCollectTypeRefsDefault(t *testing.T) { func TestDetectCommonTypeCyclesBuiltinRef(t *testing.T) { // Verify cycle detection works correctly with __cedar:: refs. r := &resolverState{ - commonTypes: map[types.Path]ast.IsType{ + commonTypes: map[commonType]ast.IsType{ "NS::A": ast.TypeRef("__cedar::String"), }, entityTypes: make(map[types.EntityType]bool), diff --git a/x/exp/schema/resolved/resolve_test.go b/x/exp/schema/resolved/resolve_test.go index 3cb35e6f..b0556dfd 100644 --- a/x/exp/schema/resolved/resolve_test.go +++ b/x/exp/schema/resolved/resolve_test.go @@ -645,7 +645,7 @@ func TestResolveNamespaceOutput(t *testing.T) { result, err := resolved.Resolve(s) testutil.OK(t, err) ns := result.Namespaces["NS"] - testutil.Equals(t, ns.Name, types.Path("NS")) + testutil.Equals(t, ns.Name, "NS") testutil.Equals(t, types.String(ns.Annotations["doc"]), types.String("test")) } @@ -1097,7 +1097,7 @@ func TestResolveUndefinedParents(t *testing.T) { } func TestResolveCedarBuiltinInTypePath(t *testing.T) { - // Exercise resolveTypeRefPath line 462-464: __cedar:: prefix in cycle detection. + // Exercise resolveCommonTypeRefPath line 462-464: __cedar:: prefix in cycle detection. s := &ast.Schema{ CommonTypes: ast.CommonTypes{ "A": ast.CommonType{Type: ast.TypeRef("__cedar::String")}, @@ -1109,7 +1109,7 @@ func TestResolveCedarBuiltinInTypePath(t *testing.T) { } func TestResolveQualifiedTypePath(t *testing.T) { - // Exercise resolveTypeRefPath line 465-467: qualified path with :: in cycle detection. + // Exercise resolveCommonTypeRefPath line 465-467: qualified path with :: in cycle detection. s := &ast.Schema{ Namespaces: ast.Namespaces{ "NS": ast.Namespace{ @@ -1126,7 +1126,7 @@ func TestResolveQualifiedTypePath(t *testing.T) { } func TestResolveNamespacedCommonTypePath(t *testing.T) { - // Exercise resolveTypeRefPath line 470-472: namespaced common type ref in cycle detection. + // Exercise resolveCommonTypeRefPath line 470-472: namespaced common type ref in cycle detection. s := &ast.Schema{ Namespaces: ast.Namespaces{ "NS": ast.Namespace{ diff --git a/x/exp/schema/schema_test.go b/x/exp/schema/schema_test.go index 7bfb1b5f..90be53cf 100644 --- a/x/exp/schema/schema_test.go +++ b/x/exp/schema/schema_test.go @@ -469,7 +469,7 @@ var wantAST = &ast.Schema{ // wantResolved is the expected resolved schema structure. // All type references have been fully qualified and common types inlined. var wantResolved = &resolved.Schema{ - Namespaces: map[types.Path]resolved.Namespace{ + Namespaces: map[types.Namespace]resolved.Namespace{ "MyApp": { Name: "MyApp", Annotations: resolved.Annotations{