// Copyright 2016 Google LLC // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package disco represents Google API discovery documents. package disco import ( "encoding/json" "fmt" "reflect" "sort" "strings" ) // A Document is an API discovery document. type Document struct { ID string `json:"id"` Name string `json:"name"` Version string `json:"version"` Title string `json:"title"` RootURL string `json:"rootUrl"` ServicePath string `json:"servicePath"` BasePath string `json:"basePath"` DocumentationLink string `json:"documentationLink"` Auth Auth `json:"auth"` Features []string `json:"features"` Methods MethodList `json:"methods"` Schemas map[string]*Schema `json:"schemas"` Resources ResourceList `json:"resources"` } // init performs additional initialization and checks that // were not done during unmarshaling. func (d *Document) init() error { schemasByID := map[string]*Schema{} for _, s := range d.Schemas { schemasByID[s.ID] = s } for name, s := range d.Schemas { if s.Ref != "" { return fmt.Errorf("top level schema %q is a reference", name) } s.Name = name if err := s.init(schemasByID); err != nil { return err } } for _, m := range d.Methods { if err := m.init(schemasByID); err != nil { return err } } for _, r := range d.Resources { if err := r.init("", schemasByID); err != nil { return err } } return nil } // NewDocument unmarshals the bytes into a Document. // It also validates the document to make sure it is error-free. func NewDocument(bytes []byte) (*Document, error) { // The discovery service returns JSON with this format if there's an error, e.g. // the document isn't found. var errDoc struct { Error struct { Code int Message string Status string } } if err := json.Unmarshal(bytes, &errDoc); err == nil && errDoc.Error.Code != 0 { return nil, fmt.Errorf("bad discovery doc: %+v", errDoc.Error) } var doc Document if err := json.Unmarshal(bytes, &doc); err != nil { return nil, err } if err := doc.init(); err != nil { return nil, err } return &doc, nil } // Auth represents the auth section of a discovery document. // Only OAuth2 information is retained. type Auth struct { OAuth2Scopes []Scope } // A Scope is an OAuth2 scope. type Scope struct { URL string Description string } // UnmarshalJSON implements the json.Unmarshaler interface. func (a *Auth) UnmarshalJSON(data []byte) error { // Pull out the oauth2 scopes and turn them into nice structs. // Ignore other auth information. var m struct { OAuth2 struct { Scopes map[string]struct { Description string } } } if err := json.Unmarshal(data, &m); err != nil { return err } // Sort keys to provide a deterministic ordering, mainly for testing. for _, k := range sortedKeys(m.OAuth2.Scopes) { a.OAuth2Scopes = append(a.OAuth2Scopes, Scope{ URL: k, Description: m.OAuth2.Scopes[k].Description, }) } return nil } // A Schema holds a JSON Schema as defined by // https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1. // We only support the subset of JSON Schema needed for Google API generation. type Schema struct { ID string // union types not supported Type string // union types not supported Format string Description string Properties PropertyList ItemSchema *Schema `json:"items"` // array of schemas not supported AdditionalProperties *Schema // boolean not supported Ref string `json:"$ref"` Default string Pattern string Enums []string `json:"enum"` // Google extensions to JSON Schema EnumDescriptions []string Variant *Variant RefSchema *Schema `json:"-"` // Schema referred to by $ref Name string `json:"-"` // Schema name, if top level Kind Kind `json:"-"` } type Variant struct { Discriminant string Map []*VariantMapItem } type VariantMapItem struct { TypeValue string `json:"type_value"` Ref string `json:"$ref"` } func (s *Schema) init(topLevelSchemas map[string]*Schema) error { if s == nil { return nil } var err error if s.Ref != "" { if s.RefSchema, err = resolveRef(s.Ref, topLevelSchemas); err != nil { return err } } s.Kind, err = s.initKind() if err != nil { return err } if s.Kind == ArrayKind && s.ItemSchema == nil { return fmt.Errorf("schema %+v: array does not have items", s) } if s.Kind != ArrayKind && s.ItemSchema != nil { return fmt.Errorf("schema %+v: non-array has items", s) } if err := s.AdditionalProperties.init(topLevelSchemas); err != nil { return err } if err := s.ItemSchema.init(topLevelSchemas); err != nil { return err } for _, p := range s.Properties { if err := p.Schema.init(topLevelSchemas); err != nil { return err } } return nil } func resolveRef(ref string, topLevelSchemas map[string]*Schema) (*Schema, error) { rs, ok := topLevelSchemas[ref] if !ok { return nil, fmt.Errorf("could not resolve schema reference %q", ref) } return rs, nil } func (s *Schema) initKind() (Kind, error) { if s.Ref != "" { return ReferenceKind, nil } switch s.Type { case "string", "number", "integer", "boolean", "any": return SimpleKind, nil case "object": if s.AdditionalProperties != nil { if s.AdditionalProperties.Type == "any" { return AnyStructKind, nil } return MapKind, nil } return StructKind, nil case "array": return ArrayKind, nil default: return 0, fmt.Errorf("unknown type %q for schema %q", s.Type, s.ID) } } // ElementSchema returns the schema for the element type of s. For maps, // this is the schema of the map values. For arrays, it is the schema // of the array item type. // // ElementSchema panics if called on a schema that is not of kind map or array. func (s *Schema) ElementSchema() *Schema { switch s.Kind { case MapKind: return s.AdditionalProperties case ArrayKind: return s.ItemSchema default: panic("ElementSchema called on schema of type " + s.Type) } } // IsIntAsString reports whether the schema represents an integer value // formatted as a string. func (s *Schema) IsIntAsString() bool { return s.Type == "string" && strings.Contains(s.Format, "int") } // Kind classifies a Schema. type Kind int const ( // SimpleKind is the category for any JSON Schema that maps to a // primitive Go type: strings, numbers, booleans, and "any" (since it // maps to interface{}). SimpleKind Kind = iota // StructKind is the category for a JSON Schema that declares a JSON // object without any additional (arbitrary) properties. StructKind // MapKind is the category for a JSON Schema that declares a JSON // object with additional (arbitrary) properties that have a non-"any" // schema type. MapKind // AnyStructKind is the category for a JSON Schema that declares a // JSON object with additional (arbitrary) properties that can be any // type. AnyStructKind // ArrayKind is the category for a JSON Schema that declares an // "array" type. ArrayKind // ReferenceKind is the category for a JSON Schema that is a reference // to another JSON Schema. During code generation, these references // are resolved using the API.schemas map. // See https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.28 // for more details on the format. ReferenceKind ) type Property struct { Name string Schema *Schema } type PropertyList []*Property func (pl *PropertyList) UnmarshalJSON(data []byte) error { // In the discovery doc, properties are a map. Convert to a list. var m map[string]*Schema if err := json.Unmarshal(data, &m); err != nil { return err } for _, k := range sortedKeys(m) { *pl = append(*pl, &Property{ Name: k, Schema: m[k], }) } return nil } type ResourceList []*Resource func (rl *ResourceList) UnmarshalJSON(data []byte) error { // In the discovery doc, resources are a map. Convert to a list. var m map[string]*Resource if err := json.Unmarshal(data, &m); err != nil { return err } for _, k := range sortedKeys(m) { r := m[k] r.Name = k *rl = append(*rl, r) } return nil } // A Resource holds information about a Google API Resource. type Resource struct { Name string FullName string // {parent.FullName}.{Name} Methods MethodList Resources ResourceList } func (r *Resource) init(parentFullName string, topLevelSchemas map[string]*Schema) error { r.FullName = fmt.Sprintf("%s.%s", parentFullName, r.Name) for _, m := range r.Methods { if err := m.init(topLevelSchemas); err != nil { return err } } for _, r2 := range r.Resources { if err := r2.init(r.FullName, topLevelSchemas); err != nil { return err } } return nil } type MethodList []*Method func (ml *MethodList) UnmarshalJSON(data []byte) error { // In the discovery doc, resources are a map. Convert to a list. var m map[string]*Method if err := json.Unmarshal(data, &m); err != nil { return err } for _, k := range sortedKeys(m) { meth := m[k] meth.Name = k *ml = append(*ml, meth) } return nil } // A Method holds information about a resource method. type Method struct { Name string ID string Path string HTTPMethod string Description string Parameters ParameterList ParameterOrder []string Request *Schema Response *Schema Scopes []string MediaUpload *MediaUpload SupportsMediaDownload bool JSONMap map[string]interface{} `json:"-"` } type MediaUpload struct { Accept []string MaxSize string Protocols map[string]Protocol } type Protocol struct { Multipart bool Path string } func (m *Method) init(topLevelSchemas map[string]*Schema) error { if err := m.Request.init(topLevelSchemas); err != nil { return err } if err := m.Response.init(topLevelSchemas); err != nil { return err } return nil } func (m *Method) UnmarshalJSON(data []byte) error { type T Method // avoid a recursive call to UnmarshalJSON if err := json.Unmarshal(data, (*T)(m)); err != nil { return err } // Keep the unmarshalled map around, because the generator // outputs it as a comment after the method body. // TODO(jba): make this unnecessary. return json.Unmarshal(data, &m.JSONMap) } type ParameterList []*Parameter func (pl *ParameterList) UnmarshalJSON(data []byte) error { // In the discovery doc, resources are a map. Convert to a list. var m map[string]*Parameter if err := json.Unmarshal(data, &m); err != nil { return err } for _, k := range sortedKeys(m) { p := m[k] p.Name = k *pl = append(*pl, p) } return nil } // A Parameter holds information about a method parameter. type Parameter struct { Name string Schema Required bool Repeated bool Location string } // sortedKeys returns the keys of m, which must be a map[string]T, in sorted order. func sortedKeys(m interface{}) []string { vkeys := reflect.ValueOf(m).MapKeys() var keys []string for _, vk := range vkeys { keys = append(keys, vk.Interface().(string)) } sort.Strings(keys) return keys }