generate/generate.go (1,948 lines of code) (raw):

// // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // package main import ( "bytes" "encoding/json" "flag" "fmt" "go/format" "io/ioutil" "log" "os" "os/exec" "path" "sort" "strings" "unicode" ) const pkg = "cloudstack" // detailsRequireKeyValue is a prefilled map with a list of details // that need to be encoded using an explicit key and a value entry. var detailsRequireKeyValue = map[string]bool{ "addGuestOs": true, "addImageStore": true, "addResourceDetail": true, "createSecondaryStagingStore": true, "updateCloudToUseObjectStore": true, "updateGuestOs": true, "updateZone": true, } // detailsRequireZeroIndex is a prefilled map with a list of details // that need to be encoded using zero indexing var detailsRequireZeroIndex = map[string]bool{ "registerTemplate": true, "updateTemplate": true, "createAccount": true, "updateAccount": true, } var mapRequireList = map[string]map[string]bool{ "deployVirtualMachine": map[string]bool{ "dhcpoptionsnetworklist": true, "iptonetworklist": true, "nicnetworklist": true, }, "updateVirtualMachine": map[string]bool{ "dhcpoptionsnetworklist": true, }, "migrateVirtualMachineWithVolume": map[string]bool{ "migrateto": true, }, } // nestedResponse is a prefilled map with the list of endpoints // that responses fields are nested in a parent object. The map value // gives the object field name. var nestedResponse = map[string]string{ "getUploadParamsForTemplate": "getuploadparams", "getUploadParamsForVolume": "getuploadparams", "createRole": "role", "createRolePermission": "rolepermission", "getCloudIdentifier": "cloudidentifier", "getKubernetesClusterConfig": "clusterconfig", "getPathForVolume": "apipathforvolume", } // longToStringConvertedParams is a prefilled map with the list of // response fields that migrated from long to string within // the current major baseline. This fields will be parsed from // json as string and then fallback on long. var longToStringConvertedParams = map[string]bool{ "managementserverid": true, } // customResponseStructTypes maps the API call to a custom struct name // This is to change the struct type name to something other than the API name var customResponseStructTypes = map[string]string{ "findHostsForMigration": "HostForMigration", } // We prefill this one value to make sure it is not // created twice, as this is also a top level type. var typeNames = map[string]bool{"Nic": true} type apiInfo map[string][]string type allServices struct { services services } type apiInfoNotFoundError struct { api string } func (e *apiInfoNotFoundError) Error() string { return fmt.Sprintf("Could not find API details for: %s", e.api) } type generateError struct { service *service error error } func (e *generateError) Error() string { return fmt.Sprintf("API %s failed to generate code: %v", e.service.name, e.error) } type goimportError struct { output string error error } func (e *goimportError) Error() string { return fmt.Sprintf("GoImport failed to format:\n%v\n%v", e.output, e.error) } type service struct { name string apis []*API p func(format string, args ...interface{}) // print raw pn func(format string, args ...interface{}) // print with indent and newline } type services []*service // Add functions for the Sort interface func (s services) Len() int { return len(s) } func (s services) Less(i, j int) bool { return s[i].name < s[j].name } func (s services) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // APIParams represents a list of API params type APIParams []*APIParam // Len implements the Sort interface func (s APIParams) Len() int { return len(s) } // Less implements the Sort interface func (s APIParams) Less(i, j int) bool { return s[i].Name < s[j].Name } // Swap implements the Sort interface func (s APIParams) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // API represents an API endpoint we can call type API struct { Name string `json:"name"` Description string `json:"description"` Isasync bool `json:"isasync"` Params APIParams `json:"params"` Response APIResponses `json:"response"` } // APIParam represents a single API parameter type APIParam struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` Required bool `json:"required"` } // APIResponse represents a API response type APIResponse struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` Response APIResponses `json:"response"` } // APIResponses represents a list of API responses type APIResponses []*APIResponse // Len implements the Sort interface func (s APIResponses) Len() int { return len(s) } // Less implements the Sort interface func (s APIResponses) Less(i, j int) bool { return s[i].Name < s[j].Name } // Swap implements the Sort interface func (s APIResponses) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func main() { listApis := flag.String("api", "listApis.json", "path to the saved JSON output of listApis") flag.Parse() as, errors, err := getAllServices(*listApis) if err != nil { log.Fatal(err) } if err = as.WriteGeneralCode(); err != nil { log.Fatal(err) } for _, s := range as.services { if err = s.WriteGeneratedCode(); err != nil { errors = append(errors, &generateError{s, err}) } } outdir, err := sourceDir() if err != nil { log.Fatal(err) } out, err := exec.Command("goimports", "-w", outdir).CombinedOutput() if err != nil { errors = append(errors, &goimportError{string(out), err}) } testdir, err := testDir() if err != nil { log.Fatal(err) } out, err = exec.Command("goimports", "-w", testdir).CombinedOutput() if err != nil { errors = append(errors, &goimportError{string(out), err}) } if len(errors) > 0 { log.Printf("%d API(s) failed to generate:", len(errors)) for _, ce := range errors { log.Print(ce.Error()) } os.Exit(1) } } func (as *allServices) WriteGeneralCode() error { outdir, err := sourceDir() if err != nil { log.Fatalf("Failed to get source dir: %s", err) } code, err := as.GeneralCode() if err != nil { return err } file := path.Join(outdir, "cloudstack.go") return ioutil.WriteFile(file, code, 0644) } func (as *allServices) GeneralCode() ([]byte, error) { // Buffer the output in memory, for gofmt'ing later in the defer. var buf bytes.Buffer p := func(format string, args ...interface{}) { _, err := fmt.Fprintf(&buf, format, args...) if err != nil { panic(err) } } pn := func(format string, args ...interface{}) { p(format+"\n", args...) } pn("//") pn("// Licensed to the Apache Software Foundation (ASF) under one") pn("// or more contributor license agreements. See the NOTICE file") pn("// distributed with this work for additional information") pn("// regarding copyright ownership. The ASF licenses this file") pn("// to you under the Apache License, Version 2.0 (the") pn("// \"License\"); you may not use this file except in compliance") pn("// with the License. You may obtain a copy of the License at") pn("//") pn("// http://www.apache.org/licenses/LICENSE-2.0") pn("//") pn("// Unless required by applicable law or agreed to in writing,") pn("// software distributed under the License is distributed on an") pn("// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY") pn("// KIND, either express or implied. See the License for the") pn("// specific language governing permissions and limitations") pn("// under the License.") pn("//") pn("") pn("package %s", pkg) pn("") pn("// UnlimitedResourceID is a special ID to define an unlimited resource") pn("const UnlimitedResourceID = \"-1\"") pn("") pn("var idRegex = regexp.MustCompile(`^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|-1)$`)") pn("") pn("// IsID return true if the passed ID is either a UUID or a UnlimitedResourceID") pn("func IsID(id string) bool {") pn(" return idRegex.MatchString(id)") pn("}") pn("") pn("// ClientOption can be passed to new client functions to set custom options") pn("type ClientOption func(*CloudStackClient)") pn("") pn("// OptionFunc can be passed to the courtesy helper functions to set additional parameters") pn("type OptionFunc func(*CloudStackClient, interface{}) error") pn("") pn("type CSError struct {") pn(" ErrorCode int `json:\"errorcode\"`") pn(" CSErrorCode int `json:\"cserrorcode\"`") pn(" ErrorText string `json:\"errortext\"`") pn("}") pn("") pn("func (e *CSError) Error() error {") pn(" return fmt.Errorf(\"CloudStack API error %%d (CSExceptionErrorCode: %%d): %%s\", e.ErrorCode, e.CSErrorCode, e.ErrorText)") pn("}") pn("") pn("type UUID string") pn("") pn("func (c UUID) MarshalJSON() ([]byte, error) {") pn(" return json.Marshal(string(c))") pn("}") pn("") pn("func (c *UUID) UnmarshalJSON(data []byte) error {") pn(" value := strings.Trim(string(data), \"\\\"\")") pn(" if strings.HasPrefix(string(data), \"\\\"\") {") pn(" *c = UUID(value)") pn(" return nil") pn(" }") pn(" _, err := strconv.ParseInt(value, 10, 64)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" *c = UUID(value)") pn(" return nil") pn("}") pn("type CloudStackClient struct {") pn(" HTTPGETOnly bool // If `true` only use HTTP GET calls") pn("") pn(" client *http.Client // The http client for communicating") pn(" baseURL string // The base URL of the API") pn(" apiKey string // Api key") pn(" secret string // Secret key") pn(" async bool // Wait for async calls to finish") pn(" options []OptionFunc // A list of option functions to apply to all API calls") pn(" timeout int64 // Max waiting timeout in seconds for async jobs to finish; defaults to 300 seconds") pn("") for _, s := range as.services { pn(" %s %sIface", strings.TrimSuffix(s.name, "Service"), s.name) } pn("}") pn("") pn("// Creates a new client for communicating with CloudStack") pn("func newClient(apiurl string, apikey string, secret string, async bool, verifyssl bool, options ...ClientOption) *CloudStackClient {") pn(" jar, _ := cookiejar.New(nil)") pn(" cs := &CloudStackClient{") pn(" client: &http.Client{") pn(" Jar: jar,") pn(" Transport: &http.Transport{") pn(" Proxy: http.ProxyFromEnvironment,") pn(" DialContext: (&net.Dialer{") pn(" Timeout: 30 * time.Second,") pn(" KeepAlive: 30 * time.Second,") pn(" DualStack: true,") pn(" }).DialContext,") pn(" MaxIdleConns: 100,") pn(" IdleConnTimeout: 90 * time.Second,") pn(" TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifyssl},") pn(" TLSHandshakeTimeout: 10 * time.Second,") pn(" ExpectContinueTimeout: 1 * time.Second,") pn(" },") pn(" Timeout: time.Duration(60 * time.Second),") pn(" },") pn(" baseURL: apiurl,") pn(" apiKey: apikey,") pn(" secret: secret,") pn(" async: async,") pn(" options: []OptionFunc{},") pn(" timeout: 300,") pn(" }") pn("") pn(" for _, fn := range options {") pn(" fn(cs)") pn(" }") pn("") for _, s := range as.services { pn(" cs.%s = New%s(cs)", strings.TrimSuffix(s.name, "Service"), s.name) } pn("") pn(" return cs") pn("}") pn("") pn("// Creates a new mock client for communicating with CloudStack") pn("func newMockClient(ctrl *gomock.Controller) *CloudStackClient {") pn(" cs := &CloudStackClient{}") pn("") for _, s := range as.services { pn(" cs.%s = NewMock%sIface(ctrl)", strings.TrimSuffix(s.name, "Service"), s.name) } pn("") pn(" return cs") pn("}") pn("") pn("// Default non-async client. So for async calls you need to implement and check the async job result yourself. When using") pn("// HTTPS with a self-signed certificate to connect to your CloudStack API, you would probably want to set 'verifyssl' to") pn("// false so the call ignores the SSL errors/warnings.") pn("func NewClient(apiurl string, apikey string, secret string, verifyssl bool, options ...ClientOption) *CloudStackClient {") pn(" cs := newClient(apiurl, apikey, secret, false, verifyssl, options...)") pn(" return cs") pn("}") pn("") pn("// For sync API calls this client behaves exactly the same as a standard client call, but for async API calls") pn("// this client will wait until the async job is finished or until the configured AsyncTimeout is reached. When the async") pn("// job finishes successfully it will return actual object received from the API and nil, but when the timout is") pn("// reached it will return the initial object containing the async job ID for the running job and a warning.") pn("func NewAsyncClient(apiurl string, apikey string, secret string, verifyssl bool, options ...ClientOption) *CloudStackClient {") pn(" cs := newClient(apiurl, apikey, secret, true, verifyssl, options...)") pn(" return cs") pn("}") pn("") pn("// Creates a new mock client for communicating with CloudStack") pn("func NewMockClient(ctrl *gomock.Controller) *CloudStackClient {") pn(" cs := newMockClient(ctrl)") pn(" return cs") pn("}") pn("") pn("// When using the async client an api call will wait for the async call to finish before returning. The default is to poll for 300 seconds") pn("// seconds, to check if the async job is finished.") pn("func (cs *CloudStackClient) AsyncTimeout(timeoutInSeconds int64) {") pn(" cs.timeout = timeoutInSeconds") pn("}") pn("") pn("// Sets timeout when using sync api calls. Default is 60 seconds") pn("func (cs *CloudStackClient) Timeout(timeout time.Duration) {") pn(" cs.client.Timeout = timeout") pn("}") pn("") pn("// Set any default options that would be added to all API calls that support it.") pn("func (cs *CloudStackClient) DefaultOptions(options ...OptionFunc) {") pn(" if options != nil {") pn(" cs.options = options") pn(" } else {") pn(" cs.options = []OptionFunc{}") pn(" }") pn("}") pn("") pn("var AsyncTimeoutErr = errors.New(\"Timeout while waiting for async job to finish\")") pn("") pn("// A helper function that you can use to get the result of a running async job. If the job is not finished within the configured") pn("// timeout, the async job returns a AsyncTimeoutErr.") pn("func (cs *CloudStackClient) GetAsyncJobResult(jobid string, timeout int64) (json.RawMessage, error) {") pn(" var timer time.Duration") pn(" currentTime := time.Now().Unix()") pn("") pn(" for {") pn(" p := cs.Asyncjob.NewQueryAsyncJobResultParams(jobid)") pn(" r, err := cs.Asyncjob.QueryAsyncJobResult(p)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" // Status 1 means the job is finished successfully") pn(" if r.Jobstatus == 1 {") pn(" return r.Jobresult, nil") pn(" }") pn("") pn(" // When the status is 2, the job has failed") pn(" if r.Jobstatus == 2 {") pn(" if r.Jobresulttype == \"text\" {") pn(" return nil, fmt.Errorf(string(r.Jobresult))") pn(" } else {") pn(" return nil, fmt.Errorf(\"Undefined error: %%s\", string(r.Jobresult))") pn(" }") pn(" }") pn("") pn(" if time.Now().Unix()-currentTime > timeout {") pn(" return nil, AsyncTimeoutErr") pn(" }") pn("") pn(" // Add an (extremely simple) exponential backoff like feature to prevent") pn(" // flooding the CloudStack API") pn(" if timer < 15 {") pn(" timer++") pn(" }") pn("") pn(" time.Sleep(timer * time.Second)") pn(" }") pn("}") pn("") pn("// Execute the request against a CS API. Will return the raw JSON data returned by the API and nil if") pn("// no error occurred. If the API returns an error the result will be nil and the HTTP error code and CS") pn("// error details. If a processing (code) error occurs the result will be nil and the generated error") pn("func (cs *CloudStackClient) newRequest(api string, params url.Values) (json.RawMessage, error) {") pn(" return cs.newRawRequest(api, false, params)") pn("}") pn("") pn("// Execute the request against a CS API using POST. Will return the raw JSON data returned by the API and") pn("// nil if no error occurred. If the API returns an error the result will be nil and the HTTP error code") pn("// and CS error details. If a processing (code) error occurs the result will be nil and the generated error") pn("func (cs *CloudStackClient) newPostRequest(api string, params url.Values) (json.RawMessage, error) {") pn(" return cs.newRawRequest(api, true, params)") pn("}") pn("") pn("// Execute a raw request against a CS API. Will return the raw JSON data returned by the API and nil if") pn("// no error occurred. If the API returns an error the result will be nil and the HTTP error code and CS") pn("// error details. If a processing (code) error occurs the result will be nil and the generated error") pn("func (cs *CloudStackClient) newRawRequest(api string, post bool, params url.Values) (json.RawMessage, error) {") pn(" params.Set(\"apiKey\", cs.apiKey)") pn(" params.Set(\"command\", api)") pn(" params.Set(\"response\", \"json\")") pn("") pn(" // Generate signature for API call") pn(" // * Serialize parameters, URL encoding only values and sort them by key, done by EncodeValues") pn(" // * Convert the entire argument string to lowercase") pn(" // * Replace all instances of '+' to '%%20'") pn(" // * Calculate HMAC SHA1 of argument string with CloudStack secret") pn(" // * URL encode the string and convert to base64") pn(" s := EncodeValues(params)") pn(" s2 := strings.ToLower(s)") pn(" mac := hmac.New(sha1.New, []byte(cs.secret))") pn(" mac.Write([]byte(s2))") pn(" signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))") pn("") pn(" var err error") pn(" var resp *http.Response") pn(" if !cs.HTTPGETOnly && post {") pn(" // The deployVirtualMachine API should be called using a POST call") pn(" // so we don't have to worry about the userdata size") pn("") pn(" // Add the unescaped signature to the POST params") pn(" params.Set(\"signature\", signature)") pn("") pn(" // Make a POST call") pn(" resp, err = cs.client.PostForm(cs.baseURL, params)") pn(" } else {") pn(" // Create the final URL before we issue the request") pn(" url := cs.baseURL + \"?\" + s + \"&signature=\" + url.QueryEscape(signature)") pn("") pn(" // Make a GET call") pn(" resp, err = cs.client.Get(url)") pn(" }") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn(" defer resp.Body.Close()") pn("") pn(" b, err := ioutil.ReadAll(resp.Body)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" // Need to get the raw value to make the result play nice") pn(" b, err = getRawValue(b)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" if resp.StatusCode != 200 {") pn(" var e CSError") pn(" if err := json.Unmarshal(b, &e); err != nil {") pn(" return nil, err") pn(" }") pn(" return nil, e.Error()") pn(" }") pn(" return b, nil") pn("}") pn("") pn("// Custom version of net/url Encode that only URL escapes values") pn("// Unmodified portions here remain under BSD license of The Go Authors: https://go.googlesource.com/go/+/master/LICENSE") pn("func EncodeValues(v url.Values) string {") pn(" if v == nil {") pn(" return \"\"") pn(" }") pn(" var buf bytes.Buffer") pn(" keys := make([]string, 0, len(v))") pn(" for k := range v {") pn(" keys = append(keys, k)") pn(" }") pn(" sort.Strings(keys)") pn(" for _, k := range keys {") pn(" vs := v[k]") pn(" prefix := k + \"=\"") pn(" for _, v := range vs {") pn(" if buf.Len() > 0 {") pn(" buf.WriteByte('&')") pn(" }") pn(" buf.WriteString(prefix)") pn(" escaped := url.QueryEscape(v)") pn(" // we need to ensure + (representing a space) is encoded as %%20") pn(" escaped = strings.Replace(escaped, \"+\", \"%%20\", -1)") pn(" // we need to ensure * is not escaped") pn(" escaped = strings.Replace(escaped, \"%%2A\", \"*\", -1)") pn(" buf.WriteString(escaped)") pn(" }") pn(" }") pn(" return buf.String()") pn("}") pn("") pn("// Generic function to get the first non-count raw value from a response as json.RawMessage") pn("func getRawValue(b json.RawMessage) (json.RawMessage, error) {") pn(" var m map[string]json.RawMessage") pn(" if err := json.Unmarshal(b, &m); err != nil {") pn(" return nil, err") pn(" }") pn(" getArrayResponse := false") pn(" for k := range m {") pn(" if k == \"count\" {") pn(" getArrayResponse = true") pn(" }") pn(" }") pn(" if getArrayResponse {") pn(" var resp []json.RawMessage") pn(" for k, v := range m {") pn(" if k != \"count\" {") pn(" if err := json.Unmarshal(v, &resp); err != nil {") pn(" return nil, err") pn(" }") pn(" return resp[0], nil") pn(" }") pn(" }") pn("") pn(" } else {") pn(" for _, v := range m {") pn(" return v, nil") pn(" }") pn(" }") pn(" return nil, fmt.Errorf(\"Unable to extract the raw value from:\\n\\n%%s\\n\\n\", string(b))") pn("}") pn("") pn("// getSortedKeysFromMap returns the keys from m in increasing order.") pn("func getSortedKeysFromMap(m map[string]string) (keys []string) {") pn(" for k := range m {") pn(" keys = append(keys, k)") pn(" }") pn(" sort.Strings(keys)") pn(" return keys") pn("}") pn("") pn("// WithAsyncTimeout takes a custom timeout to be used by the CloudStackClient") pn("func WithAsyncTimeout(timeout int64) ClientOption {") pn(" return func(cs *CloudStackClient) {") pn(" if timeout != 0 {") pn(" cs.timeout = timeout") pn(" }") pn(" }") pn("}") pn("") pn("// DomainIDSetter is an interface that every type that can set a domain ID must implement") pn("type DomainIDSetter interface {") pn(" SetDomainid(string)") pn("}") pn("") pn("// WithDomain takes either a domain name or ID and sets the `domainid` parameter") pn("func WithDomain(domain string) OptionFunc {") pn(" return func(cs *CloudStackClient, p interface{}) error {") pn(" ps, ok := p.(DomainIDSetter)") pn("") pn(" if !ok || domain == \"\" {") pn(" return nil") pn(" }") pn("") pn(" if !IsID(domain) {") pn(" id, _, err := cs.Domain.GetDomainID(domain)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" domain = id") pn(" }") pn("") pn(" ps.SetDomainid(domain)") pn("") pn(" return nil") pn(" }") pn("}") pn("") pn("// WithHTTPClient takes a custom HTTP client to be used by the CloudStackClient") pn("func WithHTTPClient(client *http.Client) ClientOption {") pn(" return func(cs *CloudStackClient) {") pn(" if client != nil {") pn(" if client.Jar == nil {") pn(" client.Jar = cs.client.Jar") pn(" }") pn(" cs.client = client") pn(" }") pn(" }") pn("}") pn("") pn("// ListallSetter is an interface that every type that can set listall must implement") pn("type ListallSetter interface {") pn(" SetListall(bool)") pn("}") pn("") pn("// WithListall takes either a project name or ID and sets the `listall` parameter") pn("func WithListall(listall bool) OptionFunc {") pn(" return func(cs *CloudStackClient, p interface{}) error {") pn(" ps, ok := p.(ListallSetter)") pn("") pn(" if !ok {") pn(" return nil") pn(" }") pn("") pn(" ps.SetListall(listall)") pn("") pn(" return nil") pn(" }") pn("}") pn("// ProjectIDSetter is an interface that every type that can set a project ID must implement") pn("type ProjectIDSetter interface {") pn(" SetProjectid(string)") pn("}") pn("") pn("// WithProject takes either a project name or ID and sets the `projectid` parameter") pn("func WithProject(project string) OptionFunc {") pn(" return func(cs *CloudStackClient, p interface{}) error {") pn(" ps, ok := p.(ProjectIDSetter)") pn("") pn(" if !ok || project == \"\" {") pn(" return nil") pn(" }") pn("") pn(" if !IsID(project) {") pn(" id, _, err := cs.Project.GetProjectID(project)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" project = id") pn(" }") pn("") pn(" ps.SetProjectid(project)") pn("") pn(" return nil") pn(" }") pn("}") pn("") pn("// VPCIDSetter is an interface that every type that can set a vpc ID must implement") pn("type VPCIDSetter interface {") pn(" SetVpcid(string)") pn("}") pn("") pn("// WithVPCID takes a vpc ID and sets the `vpcid` parameter") pn("func WithVPCID(id string) OptionFunc {") pn(" return func(cs *CloudStackClient, p interface{}) error {") pn(" vs, ok := p.(VPCIDSetter)") pn("") pn(" if !ok || id == \"\" {") pn(" return nil") pn(" }") pn("") pn(" vs.SetVpcid(id)") pn("") pn(" return nil") pn(" }") pn("}") pn("") pn("// ZoneIDSetter is an interface that every type that can set a zone ID must implement") pn("type ZoneIDSetter interface {") pn(" SetZoneid(string)") pn("}") pn("") pn("// WithZone takes either a zone name or ID and sets the `zoneid` parameter") pn("func WithZone(zone string) OptionFunc {") pn(" return func(cs *CloudStackClient, p interface{}) error {") pn(" zs, ok := p.(ZoneIDSetter)") pn("") pn(" if !ok || zone == \"\" {") pn(" return nil") pn(" }") pn("") pn(" if !IsID(zone) {") pn(" id, _, err := cs.Zone.GetZoneID(zone)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" zone = id") pn(" }") pn("") pn(" zs.SetZoneid(zone)") pn("") pn(" return nil") pn(" }") pn("}") pn("") for _, s := range as.services { pn("type %s struct {", s.name) pn(" cs *CloudStackClient") pn("}") pn("") pn("func New%s(cs *CloudStackClient) %sIface {", s.name, s.name) pn(" return &%s{cs: cs}", s.name) pn("}") pn("") } clean, err := format.Source(buf.Bytes()) if err != nil { return buf.Bytes(), err } return clean, err } func (s *service) WriteGeneratedCode() error { outdir, err := sourceDir() if err != nil { log.Fatalf("Failed to get source dir: %s", err) } code, err := s.GenerateCode() if err != nil { return err } if s.name != "CustomService" { tests, err := s.GenerateTestCode() if err != nil { return err } testdir, err := testDir() file := path.Join(testdir, s.name+"_test.go") ioutil.WriteFile(file, tests, 0644) } file := path.Join(outdir, s.name+".go") return ioutil.WriteFile(file, code, 0644) } func (s *service) GenerateCode() ([]byte, error) { // Buffer the output in memory, for gofmt'ing later in the defer. var buf bytes.Buffer s.p = func(format string, args ...interface{}) { _, err := fmt.Fprintf(&buf, format, args...) if err != nil { panic(err) } } s.pn = func(format string, args ...interface{}) { s.p(format+"\n", args...) } pn := s.pn pn("//") pn("// Licensed to the Apache Software Foundation (ASF) under one") pn("// or more contributor license agreements. See the NOTICE file") pn("// distributed with this work for additional information") pn("// regarding copyright ownership. The ASF licenses this file") pn("// to you under the Apache License, Version 2.0 (the") pn("// \"License\"); you may not use this file except in compliance") pn("// with the License. You may obtain a copy of the License at") pn("//") pn("// http://www.apache.org/licenses/LICENSE-2.0") pn("//") pn("// Unless required by applicable law or agreed to in writing,") pn("// software distributed under the License is distributed on an") pn("// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY") pn("// KIND, either express or implied. See the License for the") pn("// specific language governing permissions and limitations") pn("// under the License.") pn("//") pn("") pn("package %s", pkg) pn("") if s.name == "FirewallService" { pn("// Helper function for maintaining backwards compatibility") pn("func convertFirewallServiceResponse(b []byte) ([]byte, error) {") pn(" var raw map[string]interface{}") pn(" if err := json.Unmarshal(b, &raw); err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" if _, ok := raw[\"firewallrule\"]; ok {") pn(" return convertFirewallServiceListResponse(b)") pn(" }") pn("") pn(" for _, k := range []string{\"endport\", \"startport\"} {") pn(" if sVal, ok := raw[k].(string); ok {") pn(" iVal, err := strconv.Atoi(sVal)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn(" raw[k] = iVal") pn(" }") pn(" }") pn("") pn(" return json.Marshal(raw)") pn("}") pn("") pn("// Helper function for maintaining backwards compatibility") pn("func convertFirewallServiceListResponse(b []byte) ([]byte, error) {") pn(" var rawList struct {") pn(" Count int `json:\"count\"`") pn(" FirewallRules []map[string]interface{} `json:\"firewallrule\"`") pn(" }") pn("") pn(" if err := json.Unmarshal(b, &rawList); err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" for _, r := range rawList.FirewallRules {") pn(" for _, k := range []string{\"endport\", \"startport\"} {") pn(" if sVal, ok := r[k].(string); ok {") pn(" iVal, err := strconv.Atoi(sVal)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn(" r[k] = iVal") pn(" }") pn(" }") pn(" }") pn("") pn(" return json.Marshal(rawList)") pn("}") pn("") } if s.name == "SecurityGroupService" { pn("// Helper function for maintaining backwards compatibility") pn("func convertAuthorizeSecurityGroupIngressResponse(b []byte) ([]byte, error) {") pn(" var raw struct {") pn(" Ingressrule []interface{} `json:\"ingressrule\"`") pn(" }") pn(" if err := json.Unmarshal(b, &raw); err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" if len(raw.Ingressrule) != 1 {") pn(" return b, nil") pn(" }") pn("") pn(" return json.Marshal(raw.Ingressrule[0])") pn("}") pn("") pn("// Helper function for maintaining backwards compatibility") pn("func convertAuthorizeSecurityGroupEgressResponse(b []byte) ([]byte, error) {") pn(" var raw struct {") pn(" Egressrule []interface{} `json:\"egressrule\"`") pn(" }") pn(" if err := json.Unmarshal(b, &raw); err != nil {") pn(" return nil, err") pn(" }") pn("") pn(" if len(raw.Egressrule) != 1 {") pn(" return b, nil") pn(" }") pn("") pn(" return json.Marshal(raw.Egressrule[0])") pn("}") pn("") } if s.name == "CustomService" { pn("type CustomServiceParams struct {") pn(" p map[string]interface{}") pn("}") pn("") pn("func (p *CustomServiceParams) toURLValues() url.Values {") pn(" u := url.Values{}") pn(" if p.p == nil {") pn(" return u") pn(" }") pn("") pn(" for k, v := range p.p {") pn(" switch t := v.(type) {") pn(" case bool:") pn(" u.Set(k, strconv.FormatBool(t))") pn(" case int:") pn(" u.Set(k, strconv.Itoa(t))") pn(" case int64:") pn(" vv := strconv.FormatInt(t, 10)") pn(" u.Set(k, vv)") pn(" case string:") pn(" u.Set(k, t)") pn(" case []string:") pn(" u.Set(k, strings.Join(t, \", \"))") pn(" case map[string]string:") pn(" i := 0") pn(" for kk, vv := range t {") pn(" u.Set(fmt.Sprintf(\"%%s[%%d].%%s\", k, i, kk), vv)") pn(" i++") pn(" }") pn(" }") pn(" }") pn("") pn(" return u") pn("}") pn("") pn("func (p *CustomServiceParams) SetParam(param string, v interface{}) {") pn(" if p.p == nil {") pn(" p.p = make(map[string]interface{})") pn(" }") pn(" p.p[param] = v") pn("}") pn("func (p *CustomServiceParams) GetParam(param string) (interface{}, bool) {") pn(" if p.p == nil {") pn(" p.p = make(map[string]interface{})") pn(" }") pn(" value, ok := p.p[param].(interface{})") pn(" return value, ok") pn("}") pn("") pn("func (s *CustomService) CustomRequest(api string, p *CustomServiceParams, result interface{}) error {") pn(" resp, err := s.cs.newRequest(api, p.toURLValues())") pn(" if err != nil {") pn(" return err") pn(" }") pn("") pn(" return json.Unmarshal(resp, result)") pn("}") pn("func (s *CustomService) CustomPostRequest(api string, p *CustomServiceParams, result interface{}) error {") pn(" resp, err := s.cs.newPostRequest(api, p.toURLValues())") pn(" if err != nil {") pn(" return err") pn(" }") pn("") pn(" return json.Unmarshal(resp, result)") pn("}") } s.generateInterfaceType() for _, a := range s.apis { s.generateParamType(a) s.generateToURLValuesFunc(a) s.generateParamGettersAndSettersFunc(a) s.generateNewParamTypeFunc(a) s.generateHelperFuncs(a) s.generateNewAPICallFunc(a) s.generateResponseType(a) } clean, err := format.Source(buf.Bytes()) if err != nil { buf.WriteTo(os.Stdout) return buf.Bytes(), err } return clean, nil } func (s *service) GenerateTestCode() ([]byte, error) { var buf bytes.Buffer s.p = func(format string, args ...interface{}) { _, err := fmt.Fprintf(&buf, format, args...) if err != nil { panic(err) } } s.pn = func(format string, args ...interface{}) { s.p(format+"\n", args...) } pn := s.pn pn("//") pn("// Licensed to the Apache Software Foundation (ASF) under one") pn("// or more contributor license agreements. See the NOTICE file") pn("// distributed with this work for additional information") pn("// regarding copyright ownership. The ASF licenses this file") pn("// to you under the Apache License, Version 2.0 (the") pn("// \"License\"); you may not use this file except in compliance") pn("// with the License. You may obtain a copy of the License at") pn("//") pn("// http://www.apache.org/licenses/LICENSE-2.0") pn("//") pn("// Unless required by applicable law or agreed to in writing,") pn("// software distributed under the License is distributed on an") pn("// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY") pn("// KIND, either express or implied. See the License for the") pn("// specific language governing permissions and limitations") pn("// under the License.") pn("//") pn("") pn("package test") pn("") pn("func Test%s(t *testing.T) {", s.name) pn(" service := \"%s\"", s.name) pn(" response, err := readData(service)") pn(" if err != nil {") pn(" t.Skipf(\"Skipping test as %%v\", err)") pn(" }") pn(" server := CreateTestServer(t, response)") pn(" client := cloudstack.NewClient(server.URL, \"APIKEY\", \"SECRETKEY\", true)") pn(" defer server.Close()") pn("") for _, a := range s.apis { s.generateAPITest(a) } pn("}") clean, err := format.Source(buf.Bytes()) if err != nil { buf.WriteTo(os.Stdout) return buf.Bytes(), err } return clean, nil } func (s *service) generateAPITest(a *API) { p, pn := s.p, s.pn tn := capitalize(a.Name + "Params") rp := APIParams{} pn(" test%s := func(t *testing.T) {", a.Name) pn(" if _, ok := response[\"%s\"]; !ok {", a.Name) pn(" t.Skipf(\"Skipping as no json response is provided in testdata\")") pn(" }") p(" p := client.%s.New%s(", strings.TrimSuffix(s.name, "Service"), tn) for _, ap := range a.Params { if ap.Required { rp = append(rp, ap) p("%s, ", getDefaultValueForType(a.Name, ap.Name, ap.Type)) } } pn(")") idPresent := false if !(strings.HasPrefix(a.Name, "list") || a.Name == "registerTemplate" || a.Name == "findHostsForMigration") { for _, ap := range a.Response { if ap.Name == "id" && ap.Type == "string" { pn(" r, err := client.%s.%s(p)", strings.TrimSuffix(s.name, "Service"), capitalize(a.Name)) idPresent = true break } } } if !idPresent { pn(" _, err := client.%s.%s(p)", strings.TrimSuffix(s.name, "Service"), capitalize(a.Name)) } pn(" if err != nil {") pn(" t.Errorf(err.Error())") pn(" }") if idPresent { pn(" if r.Id == \"\" {") pn(" t.Errorf(\"Failed to parse response. ID not found\")") pn(" }") } pn(" }") pn(" t.Run(\"%s\", test%s)", capitalize(a.Name), a.Name) pn("") } func getDefaultValueForType(aName string, pName string, pType string) string { switch pType { case "boolean": return "true" case "short", "int", "integer", "long", "float", "double": return "0" case "list": return "[]string{}" case "map": return "map[string]string{}" default: return fmt.Sprintf("\"%s\"", pName) } } func (s *service) generateParamType(a *API) { pn := s.pn pn("type %s struct {", capitalize(a.Name+"Params")) pn(" p map[string]interface{}") pn("}\n") } func (s *service) generateInterfaceType() { p, pn := s.p, s.pn pn("type %sIface interface {", capitalize(s.name)) for _, api := range s.apis { n := capitalize(api.Name) tn := capitalize(api.Name + "Params") // API Calls pn(" %s(p *%s) (*%s, error)", n, n+"Params", strings.TrimPrefix(n, "Configure")+"Response") // NewParam funcs p("New%s(", tn) for _, ap := range api.Params { if ap.Required { // rp = append(rp, ap) p("%s %s, ", s.parseParamName(ap.Name), mapType(api.Name, ap.Name, ap.Type)) } } pn(") *%s", tn) // Helper funcs if strings.HasPrefix(api.Name, "list") { v, found := hasNameOrKeywordParamField(api.Name, api.Params) if found && hasIDAndNameResponseField(api.Name, api.Response) { ln := strings.TrimPrefix(api.Name, "list") // Check if ID is a required parameters and bail if so for _, ap := range api.Params { if ap.Required && ap.Name == "id" { return } } // Generate the function signature p("Get%sID(%s string, ", parseSingular(ln), v) for _, ap := range api.Params { if ap.Required { p("%s %s, ", s.parseParamName(ap.Name), mapType(api.Name, ap.Name, ap.Type)) } } if parseSingular(ln) == "Iso" { p("isofilter string, ") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { p("zoneid string, ") } pn("opts ...OptionFunc) (string, int, error)") if hasIDParamField(api.Name, api.Params) { p("Get%sByName(name string, ", parseSingular(ln)) for _, ap := range api.Params { if ap.Required { p("%s %s, ", s.parseParamName(ap.Name), mapType(api.Name, ap.Name, ap.Type)) } } if parseSingular(ln) == "Iso" { p("isofilter string, ") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { p("zoneid string, ") } pn("opts ...OptionFunc) (*%s, int, error)", parseSingular(ln)) } } if hasIDParamField(api.Name, api.Params) { ln := strings.TrimPrefix(api.Name, "list") // Generate the function signature p("Get%sByID(id string, ", parseSingular(ln)) for _, ap := range api.Params { if ap.Required && s.parseParamName(ap.Name) != "id" { p("%s %s, ", ap.Name, mapType(api.Name, ap.Name, ap.Type)) } } if ln == "LoadBalancerRuleInstances" { pn("opts ...OptionFunc) (*VirtualMachine, int, error)") } else { pn("opts ...OptionFunc) (*%s, int, error)", parseSingular(ln)) } } } } pn("}\n") } func (s *service) generateToURLValuesFunc(a *API) { pn := s.pn pn("func (p *%s) toURLValues() url.Values {", capitalize(a.Name+"Params")) pn(" u := url.Values{}") pn(" if p.p == nil {") pn(" return u") pn(" }") for _, ap := range a.Params { pn(" if v, found := p.p[\"%s\"]; found {", ap.Name) s.generateConvertCode(a.Name, ap.Name, mapType(a.Name, ap.Name, ap.Type)) pn(" }") } pn(" return u") pn("}") pn("") } func (s *service) generateConvertCode(cmd, name, typ string) { pn := s.pn switch typ { case "string", "UUID": pn("u.Set(\"%s\", v.(string))", name) case "int": pn("vv := strconv.Itoa(v.(int))") pn("u.Set(\"%s\", vv)", name) case "int64": pn("vv := strconv.FormatInt(v.(int64), 10)") pn("u.Set(\"%s\", vv)", name) case "bool": pn("vv := strconv.FormatBool(v.(bool))") pn("u.Set(\"%s\", vv)", name) case "[]string": pn("vv := strings.Join(v.([]string), \",\")") pn("u.Set(\"%s\", vv)", name) case "[]map[string]string": pn("l := v.([]map[string]string)") pn("for i, m := range l {") pn(" for key, val := range m {") pn(" u.Set(fmt.Sprintf(\"%s[%%d].%%s\", i, key), val)", name) pn(" }") pn("}") case "map[string]string": pn("m := v.(map[string]string)") zeroIndex := detailsRequireZeroIndex[cmd] if zeroIndex { pn("for _, k := range getSortedKeysFromMap(m) {") } else { pn("for i, k := range getSortedKeysFromMap(m) {") } switch name { case "details": if detailsRequireKeyValue[cmd] { pn(" u.Set(fmt.Sprintf(\"%s[%%d].key\", i), k)", name) pn(" u.Set(fmt.Sprintf(\"%s[%%d].value\", i), m[k])", name) } else { if zeroIndex { pn(" u.Set(fmt.Sprintf(\"%s[0].%%s\", k), m[k])", name) } else { pn(" u.Set(fmt.Sprintf(\"%s[%%d].%%s\", i, k), m[k])", name) } } case "serviceproviderlist": pn(" u.Set(fmt.Sprintf(\"%s[%%d].service\", i), k)", name) pn(" u.Set(fmt.Sprintf(\"%s[%%d].provider\", i), m[k])", name) case "usersecuritygrouplist": pn(" u.Set(fmt.Sprintf(\"%s[%%d].account\", i), k)", name) pn(" u.Set(fmt.Sprintf(\"%s[%%d].group\", i), m[k])", name) case "tags": pn(" u.Set(fmt.Sprintf(\"%s[%%d].key\", i), k)", name) if cmd == "deleteTags" { pn(" if m[k] != \"\" {") pn(" u.Set(fmt.Sprintf(\"%s[%%d].value\", i), m[k])", name) pn(" }") } else { pn(" u.Set(fmt.Sprintf(\"%s[%%d].value\", i), m[k])", name) } default: if zeroIndex && !detailsRequireKeyValue[cmd] { pn(" u.Set(fmt.Sprintf(\"%s[0].%%s\", k), m[k])", name) } else { pn(" u.Set(fmt.Sprintf(\"%s[%%d].key\", i), k)", name) pn(" u.Set(fmt.Sprintf(\"%s[%%d].value\", i), m[k])", name) } } pn("}") } } func (s *service) parseParamName(name string) string { if name != "type" { return name } return uncapitalize(strings.TrimSuffix(s.name, "Service")) + "Type" } func (s *service) generateParamGettersAndSettersFunc(a *API) { pn := s.pn found := make(map[string]bool) for _, ap := range a.Params { if !found[ap.Name] { pn("func (p *%s) Set%s(v %s) {", capitalize(a.Name+"Params"), capitalize(ap.Name), mapType(a.Name, ap.Name, ap.Type)) pn(" if p.p == nil {") pn(" p.p = make(map[string]interface{})") pn(" }") pn(" p.p[\"%s\"] = v", ap.Name) pn("}") pn("") pn("func (p *%s) Get%s() (%s, bool) {", capitalize(a.Name+"Params"), capitalize(ap.Name), mapType(a.Name, ap.Name, ap.Type)) pn(" if p.p == nil {") pn(" p.p = make(map[string]interface{})") pn(" }") pn(" value, ok := p.p[\"%s\"].(%s)", ap.Name, mapType(a.Name, ap.Name, ap.Type)) pn(" return value, ok") pn("}") pn("") if mapRequireList[a.Name] != nil && mapRequireList[a.Name][ap.Name] { pn("func (p *%s) Add%s(item map[string]string) {", capitalize(a.Name+"Params"), capitalize(ap.Name)) pn(" if p.p == nil {") pn(" p.p = make(map[string]interface{})") pn(" }") pn(" val, found := p.p[\"%s\"]", ap.Name) pn(" if !found {") pn(" p.p[\"%s\"] = []map[string]string{}", ap.Name) pn(" val = p.p[\"%s\"]", ap.Name) pn(" }") pn(" l := val.([]map[string]string)") pn(" l = append(l, item)") pn(" p.p[\"%s\"] = l", ap.Name) pn("}") pn("") } found[ap.Name] = true } } } func (s *service) generateNewParamTypeFunc(a *API) { p, pn := s.p, s.pn tn := capitalize(a.Name + "Params") rp := APIParams{} // Generate the function signature pn("// You should always use this function to get a new %s instance,", tn) pn("// as then you are sure you have configured all required params") p("func (s *%s) New%s(", s.name, tn) for _, ap := range a.Params { if ap.Required { rp = append(rp, ap) p("%s %s, ", s.parseParamName(ap.Name), mapType(a.Name, ap.Name, ap.Type)) } } pn(") *%s {", tn) // Generate the function body pn(" p := &%s{}", tn) pn(" p.p = make(map[string]interface{})") sort.Sort(rp) for _, ap := range rp { pn(" p.p[\"%s\"] = %s", ap.Name, s.parseParamName(ap.Name)) } pn(" return p") pn("}") pn("") } func (s *service) generateHelperFuncs(a *API) { p, pn := s.p, s.pn if strings.HasPrefix(a.Name, "list") { v, found := hasNameOrKeywordParamField(a.Name, a.Params) if found && hasIDAndNameResponseField(a.Name, a.Response) { ln := strings.TrimPrefix(a.Name, "list") // Check if ID is a required parameters and bail if so for _, ap := range a.Params { if ap.Required && ap.Name == "id" { return } } // Generate the function signature pn("// This is a courtesy helper function, which in some cases may not work as expected!") p("func (s *%s) Get%sID(%s string, ", s.name, parseSingular(ln), v) for _, ap := range a.Params { if ap.Required { p("%s %s, ", s.parseParamName(ap.Name), mapType(a.Name, ap.Name, ap.Type)) } } if parseSingular(ln) == "Iso" { p("isofilter string, ") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { p("zoneid string, ") } pn("opts ...OptionFunc) (string, int, error) {") // Generate the function body pn(" p := &List%sParams{}", ln) pn(" p.p = make(map[string]interface{})") pn("") pn(" p.p[\"%s\"] = %s", v, v) for _, ap := range a.Params { if ap.Required { pn(" p.p[\"%s\"] = %s", ap.Name, s.parseParamName(ap.Name)) } } if parseSingular(ln) == "Iso" { pn(" p.p[\"isofilter\"] = isofilter") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { pn(" p.p[\"zoneid\"] = zoneid") } pn("") pn(" for _, fn := range append(s.cs.options, opts...) {") pn(" if err := fn(s.cs, p); err != nil {") pn(" return \"\", -1, err") pn(" }") pn(" }") pn("") pn(" l, err := s.List%s(p)", ln) pn(" if err != nil {") pn(" return \"\", -1, err") pn(" }") pn("") if ln == "AffinityGroups" { pn(" // This is needed because of a bug with the listAffinityGroup call. It reports the") pn(" // number of VirtualMachines in the groups as being the number of groups found.") pn(" l.Count = len(l.%s)", ln) pn("") } pn(" if l.Count == 0 {") pn(" return \"\", l.Count, fmt.Errorf(\"No match found for %%s: %%+v\", %s, l)", v) pn(" }") pn("") pn(" if l.Count == 1 {") pn(" return l.%s[0].Id, l.Count, nil", ln) pn(" }") pn("") pn(" if l.Count > 1 {") pn(" for _, v := range l.%s {", ln) pn(" if v.Name == %s {", v) pn(" return v.Id, l.Count, nil") pn(" }") pn(" }") pn(" }") pn(" return \"\", l.Count, fmt.Errorf(\"Could not find an exact match for %%s: %%+v\", %s, l)", v) pn("}\n") pn("") if hasIDParamField(a.Name, a.Params) { // Generate the function signature pn("// This is a courtesy helper function, which in some cases may not work as expected!") p("func (s *%s) Get%sByName(name string, ", s.name, parseSingular(ln)) for _, ap := range a.Params { if ap.Required { p("%s %s, ", s.parseParamName(ap.Name), mapType(a.Name, ap.Name, ap.Type)) } } if parseSingular(ln) == "Iso" { p("isofilter string, ") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { p("zoneid string, ") } pn("opts ...OptionFunc) (*%s, int, error) {", parseSingular(ln)) // Generate the function body p(" id, count, err := s.Get%sID(name, ", parseSingular(ln)) for _, ap := range a.Params { if ap.Required { p("%s, ", s.parseParamName(ap.Name)) } } if parseSingular(ln) == "Iso" { p("isofilter, ") } if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { p("zoneid, ") } pn("opts...)") pn(" if err != nil {") pn(" return nil, count, err") pn(" }") pn("") p(" r, count, err := s.Get%sByID(id, ", parseSingular(ln)) for _, ap := range a.Params { if ap.Required { p("%s, ", s.parseParamName(ap.Name)) } } pn("opts...)") pn(" if err != nil {") pn(" return nil, count, err") pn(" }") pn(" return r, count, nil") pn("}") pn("") } } if hasIDParamField(a.Name, a.Params) { ln := strings.TrimPrefix(a.Name, "list") // Generate the function signature pn("// This is a courtesy helper function, which in some cases may not work as expected!") p("func (s *%s) Get%sByID(id string, ", s.name, parseSingular(ln)) for _, ap := range a.Params { if ap.Required && s.parseParamName(ap.Name) != "id" { p("%s %s, ", ap.Name, mapType(a.Name, ap.Name, ap.Type)) } } if ln == "LoadBalancerRuleInstances" { pn("opts ...OptionFunc) (*VirtualMachine, int, error) {") } else { pn("opts ...OptionFunc) (*%s, int, error) {", parseSingular(ln)) } // Generate the function body pn(" p := &List%sParams{}", ln) pn(" p.p = make(map[string]interface{})") pn("") pn(" p.p[\"id\"] = id") for _, ap := range a.Params { if ap.Required && s.parseParamName(ap.Name) != "id" { pn(" p.p[\"%s\"] = %s", ap.Name, s.parseParamName(ap.Name)) } } pn("") pn(" for _, fn := range append(s.cs.options, opts...) {") pn(" if err := fn(s.cs, p); err != nil {") pn(" return nil, -1, err") pn(" }") pn(" }") pn("") pn(" l, err := s.List%s(p)", ln) pn(" if err != nil {") pn(" if strings.Contains(err.Error(), fmt.Sprintf(") pn(" \"Invalid parameter id value=%%s due to incorrect long value format, \"+") pn(" \"or entity does not exist\", id)) {") pn(" return nil, 0, fmt.Errorf(\"No match found for %%s: %%+v\", id, l)") pn(" }") pn(" return nil, -1, err") pn(" }") pn("") if ln == "AffinityGroups" { pn(" // This is needed because of a bug with the listAffinityGroup call. It reports the") pn(" // number of VirtualMachines in the groups as being the number of groups found.") pn(" l.Count = len(l.%s)", ln) pn("") } pn(" if l.Count == 0 {") pn(" return nil, l.Count, fmt.Errorf(\"No match found for %%s: %%+v\", id, l)") pn(" }") pn("") pn(" if l.Count == 1 {") pn(" return l.%s[0], l.Count, nil", ln) pn(" }") pn(" return nil, l.Count, fmt.Errorf(\"There is more then one result for %s UUID: %%s!\", id)", parseSingular(ln)) pn("}\n") pn("") } } } func hasNameOrKeywordParamField(aName string, params APIParams) (v string, found bool) { for _, p := range params { if p.Name == "keyword" && mapType(aName, p.Name, p.Type) == "string" { v = "keyword" found = true } if p.Name == "name" && mapType(aName, p.Name, p.Type) == "string" { return "name", true } } return v, found } func hasIDParamField(aName string, params APIParams) bool { for _, p := range params { if p.Name == "id" && mapType(aName, p.Name, p.Type) == "string" { return true } } return false } func hasIDAndNameResponseField(aName string, resp APIResponses) bool { id := false name := false for _, r := range resp { if r.Name == "id" && mapType(aName, r.Name, r.Type) == "string" { id = true } if r.Name == "name" && mapType(aName, r.Name, r.Type) == "string" { name = true } } return id && name } func (s *service) generateNewAPICallFunc(a *API) { pn := s.pn n := capitalize(a.Name) // Generate the function signature pn("// %s", a.Description) pn("func (s *%s) %s(p *%s) (*%s, error) {", s.name, n, n+"Params", strings.TrimPrefix(n, "Configure")+"Response") // Generate the function body if n == "QueryAsyncJobResult" { pn(" var resp json.RawMessage") pn(" var err error") pn("") pn(" // We should be able to retry on failure as this call is idempotent") pn(" for i := 0; i < 3; i++ {") pn(" resp, err = s.cs.newRequest(\"%s\", p.toURLValues())", a.Name) pn(" if err == nil {") pn(" break") pn(" }") pn(" time.Sleep(500 * time.Millisecond)") pn(" }") } else { if a.Name == "deployVirtualMachine" || a.Name == "login" || a.Name == "updateVirtualMachine" { pn(" resp, err := s.cs.newPostRequest(\"%s\", p.toURLValues())", a.Name) } else { pn(" resp, err := s.cs.newRequest(\"%s\", p.toURLValues())", a.Name) } } pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") switch n { case "AddImageStore", "CreateAccount", "CreateDomain", "UpdateDomain", "CreateNetwork", "CreateStoragePool", "CreateNetworkOffering", "UpdateNetworkOffering", "UpdateServiceOffering", "UpdateConfiguration", "UpdateCluster", "CreateSSHKeyPair", "CreateSecurityGroup", "CreateServiceOffering", "CreateUser", "CreateZone", "DedicateGuestVlanRange", "EnableUser", "GetVirtualMachineUserData", "LockUser", "RegisterSSHKeyPair", "RegisterUserKeys", "GetUserKeys", "AddAnnotation", "RemoveAnnotation", "AddKubernetesSupportedVersion", "CreateDiskOffering", "AddHost", "RegisterIso": pn(" if resp, err = getRawValue(resp); err != nil {") pn(" return nil, err") pn(" }") pn("") } if !a.Isasync && s.name == "FirewallService" { pn(" resp, err = convertFirewallServiceResponse(resp)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") } if field, isNested := nestedResponse[a.Name]; isNested { pn(" var nested struct {") pn(" Response %sResponse `json:\"%s\"`", strings.TrimPrefix(n, "Configure"), field) pn(" }") pn(" if err := json.Unmarshal(resp, &nested); err != nil {") pn(" return nil, err") pn(" }") pn(" r := nested.Response") } else { pn(" var r %sResponse", strings.TrimPrefix(n, "Configure")) pn(" if err := json.Unmarshal(resp, &r); err != nil {") pn(" return nil, err") pn(" }") } pn("") if a.Isasync { pn(" // If we have a async client, we need to wait for the async result") pn(" if s.cs.async {") pn(" b, err := s.cs.GetAsyncJobResult(r.JobID, s.cs.timeout)") pn(" if err != nil {") pn(" if err == AsyncTimeoutErr {") pn(" return &r, err") pn(" }") pn(" return nil, err") pn(" }") pn("") if !isSuccessOnlyResponse(a.Response) { pn(" b, err = getRawValue(b)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") } if s.name == "FirewallService" { pn(" b, err = convertFirewallServiceResponse(b)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") } if n == "AuthorizeSecurityGroupIngress" { pn(" b, err = convertAuthorizeSecurityGroupIngressResponse(b)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") } if n == "AuthorizeSecurityGroupEgress" { pn(" b, err = convertAuthorizeSecurityGroupEgressResponse(b)") pn(" if err != nil {") pn(" return nil, err") pn(" }") pn("") } pn(" if err := json.Unmarshal(b, &r); err != nil {") pn(" return nil, err") pn(" }") pn(" }") pn("") } pn(" return &r, nil") pn("}") pn("") } func isSuccessOnlyResponse(resp APIResponses) bool { success := false displaytext := false for _, r := range resp { if r.Name == "displaytext" { displaytext = true } if r.Name == "success" { success = true } } return displaytext && success } func (s *service) generateResponseType(a *API) { pn := s.pn tn := capitalize(strings.TrimPrefix(a.Name, "configure") + "Response") ln := capitalize(strings.TrimPrefix(a.Name, "list")) // If this is a 'list' response, we need an separate list struct. There seem to be other // types of responses that also need a separate list struct, so checking on exact matches // for those once. if strings.HasPrefix(a.Name, "list") || a.Name == "registerTemplate" || a.Name == "findHostsForMigration" { pn("type %s struct {", tn) // This nasty check is for some specific response that do not behave consistent switch a.Name { case "listAsyncJobs": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "asyncjobs") case "listCapabilities": pn(" %s *%s `json:\"%s\"`", ln, parseSingular(ln), "capability") case "listEgressFirewallRules": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "firewallrule") case "listLoadBalancerRuleInstances": pn(" Count int `json:\"count\"`") pn(" LBRuleVMIDIPs []*%s `json:\"%s\"`", parseSingular(ln), "lbrulevmidip") pn(" LoadBalancerRuleInstances []*VirtualMachine `json:\"%s\"`", strings.ToLower(parseSingular(ln))) case "listVirtualMachinesMetrics": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "virtualmachine") case "registerTemplate": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "template") case "listDomainChildren": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "domain") case "findHostsForMigration": pn(" Count int `json:\"count\"`") pn(" Host []*%s `json:\"%s\"`", customResponseStructTypes[a.Name], "host") default: pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), strings.ToLower(parseSingular(ln))) } pn("}") pn("") tn = parseSingular(ln) } sort.Sort(a.Response) customMarshal := s.recusiveGenerateResponseType(a.Name, tn, a.Response, a.Isasync) if customMarshal { pn("func (r *%s) UnmarshalJSON(b []byte) error {", tn) pn(" var m map[string]interface{}") pn(" err := json.Unmarshal(b, &m)") pn(" if err != nil {") pn(" return err") pn(" }") pn("") pn(" if success, ok := m[\"success\"].(string); ok {") pn(" m[\"success\"] = success == \"true\"") pn(" b, err = json.Marshal(m)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" }") pn("") pn(" if ostypeid, ok := m[\"ostypeid\"].(float64); ok {") pn(" m[\"ostypeid\"] = strconv.Itoa(int(ostypeid))") pn(" b, err = json.Marshal(m)") pn(" if err != nil {") pn(" return err") pn(" }") pn(" }") pn("") pn(" type alias %s", tn) pn(" return json.Unmarshal(b, (*alias)(r))") pn("}") pn("") } } func parseSingular(n string) string { if strings.HasSuffix(n, "ies") { return strings.TrimSuffix(n, "ies") + "y" } if strings.HasSuffix(n, "sses") { return strings.TrimSuffix(n, "es") } return strings.TrimSuffix(n, "s") } func (s *service) recusiveGenerateResponseType(aName string, tn string, resp APIResponses, async bool) bool { pn := s.pn customMarshal := false found := make(map[string]bool) if val, ok := customResponseStructTypes[aName]; ok { tn = val } pn("type %s struct {", tn) for _, r := range resp { if r.Name == "" { continue } if r.Name == "secondaryip" { pn("%s []struct {", capitalize(r.Name)) pn(" Id string `json:\"id\"`") pn(" Ipaddress string `json:\"ipaddress\"`") pn("} `json:\"%s\"`", r.Name) continue } if r.Response != nil { sort.Sort(r.Response) typeName, create := getUniqueTypeName(tn, r.Name) pn("%s []%s `json:\"%s\"`", capitalize(r.Name), typeName, r.Name) if create { defer s.recusiveGenerateResponseType(aName, typeName, r.Response, false) } } else { if !found[r.Name] { switch r.Name { case "success": // This case is because the response field is different for sync and async calls :( pn("%s bool `json:\"%s\"`", capitalize(r.Name), r.Name) if !async { customMarshal = true } case "ostypeid": // This case is needed for backwards compatibility. pn("%s string `json:\"%s\"`", capitalize(r.Name), r.Name) customMarshal = true default: pn("%s %s `json:\"%s\"`", capitalize(r.Name), mapType(aName, r.Name, r.Type), r.Name) } found[r.Name] = true } } } pn("}") pn("") return customMarshal } func getUniqueTypeName(prefix, name string) (string, bool) { // We have special cases for [in|e]gressrules, nics and tags as the exact // sames types are used used in multiple different locations. switch { case strings.HasSuffix(name, "gressrule"): name = "rule" case strings.HasSuffix(name, "nic"): prefix = "" name = "nic" case strings.HasSuffix(name, "tags"): prefix = "" name = "tags" } tn := prefix + capitalize(name) if !typeNames[tn] { typeNames[tn] = true return tn, true } // Return here as this means the type already exists. if name == "rule" || name == "nic" || name == "tags" { return tn, false } return getUniqueTypeName(prefix, name+"Internal") } func logMissingApis(ai map[string]*API, as *allServices) { asMap := make(map[string]*API) for _, svc := range as.services { for _, api := range svc.apis { asMap[api.Name] = api } } for apiName, _ := range ai { _, found := asMap[apiName] if !found { log.Printf("Api missing in layout: %s", apiName) } } } func getAllServices(listApis string) (*allServices, []error, error) { // Get a map with all API info ai, err := getAPIInfo(listApis) if err != nil { return nil, nil, err } // Generate a complete set of services with their methods (APIs) as := &allServices{} errors := []error{} for sn, apis := range layout { typeNames[sn] = true s := &service{name: sn} for _, api := range apis { a, found := ai[api] if !found { errors = append(errors, &apiInfoNotFoundError{api}) continue } s.apis = append(s.apis, a) } for _, apis := range s.apis { sort.Sort(apis.Params) } as.services = append(as.services, s) } // Add an extra field to enable adding a custom service as.services = append(as.services, &service{name: "CustomService"}) sort.Sort(as.services) logMissingApis(ai, as) return as, errors, nil } func getAPIInfo(listApis string) (map[string]*API, error) { apis, err := ioutil.ReadFile(listApis) if err != nil { return nil, err } var ar struct { Count int `json:"count"` APIs []*API `json:"api"` } if err := json.Unmarshal(apis, &ar); err != nil { return nil, err } // Make a map of all retrieved APIs ai := make(map[string]*API) for _, api := range ar.APIs { ai[api.Name] = api } return ai, nil } func sourceDir() (string, error) { wd, err := os.Getwd() if err != nil { return "", err } outdir := path.Join(wd, pkg) if err := os.MkdirAll(outdir, 0755); err != nil { return "", fmt.Errorf("Failed to Mkdir %s: %v", outdir, err) } return outdir, nil } func testDir() (string, error) { wd, err := os.Getwd() if err != nil { return "", err } testdir := path.Join(wd, "test") if err := os.MkdirAll(testdir, 0755); err != nil { return "", fmt.Errorf("Failed to Mkdir %s: %v", testdir, err) } return testdir, nil } func mapType(aName string, pName string, pType string) string { if _, ok := longToStringConvertedParams[pName]; ok { pType = "UUID" } switch pType { case "UUID": return "UUID" case "boolean": return "bool" case "short", "int", "integer": return "int" case "long": return "int64" case "float", "double": return "float64" case "list": if pName == "downloaddetails" || pName == "owner" { return "[]map[string]string" } else if pName == "network" { return "[]*Network" } if pName == "virtualmachines" { return "[]*VirtualMachine" } return "[]string" case "map": if mapRequireList[aName] != nil && mapRequireList[aName][pName] { return "[]map[string]string" } return "map[string]string" case "set": return "[]interface{}" case "resourceiconresponse": return "interface{}" case "responseobject": return "json.RawMessage" case "uservmresponse": // This is a really specific anomaly of the API return "*VirtualMachine" case "outofbandmanagementresponse": return "OutOfBandManagementResponse" case "hostharesponse": return "HAForHostResponse" default: return "string" } } func capitalize(s string) string { if s == "jobid" { return "JobID" } r := []rune(s) r[0] = unicode.ToUpper(r[0]) return string(r) } func uncapitalize(s string) string { r := []rune(s) r[0] = unicode.ToLower(r[0]) return string(r) }