mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-09 04:20:17 +00:00
Added docs, more tests and cleanup.
Signed-off-by: Quobix <dave@quobix.com>
This commit is contained in:
@@ -22,21 +22,47 @@ const (
|
||||
MockYAML
|
||||
)
|
||||
|
||||
// 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: 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
|
||||
}
|
||||
|
||||
// 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: 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) {
|
||||
v := reflect.ValueOf(mock).Elem()
|
||||
num := v.NumField()
|
||||
@@ -130,7 +156,11 @@ func (mg *MockGenerator) renderMockJSON(v any) []byte {
|
||||
// determine the type, render properly.
|
||||
switch reflect.ValueOf(v).Kind() {
|
||||
case reflect.Map, reflect.Slice, reflect.Array, reflect.Struct, reflect.Ptr:
|
||||
data, _ = json.Marshal(v)
|
||||
if mg.pretty {
|
||||
data, _ = json.MarshalIndent(v, "", " ")
|
||||
} else {
|
||||
data, _ = json.Marshal(v)
|
||||
}
|
||||
default:
|
||||
data = []byte(fmt.Sprint(v))
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestMockGenerator_GenerateJSONMock_MultiExamples_NoName_JSON(t *testing.T)
|
||||
mg := NewMockGenerator(MockJSON)
|
||||
mock, err := mg.GenerateMock(fake, "JimmyJammyJimJams") // does not exist
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"fish-and-chips\":\"cod-and-chips-twice\"}", string(mock))
|
||||
assert.NotEmpty(t, string(mock))
|
||||
}
|
||||
|
||||
func TestMockGenerator_GenerateJSONMock_MultiExamples_JSON(t *testing.T) {
|
||||
@@ -170,6 +170,24 @@ func TestMockGenerator_GenerateJSONMock_MultiExamples_JSON(t *testing.T) {
|
||||
assert.Equal(t, "{\"rice-and-peas\":\"brown-or-white-rice\"}", string(mock))
|
||||
}
|
||||
|
||||
func TestMockGenerator_GenerateJSONMock_MultiExamples_PrettyJSON(t *testing.T) {
|
||||
fakeExample := map[string]any{
|
||||
"exampleOne": map[string]any{
|
||||
"fish-and-chips": "cod-and-chips-twice",
|
||||
},
|
||||
"exampleTwo": map[string]any{
|
||||
"rice-and-peas": "brown-or-white-rice",
|
||||
"peas": "buttery",
|
||||
},
|
||||
}
|
||||
fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
|
||||
mg := NewMockGenerator(MockJSON)
|
||||
mg.SetPretty()
|
||||
mock, err := mg.GenerateMock(fake, "exampleTwo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\n \"peas\": \"buttery\",\n \"rice-and-peas\": \"brown-or-white-rice\"\n}", string(mock))
|
||||
}
|
||||
|
||||
func TestMockGenerator_GenerateJSONMock_MultiExamples_YAML(t *testing.T) {
|
||||
fakeExample := map[string]any{
|
||||
"exampleOne": map[string]any{
|
||||
|
||||
@@ -17,6 +17,34 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const rootType = "rootType"
|
||||
const stringType = "string"
|
||||
const numberType = "number"
|
||||
const integerType = "integer"
|
||||
const booleanType = "boolean"
|
||||
const objectType = "object"
|
||||
const arrayType = "array"
|
||||
const int32Type = "int32"
|
||||
const floatType = "float"
|
||||
const doubleType = "double"
|
||||
const byteType = "byte"
|
||||
const binaryType = "binary"
|
||||
const passwordType = "password"
|
||||
const dateType = "date"
|
||||
const dateTimeType = "date-time"
|
||||
const timeType = "time"
|
||||
const emailType = "email"
|
||||
const hostnameType = "hostname"
|
||||
const ipv4Type = "ipv4"
|
||||
const ipv6Type = "ipv6"
|
||||
const uriType = "uri"
|
||||
const uriReferenceType = "uri-reference"
|
||||
const uuidType = "uuid"
|
||||
const allOfType = "allOf"
|
||||
const anyOfType = "anyOf"
|
||||
const oneOfType = "oneOf"
|
||||
const itemsType = "items"
|
||||
|
||||
// used to generate random words if there is no dictionary applied.
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
@@ -31,6 +59,8 @@ type SchemaRenderer struct {
|
||||
words []string
|
||||
}
|
||||
|
||||
// CreateRendererUsingDictionary will create a new SchemaRenderer using a custom dictionary file.
|
||||
// The location of a text file with one word per line is expected.
|
||||
func CreateRendererUsingDictionary(dictionaryLocation string) *SchemaRenderer {
|
||||
// try and read in the dictionary file
|
||||
words := ReadDictionary(dictionaryLocation)
|
||||
@@ -38,6 +68,8 @@ func CreateRendererUsingDictionary(dictionaryLocation string) *SchemaRenderer {
|
||||
}
|
||||
|
||||
// CreateRendererUsingDefaultDictionary will create a new SchemaRenderer using the default dictionary file.
|
||||
// The default dictionary is located at /usr/share/dict/words on most systems.
|
||||
// Windows users will need to use CreateRendererUsingDictionary to specify a custom dictionary.
|
||||
func CreateRendererUsingDefaultDictionary() *SchemaRenderer {
|
||||
wr := new(SchemaRenderer)
|
||||
wr.words = ReadDictionary("/usr/share/dict/words")
|
||||
@@ -48,8 +80,8 @@ func CreateRendererUsingDefaultDictionary() *SchemaRenderer {
|
||||
func (wr *SchemaRenderer) RenderSchema(schema *base.Schema) any {
|
||||
// dive into the schema and render it
|
||||
structure := make(map[string]any)
|
||||
wr.DiveIntoSchema(schema, "root", structure, 0)
|
||||
return structure["root"].(any)
|
||||
wr.DiveIntoSchema(schema, rootType, structure, 0)
|
||||
return structure[rootType].(any)
|
||||
}
|
||||
|
||||
// DiveIntoSchema will dive into a schema and inject values from examples into a map. If there are no examples in
|
||||
@@ -69,7 +101,7 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
}
|
||||
|
||||
// render out a string.
|
||||
if slices.Contains(schema.Type, "string") {
|
||||
if slices.Contains(schema.Type, stringType) {
|
||||
// check for an enum, if there is one, then pick a random value from it.
|
||||
if schema.Enum != nil && len(schema.Enum) > 0 {
|
||||
structure[key] = schema.Enum[rand.Int()%len(schema.Enum)]
|
||||
@@ -87,43 +119,43 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
}
|
||||
|
||||
switch schema.Format {
|
||||
case "date-time":
|
||||
case dateTimeType:
|
||||
structure[key] = time.Now().Format(time.RFC3339)
|
||||
case "date":
|
||||
case dateType:
|
||||
structure[key] = time.Now().Format("2006-01-02")
|
||||
case "time":
|
||||
case timeType:
|
||||
structure[key] = time.Now().Format("15:04:05")
|
||||
case "email":
|
||||
case emailType:
|
||||
structure[key] = fmt.Sprintf("%s@%s.com",
|
||||
wr.RandomWord(minLength, maxLength, 0),
|
||||
wr.RandomWord(minLength, maxLength, 0))
|
||||
case "hostname":
|
||||
case hostnameType:
|
||||
structure[key] = fmt.Sprintf("%s.com", wr.RandomWord(minLength, maxLength, 0))
|
||||
case "ipv4":
|
||||
case ipv4Type:
|
||||
structure[key] = fmt.Sprintf("%d.%d.%d.%d",
|
||||
rand.Int()%255, rand.Int()%255, rand.Int()%255, rand.Int()%255)
|
||||
case "ipv6":
|
||||
case ipv6Type:
|
||||
structure[key] = fmt.Sprintf("%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x",
|
||||
rand.Intn(65535), rand.Intn(65535), rand.Intn(65535), rand.Intn(65535),
|
||||
rand.Intn(65535), rand.Intn(65535), rand.Intn(65535), rand.Intn(65535),
|
||||
)
|
||||
case "uri":
|
||||
case uriType:
|
||||
structure[key] = fmt.Sprintf("https://%s-%s-%s.com/%s",
|
||||
wr.RandomWord(minLength, maxLength, 0),
|
||||
wr.RandomWord(minLength, maxLength, 0),
|
||||
wr.RandomWord(minLength, maxLength, 0),
|
||||
wr.RandomWord(minLength, maxLength, 0))
|
||||
case "uri-reference":
|
||||
case uriReferenceType:
|
||||
structure[key] = fmt.Sprintf("/%s/%s",
|
||||
wr.RandomWord(minLength, maxLength, 0),
|
||||
wr.RandomWord(minLength, maxLength, 0))
|
||||
case "uuid":
|
||||
case uuidType:
|
||||
structure[key] = wr.PseudoUUID()
|
||||
case "byte":
|
||||
case byteType:
|
||||
structure[key] = fmt.Sprintf("%x", wr.RandomWord(minLength, maxLength, 0))
|
||||
case "password":
|
||||
case passwordType:
|
||||
structure[key] = fmt.Sprintf("%s", wr.RandomWord(minLength, maxLength, 0))
|
||||
case "binary":
|
||||
case binaryType:
|
||||
structure[key] = fmt.Sprintf("%s",
|
||||
base64.StdEncoding.EncodeToString([]byte(wr.RandomWord(minLength, maxLength, 0))))
|
||||
default:
|
||||
@@ -142,7 +174,7 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
}
|
||||
|
||||
// handle numbers
|
||||
if slices.Contains(schema.Type, "number") || slices.Contains(schema.Type, "integer") {
|
||||
if slices.Contains(schema.Type, numberType) || slices.Contains(schema.Type, integerType) {
|
||||
|
||||
if schema.Enum != nil && len(schema.Enum) > 0 {
|
||||
structure[key] = schema.Enum[rand.Int()%len(schema.Enum)]
|
||||
@@ -159,11 +191,11 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
}
|
||||
|
||||
switch schema.Format {
|
||||
case "float":
|
||||
case floatType:
|
||||
structure[key] = rand.Float32()
|
||||
case "double":
|
||||
case doubleType:
|
||||
structure[key] = rand.Float64()
|
||||
case "int32":
|
||||
case int32Type:
|
||||
structure[key] = int(wr.RandomInt(minimum, maximum))
|
||||
default:
|
||||
structure[key] = wr.RandomInt(minimum, maximum)
|
||||
@@ -173,12 +205,12 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
}
|
||||
|
||||
// handle booleans
|
||||
if slices.Contains(schema.Type, "boolean") {
|
||||
if slices.Contains(schema.Type, booleanType) {
|
||||
structure[key] = true
|
||||
}
|
||||
|
||||
// handle objects
|
||||
if slices.Contains(schema.Type, "object") {
|
||||
if slices.Contains(schema.Type, objectType) {
|
||||
properties := schema.Properties
|
||||
propertyMap := make(map[string]any)
|
||||
|
||||
@@ -206,8 +238,8 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
allOfMap := make(map[string]any)
|
||||
for _, allOfSchema := range allOf {
|
||||
allOfCompiled := allOfSchema.Schema()
|
||||
wr.DiveIntoSchema(allOfCompiled, "allOf", allOfMap, depth+1)
|
||||
for k, v := range allOfMap["allOf"].(map[string]any) {
|
||||
wr.DiveIntoSchema(allOfCompiled, allOfType, allOfMap, depth+1)
|
||||
for k, v := range allOfMap[allOfType].(map[string]any) {
|
||||
propertyMap[k] = v
|
||||
}
|
||||
}
|
||||
@@ -235,8 +267,8 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
oneOfMap := make(map[string]any)
|
||||
for _, oneOfSchema := range oneOf {
|
||||
oneOfCompiled := oneOfSchema.Schema()
|
||||
wr.DiveIntoSchema(oneOfCompiled, "oneOf", oneOfMap, depth+1)
|
||||
for k, v := range oneOfMap["oneOf"].(map[string]any) {
|
||||
wr.DiveIntoSchema(oneOfCompiled, oneOfType, oneOfMap, depth+1)
|
||||
for k, v := range oneOfMap[oneOfType].(map[string]any) {
|
||||
propertyMap[k] = v
|
||||
}
|
||||
break // one run once for the first result.
|
||||
@@ -249,8 +281,8 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
anyOfMap := make(map[string]any)
|
||||
for _, anyOfSchema := range anyOf {
|
||||
anyOfCompiled := anyOfSchema.Schema()
|
||||
wr.DiveIntoSchema(anyOfCompiled, "anyOf", anyOfMap, depth+1)
|
||||
for k, v := range anyOfMap["anyOf"].(map[string]any) {
|
||||
wr.DiveIntoSchema(anyOfCompiled, anyOfType, anyOfMap, depth+1)
|
||||
for k, v := range anyOfMap[anyOfType].(map[string]any) {
|
||||
propertyMap[k] = v
|
||||
}
|
||||
break // one run once for the first result only, same as oneOf
|
||||
@@ -260,7 +292,7 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(schema.Type, "array") {
|
||||
if slices.Contains(schema.Type, arrayType) {
|
||||
|
||||
// an array needs an items schema
|
||||
itemsSchema := schema.Items
|
||||
@@ -280,8 +312,8 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct
|
||||
for i := int64(0); i < minItems; i++ {
|
||||
itemMap := make(map[string]any)
|
||||
itemsSchemaCompiled := itemsSchema.A.Schema()
|
||||
wr.DiveIntoSchema(itemsSchemaCompiled, "items", itemMap, depth+1)
|
||||
renderedItems = append(renderedItems, itemMap["items"])
|
||||
wr.DiveIntoSchema(itemsSchemaCompiled, itemsType, itemMap, depth+1)
|
||||
renderedItems = append(renderedItems, itemMap[itemsType])
|
||||
}
|
||||
structure[key] = renderedItems
|
||||
return
|
||||
|
||||
@@ -989,6 +989,36 @@ properties:
|
||||
assert.Equal(t, `{"name":"pb33f"}`, string(rendered))
|
||||
}
|
||||
|
||||
func TestRenderSchema_WithEnum_Float(t *testing.T) {
|
||||
testObject := `type: [object]
|
||||
properties:
|
||||
count:
|
||||
type: number
|
||||
enum: [9934.223]`
|
||||
|
||||
compiled := getSchema([]byte(testObject))
|
||||
schema := make(map[string]any)
|
||||
wr := createSchemaRenderer()
|
||||
wr.DiveIntoSchema(compiled, "pb33f", schema, 0)
|
||||
rendered, _ := json.Marshal(schema["pb33f"])
|
||||
assert.Equal(t, `{"count":9934.223}`, string(rendered))
|
||||
}
|
||||
|
||||
func TestRenderSchema_WithEnum_Integer(t *testing.T) {
|
||||
testObject := `type: [object]
|
||||
properties:
|
||||
count:
|
||||
type: number
|
||||
enum: [9934]`
|
||||
|
||||
compiled := getSchema([]byte(testObject))
|
||||
schema := make(map[string]any)
|
||||
wr := createSchemaRenderer()
|
||||
wr.DiveIntoSchema(compiled, "pb33f", schema, 0)
|
||||
rendered, _ := json.Marshal(schema["pb33f"])
|
||||
assert.Equal(t, `{"count":9934}`, string(rendered))
|
||||
}
|
||||
|
||||
func TestCreateRendererUsingDefaultDictionary(t *testing.T) {
|
||||
assert.NotNil(t, CreateRendererUsingDefaultDictionary())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user