2017-08-05 18:17:15 +00:00
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/json"
2017-12-03 20:01:25 +00:00
"errors"
2017-08-05 18:17:15 +00:00
"flag"
"fmt"
"go/format"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
"google.golang.org/api/google-api-go-generator/internal/disco"
)
const (
googleDiscoveryURL = "https://www.googleapis.com/discovery/v1/apis"
generatorVersion = "20170210"
)
var (
apiToGenerate = flag . String ( "api" , "*" , "The API ID to generate, like 'tasks:v1'. A value of '*' means all." )
useCache = flag . Bool ( "cache" , true , "Use cache of discovered Google API discovery documents." )
genDir = flag . String ( "gendir" , "" , "Directory to use to write out generated Go files" )
build = flag . Bool ( "build" , false , "Compile generated packages." )
install = flag . Bool ( "install" , false , "Install generated packages." )
apisURL = flag . String ( "discoveryurl" , googleDiscoveryURL , "URL to root discovery document" )
publicOnly = flag . Bool ( "publiconly" , true , "Only build public, released APIs. Only applicable for Google employees." )
jsonFile = flag . String ( "api_json_file" , "" , "If non-empty, the path to a local file on disk containing the API to generate. Exclusive with setting --api." )
output = flag . String ( "output" , "" , "(optional) Path to source output file. If not specified, the API name and version are used to construct an output path (e.g. tasks/v1)." )
apiPackageBase = flag . String ( "api_pkg_base" , "google.golang.org/api" , "Go package prefix to use for all generated APIs." )
baseURL = flag . String ( "base_url" , "" , "(optional) Override the default service API URL. If empty, the service's root URL will be used." )
headerPath = flag . String ( "header_path" , "" , "If non-empty, prepend the contents of this file to generated services." )
contextHTTPPkg = flag . String ( "ctxhttp_pkg" , "golang.org/x/net/context/ctxhttp" , "Go package path of the 'ctxhttp' package." )
contextPkg = flag . String ( "context_pkg" , "golang.org/x/net/context" , "Go package path of the 'context' package." )
gensupportPkg = flag . String ( "gensupport_pkg" , "google.golang.org/api/gensupport" , "Go package path of the 'api/gensupport' support package." )
googleapiPkg = flag . String ( "googleapi_pkg" , "google.golang.org/api/googleapi" , "Go package path of the 'api/googleapi' support package." )
serviceTypes = [ ] string { "Service" , "APIService" }
)
// API represents an API to generate, as well as its state while it's
// generating.
type API struct {
// Fields needed before generating code, to select and find the APIs
// to generate.
// These fields usually come from the "directory item" JSON objects
// that are provided by the googleDiscoveryURL. We unmarshal a directory
// item directly into this struct.
ID string ` json:"id" `
Name string ` json:"name" `
Version string ` json:"version" `
DiscoveryLink string ` json:"discoveryRestUrl" ` // absolute
doc * disco . Document
// TODO(jba): remove m when we've fully converted to using disco.
m map [ string ] interface { }
forceJSON [ ] byte // if non-nil, the JSON schema file. else fetched.
usedNames namePool
schemas map [ string ] * Schema // apiName -> schema
responseTypes map [ string ] bool
p func ( format string , args ... interface { } ) // print raw
pn func ( format string , args ... interface { } ) // print with newline
}
func ( a * API ) sortedSchemaNames ( ) ( names [ ] string ) {
for name := range a . schemas {
names = append ( names , name )
}
sort . Strings ( names )
return
}
func ( a * API ) Schema ( name string ) * Schema {
return a . schemas [ name ]
}
type generateError struct {
api * API
error error
}
func ( e * generateError ) Error ( ) string {
return fmt . Sprintf ( "API %s failed to generate code: %v" , e . api . ID , e . error )
}
type compileError struct {
api * API
output string
}
func ( e * compileError ) Error ( ) string {
return fmt . Sprintf ( "API %s failed to compile:\n%v" , e . api . ID , e . output )
}
func main ( ) {
flag . Parse ( )
if * install {
* build = true
}
var (
apiIds = [ ] string { }
matches = [ ] * API { }
errors = [ ] error { }
)
for _ , api := range getAPIs ( ) {
apiIds = append ( apiIds , api . ID )
if ! api . want ( ) {
continue
}
matches = append ( matches , api )
log . Printf ( "Generating API %s" , api . ID )
err := api . WriteGeneratedCode ( )
2017-12-03 20:01:25 +00:00
if err != nil && err != errNoDoc {
2017-08-05 18:17:15 +00:00
errors = append ( errors , & generateError { api , err } )
continue
}
2017-12-03 20:01:25 +00:00
if * build && err == nil {
2017-08-05 18:17:15 +00:00
var args [ ] string
if * install {
args = append ( args , "install" )
} else {
args = append ( args , "build" )
}
args = append ( args , api . Target ( ) )
out , err := exec . Command ( "go" , args ... ) . CombinedOutput ( )
if err != nil {
errors = append ( errors , & compileError { api , string ( out ) } )
}
}
}
if len ( matches ) == 0 {
log . Fatalf ( "No APIs matched %q; options are %v" , * apiToGenerate , apiIds )
}
if len ( errors ) > 0 {
log . Printf ( "%d API(s) failed to generate or compile:" , len ( errors ) )
for _ , ce := range errors {
log . Printf ( ce . Error ( ) )
}
os . Exit ( 1 )
}
}
func ( a * API ) want ( ) bool {
if * jsonFile != "" {
// Return true early, before calling a.JSONFile()
// which will require a GOPATH be set. This is for
// integration with Google's build system genrules
// where there is no GOPATH.
return true
}
// Skip this API if we're in cached mode and the files don't exist on disk.
if * useCache {
if _ , err := os . Stat ( a . JSONFile ( ) ) ; os . IsNotExist ( err ) {
return false
}
}
return * apiToGenerate == "*" || * apiToGenerate == a . ID
}
func getAPIs ( ) [ ] * API {
if * jsonFile != "" {
return getAPIsFromFile ( )
}
var bytes [ ] byte
var source string
apiListFile := filepath . Join ( genDirRoot ( ) , "api-list.json" )
if * useCache {
if ! * publicOnly {
log . Fatalf ( "-cache=true not compatible with -publiconly=false" )
}
var err error
bytes , err = ioutil . ReadFile ( apiListFile )
if err != nil {
log . Fatal ( err )
}
source = apiListFile
} else {
bytes = slurpURL ( * apisURL )
if * publicOnly {
if err := writeFile ( apiListFile , bytes ) ; err != nil {
log . Fatal ( err )
}
}
source = * apisURL
}
apis , err := unmarshalAPIs ( bytes )
if err != nil {
log . Fatalf ( "error decoding JSON in %s: %v" , source , err )
}
if ! * publicOnly && * apiToGenerate != "*" {
apis = append ( apis , apiFromID ( * apiToGenerate ) )
}
return apis
}
func unmarshalAPIs ( bytes [ ] byte ) ( [ ] * API , error ) {
var itemObj struct { Items [ ] * API }
if err := json . Unmarshal ( bytes , & itemObj ) ; err != nil {
return nil , err
}
return itemObj . Items , nil
}
func apiFromID ( apiID string ) * API {
parts := strings . Split ( apiID , ":" )
if len ( parts ) != 2 {
log . Fatalf ( "malformed API name: %q" , apiID )
}
return & API {
ID : apiID ,
Name : parts [ 0 ] ,
Version : parts [ 1 ] ,
}
}
// getAPIsFromFile handles the case of generating exactly one API
// from the flag given in --api_json_file
func getAPIsFromFile ( ) [ ] * API {
if * apiToGenerate != "*" {
log . Fatalf ( "Can't set --api with --api_json_file." )
}
if ! * publicOnly {
log . Fatalf ( "Can't set --publiconly with --api_json_file." )
}
a , err := apiFromFile ( * jsonFile )
if err != nil {
log . Fatal ( err )
}
return [ ] * API { a }
}
func apiFromFile ( file string ) ( * API , error ) {
jsonBytes , err := ioutil . ReadFile ( file )
if err != nil {
return nil , fmt . Errorf ( "Error reading %s: %v" , file , err )
}
doc , err := disco . NewDocument ( jsonBytes )
if err != nil {
return nil , fmt . Errorf ( "reading document from %q: %v" , file , err )
}
a := & API {
ID : doc . ID ,
Name : doc . Name ,
Version : doc . Version ,
forceJSON : jsonBytes ,
doc : doc ,
}
return a , nil
}
func writeFile ( file string , contents [ ] byte ) error {
// Don't write it if the contents are identical.
existing , err := ioutil . ReadFile ( file )
if err == nil && ( bytes . Equal ( existing , contents ) || basicallyEqual ( existing , contents ) ) {
return nil
}
outdir := filepath . Dir ( file )
if err = os . MkdirAll ( outdir , 0755 ) ; err != nil {
return fmt . Errorf ( "failed to Mkdir %s: %v" , outdir , err )
}
return ioutil . WriteFile ( file , contents , 0644 )
}
var ignoreLines = regexp . MustCompile ( ` (?m)^\s+"(?:etag|revision)": ".+\n ` )
// basicallyEqual reports whether a and b are equal except for boring
// differences like ETag updates.
func basicallyEqual ( a , b [ ] byte ) bool {
return ignoreLines . Match ( a ) && ignoreLines . Match ( b ) &&
bytes . Equal ( ignoreLines . ReplaceAll ( a , nil ) , ignoreLines . ReplaceAll ( b , nil ) )
}
func slurpURL ( urlStr string ) [ ] byte {
if * useCache {
log . Fatalf ( "Invalid use of slurpURL in cached mode for URL %s" , urlStr )
}
req , err := http . NewRequest ( "GET" , urlStr , nil )
if err != nil {
log . Fatal ( err )
}
if * publicOnly {
req . Header . Add ( "X-User-IP" , "0.0.0.0" ) // hack
}
res , err := http . DefaultClient . Do ( req )
if err != nil {
log . Fatalf ( "Error fetching URL %s: %v" , urlStr , err )
}
2017-12-03 20:01:25 +00:00
if res . StatusCode >= 300 {
log . Printf ( "WARNING: URL %s served status code %d" , urlStr , res . StatusCode )
return nil
}
2017-08-05 18:17:15 +00:00
bs , err := ioutil . ReadAll ( res . Body )
if err != nil {
log . Fatalf ( "Error reading body of URL %s: %v" , urlStr , err )
}
return bs
}
func panicf ( format string , args ... interface { } ) {
panic ( fmt . Sprintf ( format , args ... ) )
}
// namePool keeps track of used names and assigns free ones based on a
// preferred name
type namePool struct {
m map [ string ] bool // lazily initialized
}
// oddVersionRE matches unusual API names like directory_v1.
var oddVersionRE = regexp . MustCompile ( ` ^(.+)_(v[\d\.]+)$ ` )
// renameVersion conditionally rewrites the provided version such
// that the final path component of the import path doesn't look
// like a Go identifier. This keeps the consistency that import paths
// for the generated Go packages look like:
// google.golang.org/api/NAME/v<version>
// and have package NAME.
// See https://github.com/google/google-api-go-client/issues/78
func renameVersion ( version string ) string {
if version == "alpha" || version == "beta" {
return "v0." + version
}
if m := oddVersionRE . FindStringSubmatch ( version ) ; m != nil {
return m [ 1 ] + "/" + m [ 2 ]
}
return version
}
func ( p * namePool ) Get ( preferred string ) string {
if p . m == nil {
p . m = make ( map [ string ] bool )
}
name := preferred
tries := 0
for p . m [ name ] {
tries ++
name = fmt . Sprintf ( "%s%d" , preferred , tries )
}
p . m [ name ] = true
return name
}
func genDirRoot ( ) string {
if * genDir != "" {
return * genDir
}
paths := filepath . SplitList ( os . Getenv ( "GOPATH" ) )
if len ( paths ) == 0 {
log . Fatalf ( "No GOPATH set." )
}
return filepath . Join ( paths [ 0 ] , "src" , "google.golang.org" , "api" )
}
func ( a * API ) SourceDir ( ) string {
return filepath . Join ( genDirRoot ( ) , a . Package ( ) , renameVersion ( a . Version ) )
}
func ( a * API ) DiscoveryURL ( ) string {
if a . DiscoveryLink == "" {
log . Fatalf ( "API %s has no DiscoveryLink" , a . ID )
}
return a . DiscoveryLink
}
func ( a * API ) Package ( ) string {
return strings . ToLower ( a . Name )
}
func ( a * API ) Target ( ) string {
return fmt . Sprintf ( "%s/%s/%s" , * apiPackageBase , a . Package ( ) , renameVersion ( a . Version ) )
}
// ServiceType returns the name of the type to use for the root API struct
// (typically "Service").
func ( a * API ) ServiceType ( ) string {
switch a . Name {
case "appengine" , "content" : // retained for historical compatibility.
return "APIService"
default :
for _ , t := range serviceTypes {
if _ , ok := a . schemas [ t ] ; ! ok {
return t
}
}
panic ( "all service types are used, please consider introducing a new type to serviceTypes." )
}
}
// GetName returns a free top-level function/type identifier in the package.
// It tries to return your preferred match if it's free.
func ( a * API ) GetName ( preferred string ) string {
return a . usedNames . Get ( preferred )
}
func ( a * API ) apiBaseURL ( ) string {
var base , rel string
switch {
case * baseURL != "" :
base , rel = * baseURL , a . doc . BasePath
case a . doc . RootURL != "" :
base , rel = a . doc . RootURL , a . doc . ServicePath
default :
base , rel = * apisURL , a . doc . BasePath
}
return resolveRelative ( base , rel )
}
func ( a * API ) needsDataWrapper ( ) bool {
for _ , feature := range a . doc . Features {
if feature == "dataWrapper" {
return true
}
}
return false
}
func ( a * API ) jsonBytes ( ) [ ] byte {
2017-12-03 20:01:25 +00:00
if a . forceJSON == nil {
var slurp [ ] byte
var err error
if * useCache {
slurp , err = ioutil . ReadFile ( a . JSONFile ( ) )
if err != nil {
log . Fatal ( err )
}
} else {
slurp = slurpURL ( a . DiscoveryURL ( ) )
2017-08-05 18:17:15 +00:00
}
2017-12-03 20:01:25 +00:00
a . forceJSON = slurp
2017-08-05 18:17:15 +00:00
}
2017-12-03 20:01:25 +00:00
return a . forceJSON
2017-08-05 18:17:15 +00:00
}
func ( a * API ) JSONFile ( ) string {
return filepath . Join ( a . SourceDir ( ) , a . Package ( ) + "-api.json" )
}
2017-12-03 20:01:25 +00:00
var errNoDoc = errors . New ( "could not read discovery doc" )
// WriteGeneratedCode generates code for a.
// It returns errNoDoc if we couldn't read the discovery doc.
2017-08-05 18:17:15 +00:00
func ( a * API ) WriteGeneratedCode ( ) error {
genfilename := * output
2017-12-03 20:01:25 +00:00
jsonBytes := a . jsonBytes ( )
// Skip generation if we don't have the discovery doc.
if jsonBytes == nil {
// No message here, because slurpURL printed one.
return errNoDoc
}
2017-08-05 18:17:15 +00:00
if genfilename == "" {
2017-12-03 20:01:25 +00:00
if err := writeFile ( a . JSONFile ( ) , jsonBytes ) ; err != nil {
2017-08-05 18:17:15 +00:00
return err
}
outdir := a . SourceDir ( )
err := os . MkdirAll ( outdir , 0755 )
if err != nil {
return fmt . Errorf ( "failed to Mkdir %s: %v" , outdir , err )
}
pkg := a . Package ( )
genfilename = filepath . Join ( outdir , pkg + "-gen.go" )
}
code , err := a . GenerateCode ( )
errw := writeFile ( genfilename , code )
if err == nil {
err = errw
}
2017-12-03 20:01:25 +00:00
if err != nil {
return err
}
return nil
2017-08-05 18:17:15 +00:00
}
var docsLink string
func ( a * API ) GenerateCode ( ) ( [ ] byte , error ) {
pkg := a . Package ( )
jsonBytes := a . jsonBytes ( )
var err error
if a . doc == nil {
a . doc , err = disco . NewDocument ( jsonBytes )
if err != nil {
return nil , err
}
}
// Buffer the output in memory, for gofmt'ing later.
var buf bytes . Buffer
a . p = func ( format string , args ... interface { } ) {
_ , err := fmt . Fprintf ( & buf , format , args ... )
if err != nil {
panic ( err )
}
}
a . pn = func ( format string , args ... interface { } ) {
a . p ( format + "\n" , args ... )
}
wf := func ( path string ) error {
f , err := os . Open ( path )
if err != nil {
return err
}
defer f . Close ( )
_ , err = io . Copy ( & buf , f )
return err
}
p , pn := a . p , a . pn
if * headerPath != "" {
if err := wf ( * headerPath ) ; err != nil {
return nil , err
}
}
pn ( "// Package %s provides access to the %s." , pkg , a . doc . Title )
docsLink = a . doc . DocumentationLink
if docsLink != "" {
pn ( "//" )
pn ( "// See %s" , docsLink )
}
pn ( "//\n// Usage example:" )
pn ( "//" )
pn ( "// import %q" , a . Target ( ) )
pn ( "// ..." )
pn ( "// %sService, err := %s.New(oauthHttpClient)" , pkg , pkg )
pn ( "package %s // import %q" , pkg , a . Target ( ) )
p ( "\n" )
pn ( "import (" )
for _ , imp := range [ ] struct {
pkg string
lname string
} {
{ "bytes" , "" } ,
{ "encoding/json" , "" } ,
{ "errors" , "" } ,
{ "fmt" , "" } ,
{ "io" , "" } ,
{ "net/http" , "" } ,
{ "net/url" , "" } ,
{ "strconv" , "" } ,
{ "strings" , "" } ,
{ * contextHTTPPkg , "ctxhttp" } ,
{ * contextPkg , "context" } ,
{ * gensupportPkg , "gensupport" } ,
{ * googleapiPkg , "googleapi" } ,
} {
if imp . lname == "" {
pn ( " %q" , imp . pkg )
} else {
pn ( " %s %q" , imp . lname , imp . pkg )
}
}
pn ( ")" )
pn ( "\n// Always reference these packages, just in case the auto-generated code" )
pn ( "// below doesn't." )
pn ( "var _ = bytes.NewBuffer" )
pn ( "var _ = strconv.Itoa" )
pn ( "var _ = fmt.Sprintf" )
pn ( "var _ = json.NewDecoder" )
pn ( "var _ = io.Copy" )
pn ( "var _ = url.Parse" )
pn ( "var _ = gensupport.MarshalJSON" )
pn ( "var _ = googleapi.Version" )
pn ( "var _ = errors.New" )
pn ( "var _ = strings.Replace" )
pn ( "var _ = context.Canceled" )
pn ( "var _ = ctxhttp.Do" )
pn ( "" )
pn ( "const apiId = %q" , a . doc . ID )
pn ( "const apiName = %q" , a . doc . Name )
pn ( "const apiVersion = %q" , a . doc . Version )
pn ( "const basePath = %q" , a . apiBaseURL ( ) )
a . generateScopeConstants ( )
a . PopulateSchemas ( )
service := a . ServiceType ( )
// Reserve names (ignore return value; we're the first caller).
a . GetName ( "New" )
a . GetName ( service )
pn ( "func New(client *http.Client) (*%s, error) {" , service )
pn ( "if client == nil { return nil, errors.New(\"client is nil\") }" )
pn ( "s := &%s{client: client, BasePath: basePath}" , service )
for _ , res := range a . doc . Resources { // add top level resources.
pn ( "s.%s = New%s(s)" , resourceGoField ( res ) , resourceGoType ( res ) )
}
pn ( "return s, nil" )
pn ( "}" )
pn ( "\ntype %s struct {" , service )
pn ( " client *http.Client" )
pn ( " BasePath string // API endpoint base URL" )
pn ( " UserAgent string // optional additional User-Agent fragment" )
for _ , res := range a . doc . Resources {
pn ( "\n\t%s\t*%s" , resourceGoField ( res ) , resourceGoType ( res ) )
}
pn ( "}" )
pn ( "\nfunc (s *%s) userAgent() string {" , service )
pn ( ` if s.UserAgent == "" { return googleapi.UserAgent } ` )
pn ( ` return googleapi.UserAgent + " " + s.UserAgent ` )
pn ( "}\n" )
for _ , res := range a . doc . Resources {
a . generateResource ( res )
}
a . responseTypes = make ( map [ string ] bool )
for _ , meth := range a . APIMethods ( ) {
meth . cacheResponseTypes ( a )
}
for _ , res := range a . doc . Resources {
a . cacheResourceResponseTypes ( res )
}
for _ , name := range a . sortedSchemaNames ( ) {
a . schemas [ name ] . writeSchemaCode ( a )
}
for _ , meth := range a . APIMethods ( ) {
meth . generateCode ( )
}
for _ , res := range a . doc . Resources {
a . generateResourceMethods ( res )
}
clean , err := format . Source ( buf . Bytes ( ) )
if err != nil {
return buf . Bytes ( ) , err
}
return clean , nil
}
func ( a * API ) generateScopeConstants ( ) {
scopes := a . doc . Auth . OAuth2Scopes
if len ( scopes ) == 0 {
return
}
a . pn ( "// OAuth2 scopes used by this API." )
a . pn ( "const (" )
n := 0
for _ , scope := range scopes {
if n > 0 {
a . p ( "\n" )
}
n ++
ident := scopeIdentifierFromURL ( scope . URL )
if scope . Description != "" {
a . p ( "%s" , asComment ( "\t" , scope . Description ) )
}
a . pn ( "\t%s = %q" , ident , scope . URL )
}
a . p ( ")\n\n" )
}
func scopeIdentifierFromURL ( urlStr string ) string {
const prefix = "https://www.googleapis.com/auth/"
if ! strings . HasPrefix ( urlStr , prefix ) {
const https = "https://"
if ! strings . HasPrefix ( urlStr , https ) {
log . Fatalf ( "Unexpected oauth2 scope %q doesn't start with %q" , urlStr , https )
}
ident := validGoIdentifer ( depunct ( urlStr [ len ( https ) : ] , true ) ) + "Scope"
return ident
}
ident := validGoIdentifer ( initialCap ( urlStr [ len ( prefix ) : ] ) ) + "Scope"
return ident
}
// Schema is a disco.Schema that has been bestowed an identifier, whether by
// having an "id" field at the top of the schema or with an
// automatically generated one in populateSubSchemas.
//
// TODO: While sub-types shouldn't need to be promoted to schemas,
// API.GenerateCode iterates over API.schemas to figure out what
// top-level Go types to write. These should be separate concerns.
type Schema struct {
api * API
typ * disco . Schema
apiName string // the native API-defined name of this type
goName string // lazily populated by GoName
goReturnType string // lazily populated by GoReturnType
props [ ] * Property
}
type Property struct {
s * Schema // the containing Schema
p * disco . Property
assignedGoName string
}
func ( p * Property ) Type ( ) * disco . Schema {
return p . p . Schema
}
func ( p * Property ) GoName ( ) string {
return initialCap ( p . p . Name )
}
func ( p * Property ) Default ( ) string {
return p . p . Schema . Default
}
func ( p * Property ) Description ( ) string {
return p . p . Schema . Description
}
func ( p * Property ) Enum ( ) ( [ ] string , bool ) {
typ := p . p . Schema
if typ . Enums != nil {
return typ . Enums , true
}
// Check if this has an array of string enums.
if typ . ItemSchema != nil {
if enums := typ . ItemSchema . Enums ; enums != nil && typ . ItemSchema . Type == "string" {
return enums , true
}
}
return nil , false
}
func ( p * Property ) EnumDescriptions ( ) [ ] string {
if desc := p . p . Schema . EnumDescriptions ; desc != nil {
return desc
}
// Check if this has an array of string enum descriptions.
if items := p . p . Schema . ItemSchema ; items != nil {
if desc := items . EnumDescriptions ; desc != nil {
return desc
}
}
return nil
}
func ( p * Property ) Pattern ( ) ( string , bool ) {
return p . p . Schema . Pattern , ( p . p . Schema . Pattern != "" )
}
func ( p * Property ) TypeAsGo ( ) string {
return p . s . api . typeAsGo ( p . Type ( ) , false )
}
// A FieldName uniquely identifies a field within a Schema struct for an API.
type fieldName struct {
api string // The ID of an API.
schema string // The Go name of a Schema struct.
field string // The Go name of a field.
}
// pointerFields is a list of fields that should use a pointer type.
// This makes it possible to distinguish between a field being unset vs having
// an empty value.
var pointerFields = [ ] fieldName {
{ api : "androidpublisher:v2" , schema : "SubscriptionPurchase" , field : "CancelReason" } ,
{ api : "androidpublisher:v2" , schema : "SubscriptionPurchase" , field : "PaymentState" } ,
{ api : "cloudmonitoring:v2beta2" , schema : "Point" , field : "BoolValue" } ,
{ api : "cloudmonitoring:v2beta2" , schema : "Point" , field : "DoubleValue" } ,
{ api : "cloudmonitoring:v2beta2" , schema : "Point" , field : "Int64Value" } ,
{ api : "cloudmonitoring:v2beta2" , schema : "Point" , field : "StringValue" } ,
{ api : "compute:alpha" , schema : "Scheduling" , field : "AutomaticRestart" } ,
{ api : "compute:beta" , schema : "MetadataItems" , field : "Value" } ,
{ api : "compute:beta" , schema : "Scheduling" , field : "AutomaticRestart" } ,
{ api : "compute:v1" , schema : "MetadataItems" , field : "Value" } ,
{ api : "compute:v1" , schema : "Scheduling" , field : "AutomaticRestart" } ,
{ api : "content:v2" , schema : "AccountUser" , field : "Admin" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "BlobKeyValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "BlobValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "BooleanValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "DateTimeValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "DoubleValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "Indexed" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "IntegerValue" } ,
{ api : "datastore:v1beta2" , schema : "Property" , field : "StringValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "BlobValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "BooleanValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "DoubleValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "IntegerValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "StringValue" } ,
{ api : "datastore:v1beta3" , schema : "Value" , field : "TimestampValue" } ,
{ api : "genomics:v1beta2" , schema : "Dataset" , field : "IsPublic" } ,
{ api : "monitoring:v3" , schema : "TypedValue" , field : "BoolValue" } ,
{ api : "monitoring:v3" , schema : "TypedValue" , field : "DoubleValue" } ,
{ api : "monitoring:v3" , schema : "TypedValue" , field : "Int64Value" } ,
{ api : "monitoring:v3" , schema : "TypedValue" , field : "StringValue" } ,
{ api : "servicecontrol:v1" , schema : "MetricValue" , field : "BoolValue" } ,
{ api : "servicecontrol:v1" , schema : "MetricValue" , field : "DoubleValue" } ,
{ api : "servicecontrol:v1" , schema : "MetricValue" , field : "Int64Value" } ,
{ api : "servicecontrol:v1" , schema : "MetricValue" , field : "StringValue" } ,
{ api : "sqladmin:v1beta4" , schema : "Settings" , field : "StorageAutoResize" } ,
{ api : "storage:v1" , schema : "BucketLifecycleRuleCondition" , field : "IsLive" } ,
{ api : "storage:v1beta2" , schema : "BucketLifecycleRuleCondition" , field : "IsLive" } ,
{ api : "tasks:v1" , schema : "Task" , field : "Completed" } ,
{ api : "youtube:v3" , schema : "ChannelSectionSnippet" , field : "Position" } ,
}
// forcePointerType reports whether p should be represented as a pointer type in its parent schema struct.
func ( p * Property ) forcePointerType ( ) bool {
if p . UnfortunateDefault ( ) {
return true
}
name := fieldName { api : p . s . api . ID , schema : p . s . GoName ( ) , field : p . GoName ( ) }
for _ , pf := range pointerFields {
if pf == name {
return true
}
}
return false
}
// UnfortunateDefault reports whether p may be set to a zero value, but has a non-zero default.
func ( p * Property ) UnfortunateDefault ( ) bool {
switch p . TypeAsGo ( ) {
default :
return false
case "bool" :
return p . Default ( ) == "true"
case "string" :
if p . Default ( ) == "" {
return false
}
// String fields are considered to "allow" a zero value if either:
// (a) they are an enum, and one of the permitted enum values is the empty string, or
// (b) they have a validation pattern which matches the empty string.
pattern , hasPat := p . Pattern ( )
enum , hasEnum := p . Enum ( )
if hasPat && hasEnum {
log . Printf ( "Encountered enum property which also has a pattern: %#v" , p )
return false // don't know how to handle this, so ignore.
}
return ( hasPat && emptyPattern ( pattern ) ) ||
( hasEnum && emptyEnum ( enum ) )
case "float64" , "int64" , "uint64" , "int32" , "uint32" :
if p . Default ( ) == "" {
return false
}
if f , err := strconv . ParseFloat ( p . Default ( ) , 64 ) ; err == nil {
return f != 0.0
}
// The default value has an unexpected form. Whatever it is, it's non-zero.
return true
}
}
// emptyPattern reports whether a pattern matches the empty string.
func emptyPattern ( pattern string ) bool {
if re , err := regexp . Compile ( pattern ) ; err == nil {
return re . MatchString ( "" )
}
log . Printf ( "Encountered bad pattern: %s" , pattern )
return false
}
// emptyEnum reports whether a property enum list contains the empty string.
func emptyEnum ( enum [ ] string ) bool {
for _ , val := range enum {
if val == "" {
return true
}
}
return false
}
func ( a * API ) typeAsGo ( s * disco . Schema , elidePointers bool ) string {
switch s . Kind {
case disco . SimpleKind :
return mustSimpleTypeConvert ( s . Type , s . Format )
case disco . ArrayKind :
as := s . ElementSchema ( )
if as . Type == "string" {
switch as . Format {
case "int64" :
return "googleapi.Int64s"
case "uint64" :
return "googleapi.Uint64s"
case "int32" :
return "googleapi.Int32s"
case "uint32" :
return "googleapi.Uint32s"
case "float64" :
return "googleapi.Float64s"
}
}
return "[]" + a . typeAsGo ( as , elidePointers )
case disco . ReferenceKind :
rs := s . RefSchema
if rs . Kind == disco . SimpleKind {
// Simple top-level schemas get named types (see writeSchemaCode).
// Use the name instead of using the equivalent simple Go type.
return a . schemaNamed ( rs . Name ) . GoName ( )
}
return a . typeAsGo ( rs , elidePointers )
case disco . MapKind :
es := s . ElementSchema ( )
if es . Type == "string" {
// If the element schema has a type "string", it's going to be
// transmitted as a string, and the Go map type must reflect that.
// This is true even if the format is, say, "int64". When type =
// "string" and format = "int64" at top level, we can use the json
// "string" tag option to unmarshal the string to an int64, but
// inside a map we can't.
return "map[string]string"
}
// Due to historical baggage (maps used to be a separate code path),
// the element types of maps never have pointers in them. From this
// level down, elide pointers in types.
return "map[string]" + a . typeAsGo ( es , true )
case disco . AnyStructKind :
return "googleapi.RawMessage"
case disco . StructKind :
tls := a . schemaNamed ( s . Name )
if elidePointers || s . Variant != nil {
return tls . GoName ( )
}
return "*" + tls . GoName ( )
default :
panic ( fmt . Sprintf ( "unhandled typeAsGo for %+v" , s ) )
}
}
func ( a * API ) schemaNamed ( name string ) * Schema {
s := a . schemas [ name ]
if s == nil {
panicf ( "no top-level schema named %q" , name )
}
return s
}
func ( s * Schema ) properties ( ) [ ] * Property {
if s . props != nil {
return s . props
}
if s . typ . Kind != disco . StructKind {
panic ( "called properties on non-object schema" )
}
for _ , p := range s . typ . Properties {
s . props = append ( s . props , & Property {
s : s ,
p : p ,
} )
}
return s . props
}
func ( s * Schema ) HasContentType ( ) bool {
for _ , p := range s . properties ( ) {
if p . GoName ( ) == "ContentType" && p . TypeAsGo ( ) == "string" {
return true
}
}
return false
}
func ( s * Schema ) populateSubSchemas ( ) ( outerr error ) {
defer func ( ) {
r := recover ( )
if r == nil {
return
}
outerr = fmt . Errorf ( "%v" , r )
} ( )
addSubStruct := func ( subApiName string , t * disco . Schema ) {
if s . api . schemas [ subApiName ] != nil {
panic ( "dup schema apiName: " + subApiName )
}
if t . Name != "" {
panic ( "subtype already has name: " + t . Name )
}
t . Name = subApiName
subs := & Schema {
api : s . api ,
typ : t ,
apiName : subApiName ,
}
s . api . schemas [ subApiName ] = subs
err := subs . populateSubSchemas ( )
if err != nil {
panicf ( "in sub-struct %q: %v" , subApiName , err )
}
}
switch s . typ . Kind {
case disco . StructKind :
for _ , p := range s . properties ( ) {
subApiName := fmt . Sprintf ( "%s.%s" , s . apiName , p . p . Name )
switch p . Type ( ) . Kind {
case disco . SimpleKind , disco . ReferenceKind , disco . AnyStructKind :
// Do nothing.
case disco . MapKind :
mt := p . Type ( ) . ElementSchema ( )
if mt . Kind == disco . SimpleKind || mt . Kind == disco . ReferenceKind {
continue
}
addSubStruct ( subApiName , mt )
case disco . ArrayKind :
at := p . Type ( ) . ElementSchema ( )
if at . Kind == disco . SimpleKind || at . Kind == disco . ReferenceKind {
continue
}
addSubStruct ( subApiName , at )
case disco . StructKind :
addSubStruct ( subApiName , p . Type ( ) )
default :
panicf ( "Unknown type for %q: %s" , subApiName , p . Type ( ) )
}
}
case disco . ArrayKind :
subApiName := fmt . Sprintf ( "%s.Item" , s . apiName )
switch at := s . typ . ElementSchema ( ) ; at . Kind {
case disco . SimpleKind , disco . ReferenceKind , disco . AnyStructKind :
// Do nothing.
case disco . MapKind :
mt := at . ElementSchema ( )
if k := mt . Kind ; k != disco . SimpleKind && k != disco . ReferenceKind {
addSubStruct ( subApiName , mt )
}
case disco . ArrayKind :
at := at . ElementSchema ( )
if k := at . Kind ; k != disco . SimpleKind && k != disco . ReferenceKind {
addSubStruct ( subApiName , at )
}
case disco . StructKind :
addSubStruct ( subApiName , at )
default :
panicf ( "Unknown array type for %q: %s" , subApiName , at )
}
case disco . AnyStructKind , disco . MapKind , disco . SimpleKind , disco . ReferenceKind :
// Do nothing.
default :
fmt . Fprintf ( os . Stderr , "in populateSubSchemas, schema is: %v" , s . typ )
panicf ( "populateSubSchemas: unsupported type for schema %q" , s . apiName )
panic ( "unreachable" )
}
return nil
}
// GoName returns (or creates and returns) the bare Go name
// of the apiName, making sure that it's a proper Go identifier
// and doesn't conflict with an existing name.
func ( s * Schema ) GoName ( ) string {
if s . goName == "" {
if s . typ . Kind == disco . MapKind {
s . goName = s . api . typeAsGo ( s . typ , false )
} else {
base := initialCap ( s . apiName )
s . goName = s . api . GetName ( base )
if base == "Service" && s . goName != "Service" {
// Detect the case where a resource is going to clash with the
// root service object.
panicf ( "Clash on name Service" )
}
}
}
return s . goName
}
// GoReturnType returns the Go type to use as the return type.
// If a type is a struct, it will return *StructType,
// for a map it will return map[string]ValueType,
// for (not yet supported) slices it will return []ValueType.
func ( s * Schema ) GoReturnType ( ) string {
if s . goReturnType == "" {
if s . typ . Kind == disco . MapKind {
s . goReturnType = s . GoName ( )
} else {
s . goReturnType = "*" + s . GoName ( )
}
}
return s . goReturnType
}
func ( s * Schema ) writeSchemaCode ( api * API ) {
switch s . typ . Kind {
case disco . SimpleKind :
apitype := s . typ . Type
typ := mustSimpleTypeConvert ( apitype , s . typ . Format )
s . api . pn ( "\ntype %s %s" , s . GoName ( ) , typ )
case disco . StructKind :
s . writeSchemaStruct ( api )
case disco . MapKind , disco . AnyStructKind :
// Do nothing.
case disco . ArrayKind :
log . Printf ( "TODO writeSchemaCode for arrays for %s" , s . GoName ( ) )
default :
fmt . Fprintf ( os . Stderr , "in writeSchemaCode, schema is: %+v" , s . typ )
panicf ( "writeSchemaCode: unsupported type for schema %q" , s . apiName )
}
}
func ( s * Schema ) writeVariant ( api * API , v * disco . Variant ) {
s . api . p ( "\ntype %s map[string]interface{}\n\n" , s . GoName ( ) )
// Write out the "Type" method that identifies the variant type.
s . api . pn ( "func (t %s) Type() string {" , s . GoName ( ) )
s . api . pn ( " return googleapi.VariantType(t)" )
s . api . p ( "}\n\n" )
// Write out helper methods to convert each possible variant.
for _ , m := range v . Map {
if m . TypeValue == "" && m . Ref == "" {
log . Printf ( "TODO variant %s ref %s not yet supported." , m . TypeValue , m . Ref )
continue
}
s . api . pn ( "func (t %s) %s() (r %s, ok bool) {" , s . GoName ( ) , initialCap ( m . TypeValue ) , m . Ref )
s . api . pn ( " if t.Type() != %q {" , initialCap ( m . TypeValue ) )
s . api . pn ( " return r, false" )
s . api . pn ( " }" )
s . api . pn ( " ok = googleapi.ConvertVariant(map[string]interface{}(t), &r)" )
s . api . pn ( " return r, ok" )
s . api . p ( "}\n\n" )
}
}
func ( s * Schema ) Description ( ) string {
return s . typ . Description
}
func ( s * Schema ) writeSchemaStruct ( api * API ) {
if v := s . typ . Variant ; v != nil {
s . writeVariant ( api , v )
return
}
s . api . p ( "\n" )
des := s . Description ( )
if des != "" {
s . api . p ( "%s" , asComment ( "" , fmt . Sprintf ( "%s: %s" , s . GoName ( ) , des ) ) )
}
s . api . pn ( "type %s struct {" , s . GoName ( ) )
np := new ( namePool )
forceSendName := np . Get ( "ForceSendFields" )
nullFieldsName := np . Get ( "NullFields" )
if s . isResponseType ( ) {
np . Get ( "ServerResponse" ) // reserve the name
}
firstFieldName := "" // used to store a struct field name for use in documentation.
for i , p := range s . properties ( ) {
if i > 0 {
s . api . p ( "\n" )
}
pname := np . Get ( p . GoName ( ) )
if pname [ 0 ] == '@' {
// HACK(cbro): ignore JSON-LD special fields until we can figure out
// the correct Go representation for them.
continue
}
p . assignedGoName = pname
des := p . Description ( )
if des != "" {
s . api . p ( "%s" , asComment ( "\t" , fmt . Sprintf ( "%s: %s" , pname , des ) ) )
}
addFieldValueComments ( s . api . p , p , "\t" , des != "" )
var extraOpt string
if p . Type ( ) . IsIntAsString ( ) {
extraOpt += ",string"
}
typ := p . TypeAsGo ( )
if p . forcePointerType ( ) {
typ = "*" + typ
}
s . api . pn ( " %s %s `json:\"%s,omitempty%s\"`" , pname , typ , p . p . Name , extraOpt )
if firstFieldName == "" {
firstFieldName = pname
}
}
if s . isResponseType ( ) {
if firstFieldName != "" {
s . api . p ( "\n" )
}
s . api . p ( "%s" , asComment ( "\t" , "ServerResponse contains the HTTP response code and headers from the server." ) )
s . api . pn ( " googleapi.ServerResponse `json:\"-\"`" )
}
if firstFieldName == "" {
// There were no fields in the struct, so there is no point
// adding any custom JSON marshaling code.
s . api . pn ( "}" )
return
}
commentFmtStr := "%s is a list of field names (e.g. %q) to " +
"unconditionally include in API requests. By default, fields " +
"with empty values are omitted from API requests. However, " +
"any non-pointer, non-interface field appearing in %s will " +
"be sent to the server regardless of whether the field is " +
"empty or not. This may be used to include empty fields in " +
"Patch requests."
comment := fmt . Sprintf ( commentFmtStr , forceSendName , firstFieldName , forceSendName )
s . api . p ( "\n" )
s . api . p ( "%s" , asComment ( "\t" , comment ) )
s . api . pn ( "\t%s []string `json:\"-\"`" , forceSendName )
commentFmtStr = "%s is a list of field names (e.g. %q) to " +
"include in API requests with the JSON null value. " +
"By default, fields with empty values are omitted from API requests. However, " +
"any field with an empty value appearing in %s will be sent to the server as null. " +
"It is an error if a field in this list has a non-empty value. This may be used to " +
"include null fields in Patch requests."
comment = fmt . Sprintf ( commentFmtStr , nullFieldsName , firstFieldName , nullFieldsName )
s . api . p ( "\n" )
s . api . p ( "%s" , asComment ( "\t" , comment ) )
s . api . pn ( "\t%s []string `json:\"-\"`" , nullFieldsName )
s . api . pn ( "}" )
s . writeSchemaMarshal ( forceSendName , nullFieldsName )
s . writeSchemaUnmarshal ( )
}
// writeSchemaMarshal writes a custom MarshalJSON function for s, which allows
// fields to be explicitly transmitted by listing them in the field identified
// by forceSendFieldName, and allows fields to be transmitted with the null value
// by listing them in the field identified by nullFieldsName.
func ( s * Schema ) writeSchemaMarshal ( forceSendFieldName , nullFieldsName string ) {
s . api . pn ( "func (s *%s) MarshalJSON() ([]byte, error) {" , s . GoName ( ) )
2017-12-03 20:01:25 +00:00
s . api . pn ( "\ttype NoMethod %s" , s . GoName ( ) )
2017-08-05 18:17:15 +00:00
// pass schema as methodless type to prevent subsequent calls to MarshalJSON from recursing indefinitely.
2017-12-03 20:01:25 +00:00
s . api . pn ( "\traw := NoMethod(*s)" )
2017-08-05 18:17:15 +00:00
s . api . pn ( "\treturn gensupport.MarshalJSON(raw, s.%s, s.%s)" , forceSendFieldName , nullFieldsName )
s . api . pn ( "}" )
}
func ( s * Schema ) writeSchemaUnmarshal ( ) {
var floatProps [ ] * Property
for _ , p := range s . properties ( ) {
if p . p . Schema . Type == "number" {
floatProps = append ( floatProps , p )
}
}
if len ( floatProps ) == 0 {
return
}
pn := s . api . pn
pn ( "\nfunc (s *%s) UnmarshalJSON(data []byte) error {" , s . GoName ( ) )
2017-12-03 20:01:25 +00:00
pn ( " type NoMethod %s" , s . GoName ( ) ) // avoid infinite recursion
2017-08-05 18:17:15 +00:00
pn ( " var s1 struct {" )
// Hide the float64 fields of the schema with fields that correctly
// unmarshal special values.
for _ , p := range floatProps {
typ := "gensupport.JSONFloat64"
if p . forcePointerType ( ) {
typ = "*" + typ
}
pn ( "%s %s `json:\"%s\"`" , p . assignedGoName , typ , p . p . Name )
}
2017-12-03 20:01:25 +00:00
pn ( " *NoMethod" ) // embed the schema
2017-08-05 18:17:15 +00:00
pn ( " }" )
// Set the schema value into the wrapper so its other fields are unmarshaled.
2017-12-03 20:01:25 +00:00
pn ( " s1.NoMethod = (*NoMethod)(s)" )
2017-08-05 18:17:15 +00:00
pn ( " if err := json.Unmarshal(data, &s1); err != nil {" )
pn ( " return err" )
pn ( " }" )
// Copy each shadowing field into the field it shadows.
for _ , p := range floatProps {
n := p . assignedGoName
if p . forcePointerType ( ) {
pn ( "if s1.%s != nil { s.%s = (*float64)(s1.%s) }" , n , n , n )
} else {
pn ( "s.%s = float64(s1.%s)" , n , n )
}
}
pn ( " return nil" )
pn ( "}" )
}
// isResponseType returns true for all types that are used as a response.
func ( s * Schema ) isResponseType ( ) bool {
return s . api . responseTypes [ "*" + s . goName ]
}
// PopulateSchemas reads all the API types ("schemas") from the JSON file
// and converts them to *Schema instances, returning an identically
// keyed map, additionally containing subresources. For instance,
//
// A resource "Foo" of type "object" with a property "bar", also of type
// "object" (an anonymous sub-resource), will get a synthetic API name
// of "Foo.bar".
//
// A resource "Foo" of type "array" with an "items" of type "object"
// will get a synthetic API name of "Foo.Item".
func ( a * API ) PopulateSchemas ( ) {
if a . schemas != nil {
panic ( "" )
}
a . schemas = make ( map [ string ] * Schema )
for name , ds := range a . doc . Schemas {
s := & Schema {
api : a ,
apiName : name ,
typ : ds ,
}
a . schemas [ name ] = s
err := s . populateSubSchemas ( )
if err != nil {
panicf ( "Error populating schema with API name %q: %v" , name , err )
}
}
}
func ( a * API ) generateResource ( r * disco . Resource ) {
pn := a . pn
t := resourceGoType ( r )
pn ( fmt . Sprintf ( "func New%s(s *%s) *%s {" , t , a . ServiceType ( ) , t ) )
pn ( "rs := &%s{s : s}" , t )
for _ , res := range r . Resources {
pn ( "rs.%s = New%s(s)" , resourceGoField ( res ) , resourceGoType ( res ) )
}
pn ( "return rs" )
pn ( "}" )
pn ( "\ntype %s struct {" , t )
pn ( " s *%s" , a . ServiceType ( ) )
for _ , res := range r . Resources {
pn ( "\n\t%s\t*%s" , resourceGoField ( res ) , resourceGoType ( res ) )
}
pn ( "}" )
for _ , res := range r . Resources {
a . generateResource ( res )
}
}
func ( a * API ) cacheResourceResponseTypes ( r * disco . Resource ) {
for _ , meth := range a . resourceMethods ( r ) {
meth . cacheResponseTypes ( a )
}
for _ , res := range r . Resources {
a . cacheResourceResponseTypes ( res )
}
}
func ( a * API ) generateResourceMethods ( r * disco . Resource ) {
for _ , meth := range a . resourceMethods ( r ) {
meth . generateCode ( )
}
for _ , res := range r . Resources {
a . generateResourceMethods ( res )
}
}
func resourceGoField ( r * disco . Resource ) string {
return initialCap ( r . Name )
}
func resourceGoType ( r * disco . Resource ) string {
return initialCap ( r . FullName + "Service" )
}
func ( a * API ) resourceMethods ( r * disco . Resource ) [ ] * Method {
ms := [ ] * Method { }
for _ , m := range r . Methods {
ms = append ( ms , & Method {
api : a ,
r : r ,
m : m ,
} )
}
return ms
}
type Method struct {
api * API
r * disco . Resource // or nil if a API-level (top-level) method
m * disco . Method
params [ ] * Param // all Params, of each type, lazily set by first access to Parameters
}
func ( m * Method ) Id ( ) string {
return m . m . ID
}
func ( m * Method ) responseType ( ) * Schema {
return m . api . schemas [ m . m . Response . RefSchema . Name ]
}
func ( m * Method ) supportsMediaUpload ( ) bool {
return m . m . MediaUpload != nil
}
func ( m * Method ) mediaUploadPath ( ) string {
return m . m . MediaUpload . Protocols [ "simple" ] . Path
}
func ( m * Method ) supportsMediaDownload ( ) bool {
if m . supportsMediaUpload ( ) {
// storage.objects.insert claims support for download in
// addition to upload but attempting to do so fails.
// This situation doesn't apply to any other methods.
return false
}
return m . m . SupportsMediaDownload
}
func ( m * Method ) supportsPaging ( ) ( * pageTokenGenerator , string , bool ) {
ptg := m . pageTokenGenerator ( )
if ptg == nil {
return nil , "" , false
}
// Check that the response type has the next page token.
s := m . responseType ( )
if s == nil || s . typ . Kind != disco . StructKind {
return nil , "" , false
}
for _ , prop := range s . properties ( ) {
if isPageTokenName ( prop . p . Name ) && prop . Type ( ) . Type == "string" {
return ptg , prop . GoName ( ) , true
}
}
return nil , "" , false
}
type pageTokenGenerator struct {
isParam bool // is the page token a URL parameter?
name string // param or request field name
requestName string // empty for URL param
}
func ( p * pageTokenGenerator ) genGet ( ) string {
if p . isParam {
return fmt . Sprintf ( "c.urlParams_.Get(%q)" , p . name )
}
return fmt . Sprintf ( "c.%s.%s" , p . requestName , p . name )
}
func ( p * pageTokenGenerator ) genSet ( valueExpr string ) string {
if p . isParam {
return fmt . Sprintf ( "c.%s(%s)" , initialCap ( p . name ) , valueExpr )
}
return fmt . Sprintf ( "c.%s.%s = %s" , p . requestName , p . name , valueExpr )
}
func ( p * pageTokenGenerator ) genDeferBody ( ) string {
if p . isParam {
return p . genSet ( p . genGet ( ) )
}
return fmt . Sprintf ( "func (pt string) { %s }(%s)" , p . genSet ( "pt" ) , p . genGet ( ) )
}
// pageTokenGenerator returns a pageTokenGenerator that will generate code to
// get/set the page token for a subsequent page in the context of the generated
// Pages method. It returns nil if there is no page token.
func ( m * Method ) pageTokenGenerator ( ) * pageTokenGenerator {
matches := m . grepParams ( func ( p * Param ) bool { return isPageTokenName ( p . p . Name ) } )
switch len ( matches ) {
case 1 :
if matches [ 0 ] . p . Required {
// The page token is a required parameter (e.g. because there is
// a separate API call to start an iteration), and so the relevant
// call factory method takes the page token instead.
return nil
}
n := matches [ 0 ] . p . Name
return & pageTokenGenerator { true , n , "" }
case 0 : // No URL parameter, but maybe a request field.
if m . m . Request == nil {
return nil
}
rs := m . m . Request
if rs . RefSchema != nil {
rs = rs . RefSchema
}
for _ , p := range rs . Properties {
if isPageTokenName ( p . Name ) {
return & pageTokenGenerator { false , initialCap ( p . Name ) , validGoIdentifer ( strings . ToLower ( rs . Name ) ) }
}
}
return nil
default :
panicf ( "too many page token parameters for method %s" , m . m . Name )
return nil
}
}
func isPageTokenName ( s string ) bool {
return s == "pageToken" || s == "nextPageToken"
}
func ( m * Method ) Params ( ) [ ] * Param {
if m . params == nil {
for _ , p := range m . m . Parameters {
m . params = append ( m . params , & Param {
method : m ,
p : p ,
} )
}
}
return m . params
}
func ( m * Method ) grepParams ( f func ( * Param ) bool ) [ ] * Param {
matches := make ( [ ] * Param , 0 )
for _ , param := range m . Params ( ) {
if f ( param ) {
matches = append ( matches , param )
}
}
return matches
}
func ( m * Method ) NamedParam ( name string ) * Param {
matches := m . grepParams ( func ( p * Param ) bool {
return p . p . Name == name
} )
if len ( matches ) < 1 {
log . Panicf ( "failed to find named parameter %q" , name )
}
if len ( matches ) > 1 {
log . Panicf ( "found multiple parameters for parameter name %q" , name )
}
return matches [ 0 ]
}
func ( m * Method ) OptParams ( ) [ ] * Param {
return m . grepParams ( func ( p * Param ) bool {
return ! p . p . Required
} )
}
func ( meth * Method ) cacheResponseTypes ( api * API ) {
if retType := responseType ( api , meth . m ) ; retType != "" && strings . HasPrefix ( retType , "*" ) {
api . responseTypes [ retType ] = true
}
}
// convertMultiParams builds a []string temp variable from a slice
// of non-strings and returns the name of the temp variable.
func convertMultiParams ( a * API , param string ) string {
a . pn ( " var %v_ []string" , param )
a . pn ( " for _, v := range %v {" , param )
a . pn ( " %v_ = append(%v_, fmt.Sprint(v))" , param , param )
a . pn ( " }" )
return param + "_"
}
func ( meth * Method ) generateCode ( ) {
res := meth . r // may be nil if a top-level method
a := meth . api
p , pn := a . p , a . pn
pn ( "\n// method id %q:" , meth . Id ( ) )
retType := responseType ( a , meth . m )
retTypeComma := retType
if retTypeComma != "" {
retTypeComma += ", "
}
args := meth . NewArguments ( )
methodName := initialCap ( meth . m . Name )
prefix := ""
if res != nil {
prefix = initialCap ( res . FullName )
}
callName := a . GetName ( prefix + methodName + "Call" )
pn ( "\ntype %s struct {" , callName )
pn ( " s *%s" , a . ServiceType ( ) )
for _ , arg := range args . l {
if arg . location != "query" {
pn ( " %s %s" , arg . goname , arg . gotype )
}
}
pn ( " urlParams_ gensupport.URLParams" )
httpMethod := meth . m . HTTPMethod
if httpMethod == "GET" {
pn ( " ifNoneMatch_ string" )
}
if meth . supportsMediaUpload ( ) {
2017-09-13 12:09:48 +00:00
pn ( " mediaInfo_ *gensupport.MediaInfo" )
2017-08-05 18:17:15 +00:00
}
pn ( " ctx_ context.Context" )
pn ( " header_ http.Header" )
pn ( "}" )
p ( "\n%s" , asComment ( "" , methodName + ": " + meth . m . Description ) )
if res != nil {
if url := canonicalDocsURL [ fmt . Sprintf ( "%v%v/%v" , docsLink , res . Name , meth . m . Name ) ] ; url != "" {
pn ( "// For details, see %v" , url )
}
}
var servicePtr string
if res == nil {
pn ( "func (s *Service) %s(%s) *%s {" , methodName , args , callName )
servicePtr = "s"
} else {
pn ( "func (r *%s) %s(%s) *%s {" , resourceGoType ( res ) , methodName , args , callName )
servicePtr = "r.s"
}
pn ( " c := &%s{s: %s, urlParams_: make(gensupport.URLParams)}" , callName , servicePtr )
for _ , arg := range args . l {
// TODO(gmlewis): clean up and consolidate this section.
// See: https://code-review.googlesource.com/#/c/3520/18/google-api-go-generator/gen.go
if arg . location == "query" {
switch arg . gotype {
case "[]string" :
pn ( " c.urlParams_.SetMulti(%q, append([]string{}, %v...))" , arg . apiname , arg . goname )
case "string" :
pn ( " c.urlParams_.Set(%q, %v)" , arg . apiname , arg . goname )
default :
if strings . HasPrefix ( arg . gotype , "[]" ) {
tmpVar := convertMultiParams ( a , arg . goname )
pn ( " c.urlParams_.SetMulti(%q, %v)" , arg . apiname , tmpVar )
} else {
pn ( " c.urlParams_.Set(%q, fmt.Sprint(%v))" , arg . apiname , arg . goname )
}
}
continue
}
if arg . gotype == "[]string" {
pn ( " c.%s = append([]string{}, %s...)" , arg . goname , arg . goname ) // Make a copy of the []string.
continue
}
pn ( " c.%s = %s" , arg . goname , arg . goname )
}
pn ( " return c" )
pn ( "}" )
for _ , opt := range meth . OptParams ( ) {
if opt . p . Location != "query" {
panicf ( "optional parameter has unsupported location %q" , opt . p . Location )
}
setter := initialCap ( opt . p . Name )
des := opt . p . Description
des = strings . Replace ( des , "Optional." , "" , 1 )
des = strings . TrimSpace ( des )
p ( "\n%s" , asComment ( "" , fmt . Sprintf ( "%s sets the optional parameter %q: %s" , setter , opt . p . Name , des ) ) )
addFieldValueComments ( p , opt , "" , true )
np := new ( namePool )
np . Get ( "c" ) // take the receiver's name
paramName := np . Get ( validGoIdentifer ( opt . p . Name ) )
typePrefix := ""
if opt . p . Repeated {
typePrefix = "..."
}
pn ( "func (c *%s) %s(%s %s%s) *%s {" , callName , setter , paramName , typePrefix , opt . GoType ( ) , callName )
if opt . p . Repeated {
if opt . GoType ( ) == "string" {
pn ( "c.urlParams_.SetMulti(%q, append([]string{}, %v...))" , opt . p . Name , paramName )
} else {
tmpVar := convertMultiParams ( a , paramName )
pn ( " c.urlParams_.SetMulti(%q, %v)" , opt . p . Name , tmpVar )
}
} else {
if opt . GoType ( ) == "string" {
pn ( "c.urlParams_.Set(%q, %v)" , opt . p . Name , paramName )
} else {
pn ( "c.urlParams_.Set(%q, fmt.Sprint(%v))" , opt . p . Name , paramName )
}
}
pn ( "return c" )
pn ( "}" )
}
if meth . supportsMediaUpload ( ) {
comment := "Media specifies the media to upload in one or more chunks. " +
"The chunk size may be controlled by supplying a MediaOption generated by googleapi.ChunkSize. " +
"The chunk size defaults to googleapi.DefaultUploadChunkSize." +
"The Content-Type header used in the upload request will be determined by sniffing the contents of r, " +
"unless a MediaOption generated by googleapi.ContentType is supplied." +
"\nAt most one of Media and ResumableMedia may be set."
// TODO(mcgreevy): Ensure that r is always closed before Do returns, and document this.
// See comments on https://code-review.googlesource.com/#/c/3970/
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) Media(r io.Reader, options ...googleapi.MediaOption) *%s {" , callName , callName )
// We check if the body arg, if any, has a content type and apply it here.
// In practice, this only happens for the storage API today.
// TODO(djd): check if we can cope with the developer setting the body's Content-Type field
// after they've made this call.
if ba := args . bodyArg ( ) ; ba != nil {
if ba . schema . HasContentType ( ) {
pn ( " if ct := c.%s.ContentType; ct != \"\" {" , ba . goname )
pn ( " options = append([]googleapi.MediaOption{googleapi.ContentType(ct)}, options...)" )
pn ( " }" )
}
}
2017-09-13 12:09:48 +00:00
pn ( " c.mediaInfo_ = gensupport.NewInfoFromMedia(r, options)" )
2017-08-05 18:17:15 +00:00
pn ( " return c" )
pn ( "}" )
comment = "ResumableMedia specifies the media to upload in chunks and can be canceled with ctx. " +
"\n\nDeprecated: use Media instead." +
"\n\nAt most one of Media and ResumableMedia may be set. " +
` mediaType identifies the MIME media type of the upload, such as "image/png". ` +
` If mediaType is "", it will be auto-detected. ` +
` The provided ctx will supersede any context previously provided to ` +
` the Context method. `
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) ResumableMedia(ctx context.Context, r io.ReaderAt, size int64, mediaType string) *%s {" , callName , callName )
pn ( " c.ctx_ = ctx" )
2017-09-13 12:09:48 +00:00
pn ( " c.mediaInfo_ = gensupport.NewInfoFromResumableMedia(r, size, mediaType)" )
2017-08-05 18:17:15 +00:00
pn ( " return c" )
pn ( "}" )
comment = "ProgressUpdater provides a callback function that will be called after every chunk. " +
"It should be a low-latency function in order to not slow down the upload operation. " +
"This should only be called when using ResumableMedia (as opposed to Media)."
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) ProgressUpdater(pu googleapi.ProgressUpdater) *%s {" , callName , callName )
2017-09-13 12:09:48 +00:00
pn ( ` c.mediaInfo_.SetProgressUpdater(pu) ` )
2017-08-05 18:17:15 +00:00
pn ( "return c" )
pn ( "}" )
}
comment := "Fields allows partial responses to be retrieved. " +
"See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse " +
"for more information."
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) Fields(s ...googleapi.Field) *%s {" , callName , callName )
pn ( ` c.urlParams_.Set("fields", googleapi.CombineFields(s)) ` )
pn ( "return c" )
pn ( "}" )
if httpMethod == "GET" {
// Note that non-GET responses are excluded from supporting If-None-Match.
// See https://github.com/google/google-api-go-client/issues/107 for more info.
comment := "IfNoneMatch sets the optional parameter which makes the operation fail if " +
"the object's ETag matches the given value. This is useful for getting updates " +
"only after the object has changed since the last request. " +
"Use googleapi.IsNotModified to check whether the response error from Do " +
"is the result of In-None-Match."
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) IfNoneMatch(entityTag string) *%s {" , callName , callName )
pn ( " c.ifNoneMatch_ = entityTag" )
pn ( " return c" )
pn ( "}" )
}
doMethod := "Do method"
if meth . supportsMediaDownload ( ) {
doMethod = "Do and Download methods"
}
commentFmtStr := "Context sets the context to be used in this call's %s. " +
"Any pending HTTP request will be aborted if the provided context is canceled."
comment = fmt . Sprintf ( commentFmtStr , doMethod )
p ( "\n%s" , asComment ( "" , comment ) )
if meth . supportsMediaUpload ( ) {
comment = "This context will supersede any context previously provided to " +
"the ResumableMedia method."
p ( "%s" , asComment ( "" , comment ) )
}
pn ( "func (c *%s) Context(ctx context.Context) *%s {" , callName , callName )
pn ( ` c.ctx_ = ctx ` )
pn ( "return c" )
pn ( "}" )
comment = "Header returns an http.Header that can be modified by the caller to add " +
"HTTP headers to the request."
p ( "\n%s" , asComment ( "" , comment ) )
pn ( "func (c *%s) Header() http.Header {" , callName )
pn ( " if c.header_ == nil {" )
pn ( " c.header_ = make(http.Header)" )
pn ( " }" )
pn ( " return c.header_" )
pn ( "}" )
pn ( "\nfunc (c *%s) doRequest(alt string) (*http.Response, error) {" , callName )
pn ( ` reqHeaders := make(http.Header) ` )
pn ( "for k, v := range c.header_ {" )
pn ( " reqHeaders[k] = v" )
pn ( "}" )
pn ( ` reqHeaders.Set("User-Agent",c.s.userAgent()) ` )
if httpMethod == "GET" {
pn ( ` if c.ifNoneMatch_ != "" { ` )
pn ( ` reqHeaders.Set("If-None-Match", c.ifNoneMatch_) ` )
pn ( "}" )
}
pn ( "var body io.Reader = nil" )
if ba := args . bodyArg ( ) ; ba != nil && httpMethod != "GET" {
style := "WithoutDataWrapper"
if a . needsDataWrapper ( ) {
style = "WithDataWrapper"
}
pn ( "body, err := googleapi.%s.JSONReader(c.%s)" , style , ba . goname )
pn ( "if err != nil { return nil, err }" )
pn ( ` reqHeaders.Set("Content-Type", "application/json") ` )
}
pn ( ` c.urlParams_.Set("alt", alt) ` )
pn ( "urls := googleapi.ResolveRelative(c.s.BasePath, %q)" , meth . m . Path )
if meth . supportsMediaUpload ( ) {
2017-09-13 12:09:48 +00:00
pn ( "if c.mediaInfo_ != nil {" )
2017-08-05 18:17:15 +00:00
// Hack guess, since we get a 404 otherwise:
//pn("urls = googleapi.ResolveRelative(%q, %q)", a.apiBaseURL(), meth.mediaUploadPath())
// Further hack. Discovery doc is wrong?
pn ( " urls = strings.Replace(urls, %q, %q, 1)" , "https://www.googleapis.com/" , "https://www.googleapis.com/upload/" )
2017-09-13 12:09:48 +00:00
pn ( ` c.urlParams_.Set("uploadType", c.mediaInfo_.UploadType()) ` )
2017-08-05 18:17:15 +00:00
pn ( "}" )
pn ( "if body == nil {" )
pn ( " body = new(bytes.Buffer)" )
pn ( ` reqHeaders.Set("Content-Type", "application/json") ` )
pn ( "}" )
2017-09-13 12:09:48 +00:00
pn ( "body, cleanup := c.mediaInfo_.UploadRequest(reqHeaders, body)" )
pn ( "defer cleanup()" )
2017-08-05 18:17:15 +00:00
}
pn ( "urls += \"?\" + c.urlParams_.Encode()" )
pn ( "req, _ := http.NewRequest(%q, urls, body)" , httpMethod )
pn ( "req.Header = reqHeaders" )
// Replace param values after NewRequest to avoid reencoding them.
// E.g. Cloud Storage API requires '%2F' in entity param to be kept, but url.Parse replaces it with '/'.
argsForLocation := args . forLocation ( "path" )
if len ( argsForLocation ) > 0 {
pn ( ` googleapi.Expand(req.URL, map[string]string { ` )
for _ , arg := range argsForLocation {
pn ( ` "%s": %s, ` , arg . apiname , arg . exprAsString ( "c." ) )
}
pn ( ` }) ` )
}
pn ( "return gensupport.SendRequest(c.ctx_, c.s.client, req)" )
pn ( "}" )
if meth . supportsMediaDownload ( ) {
pn ( "\n// Download fetches the API endpoint's \"media\" value, instead of the normal" )
pn ( "// API response value. If the returned error is nil, the Response is guaranteed to" )
pn ( "// have a 2xx status code. Callers must close the Response.Body as usual." )
pn ( "func (c *%s) Download(opts ...googleapi.CallOption) (*http.Response, error) {" , callName )
pn ( ` gensupport.SetOptions(c.urlParams_, opts...) ` )
pn ( ` res, err := c.doRequest("media") ` )
pn ( "if err != nil { return nil, err }" )
pn ( "if err := googleapi.CheckMediaResponse(res); err != nil {" )
pn ( "res.Body.Close()" )
pn ( "return nil, err" )
pn ( "}" )
pn ( "return res, nil" )
pn ( "}" )
}
mapRetType := strings . HasPrefix ( retTypeComma , "map[" )
pn ( "\n// Do executes the %q call." , meth . m . ID )
if retTypeComma != "" && ! mapRetType {
commentFmtStr := "Exactly one of %v or error will be non-nil. " +
"Any non-2xx status code is an error. " +
"Response headers are in either %v.ServerResponse.Header " +
"or (if a response was returned at all) in error.(*googleapi.Error).Header. " +
"Use googleapi.IsNotModified to check whether the returned error was because " +
"http.StatusNotModified was returned."
comment := fmt . Sprintf ( commentFmtStr , retType , retType )
p ( "%s" , asComment ( "" , comment ) )
}
pn ( "func (c *%s) Do(opts ...googleapi.CallOption) (%serror) {" , callName , retTypeComma )
nilRet := ""
if retTypeComma != "" {
nilRet = "nil, "
}
pn ( ` gensupport.SetOptions(c.urlParams_, opts...) ` )
pn ( ` res, err := c.doRequest("json") ` )
if retTypeComma != "" && ! mapRetType {
pn ( "if res != nil && res.StatusCode == http.StatusNotModified {" )
pn ( " if res.Body != nil { res.Body.Close() }" )
pn ( " return nil, &googleapi.Error{" )
pn ( " Code: res.StatusCode," )
pn ( " Header: res.Header," )
pn ( " }" )
pn ( "}" )
}
pn ( "if err != nil { return %serr }" , nilRet )
pn ( "defer googleapi.CloseBody(res)" )
pn ( "if err := googleapi.CheckResponse(res); err != nil { return %serr }" , nilRet )
if meth . supportsMediaUpload ( ) {
2017-09-13 12:09:48 +00:00
pn ( ` rx := c.mediaInfo_.ResumableUpload(res.Header.Get("Location")) ` )
pn ( "if rx != nil {" )
pn ( " rx.Client = c.s.client" )
pn ( " rx.UserAgent = c.s.userAgent()" )
2017-08-05 18:17:15 +00:00
pn ( " ctx := c.ctx_" )
pn ( " if ctx == nil {" )
// TODO(mcgreevy): Require context when calling Media, or Do.
pn ( " ctx = context.TODO()" )
pn ( " }" )
pn ( " res, err = rx.Upload(ctx)" )
pn ( " if err != nil { return %serr }" , nilRet )
pn ( " defer res.Body.Close()" )
pn ( " if err := googleapi.CheckResponse(res); err != nil { return %serr }" , nilRet )
pn ( "}" )
}
if retTypeComma == "" {
pn ( "return nil" )
} else {
if mapRetType {
pn ( "var ret %s" , responseType ( a , meth . m ) )
} else {
pn ( "ret := &%s{" , responseTypeLiteral ( a , meth . m ) )
pn ( " ServerResponse: googleapi.ServerResponse{" )
pn ( " Header: res.Header," )
pn ( " HTTPStatusCode: res.StatusCode," )
pn ( " }," )
pn ( "}" )
}
if a . needsDataWrapper ( ) {
pn ( "target := &struct {" )
pn ( " Data %s `json:\"data\"`" , responseType ( a , meth . m ) )
pn ( "}{ret}" )
} else {
pn ( "target := &ret" )
}
2017-12-03 20:01:25 +00:00
pn ( "if err := gensupport.DecodeResponse(target, res); err != nil { return nil, err }" )
2017-08-05 18:17:15 +00:00
pn ( "return ret, nil" )
}
bs , _ := json . MarshalIndent ( meth . m . JSONMap , "\t// " , " " )
pn ( "// %s\n" , string ( bs ) )
pn ( "}" )
if ptg , rname , ok := meth . supportsPaging ( ) ; ok {
// We can assume retType is non-empty.
pn ( "" )
pn ( "// Pages invokes f for each page of results." )
pn ( "// A non-nil error returned from f will halt the iteration." )
pn ( "// The provided context supersedes any context provided to the Context method." )
pn ( "func (c *%s) Pages(ctx context.Context, f func(%s) error) error {" , callName , retType )
pn ( " c.ctx_ = ctx" )
pn ( ` defer %s // reset paging to original point ` , ptg . genDeferBody ( ) )
pn ( " for {" )
pn ( " x, err := c.Do()" )
pn ( " if err != nil { return err }" )
pn ( " if err := f(x); err != nil { return err }" )
pn ( ` if x.%s == "" { return nil } ` , rname )
pn ( ptg . genSet ( "x." + rname ) )
pn ( " }" )
pn ( "}" )
}
}
// A Field provides methods that describe the characteristics of a Param or Property.
type Field interface {
Default ( ) string
Enum ( ) ( [ ] string , bool )
EnumDescriptions ( ) [ ] string
UnfortunateDefault ( ) bool
}
type Param struct {
method * Method
p * disco . Parameter
callFieldName string // empty means to use the default
}
func ( p * Param ) Default ( ) string {
return p . p . Default
}
func ( p * Param ) Enum ( ) ( [ ] string , bool ) {
if e := p . p . Enums ; e != nil {
return e , true
}
return nil , false
}
func ( p * Param ) EnumDescriptions ( ) [ ] string {
return p . p . EnumDescriptions
}
func ( p * Param ) UnfortunateDefault ( ) bool {
// We do not do anything special for Params with unfortunate defaults.
return false
}
func ( p * Param ) GoType ( ) string {
typ , format := p . p . Type , p . p . Format
if typ == "string" && strings . Contains ( format , "int" ) && p . p . Location != "query" {
panic ( "unexpected int parameter encoded as string, not in query: " + p . p . Name )
}
t , ok := simpleTypeConvert ( typ , format )
if ! ok {
panic ( "failed to convert parameter type " + fmt . Sprintf ( "type=%q, format=%q" , typ , format ) )
}
return t
}
// goCallFieldName returns the name of this parameter's field in a
// method's "Call" struct.
func ( p * Param ) goCallFieldName ( ) string {
if p . callFieldName != "" {
return p . callFieldName
}
return validGoIdentifer ( p . p . Name )
}
// APIMethods returns top-level ("API-level") methods. They don't have an associated resource.
func ( a * API ) APIMethods ( ) [ ] * Method {
meths := [ ] * Method { }
for _ , m := range a . doc . Methods {
meths = append ( meths , & Method {
api : a ,
r : nil , // to be explicit
m : m ,
} )
}
return meths
}
func resolveRelative ( basestr , relstr string ) string {
u , err := url . Parse ( basestr )
if err != nil {
panicf ( "Error parsing base URL %q: %v" , basestr , err )
}
rel , err := url . Parse ( relstr )
if err != nil {
panicf ( "Error parsing relative URL %q: %v" , relstr , err )
}
u = u . ResolveReference ( rel )
return u . String ( )
}
func ( meth * Method ) NewArguments ( ) ( args * arguments ) {
args = & arguments {
method : meth ,
m : make ( map [ string ] * argument ) ,
}
po := meth . m . ParameterOrder
if len ( po ) > 0 {
for _ , pname := range po {
arg := meth . NewArg ( pname , meth . NamedParam ( pname ) )
args . AddArg ( arg )
}
}
if rs := meth . m . Request ; rs != nil {
args . AddArg ( meth . NewBodyArg ( rs ) )
}
return
}
func ( meth * Method ) NewBodyArg ( ds * disco . Schema ) * argument {
s := meth . api . schemaNamed ( ds . RefSchema . Name )
return & argument {
goname : validGoIdentifer ( strings . ToLower ( ds . Ref ) ) ,
apiname : "REQUEST" ,
gotype : "*" + s . GoName ( ) ,
apitype : ds . Ref ,
location : "body" ,
schema : s ,
}
}
func ( meth * Method ) NewArg ( apiname string , p * Param ) * argument {
apitype := p . p . Type
des := p . p . Description
goname := validGoIdentifer ( apiname ) // but might be changed later, if conflicts
if strings . Contains ( des , "identifier" ) && ! strings . HasSuffix ( strings . ToLower ( goname ) , "id" ) {
goname += "id" // yay
p . callFieldName = goname
}
gotype := mustSimpleTypeConvert ( apitype , p . p . Format )
if p . p . Repeated {
gotype = "[]" + gotype
}
return & argument {
apiname : apiname ,
apitype : apitype ,
goname : goname ,
gotype : gotype ,
location : p . p . Location ,
}
}
type argument struct {
method * Method
schema * Schema // Set if location == "body".
apiname , apitype string
goname , gotype string
location string // "path", "query", "body"
}
func ( a * argument ) String ( ) string {
return a . goname + " " + a . gotype
}
func ( a * argument ) exprAsString ( prefix string ) string {
switch a . gotype {
case "[]string" :
log . Printf ( "TODO(bradfitz): only including the first parameter in path query." )
return prefix + a . goname + ` [0] `
case "string" :
return prefix + a . goname
case "integer" , "int64" :
return "strconv.FormatInt(" + prefix + a . goname + ", 10)"
case "uint64" :
return "strconv.FormatUint(" + prefix + a . goname + ", 10)"
}
log . Panicf ( "unknown type: apitype=%q, gotype=%q" , a . apitype , a . gotype )
return ""
}
// arguments are the arguments that a method takes
type arguments struct {
l [ ] * argument
m map [ string ] * argument
method * Method
}
func ( args * arguments ) forLocation ( loc string ) [ ] * argument {
matches := make ( [ ] * argument , 0 )
for _ , arg := range args . l {
if arg . location == loc {
matches = append ( matches , arg )
}
}
return matches
}
func ( args * arguments ) bodyArg ( ) * argument {
for _ , arg := range args . l {
if arg . location == "body" {
return arg
}
}
return nil
}
func ( args * arguments ) AddArg ( arg * argument ) {
n := 1
oname := arg . goname
for {
_ , present := args . m [ arg . goname ]
if ! present {
args . m [ arg . goname ] = arg
args . l = append ( args . l , arg )
return
}
n ++
arg . goname = fmt . Sprintf ( "%s%d" , oname , n )
}
}
func ( a * arguments ) String ( ) string {
var buf bytes . Buffer
for i , arg := range a . l {
if i != 0 {
buf . Write ( [ ] byte ( ", " ) )
}
buf . Write ( [ ] byte ( arg . String ( ) ) )
}
return buf . String ( )
}
var urlRE = regexp . MustCompile ( ` ^http\S+$ ` )
func asComment ( pfx , c string ) string {
var buf bytes . Buffer
const maxLen = 70
r := strings . NewReplacer (
"\n" , "\n" + pfx + "// " ,
"`\"" , ` " ` ,
"\"`" , ` " ` ,
)
for len ( c ) > 0 {
line := c
if len ( line ) < maxLen {
fmt . Fprintf ( & buf , "%s// %s\n" , pfx , r . Replace ( line ) )
break
}
// Don't break URLs.
if ! urlRE . MatchString ( line [ : maxLen ] ) {
line = line [ : maxLen ]
}
si := strings . LastIndex ( line , " " )
if nl := strings . Index ( line , "\n" ) ; nl != - 1 && nl < si {
si = nl
}
if si != - 1 {
line = line [ : si ]
}
fmt . Fprintf ( & buf , "%s// %s\n" , pfx , r . Replace ( line ) )
c = c [ len ( line ) : ]
if si != - 1 {
c = c [ 1 : ]
}
}
return buf . String ( )
}
func simpleTypeConvert ( apiType , format string ) ( gotype string , ok bool ) {
// From http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
switch apiType {
case "boolean" :
gotype = "bool"
case "string" :
gotype = "string"
switch format {
case "int64" , "uint64" , "int32" , "uint32" :
gotype = format
}
case "number" :
gotype = "float64"
case "integer" :
gotype = "int64"
case "any" :
gotype = "interface{}"
}
return gotype , gotype != ""
}
func mustSimpleTypeConvert ( apiType , format string ) string {
if gotype , ok := simpleTypeConvert ( apiType , format ) ; ok {
return gotype
}
panic ( fmt . Sprintf ( "failed to simpleTypeConvert(%q, %q)" , apiType , format ) )
}
func responseType ( api * API , m * disco . Method ) string {
if m . Response == nil {
return ""
}
ref := m . Response . Ref
if ref != "" {
if s := api . schemas [ ref ] ; s != nil {
return s . GoReturnType ( )
}
return "*" + ref
}
return ""
}
// Strips the leading '*' from a type name so that it can be used to create a literal.
func responseTypeLiteral ( api * API , m * disco . Method ) string {
v := responseType ( api , m )
if strings . HasPrefix ( v , "*" ) {
return v [ 1 : ]
}
return v
}
// initialCap returns the identifier with a leading capital letter.
// it also maps "foo-bar" to "FooBar".
func initialCap ( ident string ) string {
if ident == "" {
panic ( "blank identifier" )
}
return depunct ( ident , true )
}
func validGoIdentifer ( ident string ) string {
id := depunct ( ident , false )
switch id {
case "break" , "default" , "func" , "interface" , "select" ,
"case" , "defer" , "go" , "map" , "struct" ,
"chan" , "else" , "goto" , "package" , "switch" ,
"const" , "fallthrough" , "if" , "range" , "type" ,
"continue" , "for" , "import" , "return" , "var" :
return id + "_"
}
return id
}
// depunct removes '-', '.', '$', '/', '_' from identifers, making the
// following character uppercase. Multiple '_' are preserved.
func depunct ( ident string , needCap bool ) string {
var buf bytes . Buffer
preserve_ := false
for i , c := range ident {
if c == '_' {
if preserve_ || strings . HasPrefix ( ident [ i : ] , "__" ) {
preserve_ = true
} else {
needCap = true
continue
}
} else {
preserve_ = false
}
if c == '-' || c == '.' || c == '$' || c == '/' {
needCap = true
continue
}
if needCap {
c = unicode . ToUpper ( c )
needCap = false
}
buf . WriteByte ( byte ( c ) )
}
return buf . String ( )
}
func addFieldValueComments ( p func ( format string , args ... interface { } ) , field Field , indent string , blankLine bool ) {
var lines [ ] string
if enum , ok := field . Enum ( ) ; ok {
desc := field . EnumDescriptions ( )
lines = append ( lines , asComment ( indent , "Possible values:" ) )
defval := field . Default ( )
for i , v := range enum {
more := ""
if v == defval {
more = " (default)"
}
if len ( desc ) > i && desc [ i ] != "" {
more = more + " - " + desc [ i ]
}
lines = append ( lines , asComment ( indent , ` " ` + v + ` " ` + more ) )
}
} else if field . UnfortunateDefault ( ) {
lines = append ( lines , asComment ( "\t" , fmt . Sprintf ( "Default: %s" , field . Default ( ) ) ) )
}
if blankLine && len ( lines ) > 0 {
p ( indent + "//\n" )
}
for _ , l := range lines {
p ( "%s" , l )
}
}