From 4aa16019ae37f3039280791e0c57baa75869f5d2 Mon Sep 17 00:00:00 2001 From: Patrick Jakubowski Date: Tue, 10 Feb 2026 15:36:19 -0800 Subject: [PATCH] types: add stronger types and helpers for inspecting Paths and Path derivatives Now, instead of passing types.Paths around all over the place in the schema code, we can know that we've got a Namespace or a commonType or a EntityType and the code becomes easier to understand (IMO). The new methods on Path and EntityType may also help cedar-go users write more understandable code. Signed-Off-By: Patrick Jakubowski --- types/entity_uid.go | 36 ++++++ types/entity_uid_test.go | 40 ++++++ x/exp/schema/ast/ast.go | 2 +- x/exp/schema/ast/ast_test.go | 24 ++++ x/exp/schema/ast/types.go | 30 +++++ x/exp/schema/internal/json/json.go | 8 +- .../internal/json/json_internal_test.go | 12 +- x/exp/schema/internal/parser/parser.go | 7 +- x/exp/schema/resolved/resolve.go | 114 +++++++++--------- .../schema/resolved/resolve_internal_test.go | 18 +-- x/exp/schema/resolved/resolve_test.go | 8 +- x/exp/schema/schema_test.go | 2 +- 12 files changed, 213 insertions(+), 88 deletions(-) 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{