diff --git a/common/util/util.go b/common/util/util.go index c55811e..fabb63f 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -102,8 +102,10 @@ func Capitalize(s string) string { // AppendStrings adds non-empty strings from in to out and returns a new slice. func AppendStrings(out []string, in []string, prefix string) []string { for _, s := range in { - if s != "" { - out = append(out, prefix+s) + for _, line := range strings.Split(s, "\n") { + if line != "" { + out = append(out, prefix+line) + } } } return out diff --git a/main.go b/main.go index ddfc695..b08d16e 100644 --- a/main.go +++ b/main.go @@ -52,7 +52,34 @@ func main() { opt := renderer.NewOptions() opt.DeReference = true - swagger := openapi.NewOpenAPIRenderer(openapi.NewMetaData("", ""), opt) + meta := openapi.NewMetaData("main.go demo", "v1.0.0") + meta.Info.Description = "Demonstration of OpenAPI rendering." + meta.Info.TermsOfService = "https://some.site/tos" + meta.Info.Contact = &openapi.ContactObject{ + Name: "Contact Name", + URL: "https://some.site/contact", + Email: "contact@some.site", + } + meta.Info.License = &openapi.LicenseObject{ + Name: "This is the license.", + URL: "https://license.server/license", + } + meta.ExternalDocs = &openapi.ExternalDocumentationObject{ + URL: "https://some.site/docs", + Description: "This is the documentation site.", + } + meta.Servers = []*openapi.ServerObject{ + { + URL: "https://a.b.com/", + Description: "Main server", + }, + { + URL: "https://a.b.c.com/", + Description: "Backup server", + }, + } + + swagger := openapi.NewOpenAPIRenderer(meta, opt) outLines, err := swagger.ProcessSchema(schema) if err != nil { fmt.Println(err) diff --git a/main_test.go b/main_test.go index 2a54f4e..83d7a93 100644 --- a/main_test.go +++ b/main_test.go @@ -256,9 +256,9 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringStruct`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:StringStruct`, }, true: []string{ `Root.{}`, @@ -273,9 +273,9 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringStruct`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:StringStruct`, }, true: []string{ `Root.{}`, @@ -290,8 +290,8 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ - `TypeRef.!PrivateStruct:{}! ERROR:struct has no exported fields`, `Root.!{}:PrivateStruct! ERROR:struct has no exported fields`, + `TypeRef.!PrivateStruct:{}! ERROR:struct has no exported fields`, }, true: []string{ `Root.!{}! ERROR:struct has no exported fields`, @@ -306,9 +306,9 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringStruct`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:StringStruct`, }, true: []string{ `Root.{}`, @@ -323,9 +323,9 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringStruct`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:StringStruct`, }, true: []string{ `Root.{}`, @@ -340,9 +340,9 @@ var rootGoTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringStruct`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:StringStruct`, }, true: []string{ `Root.{}`, @@ -471,9 +471,9 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:BoolTypes`, `TypeRef.BoolTypes:{}`, `TypeRef.BoolTypes:{}.Bool:boolean`, - `Root.{}:BoolTypes`, }, true: []string{ `Root.{}`, @@ -482,19 +482,11 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: boolean`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, - `components:`, - ` schemas:`, - ` BoolTypes:`, - ` type: object`, - ` additionalProperties: false`, - ` properties:`, - ` Bool:`, - ` type: boolean`, `paths:`, ` /03-type/boolean:`, ` get:`, @@ -506,12 +498,20 @@ var typeTests = []fixtures.TestCase{ ` application/json:`, ` schema:`, ` $ref: '#/components/schemas/BoolTypes'`, + `components:`, + ` schemas:`, + ` BoolTypes:`, + ` type: object`, + ` additionalProperties: false`, + ` properties:`, + ` Bool:`, + ` type: boolean`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: boolean`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/boolean:`, @@ -539,6 +539,7 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:IntegerTypes`, `TypeRef.IntegerTypes:{}`, `TypeRef.IntegerTypes:{}.Int:integer`, `TypeRef.IntegerTypes:{}.Int16:integer`, @@ -551,7 +552,6 @@ var typeTests = []fixtures.TestCase{ `TypeRef.IntegerTypes:{}.Uint64:integer`, `TypeRef.IntegerTypes:{}.Uint8:integer`, `TypeRef.IntegerTypes:{}.Uintptr:integer`, - `Root.{}:IntegerTypes`, }, true: []string{ `Root.{}`, @@ -570,11 +570,22 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: integer`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /03-type/integer:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/IntegerTypes'`, `components:`, ` schemas:`, ` IntegerTypes:`, @@ -605,23 +616,12 @@ var typeTests = []fixtures.TestCase{ ` type: integer`, ` Uintptr:`, ` type: integer`, - `paths:`, - ` /03-type/integer:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/IntegerTypes'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: integer`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/integer:`, @@ -671,10 +671,10 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:FloatTypes`, `TypeRef.FloatTypes:{}`, `TypeRef.FloatTypes:{}.Float32:float`, `TypeRef.FloatTypes:{}.Float64:float`, - `Root.{}:FloatTypes`, }, true: []string{ `Root.{}`, @@ -684,22 +684,11 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: float`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, - `components:`, - ` schemas:`, - ` FloatTypes:`, - ` type: object`, - ` additionalProperties: false`, - ` properties:`, - ` Float32:`, - ` type: number`, - ` Float64:`, - ` type: number`, - ` format: double`, `paths:`, ` /03-type/float:`, ` get:`, @@ -711,12 +700,23 @@ var typeTests = []fixtures.TestCase{ ` application/json:`, ` schema:`, ` $ref: '#/components/schemas/FloatTypes'`, + `components:`, + ` schemas:`, + ` FloatTypes:`, + ` type: object`, + ` additionalProperties: false`, + ` properties:`, + ` Float32:`, + ` type: number`, + ` Float64:`, + ` type: number`, + ` format: double`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: float`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/float:`, @@ -747,9 +747,9 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:StringTypes`, `TypeRef.StringTypes:{}`, `TypeRef.StringTypes:{}.String:string`, - `Root.{}:StringTypes`, }, true: []string{ `Root.{}`, @@ -758,19 +758,11 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: string`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, - `components:`, - ` schemas:`, - ` StringTypes:`, - ` type: object`, - ` additionalProperties: false`, - ` properties:`, - ` String:`, - ` type: string`, `paths:`, ` /03-type/string:`, ` get:`, @@ -782,12 +774,20 @@ var typeTests = []fixtures.TestCase{ ` application/json:`, ` schema:`, ` $ref: '#/components/schemas/StringTypes'`, + `components:`, + ` schemas:`, + ` StringTypes:`, + ` type: object`, + ` additionalProperties: false`, + ` properties:`, + ` String:`, + ` type: string`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: string`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/string:`, @@ -815,13 +815,13 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:InvalidTypes`, `TypeRef.InvalidTypes:{}`, `TypeRef.InvalidTypes:{}.!Chan:invalid:chan! ERROR:kind not supported`, `TypeRef.InvalidTypes:{}.!Complex128:invalid:complex128! ERROR:kind not supported`, `TypeRef.InvalidTypes:{}.!Complex64:invalid:complex64! ERROR:kind not supported`, `TypeRef.InvalidTypes:{}.!Func:invalid:func! ERROR:kind not supported`, `TypeRef.InvalidTypes:{}."!UnsafePointer:invalid:unsafe.Pointer!" ERROR:kind not supported`, - `Root.{}:InvalidTypes`, }, true: []string{ `Root.{}`, @@ -834,11 +834,22 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: invalid`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /03-type/invalid:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/InvalidTypes'`, `components:`, ` schemas:`, ` InvalidTypes:`, @@ -860,23 +871,12 @@ var typeTests = []fixtures.TestCase{ ` UnsafePointer:`, ` description: 'ERROR=kind not supported;Kind=invalid:unsafe.Pointer'`, ` type: string`, - `paths:`, - ` /03-type/invalid:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/InvalidTypes'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: invalid`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/invalid:`, @@ -917,6 +917,7 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:CompoundTypes`, `TypeRef.CompoundTypes:{}`, `TypeRef.CompoundTypes:{}.Array0:[]`, `TypeRef.CompoundTypes:{}.Array0:[].string`, @@ -932,7 +933,6 @@ var typeTests = []fixtures.TestCase{ `TypeRef.!PrivateStruct:{}! ERROR:struct has no exported fields`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:CompoundTypes`, }, true: []string{ `Root.{}`, @@ -952,11 +952,22 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: compound`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /03-type/compound:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/CompoundTypes'`, `components:`, ` schemas:`, ` CompoundTypes:`, @@ -1001,23 +1012,12 @@ var typeTests = []fixtures.TestCase{ ` properties:`, ` Value:`, ` type: string`, - `paths:`, - ` /03-type/compound:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/CompoundTypes'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: compound`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/compound:`, @@ -1078,9 +1078,9 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:SpecialTypes`, `TypeRef.SpecialTypes:{}`, `TypeRef.SpecialTypes:{}.DateTime:datetime`, - `Root.{}:SpecialTypes`, }, true: []string{ `Root.{}`, @@ -1089,20 +1089,11 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: special`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, - `components:`, - ` schemas:`, - ` SpecialTypes:`, - ` type: object`, - ` additionalProperties: false`, - ` properties:`, - ` DateTime:`, - ` type: string`, - ` format: date-time`, `paths:`, ` /03-type/special:`, ` get:`, @@ -1114,12 +1105,21 @@ var typeTests = []fixtures.TestCase{ ` application/json:`, ` schema:`, ` $ref: '#/components/schemas/SpecialTypes'`, + `components:`, + ` schemas:`, + ` SpecialTypes:`, + ` type: object`, + ` additionalProperties: false`, + ` properties:`, + ` DateTime:`, + ` type: string`, + ` format: date-time`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: special`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/special:`, @@ -1148,6 +1148,7 @@ var typeTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:RedefineStruct`, `TypeRef.MyArray0:[]`, `TypeRef.MyArray0:[].string`, `TypeRef.MyArray3:[]`, @@ -1203,7 +1204,6 @@ var typeTests = []fixtures.TestCase{ `TypeRef.RedefineStruct:{}.Uintptr:integer:MyUintptr`, `TypeRef.StringStruct:{}`, `TypeRef.StringStruct:{}.Value:string`, - `Root.{}:RedefineStruct`, }, true: []string{ `Root.{}`, @@ -1241,11 +1241,22 @@ var typeTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: redefined`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /03-type/redefined:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/RedefineStruct'`, `components:`, ` schemas:`, ` MyArray0:`, @@ -1376,23 +1387,12 @@ var typeTests = []fixtures.TestCase{ ` properties:`, ` Value:`, ` type: string`, - `paths:`, - ` /03-type/redefined:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/RedefineStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: redefined`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /03-type/redefined:`, @@ -1540,6 +1540,7 @@ var listTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:ArrayStruct`, `TypeRef.ArrayStruct:{}`, `TypeRef.ArrayStruct:{}.Array0:[]`, `TypeRef.ArrayStruct:{}.Array0:[].string`, @@ -1548,7 +1549,6 @@ var listTests = []fixtures.TestCase{ `TypeRef.ArrayStruct:{}.Array2_3:[].[].string`, `TypeRef.ArrayStruct:{}.Array3:[]`, `TypeRef.ArrayStruct:{}.Array3:[].string`, - `Root.{}:ArrayStruct`, }, true: []string{ `Root.{}`, @@ -1563,11 +1563,22 @@ var listTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: arrays`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /04-list/arrays:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/ArrayStruct'`, `components:`, ` schemas:`, ` ArrayStruct:`, @@ -1588,23 +1599,12 @@ var listTests = []fixtures.TestCase{ ` type: array`, ` items:`, ` type: string`, - `paths:`, - ` /04-list/arrays:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/ArrayStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: arrays`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /04-list/arrays:`, @@ -1666,10 +1666,10 @@ var listTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-array`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /04-list/json-array:`, @@ -1702,10 +1702,10 @@ var listTests = []fixtures.TestCase{ ` type: string`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-array`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /04-list/json-array:`, @@ -1746,13 +1746,13 @@ var listTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:SliceStruct`, `TypeRef.SliceStruct:{}`, `TypeRef.SliceStruct:{}.Array2:[]`, `TypeRef.SliceStruct:{}.Array2:[].[]`, `TypeRef.SliceStruct:{}.Array2:[].[].string`, `TypeRef.SliceStruct:{}.Slice:[]`, `TypeRef.SliceStruct:{}.Slice:[].string`, - `Root.{}:SliceStruct`, }, true: []string{ `Root.{}`, @@ -1765,11 +1765,22 @@ var listTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: slices`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /04-list/slices:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/SliceStruct'`, `components:`, ` schemas:`, ` SliceStruct:`, @@ -1786,23 +1797,12 @@ var listTests = []fixtures.TestCase{ ` type: array`, ` items:`, ` type: string`, - `paths:`, - ` /04-list/slices:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/SliceStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: slices`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /04-list/slices:`, @@ -1877,6 +1877,7 @@ var compoundTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:MapTestsStruct`, `TypeRef.MapTestsStruct:{}`, `TypeRef.MapTestsStruct:{}.MapOK:{}`, `TypeRef.MapTestsStruct:{}.MapOK:{}.BoolVal:boolean`, @@ -1890,7 +1891,6 @@ var compoundTests = []fixtures.TestCase{ `TypeRef.MapTestsStruct:{}.MapOK:{}.MapVal:{}.Key2:{}.DeepKey1:string`, `TypeRef.MapTestsStruct:{}.MapOK:{}.MapVal:{}.Key2:{}.DeepKey2:float`, `TypeRef.MapTestsStruct:{}.MapOK:{}.StringVal:string`, - `Root.{}:MapTestsStruct`, }, true: []string{ `Root.{}`, @@ -1910,11 +1910,22 @@ var compoundTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: golang-map`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /05-compound/golang-map:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/MapTestsStruct'`, `components:`, ` schemas:`, ` MapTestsStruct:`, @@ -1954,23 +1965,12 @@ var compoundTests = []fixtures.TestCase{ ` format: double`, ` StringVal:`, ` type: string`, - `paths:`, - ` /05-compound/golang-map:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/MapTestsStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: golang-map`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /05-compound/golang-map:`, @@ -2061,10 +2061,10 @@ var compoundTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-map`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /05-compound/json-map:`, @@ -2115,10 +2115,10 @@ var compoundTests = []fixtures.TestCase{ ` type: string`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-map`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /05-compound/json-map:`, @@ -2186,6 +2186,7 @@ var referenceTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:ReferenceTestsStruct`, `TypeRef.BasicStruct:{}`, `TypeRef.BasicStruct:{}.BoolVal:boolean`, `TypeRef.BasicStruct:{}.Float64Val:float`, @@ -2195,7 +2196,6 @@ var referenceTests = []fixtures.TestCase{ `TypeRef.ReferenceTestsStruct:{}.!InterfaceVal:invalid! ERROR:interface element is nil`, `TypeRef.ReferenceTestsStruct:{}.PtrPtrVal:{}:BasicStruct`, `TypeRef.ReferenceTestsStruct:{}.PtrVal:{}:BasicStruct`, - `Root.{}:ReferenceTestsStruct`, }, true: []string{ `Root.{}`, @@ -2220,6 +2220,7 @@ var referenceTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:ReferenceTestsStruct`, `TypeRef.BasicStruct:{}`, `TypeRef.BasicStruct:{}.BoolVal:boolean`, `TypeRef.BasicStruct:{}.Float64Val:float`, @@ -2229,7 +2230,6 @@ var referenceTests = []fixtures.TestCase{ `TypeRef.ReferenceTestsStruct:{}.InterfaceVal:{}:BasicStruct`, `TypeRef.ReferenceTestsStruct:{}.PtrPtrVal:{}:BasicStruct`, `TypeRef.ReferenceTestsStruct:{}.PtrVal:{}:BasicStruct`, - `Root.{}:ReferenceTestsStruct`, }, true: []string{ `Root.{}`, @@ -2252,11 +2252,22 @@ var referenceTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: reference-tests-init`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /06-reference/reference-tests-init:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/ReferenceTestsStruct'`, `components:`, ` schemas:`, ` BasicStruct:`, @@ -2282,23 +2293,12 @@ var referenceTests = []fixtures.TestCase{ ` $ref: '#/components/schemas/BasicStruct'`, ` PtrVal:`, ` $ref: '#/components/schemas/BasicStruct'`, - `paths:`, - ` /06-reference/reference-tests-init:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/ReferenceTestsStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: reference-tests-init`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /06-reference/reference-tests-init:`, @@ -2397,6 +2397,7 @@ var cycleTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:CycleTest`, `TypeRef.AStruct:{}`, `TypeRef.AStruct:{}.AChild:{}:BStruct`, `TypeRef.AStruct:{}.AName:string`, @@ -2412,7 +2413,6 @@ var cycleTests = []fixtures.TestCase{ `TypeRef.CycleTest:{}.CycleC:{}`, `TypeRef.CycleTest:{}.CycleC:{}.C:{}:CStruct`, `TypeRef.CycleTest:{}.Level:integer`, - `Root.{}:CycleTest`, }, true: []string{ `Root.{}`, @@ -2443,11 +2443,22 @@ var cycleTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: cycle-test`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /07-cycle/cycle-test:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/CycleTest'`, `components:`, ` schemas:`, ` AStruct:`, @@ -2488,23 +2499,12 @@ var cycleTests = []fixtures.TestCase{ ` properties:`, ` c:`, ` $ref: '#/components/schemas/CStruct'`, - `paths:`, - ` /07-cycle/cycle-test:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/CycleTest'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: cycle-test`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /07-cycle/cycle-test:`, @@ -2619,12 +2619,12 @@ var jsonTagTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:JSONTagTests`, `TypeRef.JSONTagTests:{}`, `TypeRef.JSONTagTests:{}.ExcludeTag:string`, `TypeRef.JSONTagTests:{}.NoTag:string`, `TypeRef.JSONTagTests:{}.RenameOne:string`, `TypeRef.JSONTagTests:{}.RenameTwo:string`, - `Root.{}:JSONTagTests`, }, true: []string{ `Root.{}`, @@ -2636,11 +2636,22 @@ var jsonTagTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-tags`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /08-json-tag/json-tags:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/JSONTagTests'`, `components:`, ` schemas:`, ` JSONTagTests:`, @@ -2653,23 +2664,12 @@ var jsonTagTests = []fixtures.TestCase{ ` type: string`, ` something:`, ` type: string`, - `paths:`, - ` /08-json-tag/json-tags:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/JSONTagTests'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: json-tags`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /08-json-tag/json-tags:`, @@ -2704,6 +2704,7 @@ var nestedTests = []fixtures.TestCase{ Want: map[string]fixtures.WantSet{ "simple": map[bool][]string{ false: []string{ + `Root.{}:OuterStruct`, `TypeRef.BasicStruct:{}`, `TypeRef.BasicStruct:{}.BoolVal:boolean`, `TypeRef.BasicStruct:{}.Float64Val:float`, @@ -2717,7 +2718,6 @@ var nestedTests = []fixtures.TestCase{ `TypeRef.OuterStruct:{}`, `TypeRef.OuterStruct:{}.ID:integer`, `TypeRef.OuterStruct:{}.Inner:{}:InnerStruct`, - `Root.{}:OuterStruct`, }, true: []string{ `Root.{}`, @@ -2735,11 +2735,22 @@ var nestedTests = []fixtures.TestCase{ }, "openapi": map[bool][]string{ false: []string{ + `openapi: 3.0.0`, `info:`, ` title: nested-struct`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, + `paths:`, + ` /09-nested/nested-struct:`, + ` get:`, + ` summary: Return data.`, + ` responses:`, + ` '200':`, + ` description: Success`, + ` content:`, + ` application/json:`, + ` schema:`, + ` $ref: '#/components/schemas/OuterStruct'`, `components:`, ` schemas:`, ` BasicStruct:`, @@ -2775,23 +2786,12 @@ var nestedTests = []fixtures.TestCase{ ` type: integer`, ` inner:`, ` $ref: '#/components/schemas/InnerStruct'`, - `paths:`, - ` /09-nested/nested-struct:`, - ` get:`, - ` summary: Return data.`, - ` responses:`, - ` '200':`, - ` description: Success`, - ` content:`, - ` application/json:`, - ` schema:`, - ` $ref: '#/components/schemas/OuterStruct'`, }, true: []string{ + `openapi: 3.0.0`, `info:`, ` title: nested-struct`, ` version: v1.0.0`, - `openapi: 3.0.0`, ``, `paths:`, ` /09-nested/nested-struct:`, diff --git a/renderer/openapi/metadata.go b/renderer/openapi/metadata.go index dfc7491..037a121 100644 --- a/renderer/openapi/metadata.go +++ b/renderer/openapi/metadata.go @@ -3,6 +3,10 @@ package openapi import ( "errors" "fmt" + "github.com/ghodss/yaml" + "github.com/gitmann/b9schema-golang/common/util" + "net/mail" + "net/url" "strings" ) @@ -46,6 +50,52 @@ func NewMetaData(title, version string) *MetaData { } } +// MarshalYAML builds YAML strings in a specific key order. +func (m *MetaData) MarshalYAML(prefix string) ([]byte, error) { + outLines := []string{} + + // OpenAPI + if b, err := yaml.Marshal(m.OpenAPI); err != nil { + return nil, err + } else { + out := fmt.Sprintf(`openapi: %s`, strings.TrimSpace(string(b))) + outLines = append(outLines, out) + } + + // Info + if b, err := m.Info.MarshalYAML(prefix); err != nil { + return nil, err + } else { + outLines = append(outLines, `info:`) + outLines = util.AppendStrings(outLines, []string{string(b)}, prefix) + } + + // ExternalDocs + if m.ExternalDocs != nil { + if b, err := yaml.Marshal(m.ExternalDocs); err != nil { + return nil, err + } else { + outLines = append(outLines, `externalDocs:`) + outLines = util.AppendStrings(outLines, []string{string(b)}, prefix) + } + } + + // Servers + if m.Servers != nil { + if b, err := yaml.Marshal(m.Servers); err != nil { + return nil, err + } else { + outLines = append(outLines, `servers:`) + outLines = util.AppendStrings(outLines, []string{string(b)}, prefix) + } + } + + outLines = append(outLines, "") + finalOut := strings.Join(outLines, "\n") + + return []byte(finalOut), nil +} + // Validate checks that metadata contains required fields. func (m *MetaData) Validate() error { if !strings.HasPrefix(m.OpenAPI, "3.0") { @@ -54,14 +104,20 @@ func (m *MetaData) Validate() error { if m.Info == nil { return errors.New("missing 'info' object") + } else if err := m.Info.Validate(); err != nil { + return err } - if m.Info.Title == "" { - return errors.New("'info.title' is required") + if m.ExternalDocs != nil { + if err := m.ExternalDocs.Validate(); err != nil { + return err + } } - if m.Info.Version == "" { - return errors.New("'info.version' is required") + for _, srv := range m.Servers { + if err := srv.Validate(); err != nil { + return err + } } return nil @@ -85,6 +141,94 @@ type InfoObject struct { License *LicenseObject `json:"license,omitempty"` } +func (i *InfoObject) Validate() error { + if i.Title == "" { + return errors.New("'info.title' is required") + } + if i.Version == "" { + return errors.New("'info.version' is required") + } + + if i.TermsOfService != "" { + if _, err := url.ParseRequestURI(i.TermsOfService); err != nil { + return errors.New("'info.termsOfService' is not a valid URL") + } + } + + if i.Contact != nil { + if err := i.Contact.Validate(); err != nil { + return err + } + } + + if i.License != nil { + if err := i.License.Validate(); err != nil { + return err + } + } + + return nil +} + +func (i *InfoObject) MarshalYAML(prefix string) ([]byte, error) { + outLines := []string{} + + // Title + if b, err := yaml.Marshal(i.Title); err != nil { + return nil, err + } else { + outLines = append(outLines, fmt.Sprintf(`title: %s`, strings.TrimSpace(string(b)))) + } + + // Version + if b, err := yaml.Marshal(i.Version); err != nil { + return nil, err + } else { + outLines = append(outLines, fmt.Sprintf(`version: %s`, strings.TrimSpace(string(b)))) + } + + // Description + if i.Description != "" { + if b, err := yaml.Marshal(i.Description); err != nil { + return nil, err + } else { + outLines = append(outLines, fmt.Sprintf(`description: %s`, strings.TrimSpace(string(b)))) + } + } + + // TermsOfService + if i.TermsOfService != "" { + if b, err := yaml.Marshal(i.TermsOfService); err != nil { + return nil, err + } else { + outLines = append(outLines, fmt.Sprintf(`termsOfService: %s`, strings.TrimSpace(string(b)))) + } + } + + // Contact + if i.Contact != nil { + if b, err := yaml.Marshal(i.Contact); err != nil { + return nil, err + } else { + outLines = append(outLines, `contact:`) + outLines = util.AppendStrings(outLines, []string{string(b)}, prefix) + } + } + + // License + if i.License != nil { + if b, err := yaml.Marshal(i.License); err != nil { + return nil, err + } else { + outLines = append(outLines, `license:`) + outLines = util.AppendStrings(outLines, []string{string(b)}, prefix) + } + } + + finalOut := strings.Join(outLines, "\n") + return []byte(finalOut), nil +} + type ContactObject struct { //The identifying name of the contact person/organization. Name string `json:"name"` @@ -94,6 +238,22 @@ type ContactObject struct { Email string `json:"email,omitempty"` } +func (c *ContactObject) Validate() error { + if c.URL != "" { + if _, err := url.ParseRequestURI(c.URL); err != nil { + return errors.New("'contact.url' is not a valid URL") + } + } + + if c.Email != "" { + if _, err := mail.ParseAddress(c.Email); err != nil { + return errors.New("`contact.email is not a valid email address") + } + } + + return nil +} + type LicenseObject struct { // REQUIRED. The license name used for the API. Name string `json:"name"` @@ -101,6 +261,20 @@ type LicenseObject struct { URL string `json:"url,omitempty"` } +func (lic *LicenseObject) Validate() error { + if lic.Name == "" { + return errors.New("'license.name' is required") + } + + if lic.URL != "" { + if _, err := url.ParseRequestURI(lic.URL); err != nil { + return errors.New("'license.url' is not a valid URL") + } + } + + return nil +} + type ServerObject struct { // REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}. URL string `json:"url"` @@ -112,6 +286,14 @@ type ServerObject struct { // NOTE: Variables is omitted here!!! } +func (s *ServerObject) Validate() error { + if _, err := url.ParseRequestURI(s.URL); err != nil { + return errors.New("'server.url' is not a valid URL") + } + + return nil +} + type ExternalDocumentationObject struct { // REQUIRED. The URL for the target documentation. Value MUST be in the format of a URL. URL string `json:"url"` @@ -119,6 +301,14 @@ type ExternalDocumentationObject struct { Description string `json:"description,omitempty"` } +func (d *ExternalDocumentationObject) Validate() error { + if _, err := url.ParseRequestURI(d.URL); err != nil { + return errors.New("'externalDocs.url' is not a valid URL") + } + + return nil +} + type PathsObject map[string]*PathItemObject type PathItemObject struct { diff --git a/renderer/openapi/metadata_test.go b/renderer/openapi/metadata_test.go index 071273b..a3d424b 100644 --- a/renderer/openapi/metadata_test.go +++ b/renderer/openapi/metadata_test.go @@ -1,7 +1,6 @@ package openapi import ( - "github.com/ghodss/yaml" "github.com/gitmann/b9schema-golang/common/util" "strings" "testing" @@ -18,10 +17,10 @@ func TestNewMetaData(t *testing.T) { name: "default", meta: NewMetaData("", ""), wantYAML: []string{ + `openapi: 3.0.0`, `info:`, ` title: default title`, ` version: default version`, - `openapi: 3.0.0`, }, }, { @@ -59,33 +58,33 @@ func TestNewMetaData(t *testing.T) { }, }, wantYAML: []string{ - `externalDocs:`, - ` description: This is the test doc site.`, - ` url: https://test.doc.site.com/path/to/docs`, + `openapi: 3.0.0`, `info:`, + ` title: This is the title.`, + ` version: v1.2.3`, + ` description: This is a description.`, + ` termsOfService: https://test.tos.site.com/terms`, ` contact:`, ` email: support@site.com`, ` name: Support Team`, ` url: https://support.site.com/`, - ` description: This is a description.`, ` license:`, ` name: This is the license.`, ` url: https://license.site.com/`, - ` termsOfService: https://test.tos.site.com/terms`, - ` title: This is the title.`, - ` version: v1.2.3`, - `openapi: 3.0.0`, + `externalDocs:`, + ` description: This is the test doc site.`, + ` url: https://test.doc.site.com/path/to/docs`, `servers:`, - `- description: Production server.`, - ` url: https://www.site.com`, - `- description: Development server.`, - ` url: https://www.dev.site.com`, + ` - description: Production server.`, + ` url: https://www.site.com`, + ` - description: Development server.`, + ` url: https://www.dev.site.com`, }, }, } for _, test := range testCases { - if b, err := yaml.Marshal(test.meta); err != nil { + if b, err := test.meta.MarshalYAML(" "); err != nil { t.Errorf("TEST_FAIL %s: yaml err=%s", test.name, err) } else { gotYAML := strings.Split(string(b), "\n") diff --git a/renderer/openapi/openapi.go b/renderer/openapi/openapi.go index f008de7..58e52d7 100644 --- a/renderer/openapi/openapi.go +++ b/renderer/openapi/openapi.go @@ -3,7 +3,6 @@ package openapi import ( "errors" "fmt" - "github.com/ghodss/yaml" "github.com/gitmann/b9schema-golang/common/enum/generictype" "github.com/gitmann/b9schema-golang/common/enum/threeflag" "github.com/gitmann/b9schema-golang/common/types" @@ -44,7 +43,7 @@ func (r *OpenAPIRenderer) ProcessSchema(schema *types.Schema, settings ...string } // Header - if b, err := yaml.Marshal(r.MetaData); err != nil { + if b, err := r.MetaData.MarshalYAML(r.Options.Prefix); err != nil { return out, err } else { out = append(out, string(b)) diff --git a/renderer/util.go b/renderer/util.go index 554d6f9..d4182d7 100644 --- a/renderer/util.go +++ b/renderer/util.go @@ -11,6 +11,16 @@ func RenderSchema(schema *types.Schema, r Renderer) []string { // Build output outLines. out := []string{} + // Print types. + if len(schema.Root.Children) > 0 { + rendered := RenderType(schema.Root, r) + for _, r := range rendered { + if r != "" { + out = append(out, r) + } + } + } + // Print type refs. if !r.DeReference() { if len(schema.TypeRef.Children) > 0 { @@ -23,16 +33,6 @@ func RenderSchema(schema *types.Schema, r Renderer) []string { } } - // Print types. - if len(schema.Root.Children) > 0 { - rendered := RenderType(schema.Root, r) - for _, r := range rendered { - if r != "" { - out = append(out, r) - } - } - } - // Return strings. return out }