func (items Items) String() string { result := "" for i, j := range items.Items { result += fmt.Sprintf("Item '%v' =\n", i) + text.Indent(j.String(), " ") } return result }
func (p Properties) String() string { result := "" for _, i := range p.SortedPropertyNames { result += "Property '" + i + "' =\n" + text.Indent(p.Properties[i].String(), " ") } return result }
func describeList(name string, value interface{}) string { if reflect.ValueOf(value).IsValid() { if !reflect.ValueOf(value).IsNil() { return fmt.Sprintf("%v\n", name) + text.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 generateGoTypes(schemaSet *SchemaSet) (string, StringSet, StringSet) { extraPackages := make(StringSet) rawMessageTypes := make(StringSet) 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... typeDefinitions := make(map[string]string) typeNames := make([]string, 0, len(schemaSet.used)) for _, i := range schemaSet.used { var newComment, newType string newComment, newType = i.typeDefinition(true, extraPackages, rawMessageTypes) typeDefinitions[i.TypeName] = text.Indent(newComment+i.TypeName+" "+newType, "\t") typeNames = append(typeNames, i.TypeName) } sort.Strings(typeNames) for _, t := range typeNames { content += typeDefinitions[t] + "\n" } return content + ")\n\n", extraPackages, rawMessageTypes }
func (job *Job) Execute() (*Result, error) { // Generate normalised names for schemas. Keep a record of generated type // names, so that we don't reuse old names. Set acts like a set // of strings. job.result = new(Result) job.result.SchemaSet = &SchemaSet{ all: make(map[string]*JsonSubSchema), used: make(map[string]*JsonSubSchema), TypeNames: make(StringSet), } if job.TypeNameBlacklist == nil { job.TypeNameBlacklist = make(StringSet) } if job.TypeNameGenerator == nil { job.TypeNameGenerator = text.GoIdentifierFrom } if job.MemberNameGenerator == nil { job.MemberNameGenerator = text.GoIdentifierFrom } for _, URL := range job.URLs { j, err := job.cacheJsonSchema(URL) if err != nil { return nil, err } // note we don't add inside cacheJsonSchema/loadJsonSchema // since we don't want to add e.g. top level items if only // definitions inside the schema are referenced job.add(j) } // link schemas (update cross references between schemas) for _, j := range job.result.SchemaSet.all { // if there is a referenced schema... if j.Ref != nil && *j.Ref != "" { // see if it is relative to current doc, or absolute reference var fullyQualifiedRef string if (*j.Ref)[0] == '#' { fullyQualifiedRef = j.SourceURL[:strings.Index(j.SourceURL, "#")] + *j.Ref } else { fullyQualifiedRef = sanitizeURL(*j.Ref) } j.RefSubSchema = job.result.SchemaSet.all[fullyQualifiedRef] job.add(j.RefSubSchema) } } var err error if !job.SkipCodeGen { types, extraPackages, rawMessageTypes := generateGoTypes(job.result.SchemaSet) content := `// This source code file is AUTO-GENERATED by github.com/taskcluster/jsonschema2go package ` + job.Package + ` ` extraPackagesContent := "" for j, k := range extraPackages { if k { extraPackagesContent += text.Indent(""+j+"\n", "\t") } } if extraPackagesContent != "" { content += `import ( ` + extraPackagesContent + `) ` } content += types content += jsonRawMessageImplementors(rawMessageTypes) // format it job.result.SourceCode, err = format.Source([]byte(content)) // imports should be good, so no need to run // https://godoc.org/golang.org/x/tools/imports#Process } return job.result, err }
func (jsonSubSchema *JsonSubSchema) typeDefinition(topLevel bool, extraPackages StringSet, rawMessageTypes StringSet) (comment, typ string) { comment = "\n" if d := jsonSubSchema.Description; d != nil { comment += text.Indent(*d, "// ") } if comment[len(comment)-1:] != "\n" { comment += "\n" } if enum := jsonSubSchema.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) } } } // Create comments for metadata in a single paragraph. Only start new // paragraph if we discover after inspecting all possible metadata, that // something has been specified. If there is no metadata, no need to create // a new paragraph. var metadata string if def := jsonSubSchema.Default; def != nil { var value string switch (*def).(type) { case bool: value = strconv.FormatBool((*def).(bool)) case float64: value = strconv.FormatFloat((*def).(float64), 'g', -1, 64) default: value = fmt.Sprintf("%q", *def) } metadata += "// Default: " + value + "\n" } if regex := jsonSubSchema.Pattern; regex != nil { metadata += "// Syntax: " + *regex + "\n" } if minItems := jsonSubSchema.MinLength; minItems != nil { metadata += "// Min length: " + strconv.Itoa(*minItems) + "\n" } if maxItems := jsonSubSchema.MaxLength; maxItems != nil { metadata += "// Max length: " + strconv.Itoa(*maxItems) + "\n" } if minimum := jsonSubSchema.Minimum; minimum != nil { metadata += "// Mininum: " + strconv.Itoa(*minimum) + "\n" } if maximum := jsonSubSchema.Maximum; maximum != nil { metadata += "// Maximum: " + strconv.Itoa(*maximum) + "\n" } // Here we check if metadata was specified, and only create new // paragraph (`//\n`) if something was. if len(metadata) > 0 { comment += "//\n" + metadata } if URL := jsonSubSchema.SourceURL; URL != "" { u, err := url.Parse(URL) if err == nil && u.Scheme != "file" { comment += "//\n// See " + URL + "\n" } } for strings.Index(comment, "\n//\n") == 0 { comment = "\n" + comment[4:] } 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 jsonSubSchema.Items != nil { if jsonSubSchema.Items.Type != nil { var arrayType string _, arrayType = jsonSubSchema.Items.typeDefinition(false, extraPackages, rawMessageTypes) typ = "[]" + arrayType } else { if refSubSchema := jsonSubSchema.Items.RefSubSchema; refSubSchema != nil { typ = "[]" + refSubSchema.TypeName } } } else { typ = "[]interface{}" } case "object": if s := jsonSubSchema.Properties; s != nil { typ = fmt.Sprintf("struct {\n") for _, j := range s.SortedPropertyNames { // recursive call to build structs inside structs var subComment, subType string subMember := s.MemberNames[j] subComment, subType = s.Properties[j].typeDefinition(false, extraPackages, rawMessageTypes) jsonStructTagOptions := "" if !s.Properties[j].IsRequired { jsonStructTagOptions = ",omitempty" } // struct member name and type, as part of struct definition typ += fmt.Sprintf("\t%v%v %v `json:\"%v%v\"`\n", subComment, subMember, subType, j, jsonStructTagOptions) } typ += "}" } else { typ = "json.RawMessage" } case "number": typ = "float64" 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 = "tcclient.Time" extraPackages["tcclient \"github.com/taskcluster/taskcluster-client-go\""] = true } } } switch typ { case "json.RawMessage": extraPackages["\"encoding/json\""] = true if topLevel { // 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 } } return comment, typ }