Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions datamodel/high/base/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ type Schema struct {
// 3.1+ only, JSON Schema 2020-12 dynamic reference for recursive schema resolution
DynamicRef string `json:"$dynamicRef,omitempty" yaml:"$dynamicRef,omitempty"`

// 3.1+ only, JSON Schema 2020-12 $comment - explanatory notes without affecting validation
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"`

// 3.1+ only, JSON Schema 2020-12 contentSchema - describes structure of decoded content
ContentSchema *SchemaProxy `json:"contentSchema,omitempty" yaml:"contentSchema,omitempty"`

// 3.1+ only, JSON Schema 2020-12 $vocabulary - defines available vocabularies in meta-schemas
Vocabulary *orderedmap.Map[string, bool] `json:"$vocabulary,omitempty" yaml:"$vocabulary,omitempty"`

// Compatible with all versions
Not *SchemaProxy `json:"not,omitempty" yaml:"not,omitempty"`
Properties *orderedmap.Map[string, *SchemaProxy] `json:"properties,omitempty" yaml:"properties,omitempty"`
Expand All @@ -106,6 +115,8 @@ type Schema struct {
Enum []*yaml.Node `json:"enum,omitempty" yaml:"enum,omitempty"`
AdditionalProperties *DynamicValue[*SchemaProxy, bool] `json:"additionalProperties,renderZero,omitempty" yaml:"additionalProperties,renderZero,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ContentEncoding string `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"`
ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"`
Default *yaml.Node `json:"default,omitempty" yaml:"default,renderZero,omitempty"`
Const *yaml.Node `json:"const,omitempty" yaml:"const,renderZero,omitempty"`
Nullable *bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
Expand Down Expand Up @@ -277,6 +288,8 @@ func NewSchema(schema *base.Schema) *Schema {
s.AdditionalProperties = additionalProperties

s.Description = schema.Description.Value
s.ContentEncoding = schema.ContentEncoding.Value
s.ContentMediaType = schema.ContentMediaType.Value
s.Default = schema.Default.Value
s.Const = schema.Const.Value
if !schema.Nullable.IsEmpty() {
Expand Down Expand Up @@ -327,6 +340,22 @@ func NewSchema(schema *base.Schema) *Schema {
if !schema.DynamicRef.IsEmpty() {
s.DynamicRef = schema.DynamicRef.Value
}
if !schema.Comment.IsEmpty() {
s.Comment = schema.Comment.Value
}
if !schema.ContentSchema.IsEmpty() {
s.ContentSchema = NewSchemaProxy(&lowmodel.NodeReference[*base.SchemaProxy]{
ValueNode: schema.ContentSchema.ValueNode,
Value: schema.ContentSchema.Value,
})
}
if schema.Vocabulary.Value != nil {
vocabularyMap := orderedmap.New[string, bool]()
for k, v := range schema.Vocabulary.Value.FromOldest() {
vocabularyMap.Set(k.Value, v.Value)
}
s.Vocabulary = vocabularyMap
}

var enum []*yaml.Node
for i := range schema.Enum.Value {
Expand Down
116 changes: 116 additions & 0 deletions datamodel/high/base/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1907,3 +1907,119 @@ description: A schema without $id`

assert.Equal(t, "", highSch.Id)
}

// TestNewSchema_Comment tests that $comment is populated in high-level schema
func TestNewSchema_Comment(t *testing.T) {
yml := `type: object
$comment: This is a test comment explaining the schema purpose
description: A schema with $comment`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)

var lowSch lowbase.Schema
_ = low.BuildModel(idxNode.Content[0], &lowSch)
_ = lowSch.Build(context.Background(), idxNode.Content[0], nil)

highSch := NewSchema(&lowSch)

assert.Equal(t, "This is a test comment explaining the schema purpose", highSch.Comment)
assert.Equal(t, "object", highSch.Type[0])
}

// TestNewSchema_ContentSchema tests that contentSchema is populated in high-level schema
func TestNewSchema_ContentSchema(t *testing.T) {
yml := `type: string
contentMediaType: application/json
contentSchema:
type: object
properties:
name:
type: string`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)

var lowSch lowbase.Schema
_ = low.BuildModel(idxNode.Content[0], &lowSch)
_ = lowSch.Build(context.Background(), idxNode.Content[0], nil)

highSch := NewSchema(&lowSch)

assert.NotNil(t, highSch.ContentSchema)
contentSch := highSch.ContentSchema.Schema()
assert.NotNil(t, contentSch)
assert.Equal(t, "object", contentSch.Type[0])
assert.NotNil(t, contentSch.Properties)
assert.Equal(t, 1, contentSch.Properties.Len())
}

// TestNewSchema_Vocabulary tests that $vocabulary is populated in high-level schema
func TestNewSchema_Vocabulary(t *testing.T) {
yml := `$vocabulary:
"https://json-schema.org/draft/2020-12/vocab/core": true
"https://json-schema.org/draft/2020-12/vocab/validation": false
"https://json-schema.org/draft/2020-12/vocab/applicator": true`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)

var lowSch lowbase.Schema
_ = low.BuildModel(idxNode.Content[0], &lowSch)
_ = lowSch.Build(context.Background(), idxNode.Content[0], nil)

highSch := NewSchema(&lowSch)

assert.NotNil(t, highSch.Vocabulary)
assert.Equal(t, 3, highSch.Vocabulary.Len())

// Check specific vocabulary entries
for k, v := range highSch.Vocabulary.FromOldest() {
switch k {
case "https://json-schema.org/draft/2020-12/vocab/core":
assert.True(t, v)
case "https://json-schema.org/draft/2020-12/vocab/validation":
assert.False(t, v)
case "https://json-schema.org/draft/2020-12/vocab/applicator":
assert.True(t, v)
}
}
}

// TestNewSchema_ContentEncoding tests that contentEncoding is populated in high-level schema
func TestNewSchema_ContentEncoding(t *testing.T) {
yml := `type: string
contentEncoding: base64
description: A base64 encoded string`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)

var lowSch lowbase.Schema
_ = low.BuildModel(idxNode.Content[0], &lowSch)
_ = lowSch.Build(context.Background(), idxNode.Content[0], nil)

highSch := NewSchema(&lowSch)

assert.Equal(t, "base64", highSch.ContentEncoding)
assert.Equal(t, "string", highSch.Type[0])
}

// TestNewSchema_ContentMediaType tests that contentMediaType is populated in high-level schema
func TestNewSchema_ContentMediaType(t *testing.T) {
yml := `type: string
contentMediaType: image/png
description: A binary image encoded as string`

var idxNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &idxNode)

var lowSch lowbase.Schema
_ = low.BuildModel(idxNode.Content[0], &lowSch)
_ = lowSch.Build(context.Background(), idxNode.Content[0], nil)

highSch := NewSchema(&lowSch)

assert.Equal(t, "image/png", highSch.ContentMediaType)
assert.Equal(t, "string", highSch.Type[0])
}
3 changes: 3 additions & 0 deletions datamodel/low/base/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const (
AnchorLabel = "$anchor"
DynamicAnchorLabel = "$dynamicAnchor"
DynamicRefLabel = "$dynamicRef"
CommentLabel = "$comment"
ContentSchemaLabel = "contentSchema"
VocabularyLabel = "$vocabulary"
)

/*
Expand Down
82 changes: 81 additions & 1 deletion datamodel/low/base/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ type Schema struct {
Description low.NodeReference[string]
ContentEncoding low.NodeReference[string]
ContentMediaType low.NodeReference[string]
ContentSchema low.NodeReference[*SchemaProxy] // JSON Schema 2020-12 contentSchema
Comment low.NodeReference[string] // JSON Schema 2020-12 $comment
Vocabulary low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[bool]]] // JSON Schema 2020-12 $vocabulary
Default low.NodeReference[*yaml.Node]
Const low.NodeReference[*yaml.Node]
Nullable low.NodeReference[bool]
Expand Down Expand Up @@ -506,6 +509,32 @@ func (s *Schema) hash(quick bool) [32]byte {
sb.WriteString(s.DynamicRef.Value)
sb.WriteByte('|')
}
if !s.Comment.IsEmpty() {
sb.WriteString(s.Comment.Value)
sb.WriteByte('|')
}
if !s.ContentSchema.IsEmpty() {
sb.WriteString(low.GenerateHashString(s.ContentSchema.Value))
sb.WriteByte('|')
}
if s.Vocabulary.Value != nil {
// sort vocabulary keys for deterministic hashing
// pre-allocate with known size for better memory efficiency
vocabSize := orderedmap.Len(s.Vocabulary.Value)
vocabKeys := make([]string, 0, vocabSize)
vocabMap := make(map[string]bool, vocabSize)
for k, v := range s.Vocabulary.Value.FromOldest() {
vocabKeys = append(vocabKeys, k.Value)
vocabMap[k.Value] = v.Value
}
sort.Strings(vocabKeys)
for _, k := range vocabKeys {
sb.WriteString(k)
sb.WriteByte(':')
sb.WriteString(fmt.Sprint(vocabMap[k]))
sb.WriteByte('|')
}
}

// Process dependent schemas and pattern properties
for _, hash := range low.AppendMapHashes(nil, orderedmap.SortAlpha(s.DependentSchemas.Value)) {
Expand Down Expand Up @@ -867,6 +896,41 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
}
}

// handle $comment if set. (JSON Schema 2020-12)
_, commentLabel, commentNode := utils.FindKeyNodeFullTop(CommentLabel, root.Content)
if commentNode != nil {
s.Comment = low.NodeReference[string]{
Value: commentNode.Value, KeyNode: commentLabel, ValueNode: commentNode,
}
}

// handle $vocabulary if set. (JSON Schema 2020-12 - typically in meta-schemas)
_, vocabLabel, vocabNode := utils.FindKeyNodeFullTop(VocabularyLabel, root.Content)
if vocabNode != nil && utils.IsNodeMap(vocabNode) {
vocabularyMap := orderedmap.New[low.KeyReference[string], low.ValueReference[bool]]()
var currentKey *yaml.Node
for i, node := range vocabNode.Content {
if i%2 == 0 {
currentKey = node
continue
}
// use strconv.ParseBool for robust boolean parsing (handles "true", "false", "1", "0", etc.)
boolVal, _ := strconv.ParseBool(node.Value)
vocabularyMap.Set(low.KeyReference[string]{
KeyNode: currentKey,
Value: currentKey.Value,
}, low.ValueReference[bool]{
Value: boolVal,
ValueNode: node,
})
}
s.Vocabulary = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[bool]]]{
Value: vocabularyMap,
KeyNode: vocabLabel,
ValueNode: vocabNode,
}
}

// handle example if set. (3.0)
_, expLabel, expNode := utils.FindKeyNodeFullTop(ExampleLabel, root.Content)
if expNode != nil {
Expand Down Expand Up @@ -1054,7 +1118,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
}

var allOf, anyOf, oneOf, prefixItems []low.ValueReference[*SchemaProxy]
var items, not, contains, sif, selse, sthen, propertyNames, unevalItems, unevalProperties, addProperties low.ValueReference[*SchemaProxy]
var items, not, contains, sif, selse, sthen, propertyNames, unevalItems, unevalProperties, addProperties, contentSch low.ValueReference[*SchemaProxy]

_, allOfLabel, allOfValue := utils.FindKeyNodeFullTop(AllOfLabel, root.Content)
_, anyOfLabel, anyOfValue := utils.FindKeyNodeFullTop(AnyOfLabel, root.Content)
Expand All @@ -1069,6 +1133,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
_, unevalItemsLabel, unevalItemsValue := utils.FindKeyNodeFullTop(UnevaluatedItemsLabel, root.Content)
_, unevalPropsLabel, unevalPropsValue := utils.FindKeyNodeFullTop(UnevaluatedPropertiesLabel, root.Content)
_, addPropsLabel, addPropsValue := utils.FindKeyNodeFullTop(AdditionalPropertiesLabel, root.Content)
_, contentSchLabel, contentSchValue := utils.FindKeyNodeFullTop(ContentSchemaLabel, root.Content)

errorChan := make(chan error)
allOfChan := make(chan schemaProxyBuildResult)
Expand All @@ -1085,6 +1150,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
unevalItemsChan := make(chan schemaProxyBuildResult)
unevalPropsChan := make(chan schemaProxyBuildResult)
addPropsChan := make(chan schemaProxyBuildResult)
contentSchChan := make(chan schemaProxyBuildResult)

totalBuilds := countSubSchemaItems(allOfValue) +
countSubSchemaItems(anyOfValue) +
Expand Down Expand Up @@ -1143,6 +1209,10 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
totalBuilds++
go buildSchema(ctx, addPropsChan, addPropsLabel, addPropsValue, errorChan, idx)
}
if contentSchValue != nil {
totalBuilds++
go buildSchema(ctx, contentSchChan, contentSchLabel, contentSchValue, errorChan, idx)
}

completeCount := 0
for completeCount < totalBuilds {
Expand Down Expand Up @@ -1191,6 +1261,9 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
case r := <-addPropsChan:
completeCount++
addProperties = r.v
case r := <-contentSchChan:
completeCount++
contentSch = r.v
}
}

Expand Down Expand Up @@ -1298,6 +1371,13 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde
ValueNode: addPropsValue,
}
}
if !contentSch.IsEmpty() {
s.ContentSchema = low.NodeReference[*SchemaProxy]{
Value: contentSch.Value,
KeyNode: contentSchLabel,
ValueNode: contentSchValue,
}
}
return nil
}

Expand Down
Loading