Skip to content

Commit 682b8a0

Browse files
authored
Merge pull request cedar-policy#129 from strongdm/patjak/schema
Add Schema parsing, programmatic creation, and resolution
2 parents 2a36626 + a52283f commit 682b8a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+6897
-9187
lines changed

.github/workflows/build_and_test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
- name: Lint
2525
uses: golangci/golangci-lint-action@v7
2626

27+
- name: Sum type linter
28+
run: go install github.com/alecthomas/go-check-sumtype/cmd/go-check-sumtype@latest && go-check-sumtype -default-signifies-exhaustive=false ./...
29+
2730
- name: Fuzz
2831
run: mkdir -p testdata && go test -fuzz=FuzzParse -fuzztime 60s && go test -fuzz=FuzzTokenize -fuzztime 60s
2932

.golangci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ linters:
1010
enable:
1111
- errcheck
1212
- errname
13-
- gochecksumtype
1413
- govet
1514
- ineffassign
1615
- revive

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
corpus-tests-json-schemas.tar.gz: corpus-tests.tar.gz
2+
@echo "Generating JSON schemas from Cedar schemas..."
3+
@rm -rf /tmp/corpus-tests /tmp/corpus-tests-json-schemas
4+
@mkdir -p /tmp/corpus-tests-json-schemas
5+
@tar -xzf corpus-tests.tar.gz -C /tmp/
6+
@for schema in /tmp/corpus-tests/*.cedarschema; do \
7+
basename=$$(basename $$schema .cedarschema); \
8+
echo "Converting $$basename.cedarschema..."; \
9+
cedar translate-schema --direction cedar-to-json --schema "$$schema" > "/tmp/corpus-tests-json-schemas/$$basename.cedarschema.json" 2>&1; \
10+
done
11+
@cd /tmp && tar -czf corpus-tests-json-schemas.tar.gz corpus-tests-json-schemas/
12+
@mv /tmp/corpus-tests-json-schemas.tar.gz .
13+
@rm -rf /tmp/corpus-tests /tmp/corpus-tests-json-schemas
14+
@echo "Done! Created corpus-tests-json-schemas.tar.gz"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ The Go implementation includes:
3636
- JSON marshalling and unmarshalling
3737
- all core and extended types (including [RFC 80](https://github.com/cedar-policy/rfcs/blob/main/text/0080-datetime-extension.md)'s datetime and duration)
3838
- integration test suite
39-
- human-readable schema parsing
40-
39+
- schema parsing and programmatic construction
40+
4141
The Go implementation does not yet include:
4242

4343
- CLI applications

corpus-tests-json-schemas.tar.gz

712 KB
Binary file not shown.

corpus_test.go

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ type corpusTest struct {
5656
//go:embed corpus-tests.tar.gz
5757
var corpusArchive []byte
5858

59+
//go:embed corpus-tests-json-schemas.tar.gz
60+
var corpusJSONSchemasArchive []byte
61+
5962
type tarFileDataPointer struct {
6063
Position int64
6164
Size int64
@@ -91,39 +94,52 @@ func (fdm TarFileMap) GetFileData(path string) ([]byte, error) {
9194
return content, nil
9295
}
9396

94-
//nolint:revive // due to test cognitive complexity
95-
func TestCorpus(t *testing.T) {
96-
t.Parallel()
97+
func loadTarGz(t testing.TB, archive []byte) TarFileMap {
98+
t.Helper()
9799

98-
gzipReader, err := gzip.NewReader(bytes.NewReader(corpusArchive))
100+
gzipReader, err := gzip.NewReader(bytes.NewReader(archive))
99101
if err != nil {
100-
t.Fatal("error reading corpus compressed archive header", err)
102+
t.Fatal("error reading compressed archive header", err)
101103
}
102104
defer gzipReader.Close() //nolint:errcheck
103105

104106
buf, err := io.ReadAll(gzipReader)
105107
if err != nil {
106-
t.Fatal("error reading corpus compressed archive", err)
108+
t.Fatal("error reading compressed archive", err)
107109
}
108110

109111
bufReader := bytes.NewReader(buf)
110112
archiveReader := tar.NewReader(bufReader)
111113

112114
fdm := NewTarFileMap(bufReader)
113-
var testFiles []string
114115
for file, err := archiveReader.Next(); err == nil; file, err = archiveReader.Next() {
115116
if file.Typeflag != tar.TypeReg {
116117
continue
117118
}
118119

119120
cursor, _ := bufReader.Seek(0, io.SeekCurrent)
120121
fdm.AddFileData(file.Name, cursor, file.Size)
122+
}
123+
124+
return fdm
125+
}
121126

122-
if strings.HasSuffix(file.Name, ".json") && !strings.HasSuffix(file.Name, ".entities.json") {
123-
testFiles = append(testFiles, file.Name)
127+
//nolint:revive // due to test cognitive complexity
128+
func TestCorpus(t *testing.T) {
129+
t.Parallel()
130+
131+
// Load corpus test files
132+
fdm := loadTarGz(t, corpusArchive)
133+
var testFiles []string
134+
for fileName := range fdm.files {
135+
if strings.HasSuffix(fileName, ".json") && !strings.HasSuffix(fileName, ".entities.json") {
136+
testFiles = append(testFiles, fileName)
124137
}
125138
}
126139

140+
// Load JSON schemas for validation
141+
jsonSchemasFdm := loadTarGz(t, corpusJSONSchemasArchive)
142+
127143
for _, testFile := range testFiles {
128144
testFile := testFile
129145
t.Run(testFile, func(t *testing.T) {
@@ -152,12 +168,70 @@ func TestCorpus(t *testing.T) {
152168
if err != nil {
153169
t.Fatal("error reading schema content", err)
154170
}
171+
// Rust converted JSON never contains the empty context record
172+
schemaContent = bytes.ReplaceAll(schemaContent, []byte("context: {}\n"), nil)
173+
155174
var s schema.Schema
156175
s.SetFilename("test.schema")
157176
if err := s.UnmarshalCedar(schemaContent); err != nil {
158177
t.Fatal("error parsing schema", err, "\n===\n", string(schemaContent))
159178
}
160179

180+
// Validate schema round-trip
181+
t.Run("schema-round-trip", func(t *testing.T) {
182+
t.Parallel()
183+
184+
js, err := s.MarshalJSON()
185+
testutil.OK(t, err)
186+
187+
var s2 schema.Schema
188+
err = s2.UnmarshalJSON(js)
189+
testutil.OK(t, err)
190+
191+
sb, err := s2.MarshalCedar()
192+
testutil.OK(t, err)
193+
194+
var s3 schema.Schema
195+
err = s3.UnmarshalCedar(sb)
196+
testutil.OK(t, err)
197+
198+
j2, err := s3.MarshalJSON()
199+
testutil.OK(t, err)
200+
201+
testutil.Equals(t, string(j2), string(js))
202+
})
203+
204+
// Validate schema matches Rust Cedar CLI output
205+
t.Run("schema-vs-rust", func(t *testing.T) {
206+
t.Parallel()
207+
208+
// Extract schema filename from path (e.g., "corpus-tests/abc123.cedarschema" -> "abc123")
209+
schemaFilename := strings.TrimSuffix(strings.TrimPrefix(tt.Schema, "corpus-tests/"), ".cedarschema")
210+
jsonSchemaPath := fmt.Sprintf("corpus-tests-json-schemas/%s.cedarschema.json", schemaFilename)
211+
212+
rustJSON, err := jsonSchemasFdm.GetFileData(jsonSchemaPath)
213+
testutil.OK(t, err)
214+
215+
// Normalize Rust JSON: appliesTo is optional - match testdata_test.go pattern
216+
// Need to handle trailing comma to avoid creating invalid JSON like {,"other":...}
217+
rustJSON = bytes.ReplaceAll(rustJSON, []byte(`"appliesTo":{"resourceTypes":[],"principalTypes":[]},`), nil)
218+
rustJSON = bytes.ReplaceAll(rustJSON, []byte(`"appliesTo":{"resourceTypes":[],"principalTypes":[]}`), nil)
219+
220+
// Unmarshal Rust JSON to handle any syntax issues from replacement
221+
var rustSchema schema.Schema
222+
err = rustSchema.UnmarshalJSON(rustJSON)
223+
testutil.OK(t, err)
224+
225+
// Marshal both schemas to JSON for comparison
226+
goJSON, err := s.MarshalJSON()
227+
testutil.OK(t, err)
228+
rustJSON2, err := rustSchema.MarshalJSON()
229+
testutil.OK(t, err)
230+
231+
// Normalize and compare
232+
stringEquals(t, string(normalizeJSON(t, goJSON)), string(normalizeJSON(t, rustJSON2)))
233+
})
234+
161235
policyContent, err := fdm.GetFileData(tt.Policies)
162236
if err != nil {
163237
t.Fatal("error reading policy content", err)
@@ -250,6 +324,21 @@ func TestCorpus(t *testing.T) {
250324
}
251325
}
252326

327+
func normalizeJSON(t *testing.T, in []byte) []byte {
328+
t.Helper()
329+
var out any
330+
err := json.Unmarshal(in, &out)
331+
testutil.OK(t, err)
332+
b, err := json.MarshalIndent(out, "", " ")
333+
testutil.OK(t, err)
334+
return b
335+
}
336+
337+
func stringEquals(t *testing.T, got, want string) {
338+
t.Helper()
339+
testutil.Equals(t, strings.TrimSpace(got), strings.TrimSpace(want))
340+
}
341+
253342
// Specific corpus tests that have been extracted for easy regression testing purposes
254343
func TestCorpusRelated(t *testing.T) {
255344
t.Parallel()

0 commit comments

Comments
 (0)