func (apiDef *APIDefinition) loadJson(reader io.Reader) { b := new(bytes.Buffer) _, err := b.ReadFrom(reader) utils.ExitOnFail(err) data := b.Bytes() f := new(interface{}) err = json.Unmarshal(data, f) utils.ExitOnFail(err) schema := (*f).(map[string]interface{})["$schema"].(string) apiDef.SchemaURL = schema var m APIModel switch schema { case "http://schemas.taskcluster.net/base/v1/api-reference.json#": m = new(API) case "http://schemas.taskcluster.net/base/v1/exchanges-reference.json#": m = new(Exchange) default: panic(fmt.Errorf("Do not know how to handle API with schema %q", schema)) } err = json.Unmarshal(data, m) utils.ExitOnFail(err) m.setAPIDefinition(apiDef) m.postPopulate(apiDef) apiDef.Data = m }
// GenerateCode takes the objects loaded into memory in LoadAPIs // and writes them out as go code. func GenerateCode(goOutputDir, modelData string) { for i := range apiDefs { apiDefs[i].PackageName = strings.ToLower(apiDefs[i].Name) apiDefs[i].PackagePath = filepath.Join(goOutputDir, "src", "main", "java", "org", "mozilla", "taskcluster", "client", apiDefs[i].PackageName) err = os.MkdirAll(apiDefs[i].PackagePath, 0755) utils.ExitOnFail(err) content := `// The following code is AUTO-GENERATED. Please DO NOT edit. // // This package was generated from the schema defined at // ` + apiDefs[i].URL + ` ` generatePayloadTypes(&apiDefs[i]) content += apiDefs[i].generateAPICode() className := strings.Title(apiDefs[i].Name) sourceFile := filepath.Join(apiDefs[i].PackagePath, className+".java") fmt.Println("Generating source code " + sourceFile + "...") utils.WriteStringToFile(content, sourceFile) utils.ExitOnFail(err) } content := "The following file is an auto-generated static dump of the API models at time of code generation.\n" content += "It is provided here for reference purposes, but is not used by any code.\n" content += "\n" for i := range apiDefs { content += utils.Underline(apiDefs[i].URL) content += apiDefs[i].Data.String() + "\n\n" for _, url := range apiDefs[i].schemas.SortedSanitizedURLs() { content += (utils.Underline(url)) content += apiDefs[i].schemas.SubSchema(url).String() + "\n\n" } } utils.WriteStringToFile(content, modelData) }
// LoadAPIs takes care of reading all json files and performing elementary // processing of the data, such as assigning unique type names to entities // which will be translated to go types. // // Data is unmarshaled into objects (or instances of go types) and then // postPopulate is called on the objects. This in turn triggers further reading // of json files and unmarshalling where schemas refer to other schemas. // // When LoadAPIs returns, all json schemas and sub schemas should have been // read and unmarhsalled into go objects. func LoadAPIs(apiManifestUrl, supplementaryDataFile string) []APIDefinition { resp, err := http.Get(apiManifestUrl) if err != nil { fmt.Printf("Could not download api manifest from url: '%v'!\n", apiManifestUrl) } supDataReader, err := os.Open(supplementaryDataFile) if err != nil { fmt.Printf("Could not load supplementary data json file: '%v'!\n", supplementaryDataFile) } utils.ExitOnFail(err) apiManifestDecoder := json.NewDecoder(resp.Body) apiMan := make(map[string]string) err = apiManifestDecoder.Decode(&apiMan) utils.ExitOnFail(err) supDataDecoder := json.NewDecoder(supDataReader) err = supDataDecoder.Decode(&apiDefs) utils.ExitOnFail(err) sort.Sort(SortedAPIDefs(apiDefs)) // build up apis based on data in *both* data sources for i := range apiMan { // seach for apiMan[i] in apis k := sort.Search(len(apiDefs), func(j int) bool { return apiDefs[j].URL >= apiMan[i] }) if k < len(apiDefs) && apiDefs[k].URL == apiMan[i] { // url is present in supplementary data apiDefs[k].Name = i } else { fmt.Printf( "\nFATAL: Manifest from url '%v' contains key '%v' with url '%v', but this url does not exist in supplementary data file '%v', therefore exiting...\n\n", apiManifestUrl, i, apiMan[i], supplementaryDataFile) os.Exit(64) } } for i := range apiDefs { if apiDefs[i].Name == "" { fmt.Printf( "\nFATAL: Manifest from url '%v' does not contain url '%v' which does exist in supplementary data file '%v', therefore exiting...\n\n", apiManifestUrl, apiDefs[i].URL, supplementaryDataFile) os.Exit(65) } } for i := range apiDefs { var resp *http.Response resp, err = http.Get(apiDefs[i].URL) utils.ExitOnFail(err) defer resp.Body.Close() apiDefs[i].loadJson(resp.Body) // check that the json schema is valid! validateJson(apiDefs[i].SchemaURL, apiDefs[i].URL) } return apiDefs }
func main() { // Parse the docopt string and exit on any error or help message. arguments, err := docopt.Parse(usage, nil, true, version, false, true) utils.ExitOnFail(err) model.LoadAPIs(arguments["-u"].(string), arguments["-f"].(string)) model.GenerateCode(arguments["-o"].(string), arguments["-m"].(string)) }
func validateJson(schemaUrl, docUrl string) { schemaLoader := gojsonschema.NewReferenceLoader(schemaUrl) docLoader := gojsonschema.NewReferenceLoader(docUrl) result, err := gojsonschema.Validate(schemaLoader, docLoader) utils.ExitOnFail(err) if result.Valid() { fmt.Printf("Document '%v' is valid against '%v'.\n", docUrl, schemaUrl) } else { fmt.Printf("Document '%v' is INVALID against '%v'.\n", docUrl, schemaUrl) for _, desc := range result.Errors() { fmt.Println("") fmt.Printf("- %s\n", desc) } // os.Exit(70) } }
// 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) { job := &jsonschema2go.Job{ Package: apiDef.PackageName, URLs: apiDef.schemaURLs, SkipCodeGen: true, TypeNameGenerator: func(name string, exported bool, blacklist map[string]bool) (identifier string) { return utils.Normalise(name, blacklist) }, MemberNameGenerator: func(name string, exported bool, blacklist map[string]bool) (identifier string) { return utils.NormaliseLower(name, blacklist) }, } result, err := job.Execute() utils.ExitOnFail(err) apiDef.schemas = result.SchemaSet for _, i := range apiDef.schemas.SortedSanitizedURLs() { extraPackages := make(map[string]bool) content := "package org.mozilla.taskcluster.client." + strings.ToLower(apiDef.PackageName) + ";\n" content += "\n" s := JsonSubSchema(*apiDef.schemas.SubSchema(i)) typeClass, typeComment, typ := (&s).TypeDefinition(0, extraPackages) if typeClass != "" { className := typ if strings.HasSuffix(typ, "[]") { className = typ[:len(typ)-2] apiDef.schemas.SubSchema(i).TypeName = typ } if len(extraPackages) > 0 { for pckage := range extraPackages { content += "import " + pckage + ";\n" } content += "\n" } content += typeComment content += typeClass[1:] utils.WriteStringToFile(content, filepath.Join(apiDef.PackagePath, className+".java")) } else { apiDef.schemas.SubSchema(i).TypeName = typ } } }