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
1,055 changes: 1,055 additions & 0 deletions renderer/mock_generation_options_test.go

Large diffs are not rendered by default.

93 changes: 52 additions & 41 deletions renderer/mock_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,80 @@ package renderer
import (
"encoding/json"
"fmt"
"reflect"
"strconv"

highbase "github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/orderedmap"
"go.yaml.in/yaml/v4"
"reflect"
"strconv"
)

const (
Example = "Example"
// Example is the field name used for a single mock example value.
Example = "Example"

// Examples is the field name used for named mock examples.
Examples = "Examples"
Schema = "Schema"
)

type MockType int
// Schema is the field name used for schema-based mock generation.
Schema = "Schema"
)

const (
// JSON renders mocks as JSON.
JSON MockType = iota

// YAML renders mocks as YAML.
YAML

// XML renders mocks as XML.
XML
)

// MockGenerator is used to generate mocks for high-level mockable structs or *base.Schema pointers.
// The mock generator will attempt to generate a mock from a struct using the following fields:
// MockType identifies the output format generated by MockGenerator.
type MockType int

// MockGenerator generates mocks for high-level mockable structs or *base.Schema pointers.
//
// Mockable structs can provide 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.
// - Schema: *base.SchemaProxy, this is the schema to use if no examples are present.
//
// The mock generator will attempt to generate a mock from a *base.Schema pointer.
// Use NewMockGenerator or NewMockGeneratorWithDictionary to create a new mock generator.
type MockGenerator struct {
renderer *SchemaRenderer
mockType MockType
pretty bool
}

// NewMockGeneratorWithDictionary creates a new mock generator using a custom dictionary. This is useful if you want to
// use a custom dictionary to generate mocks. The location of a text file with one word per line is expected.
// NewMockGeneratorWithDictionary creates a MockGenerator using a custom dictionary file.
//
// The location of a text file with one word per line is expected.
func NewMockGeneratorWithDictionary(dictionaryLocation string, mockType MockType) *MockGenerator {
renderer := CreateRendererUsingDictionary(dictionaryLocation)
return &MockGenerator{renderer: renderer, mockType: mockType}
}

// NewMockGenerator creates a new mock generator using the default dictionary. The default is located at /usr/share/dict/words
// on most systems. Windows users will need to use NewMockGeneratorWithDictionary to specify a custom dictionary.
// NewMockGenerator creates a MockGenerator using the default dictionary.
//
// The default is located at /usr/share/dict/words on most systems. Windows users need to use
// NewMockGeneratorWithDictionary to specify a custom dictionary.
func NewMockGenerator(mockType MockType) *MockGenerator {
renderer := CreateRendererUsingDefaultDictionary()
return &MockGenerator{renderer: renderer, mockType: mockType}
}

// SetPretty sets the pretty flag on the mock generator. If true, the mock will be rendered with indentation and newlines.
// If false, the mock will be rendered as a single line which is good for API responses. False is the default.
// This option only effects JSON mocks, there is no concept of pretty printing YAML.
// SetPretty configures JSON mocks to render with indentation and newlines.
//
// JSON mocks render as a single line by default. This option affects only JSON; YAML is always rendered in YAML form.
func (mg *MockGenerator) SetPretty() {
mg.pretty = true
}

// DisableRequiredCheck disables renderer required property check when rendering
// a schema for mocks. This means that all properties will be rendered, not just
// the required ones.
// DisableRequiredCheck disables required-property filtering when rendering schema-based mocks.
//
// When disabled, all properties are rendered, not just required properties.
func (mg *MockGenerator) DisableRequiredCheck() {
mg.renderer.DisableRequiredCheck()
}
Expand All @@ -74,14 +89,21 @@ func (mg *MockGenerator) SetUnresolvedRefHandler(handler UnresolvedRefHandler) {
mg.renderer.SetUnresolvedRefHandler(handler)
}

// SetMockGenerationOptions sets work and output budgets for generated mock values.
//
// Zero or negative option values are replaced with the package defaults.
func (mg *MockGenerator) SetMockGenerationOptions(options MockGenerationOptions) {
mg.renderer.SetMockGenerationOptions(options)
}

