From ebddf6ed75ee99e371a1014a2b0765926e4e5e0f Mon Sep 17 00:00:00 2001
From: Aleksandar Stojanov <me@fnd.works>
Date: Fri, 12 Apr 2024 17:49:21 +0200
Subject: [PATCH] add patternProperties from comments

Signed-off-by: Aleksandar Stojanov <me@fnd.works>
---
 pkg/parser.go             |  6 +++++
 pkg/parser_test.go        | 13 ++++++-----
 pkg/schema.go             | 46 ++++++++++++++++++++++-----------------
 testdata/full.schema.json |  5 +++++
 testdata/full.yaml        |  2 +-
 5 files changed, 45 insertions(+), 27 deletions(-)

diff --git a/pkg/parser.go b/pkg/parser.go
index ced7e49..970e2fa 100644
--- a/pkg/parser.go
+++ b/pkg/parser.go
@@ -50,6 +50,9 @@ func mergeSchemas(dest, src *Schema) *Schema {
 	if src.MinProperties != nil {
 		dest.MinProperties = src.MinProperties
 	}
+	if src.PatternProperties != nil {
+		dest.PatternProperties = src.PatternProperties
+	}
 	if src.Title != "" {
 		dest.Title = src.Title
 	}
@@ -149,6 +152,9 @@ func convertSchemaToMapRec(schema *Schema, visited map[uintptr]bool) (map[string
 	if schema.MinProperties != nil {
 		schemaMap["minProperties"] = *schema.MinProperties
 	}
+	if schema.PatternProperties != nil {
+		schemaMap["patternProperties"] = schema.PatternProperties
+	}
 	if schema.Title != "" {
 		schemaMap["title"] = schema.Title
 	}
diff --git a/pkg/parser_test.go b/pkg/parser_test.go
index f59864e..a75916d 100644
--- a/pkg/parser_test.go
+++ b/pkg/parser_test.go
@@ -95,12 +95,13 @@ func TestMergeSchemas(t *testing.T) {
 			dest: &Schema{Type: "object", Properties: map[string]*Schema{
 				"foo": {Type: "integer"},
 			}},
-			src: &Schema{Type: "object", Properties: map[string]*Schema{
-				"bar": {Type: "string"},
+			src: &Schema{Type: "object", PatternProperties: map[string]*Schema{
+				"^[a-z]$": {Type: "integer"},
 			}},
 			want: &Schema{Type: "object", Properties: map[string]*Schema{
 				"foo": {Type: "integer"},
-				"bar": {Type: "string"},
+			}, PatternProperties: map[string]*Schema{
+				"^[a-z]$": {Type: "integer"},
 			}},
 		},
 		{
@@ -150,9 +151,9 @@ func TestMergeSchemas(t *testing.T) {
 		},
 		{
 			name: "object properties",
-			dest: &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10)},
-			src:  &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10)},
-			want: &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10)},
+			dest: &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10), PatternProperties: map[string]*Schema{"^.$": {Type: "string"}}},
+			src:  &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10), PatternProperties: map[string]*Schema{"^.$": {Type: "string"}}},
+			want: &Schema{Type: "object", MinProperties: uint64Ptr(1), MaxProperties: uint64Ptr(10), PatternProperties: map[string]*Schema{"^.$": {Type: "string"}}},
 		},
 		{
 			name: "meta-data properties",
diff --git a/pkg/schema.go b/pkg/schema.go
index fa8abee..d6dcf6c 100644
--- a/pkg/schema.go
+++ b/pkg/schema.go
@@ -10,26 +10,27 @@ import (
 )
 
 type Schema struct {
-	Type          interface{}        `json:"type,omitempty"`
-	Enum          []any              `json:"enum,omitempty"`
-	MultipleOf    *float64           `json:"multipleOf,omitempty"`
-	Maximum       *float64           `json:"maximum,omitempty"`
-	Minimum       *float64           `json:"minimum,omitempty"`
-	MaxLength     *uint64            `json:"maxLength,omitempty"`
-	MinLength     *uint64            `json:"minLength,omitempty"`
-	Pattern       string             `json:"pattern,omitempty"`
-	MaxItems      *uint64            `json:"maxItems,omitempty"`
-	MinItems      *uint64            `json:"minItems,omitempty"`
-	UniqueItems   bool               `json:"uniqueItems,omitempty"`
-	MaxProperties *uint64            `json:"maxProperties,omitempty"`
-	MinProperties *uint64            `json:"minProperties,omitempty"`
-	Required      []string           `json:"required,omitempty"`
-	Items         *Schema            `json:"items,omitempty"`
-	Properties    map[string]*Schema `json:"properties,omitempty"`
-	Title         string             `json:"title,omitempty"`
-	Description   string             `json:"description,omitempty"`
-	ReadOnly      bool               `json:"readOnly,omitempty"`
-	Default       interface{}        `json:"default,omitempty"`
+	Type              interface{}        `json:"type,omitempty"`
+	Enum              []any              `json:"enum,omitempty"`
+	MultipleOf        *float64           `json:"multipleOf,omitempty"`
+	Maximum           *float64           `json:"maximum,omitempty"`
+	Minimum           *float64           `json:"minimum,omitempty"`
+	MaxLength         *uint64            `json:"maxLength,omitempty"`
+	MinLength         *uint64            `json:"minLength,omitempty"`
+	Pattern           string             `json:"pattern,omitempty"`
+	MaxItems          *uint64            `json:"maxItems,omitempty"`
+	MinItems          *uint64            `json:"minItems,omitempty"`
+	UniqueItems       bool               `json:"uniqueItems,omitempty"`
+	MaxProperties     *uint64            `json:"maxProperties,omitempty"`
+	MinProperties     *uint64            `json:"minProperties,omitempty"`
+	PatternProperties interface{}        `json:"patternProperties,omitempty"`
+	Required          []string           `json:"required,omitempty"`
+	Items             *Schema            `json:"items,omitempty"`
+	Properties        map[string]*Schema `json:"properties,omitempty"`
+	Title             string             `json:"title,omitempty"`
+	Description       string             `json:"description,omitempty"`
+	ReadOnly          bool               `json:"readOnly,omitempty"`
+	Default           interface{}        `json:"default,omitempty"`
 }
 
 func getKind(value string) string {
@@ -156,6 +157,11 @@ func processComment(schema *Schema, comment string) (isRequired bool) {
 				if v, err := strconv.ParseUint(value, 10, 64); err == nil {
 					schema.MinProperties = &v
 				}
+			case "patternProperties":
+				var jsonObject interface{}
+				if err := json.Unmarshal([]byte(value), &jsonObject); err == nil {
+					schema.PatternProperties = jsonObject
+				}
 			case "required":
 				if strings.TrimSpace(value) == "true" {
 					isRequired = strings.TrimSpace(value) == "true"
diff --git a/testdata/full.schema.json b/testdata/full.schema.json
index 157870e..439a59d 100644
--- a/testdata/full.schema.json
+++ b/testdata/full.schema.json
@@ -11,6 +11,11 @@
                             "items": {
                                 "properties": {
                                     "preference": {
+                                        "patternProperties": {
+                                            "^[a-z]$": {
+                                                "type": "string"
+                                            }
+                                        },
                                         "properties": {
                                             "matchExpressions": {
                                                 "items": {
diff --git a/testdata/full.yaml b/testdata/full.yaml
index c407166..b0eb9c5 100644
--- a/testdata/full.yaml
+++ b/testdata/full.yaml
@@ -39,7 +39,7 @@ affinity:
           - antarctica-west1
     preferredDuringSchedulingIgnoredDuringExecution:
     - weight: 1
-      preference:
+      preference: # @schema patternProperties: {"^[a-z]$": {"type": "string"}}
         matchExpressions:
         - key: another-node-label-key
           operator: In