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