mirror of
https://github.com/LukeHagar/theschemagen.git
synced 2025-12-06 04:21:44 +00:00
Initial basic schema functionality
This commit is contained in:
11
README.md
11
README.md
@@ -1 +1,12 @@
|
||||
# theschemagen
|
||||
|
||||
Generate OpenAPI Schemas from JSON bodies, Now in GO!
|
||||
|
||||
Roadmap:
|
||||
|
||||
* [x] Generate OpenAPI Schemas from JSON bodies
|
||||
* [] Generate Path schemas from Full HTTP responses
|
||||
* [] Generate an accurate schema from multiple distinct responses
|
||||
* [] oneOf
|
||||
* [] common component models
|
||||
* [] Dynamically enhance openapi schemas with additional information from the API responses
|
||||
|
||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module github.com/lukehagar/theschemagen
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/text v0.18.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
)
|
||||
12
go.sum
Normal file
12
go.sum
Normal file
@@ -0,0 +1,12 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
195
main.go
Normal file
195
main.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package theschemagen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func PrettyPrint(v interface{}, format string) {
|
||||
var b []byte
|
||||
var err error
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
b, err = json.MarshalIndent(v, "", " ")
|
||||
case "yaml":
|
||||
b, err = yaml.Marshal(v)
|
||||
default:
|
||||
b, err = yaml.Marshal(v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
type SchemaObject struct {
|
||||
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||
Format string `json:"format,omitempty" yaml:"format,omitempty"`
|
||||
Items interface{} `json:"items,omitempty" yaml:"items,omitempty"`
|
||||
Examples []interface{} `json:"examples,omitempty" yaml:"examples,omitempty"`
|
||||
Properties map[string]SchemaObject `json:"properties,omitempty" yaml:"properties,omitempty"`
|
||||
}
|
||||
|
||||
func ConvertNumber(number float64) SchemaObject {
|
||||
output := SchemaObject{}
|
||||
if IsInteger(number) {
|
||||
output.Type = "integer"
|
||||
if number < 2147483647 && number > -2147483647 {
|
||||
output.Format = "int32"
|
||||
} else if IsSafeInteger(number) {
|
||||
output.Format = "int64"
|
||||
}
|
||||
} else {
|
||||
output.Type = "number"
|
||||
}
|
||||
|
||||
output.Examples = []interface{}{number}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ConvertArray(array []interface{}) SchemaObject {
|
||||
output := SchemaObject{Type: "array", Items: nil}
|
||||
var outputItems []SchemaObject
|
||||
|
||||
for _, entry := range array {
|
||||
|
||||
objectMap := ConvertObject(entry)
|
||||
isDuplicate := false
|
||||
|
||||
for _, item := range outputItems {
|
||||
hasSameTypeAndFormat := item.Type == objectMap.Type && item.Format == objectMap.Format
|
||||
hasSameProperties := item.Properties != nil && objectMap.Properties != nil &&
|
||||
CompareKeys(item.Properties, objectMap.Properties)
|
||||
if hasSameTypeAndFormat || hasSameProperties {
|
||||
isDuplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isDuplicate {
|
||||
outputItems = append(outputItems, objectMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(outputItems) > 1 {
|
||||
output.Items = map[string]interface{}{"oneOf": outputItems}
|
||||
} else {
|
||||
output.Items = outputItems[0]
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ConvertString(str string) SchemaObject {
|
||||
output := SchemaObject{Type: "string"}
|
||||
|
||||
regxDate := regexp.MustCompile(`^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$`)
|
||||
regxDateTime := regexp.MustCompile(`^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]Z$`)
|
||||
|
||||
if regxDateTime.MatchString(str) {
|
||||
output.Format = "date-time"
|
||||
} else if regxDate.MatchString(str) {
|
||||
output.Format = "date"
|
||||
}
|
||||
|
||||
output.Examples = []interface{}{str}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ConvertObject(input interface{}) SchemaObject {
|
||||
switch v := input.(type) {
|
||||
case nil:
|
||||
return SchemaObject{Type: "null"}
|
||||
case float64:
|
||||
return ConvertNumber(v)
|
||||
case []interface{}:
|
||||
return ConvertArray(v)
|
||||
case map[string]interface{}:
|
||||
output := SchemaObject{Type: "object", Properties: make(map[string]SchemaObject)}
|
||||
for key, val := range v {
|
||||
output.Properties[key] = ConvertObject(val)
|
||||
}
|
||||
return output
|
||||
case string:
|
||||
return ConvertString(v)
|
||||
case bool:
|
||||
output := SchemaObject{Type: "boolean"}
|
||||
|
||||
output.Examples = []interface{}{v}
|
||||
|
||||
return output
|
||||
default:
|
||||
panic("Invalid type for conversion")
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertJSONToOAS(input string) SchemaObject {
|
||||
var obj map[string]interface{}
|
||||
err := json.Unmarshal([]byte(input), &obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ConvertObject(obj)
|
||||
}
|
||||
|
||||
func ConvertObjectToOAS(input map[string]interface{}) SchemaObject {
|
||||
return ConvertObject(input)
|
||||
}
|
||||
|
||||
var ignoredWords = []string{"the", "a", "an", "of", "to", "in", "for", "with", "on", "at", "from", "by", "and"}
|
||||
|
||||
func ConvertSummaryToOperationId(summary string) string {
|
||||
words := strings.Split(summary, " ")
|
||||
var filteredWords []string
|
||||
for i, word := range words {
|
||||
if i == 0 {
|
||||
filteredWords = append(filteredWords, strings.ToLower(string(word[0]))+word[1:])
|
||||
} else {
|
||||
if !Contains(ignoredWords, strings.ToLower(word)) {
|
||||
filteredWords = append(filteredWords, cases.Title(language.English, cases.NoLower).String(word))
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(filteredWords, "")
|
||||
}
|
||||
|
||||
func Contains(arr []string, str string) bool {
|
||||
for _, v := range arr {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CompareKeys(m1, m2 map[string]SchemaObject) bool {
|
||||
if len(m1) != len(m2) {
|
||||
return false
|
||||
}
|
||||
for k := range m1 {
|
||||
if _, ok := m2[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsInteger(n float64) bool {
|
||||
return n == float64(int(n))
|
||||
}
|
||||
|
||||
func IsSafeInteger(n float64) bool {
|
||||
return n <= float64(int64(^uint(0)>>1)) && n >= float64(int64(^uint(0)>>1)*-1)
|
||||
}
|
||||
61
tests/convert_test.go
Normal file
61
tests/convert_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package theschemagen_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/lukehagar/theschemagen"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertJSONToOAS(t *testing.T) {
|
||||
// assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
testJson, err := os.ReadFile("./test-files/test.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
exampleJSON := string(testJson)
|
||||
|
||||
schema := theschemagen.ConvertJSONToOAS(exampleJSON)
|
||||
// root level check
|
||||
require.Equal(schema.Type, "object")
|
||||
|
||||
require.Equal(schema.Properties["stringsMock"].Properties["stringTest"].Type, "string")
|
||||
|
||||
require.Equal(schema.Properties["stringsMock"].Properties["isoDate"].Type, "string")
|
||||
require.Equal(schema.Properties["stringsMock"].Properties["isoDate"].Format, "date")
|
||||
|
||||
require.Equal(schema.Properties["stringsMock"].Properties["isoDateTime"].Type, "string")
|
||||
require.Equal(schema.Properties["stringsMock"].Properties["isoDateTime"].Format, "date-time")
|
||||
|
||||
require.Equal(schema.Properties["numbersMock"].Properties["smallInt"].Type, "integer")
|
||||
require.Equal(schema.Properties["numbersMock"].Properties["smallInt"].Format, "int32")
|
||||
}
|
||||
|
||||
func BenchmarkConvertJSONToOAS(b *testing.B) {
|
||||
testJson, err := os.ReadFile("./test-files/test.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
exampleJSON := string(testJson)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
theschemagen.ConvertJSONToOAS(exampleJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertObject(t *testing.T) {
|
||||
testJson, err := os.ReadFile("./test-files/test.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
exampleJSON := string(testJson)
|
||||
|
||||
schema := theschemagen.ConvertJSONToOAS(exampleJSON)
|
||||
theschemagen.PrettyPrint(schema, "yaml")
|
||||
}
|
||||
36
tests/test-files/test.json
Normal file
36
tests/test-files/test.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"numbersMock": {
|
||||
"smallInt": -20,
|
||||
"bigInt": 2147483647,
|
||||
"unsafeInt": 9999999999999999,
|
||||
"notInt": 12.2
|
||||
},
|
||||
"stringsMock": {
|
||||
"stringTest": "Hello World",
|
||||
"isoDate": "1999-12-31",
|
||||
"isoDateTime": "1999-12-31T23:59:59Z"
|
||||
},
|
||||
"objectsMock": {
|
||||
"child": { "child": true },
|
||||
"childList": [{ "child": true }],
|
||||
"childMatrix": [[{ "child": true }]],
|
||||
"mixedObjectsArray": [
|
||||
[1, 2, { "test": true }],
|
||||
{ "child": true },
|
||||
{ "son": true },
|
||||
{ "son": true },
|
||||
{ "offspring": true }
|
||||
],
|
||||
"nullable": null
|
||||
},
|
||||
"listMock": [1, 2, 3, 4, 5],
|
||||
"matrixMock": [
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
],
|
||||
"mixedArrayMock": [1, "two", 3, "four", 5],
|
||||
"mixedMatrixMock": [
|
||||
[1, "two"],
|
||||
[3, "four"]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user