Skip to content

Commit

Permalink
Merge pull request #55 from dadav/fix_required
Browse files Browse the repository at this point in the history
Fix required/not
  • Loading branch information
dadav authored Aug 31, 2024
2 parents 7d8d0fb + 5900a86 commit 2f0b76f
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 41 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ foo: bar
| [`properties`](#properties) | Contains a map with keys as property names and values as schema | Takes an `object` |
| [`pattern`](#pattern) | Regex pattern to test the value | Takes an `string` |
| [`format`](#format) | The [format keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) allows for basic semantic identification of certain kinds of string values | Takes a [keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) |
| [`required`](#required) | Adds the key to the required items | `true` or `false` |
| [`required`](#required) | Adds the key to the required items | `true` or `false` or `array` |
| [`deprecated`](#deprecated) | Marks the option as deprecated | `true` or `false` |
| [`items`](#items) | Contains the schema that describes the possible array items | Takes an `object` |
| [`enum`](#enum) | Multiple allowed values. Accepts an array of `string` | Takes an `array` |
Expand All @@ -128,6 +128,7 @@ foo: bar
| [`anyOf`](#anyof) | Accepts an array of schemas. None or one must apply | Takes an `array` |
| [`oneOf`](#oneof) | Accepts an array of schemas. One or more must apply | Takes an `array` |
| [`allOf`](#allof) | Accepts an array of schemas. All must apply| Takes an `array` |
| [`not`](#not) | A schema that must not be matched. | Takes an `object` |
| [`if/then/else`](#ifthenelse) | `if` the given schema applies, `then` also apply the given schema or `else` the other schema| Takes an `object` |
| `$ref` | Accepts a URL to a valid `jsonschema`. Extend the schema for the current key | Takes an URL |

Expand Down Expand Up @@ -370,6 +371,16 @@ By default every property is a required property, you can disable this with `req
altName: foo
```

It's also possible to define an array of required properties on the parent.

```yaml
# @schema
# required: [foo]
# @schema
altName:
foo: bar
```

#### `deprecated`

Let the user know if the key is deprecated, hence should be avoided.
Expand Down Expand Up @@ -626,7 +637,7 @@ storage: 30Gib

#### `allOf`

Allows user to define multiple schema fo a single key. Key must match `oneOf` the given schemas.
Allows user to define multiple schema for a single key. Key must match `oneOf` the given schemas.

```yaml
# @schema
Expand All @@ -638,6 +649,18 @@ Allows user to define multiple schema fo a single key. Key must match `oneOf` th
storage: 10Gib
```

#### `not`

Allows to define a schema that must not be matched.

```yaml
# @schema
# not:
# type: string
# @schema
foo: bar
```

#### `if/then/else`

Conditional schema settings with `if`/`then`/`else`
Expand Down
123 changes: 85 additions & 38 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,50 @@ const (

type SchemaOrBool interface{}

type BoolOrArrayOfString struct {
Strings []string
Bool bool
}

func NewBoolOrArrayOfString(arr []string, b bool) BoolOrArrayOfString {
return BoolOrArrayOfString{
Strings: arr,
Bool: b,
}
}

func (s *BoolOrArrayOfString) MarshalJSON() ([]byte, error) {
if s.Strings == nil {
return json.Marshal([]string{})
}
return json.Marshal(s.Strings)
}

func (s *BoolOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
var multi []string
if value.ShortTag() == arrayTag {
for _, v := range value.Content {
var typeStr string
err := v.Decode(&typeStr)
if err != nil {
return err
}
multi = append(multi, typeStr)
}
s.Strings = multi
} else if value.ShortTag() == boolTag {
var single bool
err := value.Decode(&single)
if err != nil {
return err
}
s.Bool = single
} else {
return fmt.Errorf("could not unmarshal %v to slice of string or bool", value.Content)
}
return nil
}

type StringOrArrayOfString []string

func (s *StringOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
Expand Down Expand Up @@ -134,24 +178,30 @@ type Schema struct {
AllOf []*Schema `yaml:"allOf,omitempty" json:"allOf,omitempty"`
OneOf []*Schema `yaml:"oneOf,omitempty" json:"oneOf,omitempty"`
Not *Schema `yaml:"not,omitempty" json:"not,omitempty"`
RequiredProperties []string `yaml:"-" json:"required,omitempty"`
Examples []string `yaml:"examples,omitempty" json:"examples,omitempty"`
Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"`
HasData bool `yaml:"-" json:"-"`
Deprecated bool `yaml:"deprecated,omitempty" json:"deprecated,omitempty"`
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
WriteOnly bool `yaml:"writeOnly,omitempty" json:"writeOnly,omitempty"`
Required bool `yaml:"required,omitempty" json:"-"`
Required BoolOrArrayOfString `yaml:"required,omitempty" json:"required,omitempty"`
}

func NewSchema(schemaType string) *Schema {
return &Schema{
Type: []string{schemaType},
Required: NewBoolOrArrayOfString([]string{}, false),
}
}

// Set sets the HasData field to true
func (s *Schema) Set() {
s.HasData = true
}

// DisableRequiredProperties sets all RequiredProperties in this schema to an empty slice
// DisableRequiredProperties sets disables all required fields
func (s *Schema) DisableRequiredProperties() {
s.RequiredProperties = nil
s.Required = NewBoolOrArrayOfString([]string{}, false)
for _, v := range s.Properties {
v.DisableRequiredProperties()
}
Expand Down Expand Up @@ -183,6 +233,9 @@ func (s *Schema) DisableRequiredProperties() {
if s.Then != nil {
s.Then.DisableRequiredProperties()
}
if s.Not != nil {
s.Not.DisableRequiredProperties()
}
}

// ToJson converts the data to raw json
Expand Down Expand Up @@ -359,18 +412,16 @@ func typeFromTag(tag string) ([]string, error) {
return []string{}, fmt.Errorf("unsupported yaml tag found: %s", tag)
}

// FixRequiredProperties iterates over the properties and checks if required has a boolean value.
// Then the property is added to the parents required property list
func FixRequiredProperties(schema *Schema) error {
if schema.Properties != nil {
requiredProperties := []string{}
for propName, propValue := range schema.Properties {
FixRequiredProperties(propValue)
if propValue.Required {
requiredProperties = append(requiredProperties, propName)
if propValue.Required.Bool && !slices.Contains(schema.Required.Strings, propName) {
schema.Required.Strings = append(schema.Required.Strings, propName)
}
}
if len(requiredProperties) > 0 {
schema.RequiredProperties = requiredProperties
}
if !slices.Contains(schema.Type, "object") {
// If .Properties is set, type must be object
schema.Type = []string{"object"}
Expand Down Expand Up @@ -417,6 +468,10 @@ func FixRequiredProperties(schema *Schema) error {
}
}

if schema.Not != nil {
FixRequiredProperties(schema.Not)
}

return nil
}

Expand All @@ -435,11 +490,11 @@ func GetSchemaFromComment(comment string) (Schema, string, error) {
continue
}
if insideSchemaBlock {
content := strings.TrimLeft(strings.TrimPrefix(line, CommentPrefix), " ")
rawSchema = append(rawSchema, strings.TrimLeft(strings.TrimPrefix(content, CommentPrefix), " "))
content := strings.TrimPrefix(line, CommentPrefix)
rawSchema = append(rawSchema, strings.TrimPrefix(strings.TrimPrefix(content, CommentPrefix), " "))
result.Set()
} else {
description = append(description, strings.TrimLeft(strings.TrimPrefix(line, CommentPrefix), " "))
description = append(description, strings.TrimPrefix(strings.TrimPrefix(line, CommentPrefix), " "))
}
}

Expand All @@ -463,35 +518,32 @@ func YamlToSchema(
dontRemoveHelmDocsPrefix bool,
skipAutoGeneration *SkipAutoGenerationConfig,
parentRequiredProperties *[]string,
) Schema {
var schema Schema
) *Schema {
schema := NewSchema("object")

switch node.Kind {
case yaml.DocumentNode:
if len(node.Content) != 1 {
log.Fatalf("Strange yaml document found:\n%v\n", node.Content[:])
}

requiredProperties := []string{}

schema.Type = []string{"object"}
schema.Schema = "http://json-schema.org/draft-07/schema#"
schema.Properties = YamlToSchema(
node.Content[0],
keepFullComment,
dontRemoveHelmDocsPrefix,
skipAutoGeneration,
&requiredProperties,
&schema.Required.Strings,
).Properties

if _, ok := schema.Properties["global"]; !ok {
// global key must be present, otherwise helm lint will fail
if schema.Properties == nil {
schema.Properties = make(map[string]*Schema)
}
schema.Properties["global"] = &Schema{
Type: []string{"object"},
}
schema.Properties["global"] = NewSchema(
"object",
)
if !skipAutoGeneration.Title {
schema.Properties["global"].Title = "global"
}
Expand All @@ -500,9 +552,6 @@ func YamlToSchema(
}
}

if len(requiredProperties) > 0 {
schema.RequiredProperties = requiredProperties
}
// always disable on top level
if !skipAutoGeneration.AdditionalProperties {
schema.AdditionalProperties = new(bool)
Expand Down Expand Up @@ -552,8 +601,10 @@ func YamlToSchema(
if keyNodeSchema.Ref == "" {

// Add key to required array of parent
if keyNodeSchema.Required || (!skipAutoGeneration.Required && !keyNodeSchema.HasData) {
*parentRequiredProperties = append(*parentRequiredProperties, keyNode.Value)
if keyNodeSchema.Required.Bool || (len(keyNodeSchema.Required.Strings) == 0 && !skipAutoGeneration.Required && !keyNodeSchema.HasData) {
if !slices.Contains(*parentRequiredProperties, keyNode.Value) {
*parentRequiredProperties = append(*parentRequiredProperties, keyNode.Value)
}
}

if !skipAutoGeneration.AdditionalProperties && valueNode.Kind == yaml.MappingNode &&
Expand All @@ -578,44 +629,40 @@ func YamlToSchema(

// If the value is another map and no properties are set, get them from default values
if valueNode.Kind == yaml.MappingNode && keyNodeSchema.Properties == nil {
requiredProperties := []string{}
keyNodeSchema.Properties = YamlToSchema(
valueNode,
keepFullComment,
dontRemoveHelmDocsPrefix,
skipAutoGeneration,
&requiredProperties,
&keyNodeSchema.Required.Strings,
).Properties
if len(requiredProperties) > 0 {
keyNodeSchema.RequiredProperties = requiredProperties
}
} else if valueNode.Kind == yaml.SequenceNode && keyNodeSchema.Items == nil {
// If the value is a sequence, but no items are predefined
var seqSchema Schema
seqSchema := NewSchema("array")

for _, itemNode := range valueNode.Content {
if itemNode.Kind == yaml.ScalarNode {
itemNodeType, err := typeFromTag(itemNode.Tag)
if err != nil {
log.Fatal(err)
}
seqSchema.AnyOf = append(seqSchema.AnyOf, &Schema{Type: itemNodeType})
seqSchema.AnyOf = append(seqSchema.AnyOf, NewSchema(itemNodeType[0]))
} else {
itemRequiredProperties := []string{}
itemSchema := YamlToSchema(itemNode, keepFullComment, dontRemoveHelmDocsPrefix, skipAutoGeneration, &itemRequiredProperties)

if len(itemRequiredProperties) > 0 {
itemSchema.RequiredProperties = itemRequiredProperties
for _, req := range itemRequiredProperties {
itemSchema.Required.Strings = append(itemSchema.Required.Strings, req)
}

if !skipAutoGeneration.AdditionalProperties && itemNode.Kind == yaml.MappingNode && (!itemSchema.HasData || itemSchema.AdditionalProperties == nil) {
itemSchema.AdditionalProperties = new(bool)
}

seqSchema.AnyOf = append(seqSchema.AnyOf, &itemSchema)
seqSchema.AnyOf = append(seqSchema.AnyOf, itemSchema)
}
}
keyNodeSchema.Items = &seqSchema
keyNodeSchema.Items = seqSchema

// Because the `required` field isn't valid jsonschema (but just a helper boolean)
// we must convert them to valid requiredProperties fields
Expand Down
2 changes: 1 addition & 1 deletion pkg/schema/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func Worker(
continue
}

result.Schema = YamlToSchema(&values, keepFullComment, dontRemoveHelmDocsPrefix, skipAutoGenerationConfig, nil)
result.Schema = *YamlToSchema(&values, keepFullComment, dontRemoveHelmDocsPrefix, skipAutoGenerationConfig, nil)

results <- result
}
Expand Down

0 comments on commit 2f0b76f

Please sign in to comment.