// 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" "errors" "flag" "fmt" "go/format" "io/ioutil" "log" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "unicode" ) // goGenVersion is the version of the Go code generator const goGenVersion = "0.5" 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", "https://www.googleapis.com/discovery/v1/apis", "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).") googleAPIPkg = flag.String("googleapi_pkg", "google.golang.org/api/googleapi", "Go package path of the 'googleapi' support package.") ) // API represents an API to generate, as well as its state while it's // generating. type API struct { ID string `json:"id"` Name string `json:"name"` Version string `json:"version"` Title string `json:"title"` DiscoveryLink string `json:"discoveryLink"` // relative RootURL string `json:"rootUrl"` ServicePath string `json:"servicePath"` Preferred bool `json:"preferred"` m map[string]interface{} forceJSON []byte // if non-nil, the JSON schema file. else fetched. usedNames namePool schemas map[string]*Schema // apiName -> schema p func(format string, args ...interface{}) // print raw pn func(format string, args ...interface{}) // print with indent and newline } func (a *API) sortedSchemaNames() (names []string) { for name := range a.schemas { names = append(names, name) } sort.Strings(names) return } type AllAPIs struct { Items []*API `json:"items"` } 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() if err != nil { errors = append(errors, &generateError{api, err}) continue } if *build { 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 strings.Contains(a.ID, "buzz") { // R.I.P. return false } if strings.Contains(a.ID, "fusiontables") { // TODO(bradfitz): broken codegen. return false } return *apiToGenerate == "*" || *apiToGenerate == a.ID } func getAPIs() []*API { if *jsonFile != "" { return getAPIsFromFile() } var all AllAPIs disco := slurpURL(*apisURL) if err := json.Unmarshal(disco, &all); err != nil { log.Fatalf("error decoding JSON in %s: %v", apisURL, err) } if !*publicOnly && *apiToGenerate != "*" { parts := strings.SplitN(*apiToGenerate, ":", 2) apiName := parts[0] apiVersion := parts[1] all.Items = append(all.Items, &API{ ID: *apiToGenerate, Name: apiName, Version: apiVersion, DiscoveryLink: fmt.Sprintf("./apis/%s/%s/rest", apiName, apiVersion), }) } return all.Items } // 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) } a := &API{ forceJSON: jsonBytes, } if err := json.Unmarshal(jsonBytes, a); err != nil { return nil, fmt.Errorf("Decoding JSON in %s: %v", file, err) } 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 } return ioutil.WriteFile(file, contents, 0644) } var etagLine = regexp.MustCompile(`(?m)^\s+"etag": ".+\n`) // basicallyEqual reports whether a and b are equal except for boring // differences like ETag updates. func basicallyEqual(a, b []byte) bool { return etagLine.Match(a) && etagLine.Match(b) && bytes.Equal(etagLine.ReplaceAll(a, nil), etagLine.ReplaceAll(b, nil)) } func slurpURL(urlStr string) []byte { diskFile := filepath.Join(os.TempDir(), "google-api-cache-"+url.QueryEscape(urlStr)) if *useCache { bs, err := ioutil.ReadFile(diskFile) if err == nil && len(bs) > 0 { return bs } } 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) } bs, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatalf("Error reading body of URL %s: %v", urlStr, err) } if *useCache { if err := ioutil.WriteFile(diskFile, bs, 0666); err != nil { log.Printf("Warning: failed to write JSON of %s to disk file %s: %v", urlStr, diskFile, 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 } 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 (a *API) SourceDir() string { if *genDir == "" { paths := filepath.SplitList(os.Getenv("GOPATH")) if len(paths) > 0 && paths[0] != "" { *genDir = filepath.Join(paths[0], "src", "google.golang.org", "api") } } return filepath.Join(*genDir, a.Package(), a.Version) } func (a *API) DiscoveryURL() string { if a.DiscoveryLink == "" { log.Fatalf("API %s has no DiscoveryLink", a.ID) } base, _ := url.Parse(*apisURL) u, err := base.Parse(a.DiscoveryLink) if err != nil { log.Fatalf("API %s has bogus DiscoveryLink %s: %v", a.ID, a.DiscoveryLink, err) } return u.String() } func (a *API) Package() string { return strings.ToLower(a.Name) } func (a *API) Target() string { return fmt.Sprintf("google.golang.org/api/%s/%s", a.Package(), a.Version) } // 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 { if a.RootURL != "" { return a.RootURL + a.ServicePath } return resolveRelative(*apisURL, jstr(a.m, "basePath")) } func (a *API) needsDataWrapper() bool { for _, feature := range jstrlist(a.m, "features") { if feature == "dataWrapper" { return true } } return false } func (a *API) jsonBytes() []byte { if v := a.forceJSON; v != nil { return v } return slurpURL(a.DiscoveryURL()) } func (a *API) WriteGeneratedCode() error { outdir := a.SourceDir() err := os.MkdirAll(outdir, 0755) if err != nil { return fmt.Errorf("failed to Mkdir %s: %v", outdir, err) } pkg := a.Package() writeFile(filepath.Join(outdir, a.Package()+"-api.json"), a.jsonBytes()) genfilename := *output if genfilename == "" { genfilename = filepath.Join(outdir, pkg+"-gen.go") } code, err := a.GenerateCode() errw := writeFile(genfilename, code) if err == nil { err = errw } return err } func (a *API) GenerateCode() ([]byte, error) { pkg := a.Package() a.m = make(map[string]interface{}) m := a.m jsonBytes := a.jsonBytes() err := json.Unmarshal(jsonBytes, &a.m) if err != nil { return nil, err } // Buffer the output in memory, for gofmt'ing later in the defer. 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...) } p, pn := a.p, a.pn reslist := a.Resources(a.m, "") p("// Package %s provides access to the %s.\n", pkg, jstr(m, "title")) if docs := jstr(m, "documentationLink"); docs != "" { p("//\n") p("// See %s\n", docs) } p("//\n// Usage example:\n") p("//\n") p("// import %q\n", a.Target()) p("// ...\n") p("// %sService, err := %s.New(oauthHttpClient)\n", pkg, pkg) p("package %s\n", pkg) p("\n") p("import (\n") for _, pkg := range []string{ "bytes", *googleAPIPkg, "encoding/json", "errors", "fmt", "io", "net/http", "net/url", "strconv", "strings", } { p("\t%q\n", pkg) } p(")\n\n") pn("// 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 _ = googleapi.Version") pn("var _ = errors.New") pn("var _ = strings.Replace") pn("") pn("const apiId = %q", jstr(m, "id")) pn("const apiName = %q", jstr(m, "name")) pn("const apiVersion = %q", jstr(m, "version")) p("const basePath = %q\n", a.apiBaseURL()) p("\n") a.generateScopeConstants() a.GetName("New") // ignore return value; we're the first caller pn("func New(client *http.Client) (*Service, error) {") pn("if client == nil { return nil, errors.New(\"client is nil\") }") pn("s := &Service{client: client, BasePath: basePath}") for _, res := range reslist { // add top level resources. pn("s.%s = New%s(s)", res.GoField(), res.GoType()) } pn("return s, nil") pn("}") a.GetName("Service") // ignore return value; no user-defined names yet p("\ntype Service struct {\n") p("\tclient *http.Client\n") p("\tBasePath string // API endpoint base URL\n") for _, res := range reslist { p("\n\t%s\t*%s\n", res.GoField(), res.GoType()) } p("}\n") for _, res := range reslist { res.generateType() } a.PopulateSchemas() for _, name := range a.sortedSchemaNames() { a.schemas[name].writeSchemaCode(a) } for _, meth := range a.APIMethods() { meth.generateCode() } for _, res := range reslist { res.generateMethods() } clean, err := format.Source(buf.Bytes()) if err != nil { return buf.Bytes(), err } return clean, nil } func (a *API) generateScopeConstants() { auth := jobj(a.m, "auth") if auth == nil { return } oauth2 := jobj(auth, "oauth2") if oauth2 == nil { return } scopes := jobj(oauth2, "scopes") if scopes == nil || len(scopes) == 0 { return } a.p("// OAuth2 scopes used by this API.\n") a.p("const (\n") n := 0 for _, scopeName := range sortedKeys(scopes) { mi := scopes[scopeName] if n > 0 { a.p("\n") } n++ ident := scopeIdentifierFromURL(scopeName) if des := jstr(mi.(map[string]interface{}), "description"); des != "" { a.p("%s", asComment("\t", des)) } a.p("\t%s = %q\n", ident, scopeName) } 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 } type Schema struct { api *API m map[string]interface{} // original JSON map typ *Type // lazily populated by Type apiName string // the native API-defined name of this type goName string // lazily populated by GoName goReturnType string // lazily populated by GoReturnType } type Property struct { s *Schema // property of which schema apiName string // the native API-defined name of this property m map[string]interface{} // original JSON map typ *Type // lazily populated by Type } func (p *Property) Type() *Type { if p.typ == nil { p.typ = &Type{api: p.s.api, m: p.m} } return p.typ } func (p *Property) GoName() string { return initialCap(p.apiName) } func (p *Property) APIName() string { return p.apiName } func (p *Property) Description() string { return jstr(p.m, "description") } type Type struct { m map[string]interface{} // JSON map containing key "type" and maybe "items", "properties" api *API } func (t *Type) apiType() string { // Note: returns "" on reference types if t, ok := t.m["type"].(string); ok { return t } return "" } func (t *Type) apiTypeFormat() string { if f, ok := t.m["format"].(string); ok { return f } return "" } func (t *Type) isIntAsString() bool { return t.apiType() == "string" && strings.Contains(t.apiTypeFormat(), "int") } func (t *Type) asSimpleGoType() (goType string, ok bool) { return simpleTypeConvert(t.apiType(), t.apiTypeFormat()) } func (t *Type) String() string { return fmt.Sprintf("[type=%q, map=%s]", t.apiType(), prettyJSON(t.m)) } func (t *Type) AsGo() string { if t, ok := t.asSimpleGoType(); ok { return t } if at, ok := t.ArrayType(); ok { if at.apiType() == "string" { switch at.apiTypeFormat() { 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" default: return "[]" + at.AsGo() } } return "[]" + at.AsGo() } if ref, ok := t.Reference(); ok { s := t.api.schemas[ref] if s == nil { panic(fmt.Sprintf("in Type.AsGo(), failed to find referenced type %q for %s", ref, prettyJSON(t.m))) } return s.Type().AsGo() } if typ, ok := t.MapType(); ok { return typ } if t.IsStruct() { if apiName, ok := t.m["_apiName"].(string); ok { s := t.api.schemas[apiName] if s == nil { panic(fmt.Sprintf("in Type.AsGo, _apiName of %q didn't point to a valid schema; json: %s", apiName, prettyJSON(t.m))) } if v := jobj(s.m, "variant"); v != nil { return s.GoName() } return "*" + s.GoName() } panic("in Type.AsGo, no _apiName found for struct type " + prettyJSON(t.m)) } panic("unhandled Type.AsGo for " + prettyJSON(t.m)) } func (t *Type) IsSimple() bool { _, ok := simpleTypeConvert(t.apiType(), t.apiTypeFormat()) return ok } func (t *Type) IsStruct() bool { return t.apiType() == "object" } func (t *Type) Reference() (apiName string, ok bool) { apiName = jstr(t.m, "$ref") ok = apiName != "" return } func (t *Type) IsMap() bool { _, ok := t.MapType() return ok } // MapType checks if the current node is a map and if true, it returns the Go type for the map, such as map[string]string. func (t *Type) MapType() (typ string, ok bool) { props := jobj(t.m, "additionalProperties") if props == nil { return "", false } s := jstr(props, "type") if s == "string" { return "map[string]string", true } if s != "array" { if s == "" { // Check for reference s = jstr(props, "$ref") if s != "" { return "map[string]" + s, true } } log.Printf("Warning: found map to type %q which is not implemented yet.", s) return "", false } items := jobj(props, "items") if items == nil { return "", false } s = jstr(items, "type") if s != "string" { if s == "" { // Check for reference s = jstr(items, "$ref") if s != "" { return "map[string][]" + s, true } } log.Printf("Warning: found map of arrays of type %q which is not implemented yet.", s) return "", false } return "map[string][]string", true } func (t *Type) IsReference() bool { return jstr(t.m, "$ref") != "" } func (t *Type) ReferenceSchema() (s *Schema, ok bool) { apiName, ok := t.Reference() if !ok { return } s = t.api.schemas[apiName] if s == nil { panicf("failed to find t.api.schemas[%q] while resolving reference", apiName) } return s, true } func (t *Type) ArrayType() (elementType *Type, ok bool) { if t.apiType() != "array" { return } items := jobj(t.m, "items") if items == nil { panicf("can't handle array type missing its 'items' key. map is %#v", t.m) } return &Type{api: t.api, m: items}, true } func (s *Schema) Type() *Type { if s.typ == nil { s.typ = &Type{api: s.api, m: s.m} } return s.typ } func (s *Schema) properties() []*Property { if !s.Type().IsStruct() { panic("called properties on non-object schema") } pl := []*Property{} propMap := jobj(s.m, "properties") for _, name := range sortedKeys(propMap) { m := propMap[name].(map[string]interface{}) pl = append(pl, &Property{ s: s, m: m, apiName: name, }) } return pl } func (s *Schema) populateSubSchemas() (outerr error) { defer func() { r := recover() if r == nil { return } outerr = fmt.Errorf("%v", r) }() addSubStruct := func(subApiName string, t *Type) { if s.api.schemas[subApiName] != nil { panic("dup schema apiName: " + subApiName) } subm := t.m subm["_apiName"] = subApiName subs := &Schema{ api: s.api, m: subm, typ: t, apiName: subApiName, } s.api.schemas[subApiName] = subs err := subs.populateSubSchemas() if err != nil { panicf("in sub-struct %q: %v", subApiName, err) } } if s.Type().IsStruct() { for _, p := range s.properties() { if p.Type().IsSimple() || p.Type().IsMap() { continue } if at, ok := p.Type().ArrayType(); ok { if at.IsSimple() || at.IsReference() { continue } subApiName := fmt.Sprintf("%s.%s", s.apiName, p.apiName) if at.IsStruct() { addSubStruct(subApiName, at) // was p.Type()? continue } if _, ok := at.ArrayType(); ok { addSubStruct(subApiName, at) continue } panicf("Unknown property array type for %q: %s", subApiName, at) continue } subApiName := fmt.Sprintf("%s.%s", s.apiName, p.apiName) if p.Type().IsStruct() { addSubStruct(subApiName, p.Type()) continue } if p.Type().IsReference() { continue } panicf("Unknown type for %q: %s", subApiName, p.Type()) } return } if at, ok := s.Type().ArrayType(); ok { if at.IsSimple() || at.IsReference() { return } subApiName := fmt.Sprintf("%s.Item", s.apiName) if at.IsStruct() { addSubStruct(subApiName, at) return } if at, ok := at.ArrayType(); ok { if at.IsSimple() || at.IsReference() { return } addSubStruct(subApiName, at) return } panicf("Unknown array type for %q: %s", subApiName, at) return } if s.Type().IsSimple() || s.Type().IsReference() { return } fmt.Fprintf(os.Stderr, "in populateSubSchemas, schema is: %s", prettyJSON(s.m)) panicf("populateSubSchemas: unsupported type for schema %q", s.apiName) panic("unreachable") } // 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 name, ok := s.Type().MapType(); ok { s.goName = name } else { s.goName = s.api.GetName(initialCap(s.apiName)) } } 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.Type().IsMap() { s.goReturnType = s.GoName() } else { s.goReturnType = "*" + s.GoName() } } return s.goReturnType } func (s *Schema) writeSchemaCode(api *API) { if s.Type().IsStruct() && !s.Type().IsMap() { s.writeSchemaStruct(api) return } if _, ok := s.Type().ArrayType(); ok { log.Printf("TODO writeSchemaCode for arrays for %s", s.GoName()) return } if destSchema, ok := s.Type().ReferenceSchema(); ok { // Convert it to a struct using embedding. s.api.p("\ntype %s struct {\n", s.GoName()) s.api.p("\t%s\n", destSchema.GoName()) s.api.p("}\n") return } if s.Type().IsSimple() { apitype := jstr(s.m, "type") typ := mustSimpleTypeConvert(apitype, jstr(s.m, "format")) s.api.p("\ntype %s %s\n", s.GoName(), typ) return } if s.Type().IsMap() { return } fmt.Fprintf(os.Stderr, "in writeSchemaCode, schema is: %s", prettyJSON(s.m)) panicf("writeSchemaCode: unsupported type for schema %q", s.apiName) } func (s *Schema) writeVariant(api *API, v map[string]interface{}) { s.api.p("\ntype %s map[string]interface{}\n\n", s.GoName()) // Write out the "Type" method that identifies the variant type. s.api.p("func (t %s) Type() string {\n", s.GoName()) s.api.p(" return googleapi.VariantType(t)\n") s.api.p("}\n\n") // Write out helper methods to convert each possible variant. for _, m := range jobjlist(v, "map") { val := jstr(m, "type_value") reftype := jstr(m, "$ref") if val == "" && reftype == "" { log.Printf("TODO variant %s ref %s not yet supported.", val, reftype) continue } _, ok := api.schemas[reftype] if !ok { log.Printf("TODO variant %s ref %s not yet supported.", val, reftype) continue } s.api.p("func (t %s) %s() (r %s, ok bool) {\n", s.GoName(), initialCap(val), reftype) s.api.p(" if t.Type() != %q {\n", initialCap(val)) s.api.p(" return r, false\n") s.api.p(" }\n") s.api.p(" ok = googleapi.ConvertVariant(map[string]interface{}(t), &r)\n") s.api.p(" return r, ok\n") s.api.p("}\n\n") } } func (s *Schema) writeSchemaStruct(api *API) { if v := jobj(s.m, "variant"); v != nil { s.writeVariant(api, v) return } // TODO: description s.api.p("\ntype %s struct {\n", s.GoName()) for i, p := range s.properties() { if i > 0 { s.api.p("\n") } pname := p.GoName() if des := p.Description(); des != "" { s.api.p("%s", asComment("\t", fmt.Sprintf("%s: %s", pname, des))) } var extraOpt string if p.Type().isIntAsString() { extraOpt += ",string" } s.api.p("\t%s %s `json:\"%s,omitempty%s\"`\n", pname, p.Type().AsGo(), p.APIName(), extraOpt) } s.api.p("}\n") } // 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() { m := jobj(a.m, "schemas") if a.schemas != nil { panic("") } a.schemas = make(map[string]*Schema) for name, mi := range m { s := &Schema{ api: a, apiName: name, m: mi.(map[string]interface{}), } // And a little gross hack, so a map alone is good // enough to get its apiName: s.m["_apiName"] = name a.schemas[name] = s err := s.populateSubSchemas() if err != nil { panicf("Error populating schema with API name %q: %v", name, err) } } } type Resource struct { api *API name string parent string m map[string]interface{} resources []*Resource } func (r *Resource) generateType() { p, pn := r.api.p, r.api.pn t := r.GoType() pn(fmt.Sprintf("func New%s(s *Service) *%s {", t, t)) pn("rs := &%s{s : s}", t) for _, res := range r.resources { pn("rs.%s = New%s(s)", res.GoField(), res.GoType()) } pn("return rs") pn("}") p("\ntype %s struct {\n", t) p("\ts *Service\n") for _, res := range r.resources { p("\n\t%s\t*%s\n", res.GoField(), res.GoType()) } p("}\n") for _, res := range r.resources { res.generateType() } } func (r *Resource) generateMethods() { for _, meth := range r.Methods() { meth.generateCode() } for _, res := range r.resources { res.generateMethods() } } func (r *Resource) GoField() string { return initialCap(r.name) } func (r *Resource) GoType() string { return initialCap(fmt.Sprintf("%s.%s", r.parent, r.name)) + "Service" } func (r *Resource) Methods() []*Method { ms := []*Method{} methMap := jobj(r.m, "methods") for _, mname := range sortedKeys(methMap) { mi := methMap[mname] ms = append(ms, &Method{ api: r.api, r: r, name: mname, m: mi.(map[string]interface{}), }) } return ms } type Method struct { api *API r *Resource // or nil if a API-level (top-level) method name string m map[string]interface{} // original JSON params []*Param // all Params, of each type, lazily set by first access to Parameters } func (m *Method) Id() string { return jstr(m.m, "id") } func (m *Method) supportsMedia() bool { return jobj(m.m, "mediaUpload") != nil } func (m *Method) mediaPath() string { return jstr(jobj(jobj(jobj(m.m, "mediaUpload"), "protocols"), "simple"), "path") } func (m *Method) Params() []*Param { if m.params == nil { paramMap := jobj(m.m, "parameters") for _, name := range sortedKeys(paramMap) { mi := paramMap[name] pm := mi.(map[string]interface{}) m.params = append(m.params, &Param{ name: name, m: pm, method: m, }) } } 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.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.IsRequired() }) } func (m *Method) RequiredRepeatedQueryParams() []*Param { return m.grepParams(func(p *Param) bool { return p.IsRequired() && p.IsRepeated() && p.Location() == "query" }) } func (m *Method) RequiredQueryParams() []*Param { return m.grepParams(func(p *Param) bool { return p.IsRequired() && !p.IsRepeated() && p.Location() == "query" }) } 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()) retTypeComma := responseType(a, meth.m) if retTypeComma != "" { retTypeComma += ", " } args := meth.NewArguments() methodName := initialCap(meth.name) prefix := "" if res != nil { prefix = initialCap(fmt.Sprintf("%s.%s", res.parent, res.name)) } callName := a.GetName(prefix + methodName + "Call") p("\ntype %s struct {\n", callName) p("\ts *Service\n") for _, arg := range args.l { p("\t%s %s\n", arg.goname, arg.gotype) } p("\topt_ map[string]interface{}\n") if meth.supportsMedia() { p("\tmedia_ io.Reader\n") } p("}\n") p("\n%s", asComment("", methodName+": "+jstr(meth.m, "description"))) var servicePtr string if res == nil { p("func (s *Service) %s(%s) *%s {\n", methodName, args, callName) servicePtr = "s" } else { p("func (r *%s) %s(%s) *%s {\n", res.GoType(), methodName, args, callName) servicePtr = "r.s" } p("\tc := &%s{s: %s, opt_: make(map[string]interface{})}\n", callName, servicePtr) for _, arg := range args.l { p("\tc.%s = %s\n", arg.goname, arg.goname) } p("\treturn c\n") p("}\n") for _, opt := range meth.OptParams() { setter := initialCap(opt.name) des := jstr(opt.m, "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.name, des))) np := new(namePool) np.Get("c") // take the receiver's name paramName := np.Get(validGoIdentifer(opt.name)) p("func (c *%s) %s(%s %s) *%s {\n", callName, setter, paramName, opt.GoType(), callName) p("c.opt_[%q] = %s\n", opt.name, paramName) p("return c\n") p("}\n") } if meth.supportsMedia() { p("func (c *%s) Media(r io.Reader) *%s {\n", callName, callName) p("c.media_ = r\n") p("return c\n") p("}\n") } pn("\n// Fields allows partial responses to be retrieved.") pn("// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse") pn("// for more information.") pn("func (c *%s) Fields(s ...googleapi.Field) *%s {", callName, callName) pn(`c.opt_["fields"] = googleapi.CombineFields(s)`) pn("return c") pn("}") pn("\nfunc (c *%s) Do() (%serror) {", callName, retTypeComma) nilRet := "" if retTypeComma != "" { nilRet = "nil, " } pn("var body io.Reader = nil") hasContentType := false httpMethod := jstr(meth.m, "httpMethod") 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 %serr }", nilRet) pn(`ctype := "application/json"`) hasContentType = true } pn("params := make(url.Values)") // Set this first. if they override it, though, might be gross. We don't expect // XML replies elsewhere. TODO(bradfitz): hide this option in the generated code? pn(`params.Set("alt", "json")`) for _, p := range meth.RequiredQueryParams() { pn("params.Set(%q, fmt.Sprintf(\"%%v\", c.%s))", p.name, p.goCallFieldName()) } for _, p := range meth.RequiredRepeatedQueryParams() { pn("for _, v := range c.%s { params.Add(%q, fmt.Sprintf(\"%%v\", v)) }", p.name, p.name) } opts := meth.OptParams() opts = append(opts, &Param{name: "fields"}) for _, p := range opts { pn("if v, ok := c.opt_[%q]; ok { params.Set(%q, fmt.Sprintf(\"%%v\", v)) }", p.name, p.name) } p("urls := googleapi.ResolveRelative(c.s.BasePath, %q)\n", jstr(meth.m, "path")) if meth.supportsMedia() { pn("if c.media_ != nil {") // Hack guess, since we get a 404 otherwise: //pn("urls = googleapi.ResolveRelative(%q, %q)", a.apiBaseURL(), meth.mediaPath()) // Further hack. Discovery doc is wrong? pn("urls = strings.Replace(urls, %q, %q, 1)", "https://www.googleapis.com/", "https://www.googleapis.com/upload/") pn(`params.Set("uploadType", "multipart")`) pn("}") } pn("urls += \"?\" + params.Encode()") if meth.supportsMedia() && httpMethod != "GET" { if !hasContentType { // Support mediaUpload but no ctype set. pn("body = new(bytes.Buffer)") pn(`ctype := "application/json"`) hasContentType = true } pn("contentLength_, hasMedia_ := googleapi.ConditionallyIncludeMedia(c.media_, &body, &ctype)") } pn("req, _ := http.NewRequest(%q, urls, body)", httpMethod) // 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(`})`) } else { // Just call SetOpaque since we aren't calling Expand pn(`googleapi.SetOpaque(req.URL)`) } if meth.supportsMedia() { pn("if hasMedia_ { req.ContentLength = contentLength_ }") } if hasContentType { pn(`req.Header.Set("Content-Type", ctype)`) } pn(`req.Header.Set("User-Agent", "google-api-go-client/` + goGenVersion + `")`) pn("res, err := c.s.client.Do(req);") pn("if err != nil { return %serr }", nilRet) pn("defer googleapi.CloseBody(res)") pn("if err := googleapi.CheckResponse(res); err != nil { return %serr }", nilRet) if retTypeComma == "" { pn("return nil") } else { pn("var ret %s", responseType(a, meth.m)) pn("if err := json.NewDecoder(res.Body).Decode(&ret); err != nil { return nil, err }") pn("return ret, nil") } bs, _ := json.MarshalIndent(meth.m, "\t// ", " ") pn("// %s\n", string(bs)) pn("}") } type Param struct { method *Method name string m map[string]interface{} callFieldName string // empty means to use the default } func (p *Param) IsRequired() bool { v, _ := p.m["required"].(bool) return v } func (p *Param) IsRepeated() bool { v, _ := p.m["repeated"].(bool) return v } func (p *Param) Location() string { return p.m["location"].(string) } func (p *Param) GoType() string { typ, format := jstr(p.m, "type"), jstr(p.m, "format") if typ == "string" && strings.Contains(format, "int") && p.Location() != "query" { panic("unexpected int parameter encoded as string, not in query: " + 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.name) } // APIMethods returns top-level ("API-level") methods. They don't have an associated resource. func (a *API) APIMethods() []*Method { meths := []*Method{} methMap := jobj(a.m, "methods") for _, name := range sortedKeys(methMap) { mi := methMap[name] meths = append(meths, &Method{ api: a, r: nil, // to be explicit name: name, m: mi.(map[string]interface{}), }) } return meths } func (a *API) Resources(m map[string]interface{}, p string) []*Resource { res := []*Resource{} resMap := jobj(m, "resources") for _, rname := range sortedKeys(resMap) { rmi := resMap[rname] rm := rmi.(map[string]interface{}) res = append(res, &Resource{a, rname, p, rm, a.Resources(rm, fmt.Sprintf("%s.%s", p, rname))}) } return res } 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, ok := meth.m["parameterOrder"].([]interface{}) if ok { for _, poi := range po { pname := poi.(string) arg := meth.NewArg(pname, meth.NamedParam(pname)) args.AddArg(arg) } } if ro := jobj(meth.m, "request"); ro != nil { args.AddArg(meth.NewBodyArg(ro)) } return } func (meth *Method) NewBodyArg(m map[string]interface{}) *argument { reftype := jstr(m, "$ref") return &argument{ goname: validGoIdentifer(strings.ToLower(reftype)), apiname: "REQUEST", gotype: "*" + reftype, apitype: reftype, location: "body", } } func (meth *Method) NewArg(apiname string, p *Param) *argument { m := p.m apitype := jstr(m, "type") des := jstr(m, "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, jstr(m, "format")) if p.IsRepeated() { gotype = "[]" + gotype } return &argument{ apiname: apiname, apitype: apitype, goname: goname, gotype: gotype, location: jstr(m, "location"), } } type argument struct { method *Method 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() } func asComment(pfx, c string) string { var buf bytes.Buffer const maxLen = 70 removeNewlines := func(s string) string { return strings.Replace(s, "\n", "\n"+pfx+"// ", -1) } for len(c) > 0 { line := c if len(line) < maxLen { fmt.Fprintf(&buf, "%s// %s\n", pfx, removeNewlines(line)) break } line = line[:maxLen] si := strings.LastIndex(line, " ") if si != -1 { line = line[:si] } fmt.Fprintf(&buf, "%s// %s\n", pfx, removeNewlines(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 (a *API) goTypeOfJsonObject(outerName, memberName string, m map[string]interface{}) (string, error) { apitype := jstr(m, "type") switch apitype { case "array": items := jobj(m, "items") if items == nil { return "", errors.New("no items but type was array") } if ref := jstr(items, "$ref"); ref != "" { return "[]*" + ref, nil // TODO: wrong; delete this whole function } if atype := jstr(items, "type"); atype != "" { return "[]" + mustSimpleTypeConvert(atype, jstr(items, "format")), nil } return "", errors.New("unsupported 'array' type") case "object": return "*" + outerName + "_" + memberName, nil //return "", os.NewError("unsupported 'object' type") } return mustSimpleTypeConvert(apitype, jstr(m, "format")), nil } func responseType(api *API, m map[string]interface{}) string { ro := jobj(m, "response") if ro != nil { if ref := jstr(ro, "$ref"); ref != "" { if s := api.schemas[ref]; s != nil { return s.GoReturnType() } return "*" + ref } } return "" } // 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 func depunct(ident string, needCap bool) string { var buf bytes.Buffer for _, c := range ident { if c == '-' || c == '.' || c == '$' || c == '/' { needCap = true continue } if needCap { c = unicode.ToUpper(c) needCap = false } buf.WriteByte(byte(c)) } return buf.String() } func prettyJSON(m map[string]interface{}) string { bs, err := json.MarshalIndent(m, "", " ") if err != nil { return fmt.Sprintf("[JSON error %v on %#v]", err, m) } return string(bs) } func jstr(m map[string]interface{}, key string) string { if s, ok := m[key].(string); ok { return s } return "" } func sortedKeys(m map[string]interface{}) (keys []string) { for key := range m { keys = append(keys, key) } sort.Strings(keys) return } func jobj(m map[string]interface{}, key string) map[string]interface{} { if m, ok := m[key].(map[string]interface{}); ok { return m } return nil } func jobjlist(m map[string]interface{}, key string) []map[string]interface{} { si, ok := m[key].([]interface{}) if !ok { return nil } var sl []map[string]interface{} for _, si := range si { sl = append(sl, si.(map[string]interface{})) } return sl } func jstrlist(m map[string]interface{}, key string) []string { si, ok := m[key].([]interface{}) if !ok { return nil } sl := make([]string, 0) for _, si := range si { sl = append(sl, si.(string)) } return sl }