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