// 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.
// Returns an error for unresolved refs or build failures while 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{}):
Expand Down Expand Up @@ -120,12 +142,10 @@ func (mg *MockGenerator) renderForType(value any, schema *highbase.Schema) []byt
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.
// Schema: *base.SchemaProxy, this is the schema to use if no examples are present.
// The name parameter is optional, if provided, the mock generator will attempt to find an example with the given name.
// If no name is provided, the first example will be used.
// GenerateMock generates a mock for a high-level mockable struct or *base.Schema pointer.
//
// The name parameter is optional. When provided, GenerateMock attempts to select a matching named example. If name is
// empty, the first available example is used.
func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) {
if mock == nil || !reflect.ValueOf(mock).IsValid() || reflect.ValueOf(mock).IsNil() {
return nil, nil
Expand All @@ -143,7 +163,6 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) {
}
}
mockReady := false
// check if all fields are present, if so, we can generate a mock
if fieldCount == 2 {
mockReady = true
}
Expand All @@ -152,11 +171,10 @@ 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
// Extract schema before example selection so XML rendering can use schema metadata.
schemaValue, schemaErr := mg.extractSchema(mock, v)

var fallbackExample *highbase.Example = nil
// trying to find a named example
examples := v.FieldByName(Examples)
examplesValue := examples.Interface()
if examplesValue != nil && !examples.IsNil() {
Expand All @@ -165,17 +183,14 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) {
if example, ok := examplesMap.Get(name); ok {
return mg.renderForType(example.Value, schemaValue), nil
} else {
//take the first example from the list
fallbackExample = examplesMap.Oldest().Value
}
}
}
}

// looking for an inline example
f := v.FieldByName(Example)
if !f.IsNil() {
// Pointer/Interface Shenanigans
ex := f.Interface()
if y, ok := ex.(*yaml.Node); ok {
if y != nil {
Expand All @@ -185,44 +200,40 @@ 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.renderForType(ex, schemaValue), nil
}
}

// rendering fallback if it's not nil
if fallbackExample != nil {
return mg.renderForType(fallbackExample.Value, schemaValue), nil
}

// Surface schema extraction errors only after example paths have had their chance
// Surface schema extraction errors only after example paths have had their chance.
if schemaErr != nil {
return nil, schemaErr
}

if schemaValue != nil {

// now let's check the schema for `Examples` and `Example` fields.
if schemaValue.Examples != nil {
if name != "" {
// try and convert the example to an integer
if i, err := strconv.Atoi(name); err == nil {
if i < len(schemaValue.Examples) {
return mg.renderForType(schemaValue.Examples[i], schemaValue), nil
}
}
}
// if the name is empty, just return the first example
return mg.renderForType(schemaValue.Examples[0], schemaValue), nil
}

// check the example field
if schemaValue.Example != nil {
return mg.renderForType(schemaValue.Example, schemaValue), nil
}

// render the schema as our last hope.
renderMap := mg.renderer.RenderSchema(schemaValue)
renderMap, renderErr := mg.renderer.RenderSchemaWithError(schemaValue)
if renderErr != nil {
return nil, renderErr
}
if renderMap == nil {
return nil, fmt.Errorf("unable to render schema for mock, it's empty")
}
Expand Down
6 changes: 3 additions & 3 deletions renderer/mock_generator_xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ func appendNamespaceAttr(attrs []xml.Attr, prefix, namespace string) []xml.Attr

// 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).
// If schema is nil, falls back to basic element-based XML using map keys as element names.
//
// Note: nodeType "cdata" is treated as "text" in this version Go's xml.Encoder has
// Note: nodeType "cdata" is treated as "text" in this version because Go's xml.Encoder has
// no first-class CDATA token support.
func (mg *MockGenerator) RenderXML(value any, schema *highbase.Schema) []byte {
if value == nil {
Expand Down Expand Up @@ -192,7 +192,7 @@ func (mg *MockGenerator) renderXMLValue(enc *xml.Encoder, start xml.StartElement
// 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
// 1. Collect attributes and add them to the start element.
// 2. Collect text/cdata nodes
// 3. Emit child elements

Expand Down
Loading
Loading