Skip to content

Commit 9a02127

Browse files
authored
Merge pull request #7 from ryanolee/feature/not
# Implement Not, External Doument Resolution, If statements and numerous other improovements to generator behaviour
2 parents 4405455 + f0d230f commit 9a02127

102 files changed

Lines changed: 7677 additions & 429 deletions

File tree

Some content is hidden

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

README.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,34 @@ func main() {
4848
# CLI
4949
There is also a cli tool available for this package that can be installed from the [releases](https://github.com/ryanolee/go-chaff/releases) section.
5050
```
51+
CLI too for generating random JSON data matching given JSON schema
5152
Usage: go-chaff [flags]
53+
-allow-insecure
54+
Allow fetching remote $ref documents over insecure HTTP connections.
55+
-allow-outside-cwd
56+
Allow fetching $ref documents from file system paths outside the current working directory.
57+
-allowed-hosts string
58+
Comma separated list of allowed hosts to fetch remote $ref documents from over HTTP(S). If empty http and https resolution will fail.
59+
-allowed-paths string
60+
Comma separated list of allowed file system paths to fetch $ref documents from.
61+
-bypass-cyclic-reference-check
62+
Bypass cyclic reference check when generating schemas with cyclic $ref references.
63+
-cutoff-generation-steps int
64+
Maximum number of generation steps to perform before aborting generation entirely and returning what was generated. (default 2000)
5265
-file string
5366
Specify a file path to read the JSON Schema from
5467
-format
5568
Format JSON output.
5669
-help
5770
Print out help.
71+
-maximum-generation-steps int
72+
Maximum number of generation steps to perform before reducing the effort put into the generation process to a bare minimum. (default 1000)
73+
-maximum-if-attempts int
74+
Maximum number of attempts to satisfy 'if' conditions when generating data. (default 100)
75+
-maximum-oneof-attempts int
76+
Maximum number of attempts to satisfy 'oneOf' conditions when generating data. (default 100)
77+
-maximum-reference-depth int
78+
Maximum depth of $ref references to resolve at once when generating data. (default 10)
5879
-output string
5980
Specify file path to write generated output to.
6081
-verbose
@@ -75,24 +96,23 @@ echo '{"type": "string", "format": "ipv4"}' | go-chaff
7596
* Constant types: `enum`, `const`, `null`
7697
* References: `$ref`, `$defs`, `definitions`, `$id`
7798
* Object: `properties`, `patternProperties`, `additionalProperties`, `minProperties`, `maxProperties`, `required`
78-
* Array: `items`, `minItems`, `maxItems`, `contains`, `minContains`, `maxContains`, `prefixItems`, `additionalItems`, `unevalidatedItems`, `uniqueItems` (Limited support)
79-
* Combination types (`anyOf` / `oneOf` / `allOf`) **N.b These are experimental! Expect none compliant schema output for some of these**
80-
81-
# Further feature support
82-
* Schema factoring with `allOf` and `anyOf` (Experimental)
83-
* Circular references (Experimental)
84-
* Type inference
99+
* Array: `items`, `minItems`, `maxItems`, `contains`, `minContains`, `maxContains`, `prefixItems`, `additionalItems`, `unevaluatedItems`, `uniqueItems` (Limited support)
100+
* Combination types `anyOf` / `oneOf` / `allOf`
101+
* Support for `if` / `then` / `else`
102+
* Support for `not` combinator (excluding `anyOf`, `oneOf` / `allOf` and `if/then/else`)
103+
* Multi document resolution for `$ref` over `http(s)` or `file` schemes.
85104

86105
# Credits / Dependencies
87106
* [Regen](https://github.com/zach-klippenstein/goregen) (@zach-klippenstein and @AnatolyRugalev)
88107
* [Faker](https://github.com/go-faker/faker)
89108
* [Schema Store](https://github.com/SchemaStore/schemastore)
109+
* [jsonschema](github.com/kaptinlin/jsonschema) from @kaptinlin for test validation
110+
* [jsonschema](https://github.com/santhosh-tekuri/jsonschema) from @santhosh-tekuri for internal schema constrain validation
90111
* Original idea, inspiration and technical support [json-schema-faker](https://github.com/json-schema-faker/json-schema-faker)
91112

92113
# What is left to do?
93114
* Better test coverage (Property based and unit of various generators)
94115
* Handle many edge cases where this package might not generate schema compliant results
95116
* Overcome the limitations of the current `oneOf`, `anyOf` and `allOf` keywords implementation.
96-
* Add support for `if` / `else` / `not` keywords
97117

98118

all_of.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,31 @@ type allOfGenerator struct {
2727
// ]
2828
// }
2929
func parseAllOf(node schemaNode, metadata *parserMetadata) (Generator, error) {
30-
if node.OneOf != nil || node.AnyOf != nil {
31-
return &nullGenerator{}, fmt.Errorf("only one of [allOf / oneOf / anyOf] can be specified")
30+
31+
if node.AllOf == nil {
32+
return &nullGenerator{}, fmt.Errorf("allOf must be defined")
3233
}
3334

34-
mergedNode, err := mergeSchemaNodes(metadata, node.AllOf...)
35+
// Combine the base node with all of its allOf sub-schemas
36+
// then nullify the allOf to avoid infinite recursion during merge
37+
nodesToCombine := []schemaNode{}
38+
nodesToCombine = append(nodesToCombine, *node.AllOf...)
39+
node.AllOf = nil
40+
nodesToCombine = append(nodesToCombine, node)
41+
42+
mergedNode, err := mergeSchemaNodes(metadata, nodesToCombine...)
3543
if err != nil {
3644
return &nullGenerator{}, err
3745
}
3846

39-
generator, err := metadata.ReferenceHandler.ParseNodeInScope("/allOf", mergedNode, metadata)
47+
generator, err := parseSchemaNode(mergedNode, metadata)
4048

4149
if err != nil {
4250
return &nullGenerator{}, err
4351
}
4452

4553
return &allOfGenerator{
46-
SchemaNodes: node.AllOf,
47-
MergedNode: mergedNode,
48-
Generator: generator,
54+
Generator: generator,
4955
}, nil
5056
}
5157

array.go

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,38 +40,43 @@ type (
4040
// }
4141
func parseArray(node schemaNode, metadata *parserMetadata) (Generator, error) {
4242
// Handle case where contains is set with no minContains (at least 1 must match subschema)
43-
minContains := node.MinContains
43+
minContains := util.GetZeroIfNil(node.MinContains, 0)
44+
maxContains := util.GetZeroIfNil(node.MaxContains, 0)
45+
minItems := util.GetZeroIfNil(node.MinItems, 0)
46+
maxItems := util.GetZeroIfNil(node.MaxItems, 0)
47+
prefixItems := util.GetZeroIfNil(node.PrefixItems, []schemaNode{})
48+
4449
if minContains == 0 && node.Contains != nil {
4550
minContains = 1
4651
}
4752

4853
// Validate Bounds
49-
if err := assertLowerUpperBound(node.MinItems, node.MaxItems, "minItems", "maxItems"); err != nil {
54+
if err := assertLowerUpperBound(minItems, maxItems, "minItems", "maxItems"); err != nil {
5055
return nullGenerator{}, err
5156
}
5257

53-
if err := assertLowerUpperBound(node.MinContains, node.MaxContains, "minContains", "maxContains"); err != nil {
58+
if err := assertLowerUpperBound(minContains, maxContains, "minContains", "maxContains"); err != nil {
5459
return nullGenerator{}, err
5560
}
5661

57-
if err := assertLowerUpperBound(node.MinContains, node.MaxItems, "minContains", "minItems"); err != nil {
62+
if err := assertLowerUpperBound(minContains, minItems, "minContains", "minItems"); err != nil {
5863
return nullGenerator{}, err
5964
}
6065

61-
if err := assertLowerUpperBound(len(node.PrefixItems)+node.MinContains, node.MaxItems, "Tuple items Plus Prefix items. (Note contains does not assume tuple items count towards the total account)", "minItems"); err != nil {
66+
if err := assertLowerUpperBound(len(prefixItems)+minContains, maxItems, "Tuple items Plus Prefix items. (Note contains does not assume tuple items count towards the total account)", "minItems"); err != nil {
6267
return nullGenerator{}, err
6368
}
6469

6570
// Validate if tuple makes sense in this context
66-
tupleLength := len(node.PrefixItems)
67-
if tupleLength > node.MaxItems && node.MaxItems != 0 {
71+
tupleLength := len(prefixItems)
72+
if tupleLength > maxItems && node.MaxItems != nil {
6873
return nullGenerator{}, fmt.Errorf("tuple length must be less than or equal to maxItems (tupleLength: %d, maxItems: %d)", tupleLength, node.MaxItems)
6974
}
7075

71-
min := util.GetInt(node.MinItems, node.MinContains)
72-
max := util.GetInt(node.MaxItems, node.MaxContains)
76+
min := util.GetInt(minItems, minContains)
77+
max := util.GetInt(maxItems, min+defaultOffset)
7378

74-
disallowedAdditionalItems := node.Items.DisallowAdditionalItems || (node.AdditionalItems != nil && node.AdditionalItems.IsFalse)
79+
disallowedAdditionalItems := (node.Items != nil && node.Items.DisallowAdditionalItems) || (node.AdditionalItems != nil && node.AdditionalItems.IsFalse)
7580

7681
// Force the generator to use only the tuple in the event that additional items
7782
// are not allowed
@@ -116,7 +121,7 @@ func parseArray(node schemaNode, metadata *parserMetadata) (Generator, error) {
116121
UnevaluatedItemsGenerator: unevaluatedItemsGenerator,
117122
DisallowUnevaluatedItems: disallowUnevaluatedItems,
118123

119-
DisallowAdditional: node.Items.DisallowAdditionalItems,
124+
DisallowAdditional: node.Items != nil && node.Items.DisallowAdditionalItems,
120125
AdditionalItemsGenerator: additionalItemGenerator,
121126

122127
MinContains: minContains,
@@ -125,19 +130,20 @@ func parseArray(node schemaNode, metadata *parserMetadata) (Generator, error) {
125130
MinItems: min,
126131
MaxItems: max,
127132

128-
UniqueItems: node.UniqueItems,
133+
UniqueItems: util.GetZeroIfNil(node.UniqueItems, false),
129134

130135
schemaNode: node,
131136
}, nil
132137
}
133138

134139
func parseTupleGeneratorFromSchemaNode(node schemaNode, metadata *parserMetadata) ([]Generator, error) {
135-
if len(node.PrefixItems) != 0 {
136-
return parseTupleGenerator(node.PrefixItems, metadata)
140+
141+
if node.PrefixItems != nil && len(*node.PrefixItems) != 0 {
142+
return parseTupleGenerator(*node.PrefixItems, metadata)
137143
// Legacy support given "items" when passed as an array
138144
// has the same meaning as "prefixItems"
139-
} else if len(node.Items.Nodes) != 0 {
140-
return parseTupleGenerator(node.Items.Nodes, metadata)
145+
} else if node.Items != nil && node.Items.Nodes != nil && len(*node.Items.Nodes) != 0 {
146+
return parseTupleGenerator(*node.Items.Nodes, metadata)
141147
}
142148
return nil, nil
143149
}
@@ -171,7 +177,11 @@ func parseAdditionalItems(node schemaNode, metadata *parserMetadata) (Generator,
171177

172178
}
173179

174-
func parseItemGenerator(additionalData itemsData, metadata *parserMetadata) (Generator, error) {
180+
func parseItemGenerator(additionalData *itemsData, metadata *parserMetadata) (Generator, error) {
181+
if additionalData == nil {
182+
return nil, nil
183+
}
184+
175185
if additionalData.DisallowAdditionalItems || additionalData.Node == nil {
176186
return nil, nil
177187
}
@@ -189,6 +199,10 @@ func parseItemGeneratorInScope(node schemaNode, metadata *parserMetadata, scope
189199

190200
func (g arrayGenerator) Generate(opts *GeneratorOptions) interface{} {
191201
opts.overallComplexity++
202+
if opts.ShouldCutoff() {
203+
return nil
204+
}
205+
192206
tupleLength := len(g.TupleGenerators)
193207
arrayData := make([]interface{}, 0)
194208

@@ -227,7 +241,14 @@ func (g arrayGenerator) Generate(opts *GeneratorOptions) interface{} {
227241

228242
// Generate any required "contains" items
229243
for i := 0; i < minContains; i++ {
230-
arrayData = append(arrayData, g.generateConsideringUnique(opts, g.ContainsGenerator, arrayData))
244+
item, ok := g.generateConsideringUnique(opts, g.ContainsGenerator, arrayData)
245+
if !ok && i > minContains {
246+
// If we are unable to generate a unique item, and we have already satisfied the minimum contains
247+
// bail out early
248+
break
249+
}
250+
251+
arrayData = append(arrayData, item)
231252
}
232253

233254
if maxItems < minItems {
@@ -240,14 +261,22 @@ func (g arrayGenerator) Generate(opts *GeneratorOptions) interface{} {
240261
itemsToGenerate := opts.Rand.RandomInt(0, remainingItemsToGenerate)
241262

242263
// Cull the remaining items if the complexity is too high or unevaluated items are not allowed
243-
if g.DisallowUnevaluatedItems || opts.MaximumGenerationSteps > 0 && opts.overallComplexity > opts.MaximumGenerationSteps {
264+
if g.DisallowUnevaluatedItems || opts.ShouldMinimize() {
244265
itemsToGenerate = 0
245266
}
246267

247268
// Generate the remaining items up to a random number
248269
// (This might skew the distribution of the length of the array)
249270
for i := 0; i < itemsToGenerate || minItems > len(arrayData); i++ {
250-
arrayData = append(arrayData, g.generateConsideringUnique(opts, itemGen, arrayData))
271+
item, ok := g.generateConsideringUnique(opts, itemGen, arrayData)
272+
273+
if !ok && i > minItems {
274+
// If we are unable to generate a unique item, and we have already satisfied the minimum
275+
// bail out early
276+
break
277+
}
278+
279+
arrayData = append(arrayData, item)
251280
}
252281

253282
return arrayData
@@ -263,18 +292,20 @@ func (g arrayGenerator) String() string {
263292
}
264293

265294
// Will attempt to generate a unique item if the uniqueItems flag is set
266-
func (g arrayGenerator) generateConsideringUnique(opts *GeneratorOptions, itemGenerator Generator, arrayData []interface{}) interface{} {
295+
func (g arrayGenerator) generateConsideringUnique(opts *GeneratorOptions, itemGenerator Generator, arrayData []interface{}) (interface{}, bool) {
267296
if !g.UniqueItems {
268-
return itemGenerator.Generate(opts)
297+
return itemGenerator.Generate(opts), true
269298
}
270299

300+
currentItems := funk.Map(arrayData, util.MarshalJsonToString).([]string)
301+
271302
// Generate until we have a unique item
272303
for i := 0; i < opts.MaximumUniqueGeneratorAttempts; i++ {
273304
item := itemGenerator.Generate(opts)
274-
if !funk.Contains(arrayData, item) {
275-
return item
305+
if !funk.Contains(currentItems, util.MarshalJsonToString(item)) {
306+
return item, true
276307
}
277308
}
278309

279-
return fmt.Sprintf("Warning: Unable to generate unique item after %d attempts. Recheck passed schema.", opts.MaximumUniqueGeneratorAttempts)
310+
return fmt.Sprintf("Warning: Unable to generate unique item after %d attempts. Recheck passed schema.", opts.MaximumUniqueGeneratorAttempts), false
280311
}

assert.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,6 @@ func assertLowerUpperBound(lower int, upper int, lowerName string, upperName str
2626
}
2727

2828
func assertNoUnsupported(node schemaNode) error {
29-
if node.Not != nil {
30-
return fmt.Errorf("'not' keyword is not supported")
31-
}
32-
if node.If != nil {
33-
return fmt.Errorf("'if' conditional is not supported")
34-
}
35-
if node.Then != nil {
36-
return fmt.Errorf("'then' conditional is not supported")
37-
}
38-
if node.Else != nil {
39-
return fmt.Errorf("'else' conditional is not supported")
40-
}
4129
if len(node.DependentRequired) > 0 {
4230
return fmt.Errorf("'dependentRequired' is not supported")
4331
}

0 commit comments

Comments
 (0)