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