github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/goagen/gen_schema/json_schema.go (about)

     1  package genschema
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/url"
     7  	"reflect"
     8  	"sort"
     9  	"strconv"
    10  
    11  	"github.com/goadesign/goa/design"
    12  )
    13  
    14  type (
    15  	// JSONSchema represents an instance of a JSON schema.
    16  	// See http://json-schema.org/documentation.html
    17  	JSONSchema struct {
    18  		Schema string `json:"$schema,omitempty"`
    19  		// Core schema
    20  		ID           string                 `json:"id,omitempty"`
    21  		Title        string                 `json:"title,omitempty"`
    22  		Type         JSONType               `json:"type,omitempty"`
    23  		Items        *JSONSchema            `json:"items,omitempty"`
    24  		Properties   map[string]*JSONSchema `json:"properties,omitempty"`
    25  		Definitions  map[string]*JSONSchema `json:"definitions,omitempty"`
    26  		Description  string                 `json:"description,omitempty"`
    27  		DefaultValue interface{}            `json:"default,omitempty"`
    28  		Example      interface{}            `json:"example,omitempty"`
    29  
    30  		// Hyper schema
    31  		Media     *JSONMedia  `json:"media,omitempty"`
    32  		ReadOnly  bool        `json:"readOnly,omitempty"`
    33  		PathStart string      `json:"pathStart,omitempty"`
    34  		Links     []*JSONLink `json:"links,omitempty"`
    35  		Ref       string      `json:"$ref,omitempty"`
    36  
    37  		// Validation
    38  		Enum                 []interface{} `json:"enum,omitempty"`
    39  		Format               string        `json:"format,omitempty"`
    40  		Pattern              string        `json:"pattern,omitempty"`
    41  		Minimum              *float64      `json:"minimum,omitempty"`
    42  		Maximum              *float64      `json:"maximum,omitempty"`
    43  		MinLength            *int          `json:"minLength,omitempty"`
    44  		MaxLength            *int          `json:"maxLength,omitempty"`
    45  		Required             []string      `json:"required,omitempty"`
    46  		AdditionalProperties bool          `json:"additionalProperties,omitempty"`
    47  
    48  		// Union
    49  		AnyOf []*JSONSchema `json:"anyOf,omitempty"`
    50  	}
    51  
    52  	// JSONType is the JSON type enum.
    53  	JSONType string
    54  
    55  	// JSONMedia represents a "media" field in a JSON hyper schema.
    56  	JSONMedia struct {
    57  		BinaryEncoding string `json:"binaryEncoding,omitempty"`
    58  		Type           string `json:"type,omitempty"`
    59  	}
    60  
    61  	// JSONLink represents a "link" field in a JSON hyper schema.
    62  	JSONLink struct {
    63  		Title        string      `json:"title,omitempty"`
    64  		Description  string      `json:"description,omitempty"`
    65  		Rel          string      `json:"rel,omitempty"`
    66  		Href         string      `json:"href,omitempty"`
    67  		Method       string      `json:"method,omitempty"`
    68  		Schema       *JSONSchema `json:"schema,omitempty"`
    69  		TargetSchema *JSONSchema `json:"targetSchema,omitempty"`
    70  		MediaType    string      `json:"mediaType,omitempty"`
    71  		EncType      string      `json:"encType,omitempty"`
    72  	}
    73  )
    74  
    75  const (
    76  	// JSONArray represents a JSON array.
    77  	JSONArray JSONType = "array"
    78  	// JSONBoolean represents a JSON boolean.
    79  	JSONBoolean = "boolean"
    80  	// JSONInteger represents a JSON number without a fraction or exponent part.
    81  	JSONInteger = "integer"
    82  	// JSONNumber represents any JSON number. Number includes integer.
    83  	JSONNumber = "number"
    84  	// JSONNull represents the JSON null value.
    85  	JSONNull = "null"
    86  	// JSONObject represents a JSON object.
    87  	JSONObject = "object"
    88  	// JSONString represents a JSON string.
    89  	JSONString = "string"
    90  	// JSONFile is an extension used by Swagger to represent a file download.
    91  	JSONFile = "file"
    92  )
    93  
    94  // SchemaRef is the JSON Hyper-schema standard href.
    95  const SchemaRef = "http://json-schema.org/draft-04/hyper-schema"
    96  
    97  var (
    98  	// Definitions contains the generated JSON schema definitions
    99  	Definitions map[string]*JSONSchema
   100  )
   101  
   102  // Initialize the global variables
   103  func init() {
   104  	Definitions = make(map[string]*JSONSchema)
   105  }
   106  
   107  // NewJSONSchema instantiates a new JSON schema.
   108  func NewJSONSchema() *JSONSchema {
   109  	js := JSONSchema{
   110  		Properties:  make(map[string]*JSONSchema),
   111  		Definitions: make(map[string]*JSONSchema),
   112  	}
   113  	return &js
   114  }
   115  
   116  // JSON serializes the schema into JSON.
   117  // It makes sure the "$schema" standard field is set if needed prior to delegating to the standard
   118  // JSON marshaler.
   119  func (s *JSONSchema) JSON() ([]byte, error) {
   120  	if s.Ref == "" {
   121  		s.Schema = SchemaRef
   122  	}
   123  	return json.Marshal(s)
   124  }
   125  
   126  // APISchema produces the API JSON hyper schema.
   127  func APISchema(api *design.APIDefinition) *JSONSchema {
   128  	api.IterateResources(func(r *design.ResourceDefinition) error {
   129  		GenerateResourceDefinition(api, r)
   130  		return nil
   131  	})
   132  	scheme := "http"
   133  	if len(api.Schemes) > 0 {
   134  		scheme = api.Schemes[0]
   135  	}
   136  	u := url.URL{Scheme: scheme, Host: api.Host}
   137  	href := u.String()
   138  	links := []*JSONLink{
   139  		{
   140  			Href: href,
   141  			Rel:  "self",
   142  		},
   143  		{
   144  			Href:   "/schema",
   145  			Method: "GET",
   146  			Rel:    "self",
   147  			TargetSchema: &JSONSchema{
   148  				Schema:               SchemaRef,
   149  				AdditionalProperties: true,
   150  			},
   151  		},
   152  	}
   153  	s := JSONSchema{
   154  		ID:          fmt.Sprintf("%s/schema", href),
   155  		Title:       api.Title,
   156  		Description: api.Description,
   157  		Type:        JSONObject,
   158  		Definitions: Definitions,
   159  		Properties:  propertiesFromDefs(Definitions, "#/definitions/"),
   160  		Links:       links,
   161  	}
   162  	return &s
   163  }
   164  
   165  // GenerateResourceDefinition produces the JSON schema corresponding to the given API resource.
   166  // It stores the results in cachedSchema.
   167  func GenerateResourceDefinition(api *design.APIDefinition, r *design.ResourceDefinition) {
   168  	s := NewJSONSchema()
   169  	s.Description = r.Description
   170  	s.Type = JSONObject
   171  	s.Title = r.Name
   172  	Definitions[r.Name] = s
   173  	if mt, ok := api.MediaTypes[r.MediaType]; ok {
   174  		for _, v := range mt.Views {
   175  			buildMediaTypeSchema(api, mt, v.Name, s)
   176  		}
   177  	}
   178  	r.IterateActions(func(a *design.ActionDefinition) error {
   179  		var requestSchema *JSONSchema
   180  		if a.Payload != nil {
   181  			requestSchema = TypeSchema(api, a.Payload)
   182  			requestSchema.Description = a.Name + " payload"
   183  		}
   184  		if a.Params != nil {
   185  			params := design.DupAtt(a.Params)
   186  			// We don't want to keep the path params, these are defined inline in the href
   187  			for _, r := range a.Routes {
   188  				for _, p := range r.Params() {
   189  					delete(params.Type.ToObject(), p)
   190  				}
   191  			}
   192  		}
   193  		var targetSchema *JSONSchema
   194  		var identifier string
   195  		for _, resp := range a.Responses {
   196  			if mt, ok := api.MediaTypes[resp.MediaType]; ok {
   197  				if identifier == "" {
   198  					identifier = mt.Identifier
   199  				} else {
   200  					identifier = ""
   201  				}
   202  				if targetSchema == nil {
   203  					targetSchema = TypeSchema(api, mt)
   204  				} else if targetSchema.AnyOf == nil {
   205  					firstSchema := targetSchema
   206  					targetSchema = NewJSONSchema()
   207  					targetSchema.AnyOf = []*JSONSchema{firstSchema, TypeSchema(api, mt)}
   208  				} else {
   209  					targetSchema.AnyOf = append(targetSchema.AnyOf, TypeSchema(api, mt))
   210  				}
   211  			}
   212  		}
   213  		for i, r := range a.Routes {
   214  			link := JSONLink{
   215  				Title:        a.Name,
   216  				Rel:          a.Name,
   217  				Href:         toSchemaHref(api, r),
   218  				Method:       r.Verb,
   219  				Schema:       requestSchema,
   220  				TargetSchema: targetSchema,
   221  				MediaType:    identifier,
   222  			}
   223  			if i == 0 {
   224  				if ca := a.Parent.CanonicalAction(); ca != nil {
   225  					if ca.Name == a.Name {
   226  						link.Rel = "self"
   227  					}
   228  				}
   229  			}
   230  			s.Links = append(s.Links, &link)
   231  		}
   232  		return nil
   233  	})
   234  }
   235  
   236  // MediaTypeRef produces the JSON reference to the media type definition with the given view.
   237  func MediaTypeRef(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string) string {
   238  	projected, _, err := mt.Project(view)
   239  	if err != nil {
   240  		panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug
   241  	}
   242  	if _, ok := Definitions[projected.TypeName]; !ok {
   243  		GenerateMediaTypeDefinition(api, projected, "default")
   244  	}
   245  	ref := fmt.Sprintf("#/definitions/%s", projected.TypeName)
   246  	return ref
   247  }
   248  
   249  // TypeRef produces the JSON reference to the type definition.
   250  func TypeRef(api *design.APIDefinition, ut *design.UserTypeDefinition) string {
   251  	if _, ok := Definitions[ut.TypeName]; !ok {
   252  		GenerateTypeDefinition(api, ut)
   253  	}
   254  	return fmt.Sprintf("#/definitions/%s", ut.TypeName)
   255  }
   256  
   257  // GenerateMediaTypeDefinition produces the JSON schema corresponding to the given media type and
   258  // given view.
   259  func GenerateMediaTypeDefinition(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string) {
   260  	if _, ok := Definitions[mt.TypeName]; ok {
   261  		return
   262  	}
   263  	s := NewJSONSchema()
   264  	s.Title = fmt.Sprintf("Mediatype identifier: %s", mt.Identifier)
   265  	Definitions[mt.TypeName] = s
   266  	buildMediaTypeSchema(api, mt, view, s)
   267  }
   268  
   269  // GenerateTypeDefinition produces the JSON schema corresponding to the given type.
   270  func GenerateTypeDefinition(api *design.APIDefinition, ut *design.UserTypeDefinition) {
   271  	if _, ok := Definitions[ut.TypeName]; ok {
   272  		return
   273  	}
   274  	s := NewJSONSchema()
   275  	s.Title = ut.TypeName
   276  	Definitions[ut.TypeName] = s
   277  	buildAttributeSchema(api, s, ut.AttributeDefinition)
   278  }
   279  
   280  // TypeSchema produces the JSON schema corresponding to the given data type.
   281  func TypeSchema(api *design.APIDefinition, t design.DataType) *JSONSchema {
   282  	s := NewJSONSchema()
   283  	switch actual := t.(type) {
   284  	case design.Primitive:
   285  		if name := actual.Name(); name != "any" {
   286  			s.Type = JSONType(actual.Name())
   287  		}
   288  		switch actual.Kind() {
   289  		case design.UUIDKind:
   290  			s.Format = "uuid"
   291  		case design.DateTimeKind:
   292  			s.Format = "date-time"
   293  		case design.NumberKind:
   294  			s.Format = "double"
   295  		case design.IntegerKind:
   296  			s.Format = "int64"
   297  		}
   298  	case *design.Array:
   299  		s.Type = JSONArray
   300  		s.Items = NewJSONSchema()
   301  		buildAttributeSchema(api, s.Items, actual.ElemType)
   302  	case design.Object:
   303  		s.Type = JSONObject
   304  		for n, at := range actual {
   305  			prop := NewJSONSchema()
   306  			buildAttributeSchema(api, prop, at)
   307  			s.Properties[n] = prop
   308  		}
   309  	case *design.Hash:
   310  		s.Type = JSONObject
   311  		s.AdditionalProperties = true
   312  	case *design.UserTypeDefinition:
   313  		s.Ref = TypeRef(api, actual)
   314  	case *design.MediaTypeDefinition:
   315  		// Use "default" view by default
   316  		s.Ref = MediaTypeRef(api, actual, design.DefaultView)
   317  	}
   318  	return s
   319  }
   320  
   321  type mergeItems []struct {
   322  	a, b   interface{}
   323  	needed bool
   324  }
   325  
   326  func (s *JSONSchema) createMergeItems(other *JSONSchema) mergeItems {
   327  	return mergeItems{
   328  		{&s.ID, other.ID, s.ID == ""},
   329  		{&s.Type, other.Type, s.Type == ""},
   330  		{&s.Ref, other.Ref, s.Ref == ""},
   331  		{&s.Items, other.Items, s.Items == nil},
   332  		{&s.DefaultValue, other.DefaultValue, s.DefaultValue == nil},
   333  		{&s.Title, other.Title, s.Title == ""},
   334  		{&s.Media, other.Media, s.Media == nil},
   335  		{&s.ReadOnly, other.ReadOnly, s.ReadOnly == false},
   336  		{&s.PathStart, other.PathStart, s.PathStart == ""},
   337  		{&s.Enum, other.Enum, s.Enum == nil},
   338  		{&s.Format, other.Format, s.Format == ""},
   339  		{&s.Pattern, other.Pattern, s.Pattern == ""},
   340  		{&s.AdditionalProperties, other.AdditionalProperties, s.AdditionalProperties == false},
   341  		{
   342  			a: s.Minimum, b: other.Minimum,
   343  			needed: (s.Minimum == nil && s.Minimum != nil) ||
   344  				(s.Minimum != nil && other.Minimum != nil && *s.Minimum > *other.Minimum),
   345  		},
   346  		{
   347  			a: s.Maximum, b: other.Maximum,
   348  			needed: (s.Maximum == nil && other.Maximum != nil) ||
   349  				(s.Maximum != nil && other.Maximum != nil && *s.Maximum < *other.Maximum),
   350  		},
   351  		{
   352  			a: s.MinLength, b: other.MinLength,
   353  			needed: (s.MinLength == nil && other.MinLength != nil) ||
   354  				(s.MinLength != nil && other.MinLength != nil && *s.MinLength > *other.MinLength),
   355  		},
   356  		{
   357  			a: s.MaxLength, b: other.MaxLength,
   358  			needed: (s.MaxLength == nil && other.MaxLength != nil) ||
   359  				(s.MaxLength != nil && other.MaxLength != nil && *s.MaxLength > *other.MaxLength),
   360  		},
   361  	}
   362  }
   363  
   364  // Merge does a two level deep merge of other into s.
   365  func (s *JSONSchema) Merge(other *JSONSchema) {
   366  	items := s.createMergeItems(other)
   367  	for _, v := range items {
   368  		if v.needed && v.b != nil {
   369  			reflect.Indirect(reflect.ValueOf(v.a)).Set(reflect.ValueOf(v.b))
   370  		}
   371  	}
   372  
   373  	for n, p := range other.Properties {
   374  		if _, ok := s.Properties[n]; !ok {
   375  			if s.Properties == nil {
   376  				s.Properties = make(map[string]*JSONSchema)
   377  			}
   378  			s.Properties[n] = p
   379  		}
   380  	}
   381  
   382  	for n, d := range other.Definitions {
   383  		if _, ok := s.Definitions[n]; !ok {
   384  			s.Definitions[n] = d
   385  		}
   386  	}
   387  
   388  	for _, l := range other.Links {
   389  		s.Links = append(s.Links, l)
   390  	}
   391  
   392  	for _, r := range other.Required {
   393  		s.Required = append(s.Required, r)
   394  	}
   395  }
   396  
   397  // Dup creates a shallow clone of the given schema.
   398  func (s *JSONSchema) Dup() *JSONSchema {
   399  	js := JSONSchema{
   400  		ID:                   s.ID,
   401  		Description:          s.Description,
   402  		Schema:               s.Schema,
   403  		Type:                 s.Type,
   404  		DefaultValue:         s.DefaultValue,
   405  		Title:                s.Title,
   406  		Media:                s.Media,
   407  		ReadOnly:             s.ReadOnly,
   408  		PathStart:            s.PathStart,
   409  		Links:                s.Links,
   410  		Ref:                  s.Ref,
   411  		Enum:                 s.Enum,
   412  		Format:               s.Format,
   413  		Pattern:              s.Pattern,
   414  		Minimum:              s.Minimum,
   415  		Maximum:              s.Maximum,
   416  		MinLength:            s.MinLength,
   417  		MaxLength:            s.MaxLength,
   418  		Required:             s.Required,
   419  		AdditionalProperties: s.AdditionalProperties,
   420  	}
   421  	for n, p := range s.Properties {
   422  		js.Properties[n] = p.Dup()
   423  	}
   424  	if s.Items != nil {
   425  		js.Items = s.Items.Dup()
   426  	}
   427  	for n, d := range s.Definitions {
   428  		js.Definitions[n] = d.Dup()
   429  	}
   430  	return &js
   431  }
   432  
   433  // buildAttributeSchema initializes the given JSON schema that corresponds to the given attribute.
   434  func buildAttributeSchema(api *design.APIDefinition, s *JSONSchema, at *design.AttributeDefinition) *JSONSchema {
   435  	if at.View != "" {
   436  		inner := NewJSONSchema()
   437  		inner.Ref = MediaTypeRef(api, at.Type.(*design.MediaTypeDefinition), at.View)
   438  		s.Merge(inner)
   439  		return s
   440  	}
   441  	s.Merge(TypeSchema(api, at.Type))
   442  	if s.Ref != "" {
   443  		// Ref is exclusive with other fields
   444  		return s
   445  	}
   446  	s.DefaultValue = toStringMap(at.DefaultValue)
   447  	s.Description = at.Description
   448  	s.Example = at.GenerateExample(api.RandomGenerator(), nil)
   449  	val := at.Validation
   450  	if val == nil {
   451  		return s
   452  	}
   453  	s.Enum = val.Values
   454  	s.Format = val.Format
   455  	s.Pattern = val.Pattern
   456  	if val.Minimum != nil {
   457  		s.Minimum = val.Minimum
   458  	}
   459  	if val.Maximum != nil {
   460  		s.Maximum = val.Maximum
   461  	}
   462  	if val.MinLength != nil {
   463  		s.MinLength = val.MinLength
   464  	}
   465  	if val.MaxLength != nil {
   466  		s.MaxLength = val.MaxLength
   467  	}
   468  	s.Required = val.Required
   469  	return s
   470  }
   471  
   472  // toStringMap converts map[interface{}]interface{} to a map[string]interface{} when possible.
   473  func toStringMap(val interface{}) interface{} {
   474  	switch actual := val.(type) {
   475  	case map[interface{}]interface{}:
   476  		m := make(map[string]interface{})
   477  		for k, v := range actual {
   478  			m[toString(k)] = toStringMap(v)
   479  		}
   480  		return m
   481  	case []interface{}:
   482  		mapSlice := make([]interface{}, len(actual))
   483  		for i, e := range actual {
   484  			mapSlice[i] = toStringMap(e)
   485  		}
   486  		return mapSlice
   487  	default:
   488  		return actual
   489  	}
   490  }
   491  
   492  // toString returns the string representation of the given type.
   493  func toString(val interface{}) string {
   494  	switch actual := val.(type) {
   495  	case string:
   496  		return actual
   497  	case int:
   498  		return strconv.Itoa(actual)
   499  	case float64:
   500  		return strconv.FormatFloat(actual, 'f', -1, 64)
   501  	case bool:
   502  		return strconv.FormatBool(actual)
   503  	default:
   504  		panic("unexpected key type")
   505  	}
   506  }
   507  
   508  // toSchemaHref produces a href that replaces the path wildcards with JSON schema references when
   509  // appropriate.
   510  func toSchemaHref(api *design.APIDefinition, r *design.RouteDefinition) string {
   511  	params := r.Params()
   512  	args := make([]interface{}, len(params))
   513  	for i, p := range params {
   514  		args[i] = fmt.Sprintf("/{%s}", p)
   515  	}
   516  	tmpl := design.WildcardRegex.ReplaceAllLiteralString(r.FullPath(), "%s")
   517  	return fmt.Sprintf(tmpl, args...)
   518  }
   519  
   520  // propertiesFromDefs creates a Properties map referencing the given definitions under the given
   521  // path.
   522  func propertiesFromDefs(definitions map[string]*JSONSchema, path string) map[string]*JSONSchema {
   523  	res := make(map[string]*JSONSchema, len(definitions))
   524  	for n := range definitions {
   525  		if n == "identity" {
   526  			continue
   527  		}
   528  		s := NewJSONSchema()
   529  		s.Ref = path + n
   530  		res[n] = s
   531  	}
   532  	return res
   533  }
   534  
   535  // buildMediaTypeSchema initializes s as the JSON schema representing mt for the given view.
   536  func buildMediaTypeSchema(api *design.APIDefinition, mt *design.MediaTypeDefinition, view string, s *JSONSchema) {
   537  	s.Media = &JSONMedia{Type: mt.Identifier}
   538  	projected, linksUT, err := mt.Project(view)
   539  	if err != nil {
   540  		panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug
   541  	}
   542  	if linksUT != nil {
   543  		links := linksUT.Type.ToObject()
   544  		lnames := make([]string, len(links))
   545  		i := 0
   546  		for n := range links {
   547  			lnames[i] = n
   548  			i++
   549  		}
   550  		sort.Strings(lnames)
   551  		for _, ln := range lnames {
   552  			var (
   553  				att  = links[ln]
   554  				lmt  = att.Type.(*design.MediaTypeDefinition)
   555  				r    = lmt.Resource
   556  				href string
   557  			)
   558  			if r != nil {
   559  				href = toSchemaHref(api, r.CanonicalAction().Routes[0])
   560  			}
   561  			sm := NewJSONSchema()
   562  			sm.Ref = MediaTypeRef(api, lmt, "default")
   563  			s.Links = append(s.Links, &JSONLink{
   564  				Title:        ln,
   565  				Rel:          ln,
   566  				Description:  att.Description,
   567  				Href:         href,
   568  				Method:       "GET",
   569  				TargetSchema: sm,
   570  				MediaType:    lmt.Identifier,
   571  			})
   572  		}
   573  	}
   574  	buildAttributeSchema(api, s, projected.AttributeDefinition)
   575  }