mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
231 lines
7.1 KiB
Go
231 lines
7.1 KiB
Go
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package renderer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
highbase "github.com/pb33f/libopenapi/datamodel/high/base"
|
|
"github.com/pb33f/libopenapi/orderedmap"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
Example = "Example"
|
|
Examples = "Examples"
|
|
Schema = "Schema"
|
|
)
|
|
|
|
type MockType int
|
|
|
|
const (
|
|
JSON MockType = iota
|
|
YAML
|
|
)
|
|
|
|
// 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:
|
|
// - 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.
|
|
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.
|
|
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.
|
|
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.
|
|
func (mg *MockGenerator) DisableRequiredCheck() {
|
|
mg.renderer.DisableRequiredCheck()
|
|
}
|
|
|
|
// 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.
|
|
func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) {
|
|
if mock == nil || !reflect.ValueOf(mock).IsValid() || reflect.ValueOf(mock).IsNil() {
|
|
return nil, nil
|
|
}
|
|
v := reflect.ValueOf(mock).Elem()
|
|
num := v.NumField()
|
|
fieldCount := 0
|
|
for i := 0; i < num; i++ {
|
|
fieldName := v.Type().Field(i).Name
|
|
switch fieldName {
|
|
case Example:
|
|
fieldCount++
|
|
case Examples:
|
|
fieldCount++
|
|
}
|
|
}
|
|
mockReady := false
|
|
// check if all fields are present, if so, we can generate a mock
|
|
if fieldCount == 2 {
|
|
mockReady = true
|
|
}
|
|
if !mockReady {
|
|
return nil, fmt.Errorf("mockable struct only contains %d of the required "+
|
|
"fields (%s, %s)", fieldCount, Example, Examples)
|
|
}
|
|
|
|
// if the value has an example, try and render it out as is.
|
|
f := v.FieldByName(Example)
|
|
if !f.IsNil() {
|
|
// Pointer/Interface Shenanigans
|
|
ex := f.Interface()
|
|
if y, ok := ex.(*yaml.Node); ok {
|
|
if y != nil {
|
|
ex = y
|
|
} else {
|
|
ex = nil
|
|
}
|
|
}
|
|
if ex != nil {
|
|
// try and serialize the example value
|
|
return mg.renderMock(ex), nil
|
|
}
|
|
}
|
|
|
|
// if there is no example, but there are multi-examples.
|
|
examples := v.FieldByName(Examples)
|
|
examplesValue := examples.Interface()
|
|
if examplesValue != nil && !examples.IsNil() {
|
|
|
|
// cast examples to *orderedmap.Map[string, *highbase.Example]
|
|
examplesMap := examplesValue.(*orderedmap.Map[string, *highbase.Example])
|
|
|
|
// if the name is not empty, try and find the example by name
|
|
for k, exp := range examplesMap.FromOldest() {
|
|
if k == name {
|
|
return mg.renderMock(exp.Value), nil
|
|
}
|
|
}
|
|
|
|
// if the name is empty, just return the first example
|
|
for exp := range examplesMap.ValuesFromOldest() {
|
|
return mg.renderMock(exp.Value), 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
if schemaValue != nil {
|
|
|
|
// now lets 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.renderMock(schemaValue.Examples[i]), nil
|
|
}
|
|
}
|
|
}
|
|
// if the name is empty, just return the first example
|
|
return mg.renderMock(schemaValue.Examples[0]), nil
|
|
}
|
|
|
|
// check the example field
|
|
if schemaValue.Example != nil {
|
|
return mg.renderMock(schemaValue.Example), nil
|
|
}
|
|
|
|
// render the schema as our last hope.
|
|
renderMap := mg.renderer.RenderSchema(schemaValue)
|
|
return mg.renderMock(renderMap), nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (mg *MockGenerator) renderMock(v any) []byte {
|
|
switch {
|
|
case mg.mockType == YAML:
|
|
return mg.renderMockYAML(v)
|
|
default:
|
|
return mg.renderMockJSON(v)
|
|
}
|
|
}
|
|
|
|
func (mg *MockGenerator) renderMockJSON(v any) []byte {
|
|
var data []byte
|
|
|
|
if y, ok := v.(*yaml.Node); ok {
|
|
_ = y.Decode(&v)
|
|
}
|
|
|
|
// determine the type, render properly.
|
|
switch reflect.ValueOf(v).Kind() {
|
|
case reflect.Map, reflect.Slice, reflect.Array, reflect.Struct, reflect.Ptr:
|
|
if mg.pretty {
|
|
data, _ = json.MarshalIndent(v, "", " ")
|
|
} else {
|
|
data, _ = json.Marshal(v)
|
|
}
|
|
default:
|
|
data = []byte(fmt.Sprint(v))
|
|
}
|
|
return data
|
|
}
|
|
|
|
func (mg *MockGenerator) renderMockYAML(v any) []byte {
|
|
var data []byte
|
|
// determine the type, render properly.
|
|
switch reflect.ValueOf(v).Kind() {
|
|
case reflect.Map, reflect.Slice, reflect.Array, reflect.Struct, reflect.Ptr:
|
|
data, _ = yaml.Marshal(v)
|
|
default:
|
|
data = []byte(fmt.Sprint(v))
|
|
}
|
|
return data
|
|
}
|