Skip to content

Commit 8db6ca1

Browse files
Fix issue with self-recursive traits and add validation (#78)
* docs: Remove incomplete comment in jsonschemaconv.go * fix: Fix self-recursive traits resolution and traits validation
1 parent 849973e commit 8db6ca1

9 files changed

Lines changed: 111 additions & 11 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ cover.html
66
cover.out
77
.build
88
/.idea
9+
test_output/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#%RAML 1.0 Library
2+
3+
types:
4+
SelfRecursiveTrait:
5+
type: object
6+
facets:
7+
my_trait:
8+
type: SelfRecursiveTrait
9+
properties:
10+
attr1: string
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#%RAML 1.0 Library
2+
3+
types:
4+
Trait:
5+
type: object
6+
facets:
7+
my_trait:
8+
type: string
9+
example: 1
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#%RAML 1.0 Library
2+
3+
types:
4+
SelfRecursiveTraitAlias:
5+
type: object
6+
facets:
7+
my_trait: SelfRecursiveTraitAlias
8+
properties:
9+
attr1: string
10+
11+
SelfRecursiveTraitNestedAlias:
12+
type: object
13+
facets:
14+
test_trait:
15+
properties:
16+
my_trait: SelfRecursiveTraitNestedAlias
17+
properties:
18+
attr1: string
19+
20+
SelfRecursiveTraitInheritedOptional:
21+
type: object
22+
facets:
23+
my_trait?:
24+
type: SelfRecursiveTraitInheritedOptional
25+
properties:
26+
attr1: string

jsonschemaconv.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ func (c *JSONSchemaConverter[T]) VisitRecursiveShape(s *RecursiveShape) T {
337337
// Ref ignores all other keywords defined within the schema per JSON Schema spec.
338338

339339
// NOTE: We create empty schema because all base RAML types are allowed to have
340-
// custom facets which can be recursive. RAML-JSON Schema wrapper
340+
// custom facets which can be recursive.
341341
// The use of `makeSchemaFromBaseShape` will lead to infinite recursion.
342342
node := c.makeEmptySchema()
343343
schema := node.Generic()

parse_test.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package raml
33
import (
44
"container/list"
55
"context"
6+
"encoding/json"
67
"errors"
78
"io"
89
"os"
@@ -14,12 +15,38 @@ import (
1415
"github.com/stretchr/testify/require"
1516
)
1617

18+
func writeToDiskHelper(t *testing.T, tt struct {
19+
name string
20+
typeName string
21+
outSchema *JSONSchemaRAML
22+
}) {
23+
outputDir := "./test_output"
24+
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
25+
t.Errorf("Failed to create output directory: %v", err)
26+
}
27+
outputPath := filepath.Join(outputDir, tt.name+"_"+tt.typeName+".json")
28+
var jsonData []byte
29+
jsonData, err := json.MarshalIndent(tt.outSchema, "", " ")
30+
if err != nil {
31+
t.Errorf("Failed to marshal JSON Schema: %v", err)
32+
}
33+
if err := os.WriteFile(outputPath, jsonData, 0644); err != nil {
34+
t.Errorf("Failed to write JSON Schema to file: %v", err)
35+
}
36+
t.Logf("Successfully wrote JSON Schema to %s", outputPath)
37+
}
38+
39+
1740
func Test_ParseFixturesIntegration(t *testing.T) {
1841
// Define test cases for valid fixtures that should parse successfully
1942
validTests := []struct {
2043
name string
2144
path string
2245
}{
46+
{
47+
name: "library_recursive_trait.raml",
48+
path: "./fixtures/library_recursive_trait.raml",
49+
},
2350
{
2451
name: "library.raml",
2552
path: "./fixtures/library.raml",
@@ -78,19 +105,25 @@ func Test_ParseFixturesIntegration(t *testing.T) {
78105
case *Library:
79106
for pair := f.AnnotationTypes.Oldest(); pair != nil; pair = pair.Next() {
80107
s := pair.Value
81-
_, errConv := conv.Convert(s.Shape)
82-
require.NoError(t, errConv, "Failed to convert annotation type shape in %s: %v", tt.path, errConv)
108+
typeName := pair.Key
109+
outSchema, err := conv.Convert(s.Shape)
110+
writeToDiskHelper(t, struct{name string; typeName string; outSchema *JSONSchemaRAML}{name: tt.name, typeName: typeName, outSchema: outSchema})
111+
require.NoError(t, err, "Failed to convert annotation type shape in %s: %v", tt.path, err)
83112
convertedCount++
84113
}
85114
for pair := f.Types.Oldest(); pair != nil; pair = pair.Next() {
86115
s := pair.Value
87-
_, errConv := conv.Convert(s.Shape)
88-
require.NoError(t, errConv, "Failed to convert type shape in %s: %v", tt.path, errConv)
116+
typeName := pair.Key
117+
outSchema, err := conv.Convert(s.Shape)
118+
writeToDiskHelper(t, struct{name string; typeName string; outSchema *JSONSchemaRAML}{name: tt.name, typeName: typeName, outSchema: outSchema})
119+
require.NoError(t, err, "Failed to convert type shape in %s: %v", tt.path, err)
89120
convertedCount++
90121
}
91122
case *DataType:
92-
_, errConv := conv.Convert(f.Shape.Shape)
93-
require.NoError(t, errConv, "Failed to convert data type shape in %s: %v", tt.path, errConv)
123+
typeName := f.Shape.Name
124+
outSchema, err := conv.Convert(f.Shape.Shape)
125+
writeToDiskHelper(t, struct{name string; typeName string; outSchema *JSONSchemaRAML}{name: tt.name, typeName: typeName, outSchema: outSchema})
126+
require.NoError(t, err, "Failed to convert data type shape in %s: %v", tt.path, err)
94127
convertedCount++
95128
}
96129
}
@@ -136,8 +169,12 @@ func Test_ParseFixturesIntegration(t *testing.T) {
136169
path: "./fixtures/library_invalid_unwrap.raml",
137170
},
138171
{
139-
name: "named_example_invalid_decode.raml",
140-
path: "./fixtures/named_example_invalid_decode.raml",
172+
name: "library_invalid_recursive_trait.raml",
173+
path: "./fixtures/library_invalid_inherited_recursive_trait.raml",
174+
},
175+
{
176+
name: "library_invalid_trait_example.raml",
177+
path: "./fixtures/library_invalid_trait_example.raml",
141178
},
142179
}
143180

shape.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,8 @@ func (r *RAML) MakeRecursiveShape(headBase *BaseShape) *BaseShape {
528528
recursiveBase.Description = headBase.Description
529529
recursiveBase.CustomDomainProperties = headBase.CustomDomainProperties
530530
recursiveBase.CustomShapeFacets = headBase.CustomShapeFacets
531-
recursiveBase.CustomShapeFacetDefinitions = headBase.CustomShapeFacetDefinitions
531+
// Recursive shapes must not provide facet definitions,
532+
// they are provided by the head shape.
532533
s := &RecursiveShape{BaseShape: recursiveBase, Head: headBase}
533534
recursiveBase.SetShape(s)
534535
return recursiveBase

unwrap.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,16 @@ func (r *RAML) FindAndMarkRecursion(base *BaseShape) (*BaseShape, error) {
199199
return nil, err
200200
}
201201

202+
// Reset the context to avoid generating recursive shape
203+
// for trait that points to the same type that defines this trait.
204+
// This is OK because traits cannot have nested traits and
205+
// cannot be used as a source for inheritance.
206+
base.ShapeVisited = false
202207
err = r.findAndMarkRecursionInCustomShapeFacetDefinitions(base)
203208
if err != nil {
204209
return nil, err
205210
}
206211

207-
base.ShapeVisited = false
208212
return nil, ErrNil
209213
}
210214

@@ -435,6 +439,8 @@ func (r *RAML) UnwrapShape(base *BaseShape) (*BaseShape, error) {
435439
return nil, StacktraceNewWrapped("custom shape facet definition unwrap", errUnwrap, base.Location,
436440
stacktrace.WithPosition(&base.Position), stacktrace.WithType(StacktraceTypeUnwrapping))
437441
}
442+
// Reset custom shape facet definitions since traits cannot have nested traits.
443+
us.CustomShapeFacetDefinitions = orderedmap.New[string, Property]()
438444
prop.Base = us
439445
base.CustomShapeFacetDefinitions.Set(pair.Key, prop)
440446
}

validate.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ func (r *RAML) validateShapeCommons(s *BaseShape) error {
285285
}
286286
}
287287
}
288+
289+
// Validate trait definition shapes
290+
for pair := s.CustomShapeFacetDefinitions.Oldest(); pair != nil; pair = pair.Next() {
291+
facetDef := pair.Value
292+
if err := r.validateShapeCommons(facetDef.Base); err != nil {
293+
return StacktraceNewWrapped("validate custom facet definition", err, facetDef.Base.Location,
294+
stacktrace.WithPosition(&facetDef.Base.Position), stacktrace.WithInfo("facet", pair.Key))
295+
}
296+
}
297+
288298
return nil
289299
}
290300

0 commit comments

Comments
 (0)