From 72cf82bdad0fda7d1f2dd6b76fb0191687fe2af4 Mon Sep 17 00:00:00 2001 From: quobix Date: Sun, 29 Mar 2026 11:24:51 -0400 Subject: [PATCH 01/11] adding more rendering options for mocking --- renderer/mock_generator.go | 83 ++- renderer/mock_generator_test.go | 107 ++++ renderer/mock_generator_xml.go | 309 ++++++++++ renderer/mock_generator_xml_test.go | 841 ++++++++++++++++++++++++++++ renderer/schema_renderer.go | 61 +- renderer/schema_renderer_test.go | 298 ++++++++++ 6 files changed, 1671 insertions(+), 28 deletions(-) create mode 100644 renderer/mock_generator_xml.go create mode 100644 renderer/mock_generator_xml_test.go diff --git a/renderer/mock_generator.go b/renderer/mock_generator.go index 3ebf084d4..4342d8869 100644 --- a/renderer/mock_generator.go +++ b/renderer/mock_generator.go @@ -24,6 +24,7 @@ type MockType int const ( JSON MockType = iota YAML + XML ) // MockGenerator is used to generate mocks for high-level mockable structs or *base.Schema pointers. @@ -68,12 +69,57 @@ func (mg *MockGenerator) DisableRequiredCheck() { mg.renderer.DisableRequiredCheck() } +// SetUnresolvedRefHandler sets a callback that is invoked when a $ref cannot be resolved during mock rendering. +func (mg *MockGenerator) SetUnresolvedRefHandler(handler UnresolvedRefHandler) { + mg.renderer.SetUnresolvedRefHandler(handler) +} + // SetSeed sets a specific seed for the random number generator used by this mock generator. // This is useful for generating deterministic mocks for testing purposes. func (mg *MockGenerator) SetSeed(seed int64) { mg.renderer.SetSeed(seed) } +// extractSchema pulls the *base.Schema from a mockable struct or direct *base.Schema. +// Returns an error for unresolved refs or build failures — preserving existing error behavior. +func (mg *MockGenerator) extractSchema(mock any, v reflect.Value) (*highbase.Schema, error) { + switch reflect.TypeOf(mock) { + case reflect.TypeOf(&highbase.Schema{}): + return mock.(*highbase.Schema), nil + default: + schemaField := v.FieldByName(Schema) + if !schemaField.IsValid() { + return nil, nil + } + if sv, ok := schemaField.Interface().(*highbase.Schema); ok && sv != nil { + return sv, nil + } + if sv, ok := schemaField.Interface().(*highbase.SchemaProxy); ok && sv != nil { + schema := sv.Schema() + if schema == nil { + if sv.IsReference() { + return nil, fmt.Errorf("unable to resolve schema reference '%s' for mock generation", + sv.GetReference()) + } + if err := sv.GetBuildError(); err != nil { + return nil, fmt.Errorf("unable to build schema for mock generation: %w", err) + } + } + return schema, nil + } + } + return nil, nil +} + +// renderForType dispatches rendering based on the configured mock type. +// For XML, it uses RenderXML with schema context; for JSON/YAML it uses renderMock. +func (mg *MockGenerator) renderForType(value any, schema *highbase.Schema) []byte { + if mg.mockType == XML { + return mg.RenderXML(value, schema) + } + return mg.renderMock(value) +} + // GenerateMock generates a mock for a given high-level mockable struct. The mockable struct must contain the following fields: // Example: any type, this is the default example to use if no examples are present. // Examples: *orderedmap.Map[string, *base.Example], this is a map of examples keyed by name. @@ -106,6 +152,9 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { "fields (%s, %s)", fieldCount, Example, Examples) } + // Extract schema EARLY so Example/Examples paths can use it for XML rendering + schemaValue, schemaErr := mg.extractSchema(mock, v) + var fallbackExample *highbase.Example = nil // trying to find a named example examples := v.FieldByName(Examples) @@ -116,7 +165,7 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { examplesMap := examplesValue.(*orderedmap.Map[string, *highbase.Example]) if examplesMap.Len() > 0 { if example, ok := examplesMap.Get(name); ok { - return mg.renderMock(example.Value), nil + return mg.renderForType(example.Value, schemaValue), nil } else { //take the first example from the list fallbackExample = examplesMap.Oldest().Value @@ -138,32 +187,18 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { } if ex != nil { // try and serialize the example value (very hacky since ex can be anything) - return mg.renderMock(ex), nil + return mg.renderForType(ex, schemaValue), nil } } // rendering fallback if it's not nil if fallbackExample != nil { - return mg.renderMock(fallbackExample.Value), nil + return mg.renderForType(fallbackExample.Value, schemaValue), nil } - // no examples? no problem, we can try and generate a mock from the schema. - // check if this is a SchemaProxy, if not, then see if it has a Schema, if not, then we can't generate a mock. - var schemaValue *highbase.Schema - switch reflect.TypeOf(mock) { - case reflect.TypeOf(&highbase.Schema{}): - schemaValue = mock.(*highbase.Schema) - default: - if sv, ok := v.FieldByName(Schema).Interface().(*highbase.Schema); ok { - if sv != nil { - schemaValue = sv - } - } - if sv, ok := v.FieldByName(Schema).Interface().(*highbase.SchemaProxy); ok { - if sv != nil { - schemaValue = sv.Schema() - } - } + // Surface schema extraction errors only after example paths have had their chance + if schemaErr != nil { + return nil, schemaErr } if schemaValue != nil { @@ -174,17 +209,17 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { // try and convert the example to an integer if i, err := strconv.Atoi(name); err == nil { if i < len(schemaValue.Examples) { - return mg.renderMock(schemaValue.Examples[i]), nil + return mg.renderForType(schemaValue.Examples[i], schemaValue), nil } } } // if the name is empty, just return the first example - return mg.renderMock(schemaValue.Examples[0]), nil + return mg.renderForType(schemaValue.Examples[0], schemaValue), nil } // check the example field if schemaValue.Example != nil { - return mg.renderMock(schemaValue.Example), nil + return mg.renderForType(schemaValue.Example, schemaValue), nil } // render the schema as our last hope. @@ -192,7 +227,7 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { if renderMap == nil { return nil, fmt.Errorf("unable to render schema for mock, it's empty") } - return mg.renderMock(renderMap), nil + return mg.renderForType(renderMap, schemaValue), nil } return nil, nil } diff --git a/renderer/mock_generator_test.go b/renderer/mock_generator_test.go index 198fdd8f0..6a09bba3d 100644 --- a/renderer/mock_generator_test.go +++ b/renderer/mock_generator_test.go @@ -597,3 +597,110 @@ func TestMockGenerator_GenerateMock_GetInline(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "inline example", strings.TrimSpace(string(mock))) } + +func TestMockGenerator_UnresolvedRefProperty(t *testing.T) { + // Construct a schema programmatically with one inline and one unresolved ref property + props := orderedmap.New[string, *base.SchemaProxy]() + props.Set("name", base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + ParentProxy: base.CreateSchemaProxy(&base.Schema{}), + })) + props.Set("broken", base.CreateSchemaProxyRef("#/components/schemas/Missing")) + + schema := &base.Schema{ + Type: []string{"object"}, + Properties: props, + ParentProxy: base.CreateSchemaProxy(&base.Schema{}), + } + schemaProxy := base.CreateSchemaProxy(schema) + + fake := &fakeMockable{ + Schema: schemaProxy, + Example: nil, + Examples: nil, + } + mg := NewMockGenerator(JSON) + mg.DisableRequiredCheck() + mock, err := mg.GenerateMock(fake, "") + assert.NoError(t, err) + + var m map[string]any + err = json.Unmarshal(mock, &m) + assert.NoError(t, err) + assert.IsType(t, "", m["name"]) + // "broken" should be null in JSON output + val, exists := m["broken"] + assert.True(t, exists) + assert.Nil(t, val) +} + +func TestMockGenerator_SetUnresolvedRefHandler(t *testing.T) { + props := orderedmap.New[string, *base.SchemaProxy]() + props.Set("broken", base.CreateSchemaProxyRef("#/components/schemas/Missing")) + + schema := &base.Schema{ + Type: []string{"object"}, + Properties: props, + ParentProxy: base.CreateSchemaProxy(&base.Schema{}), + } + schemaProxy := base.CreateSchemaProxy(schema) + + fake := &fakeMockable{ + Schema: schemaProxy, + Example: nil, + Examples: nil, + } + mg := NewMockGenerator(JSON) + mg.DisableRequiredCheck() + + var callbackName string + mg.SetUnresolvedRefHandler(func(name string, proxy *base.SchemaProxy, err error) { + callbackName = name + }) + + _, err := mg.GenerateMock(fake, "") + assert.NoError(t, err) + assert.Equal(t, "broken", callbackName) +} + +func TestMockGenerator_TopLevelUnresolvedRef(t *testing.T) { + mg := NewMockGenerator(JSON) + fake := &fakeMockable{ + Schema: base.CreateSchemaProxyRef("#/components/schemas/Missing"), + Example: nil, + Examples: nil, + } + mock, err := mg.GenerateMock(fake, "") + assert.Error(t, err) + assert.Nil(t, mock) + assert.Contains(t, err.Error(), "#/components/schemas/Missing") + assert.Contains(t, err.Error(), "unable to resolve schema reference") +} + +func TestMockGenerator_TopLevelBuildError(t *testing.T) { + // Create a low-level SchemaProxy with a nil value node. When Schema() is called, + // the low-level Schema.Build receives nil root and returns "cannot build schema from a nil node". + // This sets buildError on the proxy while IsReference() remains false. + sp := &lowbase.SchemaProxy{} + _ = sp.Build(context.Background(), nil, nil, nil) + + highProxy := base.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: sp, + }) + + // Verify preconditions + assert.Nil(t, highProxy.Schema()) + assert.False(t, highProxy.IsReference()) + assert.NotNil(t, highProxy.GetBuildError()) + + mg := NewMockGenerator(JSON) + fake := &fakeMockable{ + Schema: highProxy, + Example: nil, + Examples: nil, + } + mock, err := mg.GenerateMock(fake, "") + assert.Error(t, err) + assert.Nil(t, mock) + assert.Contains(t, err.Error(), "unable to build schema for mock generation") +} diff --git a/renderer/mock_generator_xml.go b/renderer/mock_generator_xml.go new file mode 100644 index 000000000..c75107802 --- /dev/null +++ b/renderer/mock_generator_xml.go @@ -0,0 +1,309 @@ +// Copyright 2024-2026 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package renderer + +import ( + "bytes" + "encoding/xml" + "fmt" + "regexp" + "unicode" + + highbase "github.com/pb33f/libopenapi/datamodel/high/base" + "go.yaml.in/yaml/v4" +) + +// xmlNameRegex matches characters that are NOT valid in XML names. +var xmlNameRegex = regexp.MustCompile(`[^a-zA-Z0-9._\-:]`) + +// sanitizeXMLName makes a string safe for use as an XML element or attribute name. +// Invalid characters are replaced with '_'. Names starting with a digit get a '_' prefix. +func sanitizeXMLName(name string) string { + if name == "" { + return "_" + } + s := xmlNameRegex.ReplaceAllString(name, "_") + if len(s) > 0 && (unicode.IsDigit(rune(s[0])) || s[0] == '-' || s[0] == '.') { + s = "_" + s + } + return s +} + +// resolveNodeType determines the effective nodeType for a property schema, considering +// both the OpenAPI 3.2+ nodeType field and the deprecated attribute/wrapped fields. +func resolveNodeType(propSchema *highbase.Schema) string { + if propSchema == nil || propSchema.XML == nil { + return "element" + } + x := propSchema.XML + if x.NodeType != "" { + return x.NodeType + } + // Legacy backward compat + if x.Attribute { + return "attribute" + } + return "element" +} + +// resolveElementName determines the XML element name for a property, using +// the XML name override if available, otherwise sanitizing the map key. +func resolveElementName(key string, propSchema *highbase.Schema) string { + if propSchema != nil && propSchema.XML != nil && propSchema.XML.Name != "" { + return propSchema.XML.Name + } + return sanitizeXMLName(key) +} + +// getPropertySchema looks up the schema for a specific property name. +func getPropertySchema(parentSchema *highbase.Schema, key string) *highbase.Schema { + if parentSchema == nil || parentSchema.Properties == nil { + return nil + } + if proxy, ok := parentSchema.Properties.Get(key); ok && proxy != nil { + return proxy.Schema() + } + return nil +} + +// isWrappedArray returns true if an array schema should use a wrapper element. +// In OpenAPI 3.2+ this is nodeType "element"; legacy uses wrapped: true. +func isWrappedArray(schema *highbase.Schema) bool { + if schema == nil || schema.XML == nil { + return false + } + x := schema.XML + if x.NodeType == "element" { + return true + } + if x.NodeType == "" && x.Wrapped { + return true + } + return false +} + +// buildStartElement creates an xml.StartElement with optional namespace prefix handling. +func buildStartElement(name string, schema *highbase.Schema) xml.StartElement { + local := name + var attrs []xml.Attr + + if schema != nil && schema.XML != nil { + x := schema.XML + if x.Prefix != "" && x.Namespace != "" { + local = x.Prefix + ":" + name + attrs = appendNamespaceAttr(attrs, x.Prefix, x.Namespace) + } else if x.Namespace != "" { + attrs = appendNamespaceAttr(attrs, "", x.Namespace) + } + } + + return xml.StartElement{ + Name: xml.Name{Local: local}, + Attr: attrs, + } +} + +func appendNamespaceAttr(attrs []xml.Attr, prefix, namespace string) []xml.Attr { + if namespace == "" { + return attrs + } + attrName := "xmlns" + if prefix != "" { + attrName = "xmlns:" + prefix + } + for _, attr := range attrs { + if attr.Name.Local == attrName { + return attrs + } + } + return append(attrs, xml.Attr{ + Name: xml.Name{Local: attrName}, + Value: namespace, + }) +} + +// RenderXML renders a value as XML. If schema is provided, uses its XML metadata +// (xml.name, xml.attribute, xml.namespace, xml.prefix, xml.wrapped) for correct output. +// If schema is nil, falls back to basic element-based XML (map keys → element names). +// +// Note: nodeType "cdata" is treated as "text" in this version — Go's xml.Encoder has +// no first-class CDATA token support. +func (mg *MockGenerator) RenderXML(value any, schema *highbase.Schema) []byte { + if value == nil { + return nil + } + + // Decode *yaml.Node to native Go types + if y, ok := value.(*yaml.Node); ok { + var decoded any + if err := y.Decode(&decoded); err != nil { + return nil + } + value = decoded + } + + var buf bytes.Buffer + buf.Grow(512) + enc := xml.NewEncoder(&buf) + if mg.pretty { + enc.Indent("", " ") + } + + // XML declaration + _ = enc.EncodeToken(xml.ProcInst{Target: "xml", Inst: []byte(`version="1.0" encoding="UTF-8"`)}) + if mg.pretty { + _ = enc.EncodeToken(xml.CharData("\n")) + } + + // Root element name + rootName := "root" + if schema != nil && schema.XML != nil && schema.XML.Name != "" { + rootName = schema.XML.Name + } + + start := buildStartElement(rootName, schema) + + mg.renderXMLValue(enc, start, value, schema) + + _ = enc.Flush() + return buf.Bytes() +} + +// renderXMLValue recursively renders a value as XML tokens. +func (mg *MockGenerator) renderXMLValue(enc *xml.Encoder, start xml.StartElement, value any, schema *highbase.Schema) { + if value == nil { + return + } + + switch v := value.(type) { + case map[string]any: + mg.renderXMLMap(enc, start, v, schema) + case []any: + mg.renderXMLSlice(enc, start, v, schema) + default: + // Scalar value + _ = enc.EncodeToken(start) + _ = enc.EncodeToken(xml.CharData(fmt.Sprint(v))) + _ = enc.EncodeToken(start.End()) + } +} + +// renderXMLMap renders a map as an XML element with child elements, attributes, and text content. +func (mg *MockGenerator) renderXMLMap(enc *xml.Encoder, start xml.StartElement, m map[string]any, schema *highbase.Schema) { + // Three-pass rendering: + // 1. Collect attributes → add to start element + // 2. Collect text/cdata nodes + // 3. Emit child elements + + type childEntry struct { + key string + value any + schema *highbase.Schema + } + + var textValues []any + var children []childEntry + + for key, val := range m { + propSchema := getPropertySchema(schema, key) + nodeType := resolveNodeType(propSchema) + + switch nodeType { + case "attribute": + attrName := resolveElementName(key, propSchema) + // Apply prefix for attributes too + if propSchema != nil && propSchema.XML != nil && propSchema.XML.Prefix != "" { + attrName = propSchema.XML.Prefix + ":" + attrName + start.Attr = appendNamespaceAttr(start.Attr, propSchema.XML.Prefix, propSchema.XML.Namespace) + } + start.Attr = append(start.Attr, xml.Attr{ + Name: xml.Name{Local: attrName}, + Value: fmt.Sprint(val), + }) + case "text", "cdata": + textValues = append(textValues, val) + case "none": + // Skip the node itself, include sub-properties directly + if subMap, ok := val.(map[string]any); ok { + for sk, sv := range subMap { + children = append(children, childEntry{key: sk, value: sv, schema: getPropertySchema(propSchema, sk)}) + } + } else { + children = append(children, childEntry{key: key, value: val, schema: propSchema}) + } + default: // "element" + children = append(children, childEntry{key: key, value: val, schema: propSchema}) + } + } + + _ = enc.EncodeToken(start) + + // Emit text content + for _, tv := range textValues { + _ = enc.EncodeToken(xml.CharData(fmt.Sprint(tv))) + } + + // Emit child elements + for _, child := range children { + elemName := resolveElementName(child.key, child.schema) + childStart := buildStartElement(elemName, child.schema) + + // Handle arrays + if arr, ok := child.value.([]any); ok { + mg.renderXMLArray(enc, childStart, arr, child.schema, child.key) + } else { + mg.renderXMLValue(enc, childStart, child.value, child.schema) + } + } + + _ = enc.EncodeToken(start.End()) +} + +// renderXMLSlice renders a top-level slice (when the root value is an array). +func (mg *MockGenerator) renderXMLSlice(enc *xml.Encoder, start xml.StartElement, arr []any, schema *highbase.Schema) { + _ = enc.EncodeToken(start) + itemSchema := mg.getItemsSchema(schema) + itemName := "item" + if itemSchema != nil && itemSchema.XML != nil && itemSchema.XML.Name != "" { + itemName = itemSchema.XML.Name + } + for _, item := range arr { + itemStart := buildStartElement(itemName, itemSchema) + mg.renderXMLValue(enc, itemStart, item, itemSchema) + } + _ = enc.EncodeToken(start.End()) +} + +// renderXMLArray renders an array property, handling wrapped vs unwrapped. +func (mg *MockGenerator) renderXMLArray(enc *xml.Encoder, elemStart xml.StartElement, arr []any, propSchema *highbase.Schema, key string) { + itemSchema := mg.getItemsSchema(propSchema) + itemName := "item" + if itemSchema != nil && itemSchema.XML != nil && itemSchema.XML.Name != "" { + itemName = itemSchema.XML.Name + } + + if isWrappedArray(propSchema) { + // Wrapped: ... + _ = enc.EncodeToken(elemStart) + for _, item := range arr { + itemStart := buildStartElement(itemName, itemSchema) + mg.renderXMLValue(enc, itemStart, item, itemSchema) + } + _ = enc.EncodeToken(elemStart.End()) + } else { + // Unwrapped: repeated elements directly under parent + for _, item := range arr { + itemStart := buildStartElement(itemName, itemSchema) + mg.renderXMLValue(enc, itemStart, item, itemSchema) + } + } +} + +// getItemsSchema extracts the items schema from an array schema. +func (mg *MockGenerator) getItemsSchema(schema *highbase.Schema) *highbase.Schema { + if schema == nil || schema.Items == nil || !schema.Items.IsA() { + return nil + } + return schema.Items.A.Schema() +} diff --git a/renderer/mock_generator_xml_test.go b/renderer/mock_generator_xml_test.go new file mode 100644 index 000000000..aacbbeb3c --- /dev/null +++ b/renderer/mock_generator_xml_test.go @@ -0,0 +1,841 @@ +// Copyright 2024-2026 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package renderer + +import ( + "context" + "encoding/xml" + "strings" + "testing" + + "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v4" +) + +func createSchemaFromYAML(t *testing.T, yamlStr string) *base.Schema { + t.Helper() + var root yaml.Node + err := yaml.Unmarshal([]byte(yamlStr), &root) + require.NoError(t, err) + var lowProxy lowbase.SchemaProxy + err = lowProxy.Build(context.Background(), &root, root.Content[0], nil) + require.NoError(t, err) + lowRef := low.NodeReference[*lowbase.SchemaProxy]{ + Value: &lowProxy, + } + highSchema := base.NewSchemaProxy(&lowRef) + return highSchema.Schema() +} + +func TestRenderXML_NilValue(t *testing.T) { + mg := NewMockGenerator(XML) + result := mg.RenderXML(nil, nil) + assert.Nil(t, result) +} + +func TestRenderXML_BasicMap_NoSchema(t *testing.T) { + mg := NewMockGenerator(XML) + mg.SetPretty() + + value := map[string]any{ + "name": "test", + "age": 42, + } + + result := mg.RenderXML(value, nil) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, ``) + assert.Contains(t, str, ``) + assert.Contains(t, str, ``) + assert.Contains(t, str, `test`) + assert.Contains(t, str, `42`) + + // Verify it's valid XML + assertValidXML(t, result) +} + +func TestRenderXML_ScalarValues(t *testing.T) { + mg := NewMockGenerator(XML) + + tests := []struct { + name string + value any + expected string + }{ + {"string", "hello", "hello"}, + {"int", 42, "42"}, + {"float", 3.14, "3.14"}, + {"bool", true, "true"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mg.RenderXML(tt.value, nil) + require.NotNil(t, result) + assert.Contains(t, string(result), tt.expected) + assertValidXML(t, result) + }) + } +} + +func TestRenderXML_XMLNameOverride(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: person +properties: + firstName: + type: string + xml: + name: first-name + lastName: + type: string +`) + + value := map[string]any{ + "firstName": "John", + "lastName": "Doe", + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, ``) + assert.Contains(t, str, `John`) + assert.Contains(t, str, `Doe`) + assertValidXML(t, result) +} + +func TestRenderXML_NodeTypeAttribute(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: item +properties: + id: + type: integer + xml: + nodeType: attribute + name: + type: string +`) + + value := map[string]any{ + "id": 100, + "name": "Widget", + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `id="100"`) + assert.Contains(t, str, `Widget`) + assertValidXML(t, result) +} + +func TestRenderXML_LegacyAttribute(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: item +properties: + currency: + type: string + xml: + attribute: true + amount: + type: number +`) + + value := map[string]any{ + "currency": "USD", + "amount": 120.50, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `currency="USD"`) + assert.Contains(t, str, `120.5`) + assertValidXML(t, result) +} + +func TestRenderXML_NodeTypeText(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: amount +properties: + currency: + type: string + xml: + nodeType: attribute + value: + type: number + xml: + nodeType: text +`) + + value := map[string]any{ + "currency": "USD", + "value": 120.50, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + // Should produce 120.5 + assert.Contains(t, str, `currency="USD"`) + assert.Contains(t, str, `120.5`) + // Should NOT have wrapper + assert.NotContains(t, str, ``) + assertValidXML(t, result) +} + +func TestRenderXML_NodeTypeCdata_FallsBackToText(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: note +properties: + content: + type: string + xml: + nodeType: cdata +`) + + value := map[string]any{ + "content": "Some text content", + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `Some text content`) + assertValidXML(t, result) +} + +func TestRenderXML_InvalidXMLNames(t *testing.T) { + mg := NewMockGenerator(XML) + + value := map[string]any{ + "my key": "value1", + "123start": "value2", + "normal": "value3", + } + + result := mg.RenderXML(value, nil) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `value1`) + assert.Contains(t, str, `<_123start>value2`) + assert.Contains(t, str, `value3`) + assertValidXML(t, result) +} + +func TestRenderXML_NamespaceAndPrefix(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order + namespace: http://example.com/schema + prefix: ex +properties: + id: + type: integer +`) + + value := map[string]any{ + "id": 42, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `ex:order`) + assert.Contains(t, str, `xmlns:ex="http://example.com/schema"`) + assertValidXML(t, result) +} + +func TestRenderXML_AttributePrefixDeclaresNamespace(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order +properties: + id: + type: integer + xml: + nodeType: attribute + prefix: ex + namespace: http://example.com/attr + name: + type: string +`) + + value := map[string]any{ + "id": 42, + "name": "Widget", + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `ex:id="42"`) + assert.Contains(t, str, `xmlns:ex="http://example.com/attr"`) + assert.Contains(t, str, `Widget`) + assertValidXML(t, result) +} + +func TestRenderXML_ArrayUnwrapped(t *testing.T) { + mg := NewMockGenerator(XML) + mg.SetPretty() + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order +properties: + tags: + type: array + items: + type: string + xml: + name: tag +`) + + value := map[string]any{ + "tags": []any{"food", "drink"}, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `food`) + assert.Contains(t, str, `drink`) + // Should NOT have a wrapper + assert.NotContains(t, str, ``) + assertValidXML(t, result) +} + +func TestRenderXML_ArrayWrapped(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order +properties: + tags: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: tag +`) + + value := map[string]any{ + "tags": []any{"food", "drink"}, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, ``) + assert.Contains(t, str, `food`) + assert.Contains(t, str, `drink`) + assert.Contains(t, str, ``) + assertValidXML(t, result) +} + +func TestRenderXML_ArrayWrappedByNodeTypeElement(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order +properties: + tags: + type: array + xml: + nodeType: element + items: + type: string + xml: + name: tag +`) + + value := map[string]any{ + "tags": []any{"food", "drink"}, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, ``) + assert.Contains(t, str, `food`) + assert.Contains(t, str, `drink`) + assert.Contains(t, str, ``) + assertValidXML(t, result) +} + +func TestRenderXML_ArrayXMLNameDoesNotImplyWrapping(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: order +properties: + tags: + type: array + xml: + name: collection + items: + type: string + xml: + name: tag +`) + + value := map[string]any{ + "tags": []any{"food", "drink"}, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.NotContains(t, str, ``) + assert.Contains(t, str, `food`) + assert.Contains(t, str, `drink`) + assertValidXML(t, result) +} + +func TestRenderXML_ArrayItemsDefaultName(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: data +properties: + items: + type: array + xml: + wrapped: true + items: + type: string +`) + + value := map[string]any{ + "items": []any{"a", "b"}, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + // Without xml.Name on items, should default to "item" + assert.Contains(t, str, `a`) + assert.Contains(t, str, `b`) + assertValidXML(t, result) +} + +func TestRenderXML_NestedObjects(t *testing.T) { + mg := NewMockGenerator(XML) + mg.SetPretty() + + value := map[string]any{ + "person": map[string]any{ + "name": "Alice", + "address": map[string]any{ + "city": "London", + "country": "UK", + }, + }, + } + + result := mg.RenderXML(value, nil) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, ``) + assert.Contains(t, str, `Alice`) + assert.Contains(t, str, `
`) + assert.Contains(t, str, `London`) + assert.Contains(t, str, `UK`) + assertValidXML(t, result) +} + +func TestRenderXML_NodeTypeNoneFlattensChildren(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: person +properties: + profile: + type: object + xml: + nodeType: none + properties: + firstName: + type: string + city: + type: string +`) + + value := map[string]any{ + "profile": map[string]any{ + "firstName": "Alice", + "city": "London", + "nickname": "Al", + }, + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.NotContains(t, str, ``) + assert.Contains(t, str, `Alice`) + assert.Contains(t, str, `London`) + assert.Contains(t, str, `Al`) + assertValidXML(t, result) +} + +func TestRenderXML_NodeTypeNoneScalarFallsBackToElement(t *testing.T) { + mg := NewMockGenerator(XML) + + schema := createSchemaFromYAML(t, ` +type: object +xml: + name: person +properties: + nickname: + type: string + xml: + nodeType: none +`) + + value := map[string]any{ + "nickname": "Al", + } + + result := mg.RenderXML(value, schema) + require.NotNil(t, result) + str := string(result) + assert.Contains(t, str, `Al`) + assertValidXML(t, result) +} + +func TestRenderXML_Escaping(t *testing.T) { + mg := NewMockGenerator(XML) + + value := map[string]any{ + "text": ` & more`, + } + + result := mg.RenderXML(value, nil) + require.NotNil(t, result) + str := string(result) + // xml.Encoder should escape all special characters + assert.NotContains(t, str, `