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