func (items Items) String() string { result := "" for i, j := range items { result += fmt.Sprintf("Item '%v' =\n", i) + utils.Indent(j.String(), " ") } return result }
func (p Properties) String() string { result := "" for _, i := range p.SortedPropertyNames { result += "Property '" + i + "' =\n" + utils.Indent(p.Properties[i].String(), " ") } return result }
func (entry *ExchangeEntry) generateAPICode(exchangeEntry string) string { content := "" if entry.Description != "" { content = utils.Indent(entry.Description, "// ") } if len(content) >= 1 && content[len(content)-1:] != "\n" { content += "\n" } content += "//\n" content += fmt.Sprintf("// See %v/#%v\n", entry.Parent.apiDef.DocRoot, entry.Name) content += "type " + exchangeEntry + " struct {\n" keyNames := make(map[string]bool, len(entry.RoutingKey)) for _, rk := range entry.RoutingKey { mwch := "*" if rk.MultipleWords { mwch = "#" } content += "\t" + utils.Normalise(rk.Name, keyNames) + " string `mwords:\"" + mwch + "\"`\n" } content += "}\n" content += "func (binding " + exchangeEntry + ") RoutingKey() string {\n" content += "\treturn generateRoutingKey(&binding)\n" content += "}\n" content += "\n" content += "func (binding " + exchangeEntry + ") ExchangeName() string {\n" content += "\treturn \"" + entry.Parent.ExchangePrefix + entry.Exchange + "\"\n" content += "}\n" content += "\n" content += "func (binding " + exchangeEntry + ") NewPayloadObject() interface{} {\n" content += "\treturn new(" + entry.Payload.TypeName + ")\n" content += "}\n" content += "\n" return content }
func describeList(name string, value interface{}) string { if reflect.ValueOf(value).IsValid() { if !reflect.ValueOf(value).IsNil() { return fmt.Sprintf("%v\n", name) + utils.Indent(fmt.Sprintf("%v", reflect.Indirect(reflect.ValueOf(value)).Interface()), " ") } } return "" }
// This is where we generate nested and compoound types in go to represent json payloads // which are used as inputs and outputs for the REST API endpoints, and also for Pulse // message bodies for the Exchange APIs. // Returns the generated code content, and a map of keys of extra packages to import, e.g. // a generated type might use time.Time, so if not imported, this would have to be added. // using a map of strings -> bool to simulate a set - true => include func generatePayloadTypes(apiDef *APIDefinition) (string, map[string]bool, map[string]bool) { extraPackages := make(map[string]bool) rawMessageTypes := make(map[string]bool) content := "type (" // intentionally no \n here since each type starts with one already // Loop through all json schemas that were found referenced inside the API json schemas... for _, i := range apiDef.schemaURLs { var newContent string newContent, extraPackages, rawMessageTypes = apiDef.schemas[i].TypeDefinition(true, extraPackages, rawMessageTypes) content += utils.Indent(newContent, "\t") } return content + ")\n\n", extraPackages, rawMessageTypes }
func (api *API) generateAPICode(apiName string) string { // package name and variable name are ideally not the same // so find a way to make them different... // also don't allow type variable name to be the same as // the type name // e.g. switch case of first character, and if first // character is not can't switch case for whatever // reason, prefix variable name with "my" exampleVarName := api.apiDef.ExampleVarName exampleCall := "" // here we choose an example API method to call, just the first one in the list of api.Entries // We need to first see if it returns one or two variables... if api.Entries[0].Output == "" { exampleCall = "// callSummary := " + exampleVarName + "." + api.Entries[0].MethodName + "(.....)" } else { exampleCall = "// data, callSummary := " + exampleVarName + "." + api.Entries[0].MethodName + "(.....)" } comment := "" if api.Description != "" { comment = utils.Indent(api.Description, "// ") } if len(comment) >= 1 && comment[len(comment)-1:] != "\n" { comment += "\n" } comment += "//\n" comment += fmt.Sprintf("// See: %v\n", api.apiDef.DocRoot) comment += "//\n" comment += "// How to use this package\n" comment += "//\n" comment += "// First create " + utils.IndefiniteArticle(api.apiDef.Name) + " " + api.apiDef.Name + " object:\n" comment += "//\n" comment += "// " + exampleVarName + " := " + api.apiDef.PackageName + ".New(\"myClientId\", \"myAccessToken\")\n" comment += "//\n" comment += "// and then call one or more of " + exampleVarName + "'s methods, e.g.:\n" comment += "//\n" comment += exampleCall + "\n" comment += "// handling any errors...\n" comment += "// if callSummary.Error != nil {\n" comment += "// // handle error...\n" comment += "// }\n" content := comment content += "package " + api.apiDef.PackageName + "\n" // note: we remove unused imports later, so e.g. if net/url is not used, it // will get removed later using: // https://godoc.org/golang.org/x/tools/imports content += ` import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "time" "github.com/taskcluster/httpbackoff" hawk "github.com/tent/hawk-go" D "github.com/tj/go-debug" %%{imports} ) var ( // Used for logging based on DEBUG environment variable // See github.com/tj/go-debug debug = D.Debug("` + api.apiDef.PackageName + `") ) // apiCall is the generic REST API calling method which performs all REST API // calls for this library. Each auto-generated REST API method simply is a // wrapper around this method, calling it with specific specific arguments. func (` + exampleVarName + ` *` + api.apiDef.Name + `) apiCall(payload interface{}, method, route string, result interface{}) (interface{}, *CallSummary) { callSummary := new(CallSummary) callSummary.HttpRequestObject = payload var jsonPayload []byte jsonPayload, callSummary.Error = json.Marshal(payload) if callSummary.Error != nil { return result, callSummary } callSummary.HttpRequestBody = string(jsonPayload) httpClient := &http.Client{} // function to perform http request - we call this using backoff library to // have exponential backoff in case of intermittent failures (e.g. network // blips or HTTP 5xx errors) httpCall := func() (*http.Response, error, error) { var ioReader io.Reader = nil if reflect.ValueOf(payload).IsValid() && !reflect.ValueOf(payload).IsNil() { ioReader = bytes.NewReader(jsonPayload) } httpRequest, err := http.NewRequest(method, ` + exampleVarName + `.BaseURL+route, ioReader) if err != nil { return nil, nil, fmt.Errorf("apiCall url cannot be parsed: '%v', is your BaseURL (%v) set correctly?\n%v\n", ` + exampleVarName + `.BaseURL+route, ` + exampleVarName + `.BaseURL, err) } httpRequest.Header.Set("Content-Type", "application/json") callSummary.HttpRequest = httpRequest // Refresh Authorization header with each call... // Only authenticate if client library user wishes to. if ` + exampleVarName + `.Authenticate { credentials := &hawk.Credentials{ ID: ` + exampleVarName + `.ClientId, Key: ` + exampleVarName + `.AccessToken, Hash: sha256.New, } reqAuth := hawk.NewRequestAuth(httpRequest, credentials, 0) if ` + exampleVarName + `.Certificate != "" { reqAuth.Ext = base64.StdEncoding.EncodeToString([]byte("{\"certificate\":" + ` + exampleVarName + `.Certificate + "}")) } httpRequest.Header.Set("Authorization", reqAuth.RequestHeader()) } debug("Making http request: %v", httpRequest) resp, err := httpClient.Do(httpRequest) return resp, err, nil } // Make HTTP API calls using an exponential backoff algorithm... callSummary.HttpResponse, callSummary.Attempts, callSummary.Error = httpbackoff.Retry(httpCall) if callSummary.Error != nil { return result, callSummary } // now read response into memory, so that we can return the body var body []byte body, callSummary.Error = ioutil.ReadAll(callSummary.HttpResponse.Body) if callSummary.Error != nil { return result, callSummary } callSummary.HttpResponseBody = string(body) // if result is passed in as nil, it means the API defines no response body // json if reflect.ValueOf(result).IsValid() && !reflect.ValueOf(result).IsNil() { callSummary.Error = json.Unmarshal([]byte(callSummary.HttpResponseBody), &result) if callSummary.Error != nil { // technically not needed since returned outside if, but more comprehensible return result, callSummary } } // Return result and callSummary return result, callSummary } // The entry point into all the functionality in this package is to create ` + utils.IndefiniteArticle(api.apiDef.Name) + ` // ` + api.apiDef.Name + ` object. It contains your authentication credentials, which are // required for all HTTP operations. type ` + api.apiDef.Name + ` struct { // Client ID required by Hawk ClientId string // Access Token required by Hawk AccessToken string // The URL of the API endpoint to hit. // Use ` + "\"" + api.BaseURL + "\"" + ` for production. // Please note calling ` + api.apiDef.PackageName + `.New(clientId string, accessToken string) is an // alternative way to create ` + utils.IndefiniteArticle(api.apiDef.Name) + " " + api.apiDef.Name + ` object with BaseURL set to production. BaseURL string // Whether authentication is enabled (e.g. set to 'false' when using taskcluster-proxy) // Please note calling ` + api.apiDef.PackageName + `.New(clientId string, accessToken string) is an // alternative way to create ` + utils.IndefiniteArticle(api.apiDef.Name) + " " + api.apiDef.Name + ` object with Authenticate set to true. Authenticate bool // Certificate for temporary credentials Certificate string } // CallSummary provides information about the underlying http request and // response issued for a given API call, together with details of any Error // which occured. After making an API call, be sure to check the returned // CallSummary.Error - if it is nil, no error occurred. type CallSummary struct { HttpRequest *http.Request // Keep a copy of request body in addition to the *http.Request, since // accessing the Body via the *http.Request object, you get a io.ReadCloser // - and after the request has been made, the body will have been read, and // the data lost... This way, it is still available after the api call // returns. HttpRequestBody string // The Go Type which is marshaled into json and used as the http request // body. HttpRequestObject interface{} HttpResponse *http.Response // Keep a copy of response body in addition to the *http.Response, since // accessing the Body via the *http.Response object, you get a // io.ReadCloser - and after the response has been read once (to unmarshal // json into native go types) the data is lost... This way, it is still // available after the api call returns. HttpResponseBody string Error error // Keep a record of how many http requests were attempted Attempts int } // Returns a pointer to ` + api.apiDef.Name + `, configured to run against production. If you // wish to point at a different API endpoint url, set BaseURL to the preferred // url. Authentication can be disabled (for example if you wish to use the // taskcluster-proxy) by setting Authenticate to false. // ` content += "// For example:\n" content += "// " + exampleVarName + " := " + api.apiDef.PackageName + ".New(\"123\", \"456\") " + strings.Repeat(" ", 20+len(apiName)-len(api.apiDef.PackageName)) + " // set clientId and accessToken\n" content += "// " + exampleVarName + ".Authenticate = false " + strings.Repeat(" ", len(apiName)) + " // disable authentication (true by default)\n" content += "// " + exampleVarName + ".BaseURL = \"http://localhost:1234/api/" + apiName + "/v1\" // alternative API endpoint (production by default)\n" content += exampleCall + strings.Repeat(" ", 48-len(exampleCall)+len(apiName)+len(exampleVarName)) + " // for example, call the " + api.Entries[0].MethodName + "(.....) API endpoint (described further down)...\n" content += "// if callSummary.Error != nil {\n" content += "// // handle errors...\n" content += "// }\n" content += "func New(clientId string, accessToken string) *" + api.apiDef.Name + " {\n" content += "\treturn &" + api.apiDef.Name + "{\n" content += "\t\tClientId: clientId,\n" content += "\t\tAccessToken: accessToken,\n" content += "\t\tBaseURL: \"" + api.BaseURL + "\",\n" content += "\t\tAuthenticate: true,\n" content += "\t}\n" content += "}\n" content += "\n" for _, entry := range api.Entries { content += entry.generateAPICode(apiName) } return content }
func (entry *APIEntry) generateAPICode(apiName string) string { comment := "" if entry.Description != "" { comment = utils.Indent(entry.Description, "// ") } if len(comment) >= 1 && comment[len(comment)-1:] != "\n" { comment += "\n" } if len(entry.Scopes) > 0 { comment += "//\n" comment += "// Required scopes:\n" switch len(entry.Scopes) { case 0: case 1: comment += "// * " + strings.Join(entry.Scopes[0], ", and\n// * ") + "\n" default: lines := make([]string, len(entry.Scopes)) for i, j := range entry.Scopes { switch len(j) { case 0: case 1: lines[i] = "// * " + j[0] default: lines[i] = "// * (" + strings.Join(j, " and ") + ")" } } comment += strings.Join(lines, ", or\n") + "\n" } } comment += "//\n" comment += fmt.Sprintf("// See %v/#%v\n", entry.Parent.apiDef.DocRoot, entry.Name) inputParams := "" if len(entry.Args) > 0 { inputParams += strings.Join(entry.Args, " string, ") + " string" } apiArgsPayload := "nil" if entry.Input != "" { apiArgsPayload = "payload" p := "payload *" + entry.Parent.apiDef.schemas[entry.Input].TypeName if inputParams == "" { inputParams = p } else { inputParams += ", " + p } } responseType := "*CallSummary" if entry.Output != "" { responseType = "(*" + entry.Parent.apiDef.schemas[entry.Output].TypeName + ", *CallSummary)" } content := comment content += "func (" + entry.Parent.apiDef.ExampleVarName + " *" + entry.Parent.apiDef.Name + ") " + entry.MethodName + "(" + inputParams + ") " + responseType + " {\n" if entry.Output != "" { content += "\tresponseObject, callSummary := " + entry.Parent.apiDef.ExampleVarName + ".apiCall(" + apiArgsPayload + ", \"" + strings.ToUpper(entry.Method) + "\", \"" + strings.Replace(strings.Replace(entry.Route, "<", "\" + url.QueryEscape(", -1), ">", ") + \"", -1) + "\", new(" + entry.Parent.apiDef.schemas[entry.Output].TypeName + "))\n" content += "\treturn responseObject.(*" + entry.Parent.apiDef.schemas[entry.Output].TypeName + "), callSummary\n" } else { content += "\t_, callSummary := " + entry.Parent.apiDef.ExampleVarName + ".apiCall(" + apiArgsPayload + ", \"" + strings.ToUpper(entry.Method) + "\", \"" + strings.Replace(strings.Replace(entry.Route, "<", "\" + url.QueryEscape(", -1), ">", ") + \"", -1) + "\", nil)\n" content += "\treturn callSummary\n" } content += "}\n" content += "\n" // can remove any code that added an empty string to another string return strings.Replace(content, ` + ""`, "", -1) }
func (jsonSubSchema *JsonSubSchema) TypeDefinition(withComments bool, extraPackages map[string]bool, rawMessageTypes map[string]bool) (string, map[string]bool, map[string]bool) { content := "" comment := "" if withComments { content += "\n" if d := jsonSubSchema.Description; d != nil { if desc := *d; desc != "" { comment = utils.Indent(desc, "// ") } if len(comment) >= 1 && comment[len(comment)-1:] != "\n" { comment += "\n" } } if url := jsonSubSchema.SourceURL; url != "" { comment += "//\n// See " + url + "\n" } content += comment content += jsonSubSchema.TypeName + " " } typ := "json.RawMessage" if p := jsonSubSchema.Type; p != nil { typ = *p } if p := jsonSubSchema.RefSubSchema; p != nil { typ = p.TypeName } switch typ { case "array": if jsonType := jsonSubSchema.Items.Type; jsonType != nil { var newType string newType, extraPackages, rawMessageTypes = jsonSubSchema.Items.TypeDefinition(false, extraPackages, rawMessageTypes) typ = "[]" + newType } else { if refSubSchema := jsonSubSchema.Items.RefSubSchema; refSubSchema != nil { typ = "[]" + refSubSchema.TypeName } } case "object": if s := jsonSubSchema.Properties; s != nil { typ = fmt.Sprintf("struct {\n") members := make(map[string]bool, len(s.SortedPropertyNames)) for _, j := range s.SortedPropertyNames { memberName := utils.Normalise(j, members) // recursive call to build structs inside structs var subType string subType, extraPackages, rawMessageTypes = s.Properties[j].TypeDefinition(false, extraPackages, rawMessageTypes) // comment the struct member with the description from the json comment = "" if d := s.Properties[j].Description; d != nil { comment = utils.Indent(*d, "\t// ") } if len(comment) >= 1 && comment[len(comment)-1:] != "\n" { comment += "\n" } if enum := s.Properties[j].Enum; enum != nil { comment += "//\n// Possible values:\n" for _, i := range enum { switch i.(type) { case float64: comment += fmt.Sprintf("// * %v\n", i) default: comment += fmt.Sprintf("// * %q\n", i) } } } if regex := s.Properties[j].Pattern; regex != nil { comment += "//\n// Syntax: " + *regex + "\n" } typ += comment // struct member name and type, as part of struct definition typ += fmt.Sprintf("\t%v %v `json:\"%v\"`\n", memberName, subType, j) } typ += "}" } else { typ = "json.RawMessage" } case "number": typ = "int" case "integer": typ = "int" case "boolean": typ = "bool" // json type string maps to go type string, so only need to test case of when // string is a json date-time, so we can convert to go type Time... case "string": if f := jsonSubSchema.Format; f != nil { if *f == "date-time" { typ = "Time" } } } switch typ { case "json.RawMessage": extraPackages["encoding/json"] = true if withComments { // Special case: we have here a top level RawMessage such as // queue.PostArtifactRequest - therefore need to implement // Marhsal and Unmarshal methods. See: // http://play.golang.org/p/FKHSUmWVFD vs // http://play.golang.org/p/erjM6ptIYI extraPackages["errors"] = true rawMessageTypes[jsonSubSchema.TypeName] = true } } content += typ if withComments { content += "\n" } return content, extraPackages, rawMessageTypes }
func (exchange *Exchange) generateAPICode(exchangeName string) string { comment := "" if exchange.Description != "" { comment = utils.Indent(exchange.Description, "// ") } if len(comment) >= 1 && comment[len(comment)-1:] != "\n" { comment += "\n" } comment += "//\n" comment += fmt.Sprintf("// See: %v\n", exchange.apiDef.DocRoot) comment += "//\n" comment += "// How to use this package\n" comment += "//\n" comment += "// This package is designed to sit on top of http://godoc.org/github.com/taskcluster/pulse-go/pulse. Please read\n" comment += "// the pulse package overview to get an understanding of how the pulse client is implemented in go.\n" comment += "//\n" comment += "// This package provides two things in addition to the basic pulse package: structured types for unmarshaling\n" comment += "// pulse message bodies into, and custom Binding interfaces, for defining the fixed strings for task cluster\n" comment += "// exchange names, and routing keys as structured types.\n" comment += "//\n" comment += "// For example, when specifying a binding, rather than using:\n" comment += "// \n" comment += "// pulse.Bind(\n" comment += "// \t\"*.*.*.*.*.*.gaia.#\",\n" comment += "// \t\"exchange/taskcluster-queue/v1/task-defined\")\n" comment += "// \n" comment += "// You can rather use:\n" comment += "// \n" comment += "// queueevents.TaskDefined{WorkerType: \"gaia\"}\n" comment += "// \n" comment += "// In addition, this means that you will also get objects in your callback method like *queueevents.TaskDefinedMessage\n" comment += "// rather than just interface{}.\n" content := comment content += "package " + exchange.apiDef.PackageName + "\n" content += ` import ( "reflect" "strings" "time" %%{imports} ) ` entryTypeNames := make(map[string]bool, len(exchange.Entries)) for _, entry := range exchange.Entries { content += entry.generateAPICode(utils.Normalise(entry.Name, entryTypeNames)) } content += ` func generateRoutingKey(x interface{}) string { val := reflect.ValueOf(x).Elem() p := make([]string, 0, val.NumField()) for i := 0; i < val.NumField(); i++ { valueField := val.Field(i) typeField := val.Type().Field(i) tag := typeField.Tag if t := tag.Get("mwords"); t != "" { if v := valueField.Interface(); v == "" { p = append(p, t) } else { p = append(p, v.(string)) } } } return strings.Join(p, ".") } ` return content }