Skip to content

Commit 87af386

Browse files
committed
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 <patrick.jakubowski@strongdm.com>
1 parent 4217868 commit 87af386

File tree

12 files changed

+219
-88
lines changed

12 files changed

+219
-88
lines changed

types/entity_uid.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,50 @@ import (
1414
// Path is a series of idents separated by ::
1515
type Path string
1616

17+
// IsQualified returns whether a Path has any qualifiers (i.e. at least one ::)
18+
func (p Path) IsQualified() bool {
19+
return strings.Contains(string(p), "::")
20+
}
21+
22+
// Qualifier returns a Path with everything but the last element in the original Path or "" if there is only one element.
23+
func (p Path) Qualifier() Path {
24+
idx := strings.LastIndex(string(p), "::")
25+
if idx == -1 {
26+
return ""
27+
}
28+
return p[:idx]
29+
}
30+
31+
// Basename returns the last element in the original Path
32+
func (p Path) Basename() string {
33+
idx := strings.LastIndex(string(p), "::")
34+
if idx == -1 {
35+
return string(p)
36+
}
37+
return string(p[idx+2:])
38+
}
39+
40+
// Namespace is a type of path with no base name referring to an entity type
41+
type Namespace Path
42+
1743
// EntityType is the type portion of an EntityUID
1844
type EntityType Path
1945

46+
// IsQualified reports whether the EntityType contains a namespace qualifier.
47+
func (e EntityType) IsQualified() bool {
48+
return Path(e).IsQualified()
49+
}
50+
51+
// Namespace returns the namespace for the EntityType or "" if the type is unqualified.
52+
func (e EntityType) Namespace() Namespace {
53+
return Namespace(Path(e).Qualifier())
54+
}
55+
56+
// Basename returns the unqualified entity type name
57+
func (e EntityType) Basename() string {
58+
return Path(e).Basename()
59+
}
60+
2061
// An EntityUID is the identifier for a principal, action, or resource.
2162
type EntityUID struct {
2263
Type EntityType

types/entity_uid_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,47 @@ func TestEntity(t *testing.T) {
113113
})
114114
}
115115

