package codescan import ( "encoding/json" "fmt" "go/ast" "regexp" "strconv" "strings" "github.com/go-openapi/loads/fmts" "github.com/go-openapi/spec" "github.com/pkg/errors" "gopkg.in/yaml.v2" ) func shouldAcceptTag(tags []string, includeTags map[string]bool, excludeTags map[string]bool) bool { for _, tag := range tags { if len(includeTags) > 0 { if includeTags[tag] { return true } } else if len(excludeTags) > 0 { if excludeTags[tag] { return false } } } return len(includeTags) == 0 } func shouldAcceptPkg(path string, includePkgs, excludePkgs []string) bool { if len(includePkgs) == 0 && len(excludePkgs) == 0 { return true } for _, pkgName := range includePkgs { matched, _ := regexp.MatchString(pkgName, path) if matched { return true } } for _, pkgName := range excludePkgs { matched, _ := regexp.MatchString(pkgName, path) if matched { return false } } return len(includePkgs) == 0 } // Many thanks go to https://github.com/yvasiyarov/swagger // this is loosely based on that implementation but for swagger 2.0 func joinDropLast(lines []string) string { l := len(lines) lns := lines if l > 0 && len(strings.TrimSpace(lines[l-1])) == 0 { lns = lines[:l-1] } return strings.Join(lns, "\n") } func removeEmptyLines(lines []string) (notEmpty []string) { for _, l := range lines { if len(strings.TrimSpace(l)) > 0 { notEmpty = append(notEmpty, l) } } return } func rxf(rxp, ar string) *regexp.Regexp { return regexp.MustCompile(fmt.Sprintf(rxp, ar)) } func allOfMember(comments *ast.CommentGroup) bool { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { if rxAllOf.MatchString(ln) { return true } } } } return false } func fileParam(comments *ast.CommentGroup) bool { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { if rxFileUpload.MatchString(ln) { return true } } } } return false } func strfmtName(comments *ast.CommentGroup) (string, bool) { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { matches := rxStrFmt.FindStringSubmatch(ln) if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { return strings.TrimSpace(matches[1]), true } } } } return "", false } func ignored(comments *ast.CommentGroup) bool { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { if rxIgnoreOverride.MatchString(ln) { return true } } } } return false } func enumName(comments *ast.CommentGroup) (string, bool) { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { matches := rxEnum.FindStringSubmatch(ln) if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { return strings.TrimSpace(matches[1]), true } } } } return "", false } func aliasParam(comments *ast.CommentGroup) bool { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { if rxAlias.MatchString(ln) { return true } } } } return false } func isAliasParam(prop swaggerTypable) bool { var isParam bool if param, ok := prop.(paramTypable); ok { isParam = param.param.In == "query" || param.param.In == "path" || param.param.In == "formData" } return isParam } func defaultName(comments *ast.CommentGroup) (string, bool) { if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { matches := rxDefault.FindStringSubmatch(ln) if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { return strings.TrimSpace(matches[1]), true } } } } return "", false } func typeName(comments *ast.CommentGroup) (string, bool) { var typ string if comments != nil { for _, cmt := range comments.List { for _, ln := range strings.Split(cmt.Text, "\n") { matches := rxType.FindStringSubmatch(ln) if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { typ = strings.TrimSpace(matches[1]) return typ, true } } } } return "", false } type swaggerTypable interface { Typed(string, string) SetRef(spec.Ref) Items() swaggerTypable Schema() *spec.Schema Level() int AddExtension(key string, value interface{}) } // Map all Go builtin types that have Json representation to Swagger/Json types. // See https://golang.org/pkg/builtin/ and http://swagger.io/specification/ func swaggerSchemaForType(typeName string, prop swaggerTypable) error { switch typeName { case "bool": prop.Typed("boolean", "") case "byte": prop.Typed("integer", "uint8") case "complex128", "complex64": return fmt.Errorf("unsupported builtin %q (no JSON marshaller)", typeName) case "error": // TODO: error is often marshalled into a string but not always (e.g. errors package creates // errors that are marshalled into an empty object), this could be handled the same way // custom JSON marshallers are handled (in future) prop.Typed("string", "") case "float32": prop.Typed("number", "float") case "float64": prop.Typed("number", "double") case "int": prop.Typed("integer", "int64") case "int16": prop.Typed("integer", "int16") case "int32": prop.Typed("integer", "int32") case "int64": prop.Typed("integer", "int64") case "int8": prop.Typed("integer", "int8") case "rune": prop.Typed("integer", "int32") case "string": prop.Typed("string", "") case "uint": prop.Typed("integer", "uint64") case "uint16": prop.Typed("integer", "uint16") case "uint32": prop.Typed("integer", "uint32") case "uint64": prop.Typed("integer", "uint64") case "uint8": prop.Typed("integer", "uint8") case "uintptr": prop.Typed("integer", "uint64") default: return fmt.Errorf("unsupported type %q", typeName) } return nil } func newMultiLineTagParser(name string, parser valueParser, skipCleanUp bool) tagParser { return tagParser{ Name: name, MultiLine: true, SkipCleanUp: skipCleanUp, Parser: parser, } } func newSingleLineTagParser(name string, parser valueParser) tagParser { return tagParser{ Name: name, MultiLine: false, SkipCleanUp: false, Parser: parser, } } type tagParser struct { Name string MultiLine bool SkipCleanUp bool Lines []string Parser valueParser } func (st *tagParser) Matches(line string) bool { return st.Parser.Matches(line) } func (st *tagParser) Parse(lines []string) error { return st.Parser.Parse(lines) } func newYamlParser(rx *regexp.Regexp, setter func(json.RawMessage) error) valueParser { return &yamlParser{ set: setter, rx: rx, } } type yamlParser struct { set func(json.RawMessage) error rx *regexp.Regexp } func (y *yamlParser) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } var uncommented []string uncommented = append(uncommented, removeYamlIndent(lines)...) yamlContent := strings.Join(uncommented, "\n") var yamlValue interface{} err := yaml.Unmarshal([]byte(yamlContent), &yamlValue) if err != nil { return err } var jsonValue json.RawMessage jsonValue, err = fmts.YAMLToJSON(yamlValue) if err != nil { return err } return y.set(jsonValue) } func (y *yamlParser) Matches(line string) bool { return y.rx.MatchString(line) } // aggregates lines in header until it sees `---`, // the beginning of a YAML spec type yamlSpecScanner struct { header []string yamlSpec []string setTitle func([]string) setDescription func([]string) workedOutTitle bool title []string skipHeader bool } func cleanupScannerLines(lines []string, ur *regexp.Regexp, yamlBlock *regexp.Regexp) []string { // bail early when there is nothing to parse if len(lines) == 0 { return lines } seenLine := -1 var lastContent int var uncommented []string var startBlock bool var yamlLines []string for i, v := range lines { if yamlBlock != nil && yamlBlock.MatchString(v) && !startBlock { startBlock = true if seenLine < 0 { seenLine = i } continue } if startBlock { if yamlBlock != nil && yamlBlock.MatchString(v) { startBlock = false uncommented = append(uncommented, removeIndent(yamlLines)...) continue } yamlLines = append(yamlLines, v) if v != "" { if seenLine < 0 { seenLine = i } lastContent = i } continue } str := ur.ReplaceAllString(v, "") uncommented = append(uncommented, str) if str != "" { if seenLine < 0 { seenLine = i } lastContent = i } } // fixes issue #50 if seenLine == -1 { return nil } return uncommented[seenLine : lastContent+1] } // a shared function that can be used to split given headers // into a title and description func collectScannerTitleDescription(headers []string) (title, desc []string) { hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil) idx := -1 for i, line := range hdrs { if strings.TrimSpace(line) == "" { idx = i break } } if idx > -1 { title = hdrs[:idx] if len(hdrs) > idx+1 { desc = hdrs[idx+1:] } else { desc = nil } return } if len(hdrs) > 0 { line := hdrs[0] if rxPunctuationEnd.MatchString(line) { title = []string{line} desc = hdrs[1:] } else { desc = hdrs } } return } func (sp *yamlSpecScanner) collectTitleDescription() { if sp.workedOutTitle { return } if sp.setTitle == nil { sp.header = cleanupScannerLines(sp.header, rxUncommentHeaders, nil) return } sp.workedOutTitle = true sp.title, sp.header = collectScannerTitleDescription(sp.header) } func (sp *yamlSpecScanner) Title() []string { sp.collectTitleDescription() return sp.title } func (sp *yamlSpecScanner) Description() []string { sp.collectTitleDescription() return sp.header } func (sp *yamlSpecScanner) Parse(doc *ast.CommentGroup) error { if doc == nil { return nil } var startedYAMLSpec bool COMMENTS: for _, c := range doc.List { for _, line := range strings.Split(c.Text, "\n") { if rxSwaggerAnnotation.MatchString(line) { break COMMENTS // a new swagger: annotation terminates this parser } if !startedYAMLSpec { if rxBeginYAMLSpec.MatchString(line) { startedYAMLSpec = true sp.yamlSpec = append(sp.yamlSpec, line) continue } if !sp.skipHeader { sp.header = append(sp.header, line) } // no YAML spec yet, moving on continue } sp.yamlSpec = append(sp.yamlSpec, line) } } if sp.setTitle != nil { sp.setTitle(sp.Title()) } if sp.setDescription != nil { sp.setDescription(sp.Description()) } return nil } func (sp *yamlSpecScanner) UnmarshalSpec(u func([]byte) error) (err error) { specYaml := cleanupScannerLines(sp.yamlSpec, rxUncommentYAML, nil) if len(specYaml) == 0 { return errors.New("no spec available to unmarshal") } if !strings.Contains(specYaml[0], "---") { return errors.New("yaml spec has to start with `---`") } // remove indentation specYaml = removeIndent(specYaml) // 1. parse yaml lines yamlValue := make(map[interface{}]interface{}) yamlContent := strings.Join(specYaml, "\n") err = yaml.Unmarshal([]byte(yamlContent), &yamlValue) if err != nil { return } // 2. convert to json var jsonValue json.RawMessage jsonValue, err = fmts.YAMLToJSON(yamlValue) if err != nil { return } // 3. unmarshal the json into an interface var data []byte data, err = jsonValue.MarshalJSON() if err != nil { return } err = u(data) if err != nil { return } // all parsed, returning... sp.yamlSpec = nil // spec is now consumed, so let's erase the parsed lines return } // removes indent base on the first line func removeIndent(spec []string) []string { loc := rxIndent.FindStringIndex(spec[0]) if loc[1] == 0 { return spec } for i := range spec { if len(spec[i]) >= loc[1] { spec[i] = spec[i][loc[1]-1:] } } return spec } // removes indent base on the first line func removeYamlIndent(spec []string) []string { loc := rxIndent.FindStringIndex(spec[0]) if loc[1] == 0 { return nil } var s []string for i := range spec { if len(spec[i]) >= loc[1] { s = append(s, spec[i][loc[1]-1:]) } } return s } // aggregates lines in header until it sees a tag. type sectionedParser struct { header []string matched map[string]tagParser annotation valueParser seenTag bool skipHeader bool setTitle func([]string) setDescription func([]string) workedOutTitle bool taggers []tagParser currentTagger *tagParser title []string ignored bool } func (st *sectionedParser) collectTitleDescription() { if st.workedOutTitle { return } if st.setTitle == nil { st.header = cleanupScannerLines(st.header, rxUncommentHeaders, nil) return } st.workedOutTitle = true st.title, st.header = collectScannerTitleDescription(st.header) } func (st *sectionedParser) Title() []string { st.collectTitleDescription() return st.title } func (st *sectionedParser) Description() []string { st.collectTitleDescription() return st.header } func (st *sectionedParser) Parse(doc *ast.CommentGroup) error { if doc == nil { return nil } COMMENTS: for _, c := range doc.List { for _, line := range strings.Split(c.Text, "\n") { if rxSwaggerAnnotation.MatchString(line) { if rxIgnoreOverride.MatchString(line) { st.ignored = true break COMMENTS // an explicit ignore terminates this parser } if st.annotation == nil || !st.annotation.Matches(line) { break COMMENTS // a new swagger: annotation terminates this parser } _ = st.annotation.Parse([]string{line}) if len(st.header) > 0 { st.seenTag = true } continue } var matched bool for _, tagger := range st.taggers { if tagger.Matches(line) { st.seenTag = true st.currentTagger = &tagger matched = true break } } if st.currentTagger == nil { if !st.skipHeader && !st.seenTag { st.header = append(st.header, line) } // didn't match a tag, moving on continue } if st.currentTagger.MultiLine && matched { // the first line of a multiline tagger doesn't count continue } ts, ok := st.matched[st.currentTagger.Name] if !ok { ts = *st.currentTagger } ts.Lines = append(ts.Lines, line) if st.matched == nil { st.matched = make(map[string]tagParser) } st.matched[st.currentTagger.Name] = ts if !st.currentTagger.MultiLine { st.currentTagger = nil } } } if st.setTitle != nil { st.setTitle(st.Title()) } if st.setDescription != nil { st.setDescription(st.Description()) } for _, mt := range st.matched { if !mt.SkipCleanUp { mt.Lines = cleanupScannerLines(mt.Lines, rxUncommentHeaders, nil) } if err := mt.Parse(mt.Lines); err != nil { return err } } return nil } type validationBuilder interface { SetMaximum(float64, bool) SetMinimum(float64, bool) SetMultipleOf(float64) SetMinItems(int64) SetMaxItems(int64) SetMinLength(int64) SetMaxLength(int64) SetPattern(string) SetUnique(bool) SetEnum(string) SetDefault(interface{}) SetExample(interface{}) } type valueParser interface { Parse([]string) error Matches(string) bool } type operationValidationBuilder interface { validationBuilder SetCollectionFormat(string) } type setMaximum struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMaximum) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 2 && len(matches[2]) > 0 { max, err := strconv.ParseFloat(matches[2], 64) if err != nil { return err } sm.builder.SetMaximum(max, matches[1] == "<") } return nil } func (sm *setMaximum) Matches(line string) bool { return sm.rx.MatchString(line) } type setMinimum struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMinimum) Matches(line string) bool { return sm.rx.MatchString(line) } func (sm *setMinimum) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 2 && len(matches[2]) > 0 { min, err := strconv.ParseFloat(matches[2], 64) if err != nil { return err } sm.builder.SetMinimum(min, matches[1] == ">") } return nil } type setMultipleOf struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMultipleOf) Matches(line string) bool { return sm.rx.MatchString(line) } func (sm *setMultipleOf) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 2 && len(matches[1]) > 0 { multipleOf, err := strconv.ParseFloat(matches[1], 64) if err != nil { return err } sm.builder.SetMultipleOf(multipleOf) } return nil } type setMaxItems struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMaxItems) Matches(line string) bool { return sm.rx.MatchString(line) } func (sm *setMaxItems) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { maxItems, err := strconv.ParseInt(matches[1], 10, 64) if err != nil { return err } sm.builder.SetMaxItems(maxItems) } return nil } type setMinItems struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMinItems) Matches(line string) bool { return sm.rx.MatchString(line) } func (sm *setMinItems) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { minItems, err := strconv.ParseInt(matches[1], 10, 64) if err != nil { return err } sm.builder.SetMinItems(minItems) } return nil } type setMaxLength struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMaxLength) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { maxLength, err := strconv.ParseInt(matches[1], 10, 64) if err != nil { return err } sm.builder.SetMaxLength(maxLength) } return nil } func (sm *setMaxLength) Matches(line string) bool { return sm.rx.MatchString(line) } type setMinLength struct { builder validationBuilder rx *regexp.Regexp } func (sm *setMinLength) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { minLength, err := strconv.ParseInt(matches[1], 10, 64) if err != nil { return err } sm.builder.SetMinLength(minLength) } return nil } func (sm *setMinLength) Matches(line string) bool { return sm.rx.MatchString(line) } type setPattern struct { builder validationBuilder rx *regexp.Regexp } func (sm *setPattern) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { sm.builder.SetPattern(matches[1]) } return nil } func (sm *setPattern) Matches(line string) bool { return sm.rx.MatchString(line) } type setCollectionFormat struct { builder operationValidationBuilder rx *regexp.Regexp } func (sm *setCollectionFormat) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sm.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { sm.builder.SetCollectionFormat(matches[1]) } return nil } func (sm *setCollectionFormat) Matches(line string) bool { return sm.rx.MatchString(line) } type setUnique struct { builder validationBuilder rx *regexp.Regexp } func (su *setUnique) Matches(line string) bool { return su.rx.MatchString(line) } func (su *setUnique) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := su.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } su.builder.SetUnique(req) } return nil } type setEnum struct { builder validationBuilder rx *regexp.Regexp } func (se *setEnum) Matches(line string) bool { return se.rx.MatchString(line) } func (se *setEnum) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := se.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { se.builder.SetEnum(matches[1]) } return nil } func parseValueFromSchema(s string, schema *spec.SimpleSchema) (interface{}, error) { if schema != nil { switch strings.Trim(schema.TypeName(), "\"") { case "integer", "int", "int64", "int32", "int16": return strconv.Atoi(s) case "bool", "boolean": return strconv.ParseBool(s) case "number", "float64", "float32": return strconv.ParseFloat(s, 64) case "object": var obj map[string]interface{} if err := json.Unmarshal([]byte(s), &obj); err != nil { // If we can't parse it, just return the string. return s, nil } return obj, nil case "array": var slice []interface{} if err := json.Unmarshal([]byte(s), &slice); err != nil { // If we can't parse it, just return the string. return s, nil } return slice, nil default: return s, nil } } else { return s, nil } } type setDefault struct { scheme *spec.SimpleSchema builder validationBuilder rx *regexp.Regexp } func (sd *setDefault) Matches(line string) bool { return sd.rx.MatchString(line) } func (sd *setDefault) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := sd.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { d, err := parseValueFromSchema(matches[1], sd.scheme) if err != nil { return err } sd.builder.SetDefault(d) } return nil } type setExample struct { scheme *spec.SimpleSchema builder validationBuilder rx *regexp.Regexp } func (se *setExample) Matches(line string) bool { return se.rx.MatchString(line) } func (se *setExample) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := se.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { d, err := parseValueFromSchema(matches[1], se.scheme) if err != nil { return err } se.builder.SetExample(d) } return nil } type matchOnlyParam struct { tgt *spec.Parameter rx *regexp.Regexp } func (mo *matchOnlyParam) Matches(line string) bool { return mo.rx.MatchString(line) } func (mo *matchOnlyParam) Parse(lines []string) error { return nil } type setRequiredParam struct { tgt *spec.Parameter } func (su *setRequiredParam) Matches(line string) bool { return rxRequired.MatchString(line) } func (su *setRequiredParam) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := rxRequired.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } su.tgt.Required = req } return nil } type setReadOnlySchema struct { tgt *spec.Schema } func (su *setReadOnlySchema) Matches(line string) bool { return rxReadOnly.MatchString(line) } func (su *setReadOnlySchema) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := rxReadOnly.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } su.tgt.ReadOnly = req } return nil } type setDeprecatedOp struct { tgt *spec.Operation } func (su *setDeprecatedOp) Matches(line string) bool { return rxDeprecated.MatchString(line) } func (su *setDeprecatedOp) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := rxDeprecated.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } su.tgt.Deprecated = req } return nil } type setDiscriminator struct { schema *spec.Schema field string } func (su *setDiscriminator) Matches(line string) bool { return rxDiscriminator.MatchString(line) } func (su *setDiscriminator) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := rxDiscriminator.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } if req { su.schema.Discriminator = su.field } else if su.schema.Discriminator == su.field { su.schema.Discriminator = "" } } return nil } type setRequiredSchema struct { schema *spec.Schema field string } func (su *setRequiredSchema) Matches(line string) bool { return rxRequired.MatchString(line) } func (su *setRequiredSchema) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := rxRequired.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { req, err := strconv.ParseBool(matches[1]) if err != nil { return err } midx := -1 for i, nm := range su.schema.Required { if nm == su.field { midx = i break } } if req { if midx < 0 { su.schema.Required = append(su.schema.Required, su.field) } } else if midx >= 0 { su.schema.Required = append(su.schema.Required[:midx], su.schema.Required[midx+1:]...) } } return nil } func newMultilineDropEmptyParser(rx *regexp.Regexp, set func([]string)) *multiLineDropEmptyParser { return &multiLineDropEmptyParser{ rx: rx, set: set, } } type multiLineDropEmptyParser struct { set func([]string) rx *regexp.Regexp } func (m *multiLineDropEmptyParser) Matches(line string) bool { return m.rx.MatchString(line) } func (m *multiLineDropEmptyParser) Parse(lines []string) error { m.set(removeEmptyLines(lines)) return nil } func newSetSchemes(set func([]string)) *setSchemes { return &setSchemes{ set: set, rx: rxSchemes, } } type setSchemes struct { set func([]string) rx *regexp.Regexp } func (ss *setSchemes) Matches(line string) bool { return ss.rx.MatchString(line) } func (ss *setSchemes) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } matches := ss.rx.FindStringSubmatch(lines[0]) if len(matches) > 1 && len(matches[1]) > 0 { sch := strings.Split(matches[1], ", ") schemes := []string{} for _, s := range sch { ts := strings.TrimSpace(s) if ts != "" { schemes = append(schemes, ts) } } ss.set(schemes) } return nil } func newSetSecurity(rx *regexp.Regexp, setter func([]map[string][]string)) *setSecurity { return &setSecurity{ set: setter, rx: rx, } } type setSecurity struct { set func([]map[string][]string) rx *regexp.Regexp } func (ss *setSecurity) Matches(line string) bool { return ss.rx.MatchString(line) } func (ss *setSecurity) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } var result []map[string][]string for _, line := range lines { kv := strings.SplitN(line, ":", 2) scopes := []string{} var key string if len(kv) > 1 { scs := strings.Split(kv[1], ",") for _, scope := range scs { tr := strings.TrimSpace(scope) if tr != "" { tr = strings.SplitAfter(tr, " ")[0] scopes = append(scopes, strings.TrimSpace(tr)) } } key = strings.TrimSpace(kv[0]) result = append(result, map[string][]string{key: scopes}) } } ss.set(result) return nil } func newSetResponses(definitions map[string]spec.Schema, responses map[string]spec.Response, setter func(*spec.Response, map[int]spec.Response)) *setOpResponses { return &setOpResponses{ set: setter, rx: rxResponses, definitions: definitions, responses: responses, } } type setOpResponses struct { set func(*spec.Response, map[int]spec.Response) rx *regexp.Regexp definitions map[string]spec.Schema responses map[string]spec.Response } func (ss *setOpResponses) Matches(line string) bool { return ss.rx.MatchString(line) } //ResponseTag used when specifying a response to point to a defined swagger:response const ResponseTag = "response" //BodyTag used when specifying a response to point to a model/schema const BodyTag = "body" //DescriptionTag used when specifying a response that gives a description of the response const DescriptionTag = "description" func parseTags(line string) (modelOrResponse string, arrays int, isDefinitionRef bool, description string, err error) { tags := strings.Split(line, " ") parsedModelOrResponse := false for i, tagAndValue := range tags { tagValList := strings.SplitN(tagAndValue, ":", 2) var tag, value string if len(tagValList) > 1 { tag = tagValList[0] value = tagValList[1] } else { //TODO: Print a warning, and in the long term, do not support not tagged values //Add a default tag if none is supplied if i == 0 { tag = ResponseTag } else { tag = DescriptionTag } value = tagValList[0] } foundModelOrResponse := false if !parsedModelOrResponse { if tag == BodyTag { foundModelOrResponse = true isDefinitionRef = true } if tag == ResponseTag { foundModelOrResponse = true isDefinitionRef = false } } if foundModelOrResponse { //Read the model or response tag parsedModelOrResponse = true //Check for nested arrays arrays = 0 for strings.HasPrefix(value, "[]") { arrays++ value = value[2:] } //What's left over is the model name modelOrResponse = value } else { foundDescription := false if tag == DescriptionTag { foundDescription = true } if foundDescription { //Descriptions are special, they make they read the rest of the line descriptionWords := []string{value} if i < len(tags)-1 { descriptionWords = append(descriptionWords, tags[i+1:]...) } description = strings.Join(descriptionWords, " ") break } else { if tag == ResponseTag || tag == BodyTag || tag == DescriptionTag { err = fmt.Errorf("valid tag %s, but not in a valid position", tag) } else { err = fmt.Errorf("invalid tag: %s", tag) } //return error return } } } //TODO: Maybe do, if !parsedModelOrResponse {return some error} return } func (ss *setOpResponses) Parse(lines []string) error { if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { return nil } var def *spec.Response var scr map[int]spec.Response for _, line := range lines { kv := strings.SplitN(line, ":", 2) var key, value string if len(kv) > 1 { key = strings.TrimSpace(kv[0]) if key == "" { // this must be some weird empty line continue } value = strings.TrimSpace(kv[1]) if value == "" { var resp spec.Response if strings.EqualFold("default", key) { if def == nil { def = &resp } } else { if sc, err := strconv.Atoi(key); err == nil { if scr == nil { scr = make(map[int]spec.Response) } scr[sc] = resp } } continue } refTarget, arrays, isDefinitionRef, description, err := parseTags(value) if err != nil { return err } //A possible exception for having a definition if _, ok := ss.responses[refTarget]; !ok { if _, ok := ss.definitions[refTarget]; ok { isDefinitionRef = true } } var ref spec.Ref if isDefinitionRef { if description == "" { description = refTarget } ref, err = spec.NewRef("#/definitions/" + refTarget) } else { ref, err = spec.NewRef("#/responses/" + refTarget) } if err != nil { return err } // description should used on anyway. resp := spec.Response{ResponseProps: spec.ResponseProps{Description: description}} if isDefinitionRef { resp.Schema = new(spec.Schema) resp.Description = description if arrays == 0 { resp.Schema.Ref = ref } else { cs := resp.Schema for i := 0; i < arrays; i++ { cs.Typed("array", "") cs.Items = new(spec.SchemaOrArray) cs.Items.Schema = new(spec.Schema) cs = cs.Items.Schema } cs.Ref = ref } // ref. could be empty while use description tag } else if len(refTarget) > 0 { resp.Ref = ref } if strings.EqualFold("default", key) { if def == nil { def = &resp } } else { if sc, err := strconv.Atoi(key); err == nil { if scr == nil { scr = make(map[int]spec.Response) } scr[sc] = resp } } } } ss.set(def, scr) return nil } func parseEnum(val string, s *spec.SimpleSchema) []interface{} { list := strings.Split(val, ",") interfaceSlice := make([]interface{}, len(list)) for i, d := range list { v, err := parseValueFromSchema(d, s) if err != nil { interfaceSlice[i] = d continue } interfaceSlice[i] = v } return interfaceSlice }