1// Copyright 2016 Google LLC
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package disco represents Google API discovery documents.
6package disco
7
8import (
9	"encoding/json"
10	"fmt"
11	"reflect"
12	"sort"
13	"strings"
14)
15
16// A Document is an API discovery document.
17type Document struct {
18	ID                string             `json:"id"`
19	Name              string             `json:"name"`
20	Version           string             `json:"version"`
21	Title             string             `json:"title"`
22	RootURL           string             `json:"rootUrl"`
23	ServicePath       string             `json:"servicePath"`
24	BasePath          string             `json:"basePath"`
25	DocumentationLink string             `json:"documentationLink"`
26	Auth              Auth               `json:"auth"`
27	Features          []string           `json:"features"`
28	Methods           MethodList         `json:"methods"`
29	Schemas           map[string]*Schema `json:"schemas"`
30	Resources         ResourceList       `json:"resources"`
31}
32
33// init performs additional initialization and checks that
34// were not done during unmarshaling.
35func (d *Document) init() error {
36	schemasByID := map[string]*Schema{}
37	for _, s := range d.Schemas {
38		schemasByID[s.ID] = s
39	}
40	for name, s := range d.Schemas {
41		if s.Ref != "" {
42			return fmt.Errorf("top level schema %q is a reference", name)
43		}
44		s.Name = name
45		if err := s.init(schemasByID); err != nil {
46			return err
47		}
48	}
49	for _, m := range d.Methods {
50		if err := m.init(schemasByID); err != nil {
51			return err
52		}
53	}
54	for _, r := range d.Resources {
55		if err := r.init("", schemasByID); err != nil {
56			return err
57		}
58	}
59	return nil
60}
61
62// NewDocument unmarshals the bytes into a Document.
63// It also validates the document to make sure it is error-free.
64func NewDocument(bytes []byte) (*Document, error) {
65	// The discovery service returns JSON with this format if there's an error, e.g.
66	// the document isn't found.
67	var errDoc struct {
68		Error struct {
69			Code    int
70			Message string
71			Status  string
72		}
73	}
74	if err := json.Unmarshal(bytes, &errDoc); err == nil && errDoc.Error.Code != 0 {
75		return nil, fmt.Errorf("bad discovery doc: %+v", errDoc.Error)
76	}
77
78	var doc Document
79	if err := json.Unmarshal(bytes, &doc); err != nil {
80		return nil, err
81	}
82	if err := doc.init(); err != nil {
83		return nil, err
84	}
85	return &doc, nil
86}
87
88// Auth represents the auth section of a discovery document.
89// Only OAuth2 information is retained.
90type Auth struct {
91	OAuth2Scopes []Scope
92}
93
94// A Scope is an OAuth2 scope.
95type Scope struct {
96	URL         string
97	Description string
98}
99
100// UnmarshalJSON implements the json.Unmarshaler interface.
101func (a *Auth) UnmarshalJSON(data []byte) error {
102	// Pull out the oauth2 scopes and turn them into nice structs.
103	// Ignore other auth information.
104	var m struct {
105		OAuth2 struct {
106			Scopes map[string]struct {
107				Description string
108			}
109		}
110	}
111	if err := json.Unmarshal(data, &m); err != nil {
112		return err
113	}
114	// Sort keys to provide a deterministic ordering, mainly for testing.
115	for _, k := range sortedKeys(m.OAuth2.Scopes) {
116		a.OAuth2Scopes = append(a.OAuth2Scopes, Scope{
117			URL:         k,
118			Description: m.OAuth2.Scopes[k].Description,
119		})
120	}
121	return nil
122}
123
124// A Schema holds a JSON Schema as defined by
125// https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1.
126// We only support the subset of JSON Schema needed for Google API generation.
127type Schema struct {
128	ID                   string // union types not supported
129	Type                 string // union types not supported
130	Format               string
131	Description          string
132	Properties           PropertyList
133	ItemSchema           *Schema `json:"items"` // array of schemas not supported
134	AdditionalProperties *Schema // boolean not supported
135	Ref                  string  `json:"$ref"`
136	Default              string
137	Pattern              string
138	Enums                []string `json:"enum"`
139	// Google extensions to JSON Schema
140	EnumDescriptions []string
141	Variant          *Variant
142
143	RefSchema *Schema `json:"-"` // Schema referred to by $ref
144	Name      string  `json:"-"` // Schema name, if top level
145	Kind      Kind    `json:"-"`
146}
147
148type Variant struct {
149	Discriminant string
150	Map          []*VariantMapItem
151}
152
153type VariantMapItem struct {
154	TypeValue string `json:"type_value"`
155	Ref       string `json:"$ref"`
156}
157
158func (s *Schema) init(topLevelSchemas map[string]*Schema) error {
159	if s == nil {
160		return nil
161	}
162	var err error
163	if s.Ref != "" {
164		if s.RefSchema, err = resolveRef(s.Ref, topLevelSchemas); err != nil {
165			return err
166		}
167	}
168	s.Kind, err = s.initKind()
169	if err != nil {
170		return err
171	}
172	if s.Kind == ArrayKind && s.ItemSchema == nil {
173		return fmt.Errorf("schema %+v: array does not have items", s)
174	}
175	if s.Kind != ArrayKind && s.ItemSchema != nil {
176		return fmt.Errorf("schema %+v: non-array has items", s)
177	}
178	if err := s.AdditionalProperties.init(topLevelSchemas); err != nil {
179		return err
180	}
181	if err := s.ItemSchema.init(topLevelSchemas); err != nil {
182		return err
183	}
184	for _, p := range s.Properties {
185		if err := p.Schema.init(topLevelSchemas); err != nil {
186			return err
187		}
188	}
189	return nil
190}
191
192func resolveRef(ref string, topLevelSchemas map[string]*Schema) (*Schema, error) {
193	rs, ok := topLevelSchemas[ref]
194	if !ok {
195		return nil, fmt.Errorf("could not resolve schema reference %q", ref)
196	}
197	return rs, nil
198}
199
200func (s *Schema) initKind() (Kind, error) {
201	if s.Ref != "" {
202		return ReferenceKind, nil
203	}
204	switch s.Type {
205	case "string", "number", "integer", "boolean", "any":
206		return SimpleKind, nil
207	case "object":
208		if s.AdditionalProperties != nil {
209			if s.AdditionalProperties.Type == "any" {
210				return AnyStructKind, nil
211			}
212			return MapKind, nil
213		}
214		return StructKind, nil
215	case "array":
216		return ArrayKind, nil
217	default:
218		return 0, fmt.Errorf("unknown type %q for schema %q", s.Type, s.ID)
219	}
220}
221
222// ElementSchema returns the schema for the element type of s. For maps,
223// this is the schema of the map values. For arrays, it is the schema
224// of the array item type.
225//
226// ElementSchema panics if called on a schema that is not of kind map or array.
227func (s *Schema) ElementSchema() *Schema {
228	switch s.Kind {
229	case MapKind:
230		return s.AdditionalProperties
231	case ArrayKind:
232		return s.ItemSchema
233	default:
234		panic("ElementSchema called on schema of type " + s.Type)
235	}
236}
237
238// IsIntAsString reports whether the schema represents an integer value
239// formatted as a string.
240func (s *Schema) IsIntAsString() bool {
241	return s.Type == "string" && strings.Contains(s.Format, "int")
242}
243
244// Kind classifies a Schema.
245type Kind int
246
247const (
248	// SimpleKind is the category for any JSON Schema that maps to a
249	// primitive Go type: strings, numbers, booleans, and "any" (since it
250	// maps to interface{}).
251	SimpleKind Kind = iota
252
253	// StructKind is the category for a JSON Schema that declares a JSON
254	// object without any additional (arbitrary) properties.
255	StructKind
256
257	// MapKind is the category for a JSON Schema that declares a JSON
258	// object with additional (arbitrary) properties that have a non-"any"
259	// schema type.
260	MapKind
261
262	// AnyStructKind is the category for a JSON Schema that declares a
263	// JSON object with additional (arbitrary) properties that can be any
264	// type.
265	AnyStructKind
266
267	// ArrayKind is the category for a JSON Schema that declares an
268	// "array" type.
269	ArrayKind
270
271	// ReferenceKind is the category for a JSON Schema that is a reference
272	// to another JSON Schema.  During code generation, these references
273	// are resolved using the API.schemas map.
274	// See https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.28
275	// for more details on the format.
276	ReferenceKind
277)
278
279type Property struct {
280	Name   string
281	Schema *Schema
282}
283
284type PropertyList []*Property
285
286func (pl *PropertyList) UnmarshalJSON(data []byte) error {
287	// In the discovery doc, properties are a map. Convert to a list.
288	var m map[string]*Schema
289	if err := json.Unmarshal(data, &m); err != nil {
290		return err
291	}
292	for _, k := range sortedKeys(m) {
293		*pl = append(*pl, &Property{
294			Name:   k,
295			Schema: m[k],
296		})
297	}
298	return nil
299}
300
301type ResourceList []*Resource
302
303func (rl *ResourceList) UnmarshalJSON(data []byte) error {
304	// In the discovery doc, resources are a map. Convert to a list.
305	var m map[string]*Resource
306	if err := json.Unmarshal(data, &m); err != nil {
307		return err
308	}
309	for _, k := range sortedKeys(m) {
310		r := m[k]
311		r.Name = k
312		*rl = append(*rl, r)
313	}
314	return nil
315}
316
317// A Resource holds information about a Google API Resource.
318type Resource struct {
319	Name      string
320	FullName  string // {parent.FullName}.{Name}
321	Methods   MethodList
322	Resources ResourceList
323}
324
325func (r *Resource) init(parentFullName string, topLevelSchemas map[string]*Schema) error {
326	r.FullName = fmt.Sprintf("%s.%s", parentFullName, r.Name)
327	for _, m := range r.Methods {
328		if err := m.init(topLevelSchemas); err != nil {
329			return err
330		}
331	}
332	for _, r2 := range r.Resources {
333		if err := r2.init(r.FullName, topLevelSchemas); err != nil {
334			return err
335		}
336	}
337	return nil
338}
339
340type MethodList []*Method
341
342func (ml *MethodList) UnmarshalJSON(data []byte) error {
343	// In the discovery doc, resources are a map. Convert to a list.
344	var m map[string]*Method
345	if err := json.Unmarshal(data, &m); err != nil {
346		return err
347	}
348	for _, k := range sortedKeys(m) {
349		meth := m[k]
350		meth.Name = k
351		*ml = append(*ml, meth)
352	}
353	return nil
354}
355
356// A Method holds information about a resource method.
357type Method struct {
358	Name                  string
359	ID                    string
360	Path                  string
361	HTTPMethod            string
362	Description           string
363	Parameters            ParameterList
364	ParameterOrder        []string
365	Request               *Schema
366	Response              *Schema
367	Scopes                []string
368	MediaUpload           *MediaUpload
369	SupportsMediaDownload bool
370
371	JSONMap map[string]interface{} `json:"-"`
372}
373
374type MediaUpload struct {
375	Accept    []string
376	MaxSize   string
377	Protocols map[string]Protocol
378}
379
380type Protocol struct {
381	Multipart bool
382	Path      string
383}
384
385func (m *Method) init(topLevelSchemas map[string]*Schema) error {
386	if err := m.Request.init(topLevelSchemas); err != nil {
387		return err
388	}
389	if err := m.Response.init(topLevelSchemas); err != nil {
390		return err
391	}
392	return nil
393}
394
395func (m *Method) UnmarshalJSON(data []byte) error {
396	type T Method // avoid a recursive call to UnmarshalJSON
397	if err := json.Unmarshal(data, (*T)(m)); err != nil {
398		return err
399	}
400	// Keep the unmarshalled map around, because the generator
401	// outputs it as a comment after the method body.
402	// TODO(jba): make this unnecessary.
403	return json.Unmarshal(data, &m.JSONMap)
404}
405
406type ParameterList []*Parameter
407
408func (pl *ParameterList) UnmarshalJSON(data []byte) error {
409	// In the discovery doc, resources are a map. Convert to a list.
410	var m map[string]*Parameter
411	if err := json.Unmarshal(data, &m); err != nil {
412		return err
413	}
414	for _, k := range sortedKeys(m) {
415		p := m[k]
416		p.Name = k
417		*pl = append(*pl, p)
418	}
419	return nil
420}
421
422// A Parameter holds information about a method parameter.
423type Parameter struct {
424	Name string
425	Schema
426	Required bool
427	Repeated bool
428	Location string
429}
430
431// sortedKeys returns the keys of m, which must be a map[string]T, in sorted order.
432func sortedKeys(m interface{}) []string {
433	vkeys := reflect.ValueOf(m).MapKeys()
434	var keys []string
435	for _, vk := range vkeys {
436		keys = append(keys, vk.Interface().(string))
437	}
438	sort.Strings(keys)
439	return keys
440}
441