116+
func TestPathQualification(t *testing.T) {
117+
t.Parallel()
118+
119+
tests := []struct {
120+
path types.Path
121+
qualified bool
122+
qualifier types.Path
123+
basename string
124+
}{
125+
{"NS::User", true, "NS", "User"},
126+
{"A::B::C", true, "A::B", "C"},
127+
{"User", false, "", "User"},
128+
{"", false, "", ""},
129+
}
130+
for _, tt := range tests {
131+
testutil.Equals(t, tt.path.IsQualified(), tt.qualified)
132+
testutil.Equals(t, tt.path.Qualifier(), tt.qualifier)
133+
testutil.Equals(t, tt.path.Basename(), tt.basename)
134+
}
135+
}
136+
137+
func TestEntityTypeQualification(t *testing.T) {
138+
t.Parallel()
139+
140+
tests := []struct {
141+
typ types.EntityType
142+
qualified bool
143+
namespace types.Namespace
144+
basename string
145+
}{
146+
{"NS::User", true, "NS", "User"},
147+
{"A::B::C", true, "A::B", "C"},
148+
{"User", false, "", "User"},
149+
}
150+
for _, tt := range tests {
151+
testutil.Equals(t, tt.typ.IsQualified(), tt.qualified)
152+
testutil.Equals(t, tt.typ.Namespace(), tt.namespace)
153+
testutil.Equals(t, tt.typ.Basename(), tt.basename)
154+
}
155+
}
156+
116157
func TestEntityUIDSet(t *testing.T) {
117158
t.Parallel()
118159

x/exp/schema/ast/ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Actions map[types.String]Action
2121
type CommonTypes map[types.Ident]CommonType
2222

2323
// Namespaces maps namespace paths to their definitions.
24-
type Namespaces map[types.Path]Namespace
24+
type Namespaces map[types.Namespace]Namespace
2525

2626
// Schema is the top-level Cedar schema AST.
2727
// The Entities, Enums, Actions, and CommonTypes are for the top-level namespace.

x/exp/schema/ast/ast_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ func TestConstructors(t *testing.T) {
2121
testutil.Equals(t, ast.Type("MyType"), ast.TypeRef("MyType"))
2222
}
2323

24+
func TestEntityTypeRefQualification(t *testing.T) {
25+
qualified := ast.EntityTypeRef("NS::User")
26+
testutil.Equals(t, qualified.IsQualified(), true)
27+
testutil.Equals(t, qualified.Namespace(), types.Namespace("NS"))
28+
testutil.Equals(t, qualified.Basename(), "User")
29+
30+
unqualified := ast.EntityTypeRef("User")
31+
testutil.Equals(t, unqualified.IsQualified(), false)
32+
testutil.Equals(t, unqualified.Namespace(), types.Namespace(""))
33+
testutil.Equals(t, unqualified.Basename(), "User")
34+
}
35+
36+
func TestTypeRefQualification(t *testing.T) {
37+
qualified := ast.TypeRef("NS::MyType")
38+
testutil.Equals(t, qualified.IsQualified(), true)
39+
testutil.Equals(t, qualified.Namespace(), types.Namespace("NS"))
40+
testutil.Equals(t, qualified.Basename(), "MyType")
41+
42+
unqualified := ast.TypeRef("MyType")
43+
testutil.Equals(t, unqualified.IsQualified(), false)
44+
testutil.Equals(t, unqualified.Namespace(), types.Namespace(""))
45+
testutil.Equals(t, unqualified.Basename(), "MyType")
46+
}
47+
2448
func TestParentRefFromID(t *testing.T) {
2549
ref := ast.ParentRefFromID("view")
2650
testutil.Equals(t, ref.ID, types.String("view"))

x/exp/schema/ast/types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ type EntityTypeRef types.EntityType
8181

8282
func (EntityTypeRef) isType() { _ = 0 }
8383

84+
// IsQualified reports whether the entity type reference contains a namespace qualifier.
85+
func (e EntityTypeRef) IsQualified() bool {
86+
return types.EntityType(e).IsQualified()
87+
}
88+
89+
// Namespace returns the namespace portion of a qualified entity type reference, or "" if unqualified.
90+
func (e EntityTypeRef) Namespace() types.Namespace {
91+
return types.EntityType(e).Namespace()
92+
}
93+
94+
// Basename returns the unqualified entity type name.
95+
func (e EntityTypeRef) Basename() string {
96+
return types.EntityType(e).Basename()
97+
}
98+
8499
// EntityType returns an EntityTypeRef for the given entity type name.
85100
func EntityType(name types.EntityType) EntityTypeRef {
86101
return EntityTypeRef(name)
@@ -91,6 +106,21 @@ type TypeRef types.Path
91106

92107
func (TypeRef) isType() { _ = 0 }
93108

109+
// IsQualified reports whether the type reference contains a namespace qualifier.
110+
func (t TypeRef) IsQualified() bool {
111+
return types.EntityType(t).IsQualified()
112+
}
113+
114+
// Namespace returns the namespace portion of a qualified type reference, or "" if unqualified.
115+
func (t TypeRef) Namespace() types.Namespace {
116+
return types.EntityType(t).Namespace()
117+
}
118+
119+
// Basename returns the unqualified type name.
120+
func (t TypeRef) Basename() string {
121+
return types.EntityType(t).Basename()
122+
}
123+
94124
// Type returns a TypeRef for the given path.
95125
func Type(name types.Path) TypeRef {
96126
return TypeRef(name)

x/exp/schema/internal/json/json.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
1919

2020
// Bare declarations go under the empty string key.
2121
if hasBareDecls((*ast.Schema)(s)) {
22-
ns, err := marshalNamespace("", ast.Namespace{
22+
ns, err := marshalNamespace(ast.Namespace{
2323
Entities: s.Entities,
2424
Enums: s.Enums,
2525
Actions: s.Actions,
@@ -32,7 +32,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
3232
}
3333

3434
for name, ns := range s.Namespaces {
35-
jns, err := marshalNamespace(name, ns)
35+
jns, err := marshalNamespace(ns)
3636
if err != nil {
3737
return nil, err
3838
}
@@ -67,7 +67,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error {
6767
if result.Namespaces == nil {
6868
result.Namespaces = ast.Namespaces{}
6969
}
70-
result.Namespaces[types.Path(name)] = ns
70+
result.Namespaces[types.Namespace(name)] = ns
7171
}
7272
}
7373
*s = Schema(result)
@@ -131,7 +131,7 @@ type jsonAttr struct {
131131
Annotations map[string]string `json:"annotations,omitempty"`
132132
}
133133

134-
func marshalNamespace(name types.Path, ns ast.Namespace) (jsonNamespace, error) {
134+
func marshalNamespace(ns ast.Namespace) (jsonNamespace, error) {
135135
jns := jsonNamespace{
136136
EntityTypes: make(map[string]jsonEntityType),
137137
Actions: make(map[string]jsonAction),

x/exp/schema/internal/json/json_internal_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestMarshalRecordTypeError(t *testing.T) {
2626
}
2727

2828
func TestMarshalNamespaceCommonTypeError(t *testing.T) {
29-
_, err := marshalNamespace("", ast.Namespace{
29+
_, err := marshalNamespace(ast.Namespace{
3030
CommonTypes: ast.CommonTypes{
3131
"Bad": ast.CommonType{Type: nil},
3232
},
@@ -35,7 +35,7 @@ func TestMarshalNamespaceCommonTypeError(t *testing.T) {
3535
}
3636

3737
func TestMarshalNamespaceEntityShapeError(t *testing.T) {
38-
_, err := marshalNamespace("", ast.Namespace{
38+
_, err := marshalNamespace(ast.Namespace{
3939
Entities: ast.Entities{
4040
"Foo": ast.Entity{
4141
Shape: ast.RecordType{
@@ -50,7 +50,7 @@ func TestMarshalNamespaceEntityShapeError(t *testing.T) {
5050
func TestMarshalNamespaceEntityTagsError(t *testing.T) {
5151
// Tags is nil, but the code checks `entity.Tags != nil` first
5252
// So we need a non-nil tags that fails. Use SetType{Element: nil}.
53-
_, err := marshalNamespace("", ast.Namespace{
53+
_, err := marshalNamespace(ast.Namespace{
5454
Entities: ast.Entities{
5555
"Foo": ast.Entity{Tags: nil},
5656
},
@@ -59,7 +59,7 @@ func TestMarshalNamespaceEntityTagsError(t *testing.T) {
5959
}
6060

6161
func TestMarshalNamespaceEntityTagsError2(t *testing.T) {
62-
_, err := marshalNamespace("", ast.Namespace{
62+
_, err := marshalNamespace(ast.Namespace{
6363
Entities: ast.Entities{
6464
"Foo": ast.Entity{Tags: ast.SetType{Element: nil}},
6565
},
@@ -68,7 +68,7 @@ func TestMarshalNamespaceEntityTagsError2(t *testing.T) {
6868
}
6969

7070
func TestMarshalNamespaceActionAnnotations(t *testing.T) {
71-
ns, err := marshalNamespace("", ast.Namespace{
71+
ns, err := marshalNamespace(ast.Namespace{
7272
Actions: ast.Actions{
7373
"view": ast.Action{
7474
Annotations: ast.Annotations{"doc": "test"},
@@ -80,7 +80,7 @@ func TestMarshalNamespaceActionAnnotations(t *testing.T) {
8080
}
8181

8282
func TestMarshalNamespaceContextError(t *testing.T) {
83-
_, err := marshalNamespace("", ast.Namespace{
83+
_, err := marshalNamespace(ast.Namespace{
8484
Actions: ast.Actions{
8585
"view": ast.Action{
8686
AppliesTo: &ast.AppliesTo{

x/exp/schema/internal/parser/parser.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func (p *parser) parseSchema() (*ast.Schema, error) {
141141
}
142142

143143
type parsedNamespace struct {
144-
name types.Path
144+
name types.Namespace
145145
ns ast.Namespace
146146
}
147147

@@ -150,7 +150,8 @@ func (p *parser) parseNamespace(annotations ast.Annotations) (parsedNamespace, e
150150
if err != nil {
151151
return parsedNamespace{}, err
152152
}
153-
if slices.Contains(strings.Split(string(path), "::"), "__cedar") {
153+
nsName := types.Namespace(path)
154+
if slices.Contains(strings.Split(string(nsName), "::"), "__cedar") {
154155
return parsedNamespace{}, fmt.Errorf("%s: the name %q contains \"__cedar\", which is reserved", p.tok.Pos, path)
155156
}
156157
if err := p.expect(tokenLBrace); err != nil {
@@ -177,7 +178,7 @@ func (p *parser) parseNamespace(annotations ast.Annotations) (parsedNamespace, e
177178
ns.Enums = innerSchema.Enums
178179
ns.Actions = innerSchema.Actions
179180
ns.CommonTypes = innerSchema.CommonTypes
180-
return parsedNamespace{name: path, ns: ns}, nil
181+
return parsedNamespace{name: nsName, ns: ns}, nil
181182
}
182183

183184
func (p *parser) parseDecl(annotations ast.Annotations, schema *ast.Schema) error {

0 commit comments

Comments
 (0)