diff --git a/oas.go b/oas.go index 94e1d34..95def55 100644 --- a/oas.go +++ b/oas.go @@ -1,6 +1,11 @@ package main -import "github.com/iancoleman/orderedmap" +import ( + "fmt" + "strings" + + "github.com/iancoleman/orderedmap" +) const ( OpenAPIVersion = "3.0.0" @@ -191,6 +196,207 @@ type ResponseObject struct { // Links } +type ResponseObjectPackage struct { + pkgPath string + pkgName string + jsonType string + goType string + desc string + p *parser + RO *ResponseObject + handlerError error +} + +func newMediaTypeObjectCustomSchema(schema SchemaObject) *MediaTypeObject { + return &MediaTypeObject{ + Schema: schema, + } +} + +func newMediaTypeObjectCustomRef(typeName string) *MediaTypeObject { + return &MediaTypeObject{ + Schema: SchemaObject{ + Ref: addSchemaRefLinkPrefix(typeName), + }, + } +} + +func newMediaTypeObjectCustomType(mediaType string) *MediaTypeObject { + return &MediaTypeObject{ + Schema: SchemaObject{ + Type: mediaType, + }, + } +} + +type ResponseObjectHandler interface { + execute(*ResponseObjectPackage) + setNext(ResponseObjectHandler) +} + +type EmptyType struct { + next ResponseObjectHandler +} + +type TerminalMediaType struct { + next ResponseObjectHandler +} +type RefType struct { + next ResponseObjectHandler +} + +type ComplexGoType struct { + next ResponseObjectHandler +} + +type SimpleGoType struct { + next ResponseObjectHandler +} + +type FailType struct { + next ResponseObjectHandler +} + +func (mt *EmptyType) execute(ROP *ResponseObjectPackage) { + if ROP.jsonType != "" { + mt.next.execute(ROP) + } +} + +func (mt *EmptyType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func (mt *FailType) execute(ROP *ResponseObjectPackage) { + ROP.handlerError = fmt.Errorf("getResponseObject - unknown json type %v", ROP.jsonType) +} + +func (mt *FailType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func (mt *TerminalMediaType) execute(ROP *ResponseObjectPackage) { + if isTerminal(ROP.jsonType) { + ROP.RO.Content[ContentTypeJson] = newMediaTypeObjectCustomType(cleanBrackets(ROP.jsonType)) + return + } + mt.next.execute(ROP) +} + +func (mt *TerminalMediaType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func (mt *ComplexGoType) execute(ROP *ResponseObjectPackage) { + if isComplex(ROP.jsonType) && isComplexGoType(ROP.goType) { + schema, err := ROP.p.parseSchemaObject(ROP.pkgPath, ROP.pkgName, ROP.goType) + if err != nil { + ROP.p.debug("parseResponseComment: cannot parse goType", ROP.goType) + } + ROP.RO.Content[ContentTypeJson] = newMediaTypeObjectCustomSchema(*schema) + return + } + mt.next.execute(ROP) + +} +func (mt *ComplexGoType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func (mt *SimpleGoType) execute(ROP *ResponseObjectPackage) { + typeName, err := ROP.p.registerType(ROP.pkgPath, ROP.pkgName, ROP.goType) + if err != nil { + ROP.handlerError = err + return + } + + if isComplex(ROP.jsonType) && isBasicGoType(typeName) { + ROP.RO.Content[ContentTypeText] = newMediaTypeObjectCustomType("string") + return + } + + mt.next.execute(ROP) + +} + +func (mt *SimpleGoType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func (mt *RefType) execute(ROP *ResponseObjectPackage) { + typeName, err := ROP.p.registerType(ROP.pkgPath, ROP.pkgName, ROP.goType) + if err != nil { + ROP.handlerError = err + return + } + + if isComplex(ROP.jsonType) && !isBasicGoType(typeName) { + ROP.RO.Content[ContentTypeJson] = newMediaTypeObjectCustomRef(addSchemaRefLinkPrefix(typeName)) + return + } + + mt.next.execute(ROP) + +} + +func (mt *RefType) setNext(ROH ResponseObjectHandler) { + mt.next = ROH +} + +func getResponseObject(ROP ResponseObjectPackage) (*ResponseObject, error) { + responseObject := &ResponseObject{ + Content: map[string]*MediaTypeObject{}, + } + responseObject.Description = strings.Trim(ROP.desc, "\"") + + ROP.RO = responseObject + + //linked list of ResponseObjects handlers + + emptyResponseObject := &EmptyType{} + terminalResponseObject := &TerminalMediaType{} + ComplexGoResponseObject := &ComplexGoType{} + SimpleGoResponseObject := &SimpleGoType{} + RefGoResponseObject := &RefType{} + fail := &FailType{} + + emptyResponseObject.setNext(terminalResponseObject) + terminalResponseObject.setNext(ComplexGoResponseObject) + ComplexGoResponseObject.setNext(SimpleGoResponseObject) + SimpleGoResponseObject.setNext(RefGoResponseObject) + RefGoResponseObject.setNext(fail) + + emptyResponseObject.execute(&ROP) + + /* + if isTerminal(ROP.jsonType) { + responseObject.Content[ContentTypeJson] = newMediaTypeObjectCustomType(cleanBrackets(ROP.jsonType)) + } else if isComplex(ROP.jsonType) { + if isComplexGoType(ROP.goType) { + schema, err := ROP.p.parseSchemaObject(ROP.pkgPath, ROP.pkgName, ROP.goType) + if err != nil { + ROP.p.debug("parseResponseComment: cannot parse goType", ROP.goType) + } + responseObject.Content[ContentTypeJson] = newMediaTypeObjectCustomSchema(*schema) + } else { + typeName, err := ROP.p.registerType(ROP.pkgPath, ROP.pkgName, ROP.goType) + if err != nil { + return nil, err + } + if isBasicGoType(typeName) { + responseObject.Content[ContentTypeText] = newMediaTypeObjectCustomType("string") + } else { + responseObject.Content[ContentTypeJson] = newMediaTypeObjectCustomRef(addSchemaRefLinkPrefix(typeName)) + } + } + } else if ROP.jsonType != "" { + return nil, fmt.Errorf("getResponseObject - unknown json type %v", ROP.jsonType) + } + + */ + return ROP.RO, ROP.handlerError +} + type HeaderObject struct { Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` diff --git a/parser.go b/parser.go index 402eb57..93b7ac6 100644 --- a/parser.go +++ b/parser.go @@ -647,7 +647,8 @@ func (p *parser) parseOperation(pkgPath, pkgName string, astComments []*ast.Comm for _, astComment := range astComments { comment := strings.TrimSpace(strings.TrimLeft(astComment.Text, "/")) if len(comment) == 0 { - return nil + // ignore empty lines + continue } attribute := strings.Fields(comment)[0] switch strings.ToLower(attribute) { @@ -800,56 +801,35 @@ func (p *parser) parseParamComment(pkgPath, pkgName string, operation *Operation func (p *parser) parseResponseComment(pkgPath, pkgName string, operation *OperationObject, comment string) error { // {status} {jsonType} {goType} {description} // 201 object models.User "User Model" - re := regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\[\]]+)[^"]*(.*)?`) + // if 204 or something else without empty return payload + // 204 "User Model" + re := regexp.MustCompile(`(?P[\d]+)[\s]*(?P[\w\{\}]+)?[\s]+(?P[\w\-\.\/\[\]]+)?[^"]*(?P.*)?`) matches := re.FindStringSubmatch(comment) - if len(matches) != 5 { - return fmt.Errorf("parseResponseComment can not parse response comment \"%s\"", comment) + + paramsMap := make(map[string]string) + for i, name := range re.SubexpNames() { + if i > 0 && i <= len(matches) { + paramsMap[name] = matches[i] + } } - status := matches[1] - _, err := strconv.Atoi(matches[1]) + if len(matches) <= 2 { + return fmt.Errorf("parseResponseComment can not parse response comment \"%s\", matches: %v", comment, matches) + } + + status := paramsMap["status"] + _, err := strconv.Atoi(status) if err != nil { return fmt.Errorf("parseResponseComment: http status must be int, but got %s", status) } - switch matches[2] { - case "object", "array", "{object}", "{array}": - default: - return fmt.Errorf("parseResponseComment: invalid jsonType %s", matches[2]) - } - responseObject := &ResponseObject{ - Content: map[string]*MediaTypeObject{}, - } - responseObject.Description = strings.Trim(matches[4], "\"") - re = regexp.MustCompile(`\[\w*\]`) - goType := re.ReplaceAllString(matches[3], "[]") - if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[]") { - schema, err := p.parseSchemaObject(pkgPath, pkgName, goType) - if err != nil { - p.debug("parseResponseComment cannot parse goType", goType) - } - responseObject.Content[ContentTypeJson] = &MediaTypeObject{ - Schema: *schema, - } - } else { - typeName, err := p.registerType(pkgPath, pkgName, matches[3]) - if err != nil { - return err - } - if isBasicGoType(typeName) { - responseObject.Content[ContentTypeText] = &MediaTypeObject{ - Schema: SchemaObject{ - Type: "string", - }, - } - } else { - responseObject.Content[ContentTypeJson] = &MediaTypeObject{ - Schema: SchemaObject{ - Ref: addSchemaRefLinkPrefix(typeName), - }, - } - } + RespObjPkg := ResponseObjectPackage{pkgPath, pkgName, paramsMap["jsonType"], paramsMap["goType"], paramsMap["description"], p, nil, nil} + + responseObject, err := getResponseObject(RespObjPkg) + if err != nil { + return err } + operation.Responses[status] = responseObject return nil diff --git a/util.go b/util.go index e531910..3ad3f9e 100644 --- a/util.go +++ b/util.go @@ -69,6 +69,37 @@ func isInStringList(list []string, s string) bool { return false } +func cleanBrackets(s string) string { + if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") { + return s[1 : len(s)-1] + } + return s +} + +func isTerminal(s string) bool { + cleanString := cleanBrackets(s) + _, ok := TerminalTypes[cleanString] + return ok +} + +func isComplex(s string) bool { + cleanString := cleanBrackets(s) + _, ok := ComplexTypes[cleanString] + return ok +} + +var TerminalTypes = map[string]bool{ + "string": true, + "number": true, + "integer": true, + "boolean": true, +} + +var ComplexTypes = map[string]bool{ + "array": true, + "object": true, +} + var basicGoTypes = map[string]bool{ "bool": true, "uint": true, @@ -97,6 +128,10 @@ func isBasicGoType(typeName string) bool { return ok } +func isComplexGoType(typeName string) bool { + return strings.HasPrefix(typeName, "[]") || strings.HasPrefix(typeName, "map[]") +} + var goTypesOASTypes = map[string]string{ "bool": "boolean", "uint": "integer",