-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
disco.go
441 lines (397 loc) · 11.3 KB
/
disco.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
// 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"`
MTLSRootURL string `json:"mtlsRootUrl"`
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 {
ID 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{
ID: 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
}