input/elasticapm/internal/modeldecoder/generator/jsonschema.go (284 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package generator
import (
"bytes"
"encoding/json"
"fmt"
"go/types"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// JSONSchemaGenerator holds the parsed package information
// from which it can generate a JSON schema
type JSONSchemaGenerator struct {
parsed *Parsed
}
// NewJSONSchemaGenerator takes a parsed package information as
// input parameter and returns a JSONSchemaGenerator instance
func NewJSONSchemaGenerator(parsed *Parsed) (*JSONSchemaGenerator, error) {
return &JSONSchemaGenerator{parsed: parsed}, nil
}
// Generate creates a JSON schema for the given root type,
// based on the parsed information from the JSONSchemaGenerator.
// The schema is returned as bytes.Buffer
func (g *JSONSchemaGenerator) Generate(idPath string, rootType string) (bytes.Buffer, error) {
root, ok := g.parsed.structTypes[rootType]
if !ok {
return bytes.Buffer{}, fmt.Errorf("object with root key %s not found", rootType)
}
typ := propertyType{names: []propertyTypeName{TypeNameObject}, required: true}
property := property{Type: &typ, Properties: make(map[string]*property), Description: root.comment}
if err := g.generate(root, "", &property); err != nil {
return bytes.Buffer{}, errors.Wrap(err, "json-schema generator")
}
id := filepath.Join(idPath, strings.TrimSuffix(root.name, "Event"))
b, err := json.MarshalIndent(schema{ID: id, property: property}, "", " ")
return *bytes.NewBuffer(b), errors.Wrap(err, "json-schema generator")
}
func (g *JSONSchemaGenerator) generate(st structType, key string, prop *property) error {
if key != "" {
key += "."
}
for _, f := range st.fields {
var err error
name := jsonSchemaName(f)
childProp := property{Properties: make(map[string]*property), Type: &propertyType{}, Description: f.comment}
tags, err := validationTag(f.tag)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("%s%s", key, name))
}
// handle generic tag rules applicable to all types
if _, ok := tags[tagRequired]; ok {
childProp.Type.required = true
prop.Required = append(prop.Required, name)
delete(tags, tagRequired)
}
if val, ok := tags[tagRequiredIfAny]; ok {
// add all property names as entries to the property's AllOf collection
// after processing all child fields iterate through the AllOf collection
// and ensure the types for the properties in the If and Then clauses do not allow null values
// this can only be done after all fields have been processed and their types are known
for _, ifGiven := range strings.Split(val, ";") {
prop.AllOf = append(prop.AllOf,
&property{
If: &property{Required: []string{ifGiven}},
Then: &property{Required: []string{name}}})
}
delete(tags, tagRequiredIfAny)
}
if val, ok := tags[tagRequiredAnyOf]; ok {
// add all property names as entries to the property's AnyOf collection
// after processing all child fields iterate through the AnyOf collection
// and ensure the types for the properties do not allow null values
// this can only be done after all fields have been processed and their types are known
for _, anyOf := range strings.Split(val, ";") {
prop.AnyOf = append(prop.AnyOf, &property{Required: []string{anyOf}})
}
}
if !f.Exported() {
continue
}
flattenedName := fmt.Sprintf("%s%s", key, name)
info := fieldInfo{field: f, tags: tags, parsed: g.parsed}
switch f.Type().String() {
case nullableTypeBool:
err = generateJSONPropertyBool(&info, prop, &childProp)
case nullableTypeFloat64:
err = generateJSONPropertyJSONNumber(&info, prop, &childProp)
case nullableTypeHTTPHeader:
err = generateJSONPropertyHTTPHeader(&info, prop, &childProp)
case nullableTypeInt, nullableTypeInt64, nullableTypeTimeMicrosUnix:
err = generateJSONPropertyInteger(&info, prop, &childProp)
case nullableTypeInterface:
err = generateJSONPropertyInterface(&info, prop, &childProp)
case nullableTypeString:
err = generateJSONPropertyString(&info, prop, &childProp)
default:
switch t := f.Type().Underlying().(type) {
case *types.Map:
nestedProp := property{Properties: make(map[string]*property)}
if err = generateJSONPropertyMap(&info, prop, &childProp, &nestedProp); err != nil {
break
}
if childStruct, ok := g.customStruct(t.Elem()); ok {
err = g.generate(childStruct, flattenedName, &nestedProp)
}
case *types.Slice:
if err = generateJSONPropertySlice(&info, prop, &childProp); err != nil {
break
}
child, ok := g.parsed.structTypes[t.Elem().String()]
if !ok {
break
}
childProp.Items = &property{
Type: &propertyType{names: []propertyTypeName{TypeNameObject}, required: true},
Properties: make(map[string]*property),
}
if child.name == st.name {
// if recursive reference to struct itself do not call generate function
break
}
err = g.generate(child, flattenedName, childProp.Items)
case *types.Struct:
if err = generateJSONPropertyStruct(&info, prop, &childProp); err != nil {
break
}
// all non-parsed struct types should have been handled at this point
child, ok := g.parsed.structTypes[f.Type().String()]
if !ok {
err = fmt.Errorf("unhandled type for field %s", name)
break
}
err = g.generate(child, flattenedName, &childProp)
default:
err = fmt.Errorf("unhandled type %T", t)
}
}
if err != nil {
return errors.Wrap(err, flattenedName)
}
for tagName := range tags {
// not all tags have been handled
return errors.Wrap(fmt.Errorf("unhandled tag rule %s", tagName), jsonSchemaName(f))
}
}
// iterate through AnyOf and ensure that at least one value is required
for i := 0; i < len(prop.AnyOf); i++ {
prop.AnyOf[i].Properties = make(map[string]*property)
for _, required := range prop.AnyOf[i].Required {
p, ok := prop.Properties[required]
if !ok {
return errors.Wrap(fmt.Errorf("unhandled property %s in %s tag", required, tagRequiredAnyOf), key)
}
prop.AnyOf[i].Properties[required] = &property{Type: &propertyType{required: true, names: p.Type.names}}
}
}
for i := 0; i < len(prop.AllOf); i++ {
pAllOf := prop.AllOf[i]
// set type to required for If branch
prop.AllOf[i].If.Properties = make(map[string]*property)
for _, required := range pAllOf.If.Required {
p, ok := prop.Properties[required]
if !ok {
return errors.Wrap(fmt.Errorf("unhandled property %s in %s tag", required, tagRequiredIfAny), key)
}
prop.AllOf[i].If.Properties[required] = &property{Type: &propertyType{required: true, names: p.Type.names}}
}
// set type to required for Then branch
prop.AllOf[i].Then.Properties = make(map[string]*property)
for _, required := range pAllOf.Then.Required {
p, ok := prop.Properties[required]
if !ok {
return errors.Wrap(fmt.Errorf("unhandled property %s in %s tag", required, tagRequiredIfAny), key)
}
prop.AllOf[i].Then.Properties[required] = &property{Type: &propertyType{required: true, names: p.Type.names}}
}
}
return nil
}
var (
patternHTTPHeaders = "[.*]*$"
propertyTypes = map[string]propertyTypeName{
nullableTypeBool: TypeNameBool,
"bool": TypeNameBool,
nullableTypeFloat64: TypeNameNumber,
"number": TypeNameNumber,
"float64": TypeNameNumber,
nullableTypeInt: TypeNameInteger,
"int": TypeNameInteger,
"int64": TypeNameInteger,
"uint": TypeNameInteger,
"uint64": TypeNameInteger,
nullableTypeTimeMicrosUnix: TypeNameInteger,
nullableTypeString: TypeNameString,
"string": TypeNameString,
nullableTypeHTTPHeader: TypeNameObject,
"object": TypeNameObject,
}
)
type fieldInfo struct {
field structField
tags map[string]string
parsed *Parsed
}
type schema struct {
ID string `json:"$id,omitempty"`
property
}
type property struct {
Description string `json:"description,omitempty"`
Type *propertyType `json:"type,omitempty"`
// AdditionalProperties should default to `true` and be set to `false`
// in case PatternProperties are set
AdditionalProperties interface{} `json:"additionalProperties,omitempty"`
PatternProperties map[string]*property `json:"patternProperties,omitempty"`
Properties map[string]*property `json:"properties,omitempty"`
Items *property `json:"items,omitempty"`
Required []string `json:"required,omitempty"`
Enum []*string `json:"enum,omitempty"`
Max json.Number `json:"maximum,omitempty"`
MaxLength json.Number `json:"maxLength,omitempty"`
Min json.Number `json:"minimum,omitempty"`
MinItems *int `json:"minItems,omitempty"`
MinLength json.Number `json:"minLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
AllOf []*property `json:"allOf,omitempty"`
AnyOf []*property `json:"anyOf,omitempty"`
If *property `json:"if,omitempty"`
Then *property `json:"then,omitempty"`
}
type propertyType struct {
names []propertyTypeName
required bool
}
func (t *propertyType) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString("")
if len(t.names) == 0 && !t.required {
buffer.WriteString(`""`)
return buffer.Bytes(), nil
}
multipleTypes := !t.required || len(t.names) > 1
if multipleTypes {
buffer.WriteString(`[`)
}
if !t.required {
buffer.WriteString(`"null",`)
}
for i := 0; i < len(t.names); i++ {
if i > 0 {
buffer.WriteString(`,`)
}
buffer.WriteString(`"`)
buffer.WriteString(t.names[i].String())
buffer.WriteString(`"`)
}
if multipleTypes {
buffer.WriteString(`]`)
}
return buffer.Bytes(), nil
}
func (t *propertyType) add(name propertyTypeName) {
if t.contains(name) {
return
}
t.names = append(t.names, name)
}
func (t *propertyType) contains(name propertyTypeName) bool {
for _, n := range t.names {
if n == name {
return true
}
}
return false
}
type propertyTypeName uint8
const (
//TypeNameObject represents an object
TypeNameObject propertyTypeName = iota //object
//TypeNameString represents an string
TypeNameString //string
//TypeNameBool represents an boolean
TypeNameBool //boolean
//TypeNameInteger represents an integer
TypeNameInteger //integer
//TypeNameNumber represents a number (float or integer)
TypeNameNumber //number
//TypeNameArray represents and object
TypeNameArray //array
)
func propertyTypesFromTag(tagName string, tagValue string) ([]propertyTypeName, error) {
names := strings.Split(tagValue, ";")
types := make([]propertyTypeName, len(names))
var ok bool
for i := 0; i < len(names); i++ {
if types[i], ok = propertyTypes[names[i]]; !ok {
return nil, fmt.Errorf("unhandled value %s for tag %s", names[i], tagName)
}
}
return types, nil
}
func (g *JSONSchemaGenerator) customStruct(typ types.Type) (t structType, ok bool) {
t, ok = g.parsed.structTypes[typ.String()]
return
}
func jsonSchemaName(f structField) string {
parts := parseTag(f.tag, "json")
if parts == nil {
return ""
}
if len(parts) == 0 {
return strings.ToLower(f.Name())
}
return parts[0]
}