| // 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 |
| } |
| |
| var docsLink string |
| |
| 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")) |
| docsLink = jstr(m, "documentationLink") |
| if docsLink != "" { |
| p("//\n") |
| p("// See %s\n", docsLink) |
| } |
| 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", |
| "golang.org/x/net/context", |
| } { |
| 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("var _ = context.Background") |
| 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("\tresumable_ googleapi.SizeReaderAt\n") |
| p("\tmediaType_ string\n") |
| p("\tctx_ context.Context\n") |
| p("\tprotocol_ string\n") |
| } |
| p("}\n") |
| |
| p("\n%s", asComment("", methodName+": "+jstr(meth.m, "description"))) |
| if res != nil { |
| if url := canonicalDocsURL[fmt.Sprintf("%v%v/%v", docsLink, res.name, meth.name)]; url != "" { |
| pn("// For details, see %v", url) |
| } |
| } |
| |
| 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() { |
| pn("\n// Media specifies the media to upload in a single chunk.") |
| pn("// At most one of Media and ResumableMedia may be set.") |
| pn("func (c *%s) Media(r io.Reader) *%s {", callName, callName) |
| pn("c.media_ = r") |
| pn(`c.protocol_ = "multipart"`) |
| pn("return c") |
| pn("}") |
| pn("\n// ResumableMedia specifies the media to upload in chunks and can be cancelled with ctx.") |
| pn("// At most one of Media and ResumableMedia may be set.") |
| pn(`// mediaType identifies the MIME media type of the upload, such as "image/png".`) |
| pn(`// If mediaType is "", it will be auto-detected.`) |
| pn("func (c *%s) ResumableMedia(ctx context.Context, r io.ReaderAt, size int64, mediaType string) *%s {", callName, callName) |
| pn("c.ctx_ = ctx") |
| pn("c.resumable_ = io.NewSectionReader(r, 0, size)") |
| pn("c.mediaType_ = mediaType") |
| pn(`c.protocol_ = "resumable"`) |
| pn("return c") |
| pn("}") |
| pn("\n// ProgressUpdater provides a callback function that will be called after every chunk.") |
| pn("// It should be a low-latency function in order to not slow down the upload operation.") |
| pn("// This should only be called when using ResumableMedia (as opposed to Media).") |
| pn("func (c *%s) ProgressUpdater(pu googleapi.ProgressUpdater) *%s {", callName, callName) |
| pn(`c.opt_["progressUpdater"] = pu`) |
| pn("return c") |
| pn("}") |
| } |
| |
| 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("var progressUpdater_ googleapi.ProgressUpdater") |
| pn("if v, ok := c.opt_[\"progressUpdater\"]; ok {") |
| pn(" if pu, ok := v.(googleapi.ProgressUpdater); ok {") |
| pn(" progressUpdater_ = pu") |
| pn(" }") |
| pn("}") |
| pn("if c.media_ != nil || c.resumable_ != 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", c.protocol_)`) |
| 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("var hasMedia_ bool") |
| pn(`if c.protocol_ != "resumable" {`) |
| pn(` var cancel func()`) |
| pn(" cancel, hasMedia_ = googleapi.ConditionallyIncludeMedia(c.media_, &body, &ctype)") |
| pn(" if cancel != nil { defer cancel() }") |
| pn("}") |
| } |
| 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 c.protocol_ == "resumable" {`) |
| pn(" req.ContentLength = 0") |
| pn(` if c.mediaType_ == "" {`) |
| pn(" c.mediaType_ = googleapi.DetectMediaType(c.resumable_)") |
| pn(" }") |
| pn(` req.Header.Set("X-Upload-Content-Type", c.mediaType_)`) |
| pn(" req.Body = nil") |
| pn(` if params.Get("name") == "" {`) |
| pn(` return %sfmt.Errorf("resumable uploads must set the Name parameter.")`, nilRet) |
| pn(" }") |
| pn("} else if hasMedia_ {") |
| pn(` req.Header.Set("Content-Type", ctype)`) |
| pn("}") |
| } else 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 meth.supportsMedia() { |
| pn(`if c.protocol_ == "resumable" {`) |
| pn(` loc := res.Header.Get("Location")`) |
| pn(" rx := &googleapi.ResumableUpload{") |
| pn(" Client: c.s.client,") |
| pn(" URI: loc,") |
| pn(" Media: c.resumable_,") |
| pn(" MediaType: c.mediaType_,") |
| pn(" ContentLength: c.resumable_.Size(),") |
| pn(" Callback: progressUpdater_,") |
| pn(" }") |
| pn(" res, err = rx.Upload(c.ctx_)") |
| pn(" if err != nil { return %serr }", nilRet) |
| pn("}") |
| } |
| 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 |
| } |