Skip to content

Commit

Permalink
Added docs, more tests and cleanup.
Browse files Browse the repository at this point in the history
Signed-off-by: Quobix <dave@quobix.com>
  • Loading branch information
daveshanley committed Aug 30, 2023
1 parent 2ed433e commit f37c2d5
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 33 deletions.
32 changes: 31 additions & 1 deletion renderer/mock_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
}
Expand Down
20 changes: 19 additions & 1 deletion renderer/mock_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Expand Down
94 changes: 63 additions & 31 deletions renderer/schema_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -31,13 +59,17 @@ 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)
return &SchemaRenderer{words: words}
}

// 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")
Expand All @@ -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
Expand All @@ -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)]
Expand All @@ -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:
Expand All @@ -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)]
Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions renderer/schema_renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down

0 comments on commit f37c2d5

Please sign in to comment.