From 78789cd76919d51d77a73fbcb10069a4ac433d8e Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Sun, 11 Aug 2024 00:16:56 +0700 Subject: [PATCH] misc: generate jsonschema (#33) * generate jsonschema --- Makefile | 4 + command/convert.go | 20 +- config.example.yaml | 1 + go.mod | 3 +- go.sum | 14 +- jsonschema/convert-config.jsonschema | 91 ++ jsonschema/generator.go | 40 + jsonschema/ndc-rest-schema.jsonschema | 792 ++++++++++++++++++ .../testdata/jsonplaceholder/expected.json | 1 + .../testdata/onesignal/expected-patch.json | 1 + openapi/testdata/onesignal/expected.json | 1 + openapi/testdata/openai/expected.json | 1 + openapi/testdata/petstore2/expected.json | 1 + openapi/testdata/petstore3/expected.json | 1 + schema/auth.go | 94 +++ schema/enum.go | 36 +- schema/enum_test.go | 74 ++ schema/env.go | 28 + schema/schema.go | 14 +- utils/patch.go | 2 +- 20 files changed, 1196 insertions(+), 23 deletions(-) create mode 100644 jsonschema/convert-config.jsonschema create mode 100644 jsonschema/generator.go create mode 100644 jsonschema/ndc-rest-schema.jsonschema create mode 100644 schema/enum_test.go diff --git a/Makefile b/Makefile index 36910b9..feb9048 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,10 @@ clean: build: go build -o _output/ndc-rest-schema . +.PHONY: build-jsonschema +build-jsonschema: + cd jsonschema && go run . + # build the ndc-rest-schema for all given platform/arch .PHONY: ci-build ci-build: export CGO_ENABLED=0 diff --git a/command/convert.go b/command/convert.go index 4587dd3..7b63ec1 100644 --- a/command/convert.go +++ b/command/convert.go @@ -126,16 +126,16 @@ func CommandConvertToNDCSchema(args *ConvertCommandArguments, logger *slog.Logge // ConvertConfig represents the content of convert config file type ConvertConfig struct { File string `json:"file" yaml:"file"` - Spec schema.SchemaSpecType `json:"spec" yaml:"spec"` - MethodAlias map[string]string `json:"methodAlias" yaml:"methodAlias"` - TrimPrefix string `json:"trimPrefix" yaml:"trimPrefix"` - EnvPrefix string `json:"envPrefix" yaml:"envPrefix"` - Pure bool `json:"pure" yaml:"pure"` - Strict bool `json:"strict" yaml:"strict"` - PatchBefore []utils.PatchConfig `json:"patchBefore" yaml:"patchBefore"` - PatchAfter []utils.PatchConfig `json:"patchAfter" yaml:"patchAfter"` - AllowedContentTypes []string `json:"allowedContentTypes" yaml:"allowedContentTypes"` - Output string `json:"output" yaml:"output"` + Spec schema.SchemaSpecType `json:"spec,omitempty" yaml:"spec"` + MethodAlias map[string]string `json:"methodAlias,omitempty" yaml:"methodAlias"` + TrimPrefix string `json:"trimPrefix,omitempty" yaml:"trimPrefix"` + EnvPrefix string `json:"envPrefix,omitempty" yaml:"envPrefix"` + Pure bool `json:"pure,omitempty" yaml:"pure"` + Strict bool `json:"strict,omitempty" yaml:"strict"` + PatchBefore []utils.PatchConfig `json:"patchBefore,omitempty" yaml:"patchBefore"` + PatchAfter []utils.PatchConfig `json:"patchAfter,omitempty" yaml:"patchAfter"` + AllowedContentTypes []string `json:"allowedContentTypes,omitempty" yaml:"allowedContentTypes"` + Output string `json:"output,omitempty" yaml:"output"` } // ConvertToNDCSchema converts to NDC REST schema from config diff --git a/config.example.yaml b/config.example.yaml index f703908..05f65aa 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/convert-config.jsonschema # -- File path needs to be converted. file: "" diff --git a/go.mod b/go.mod index ca63e62..e08026f 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/alecthomas/kong v0.9.0 github.com/evanphx/json-patch v0.5.2 github.com/hasura/ndc-sdk-go v1.2.5 + github.com/invopop/jsonschema v0.12.0 github.com/lmittmann/tint v1.0.5 github.com/pb33f/libopenapi v0.16.14 + github.com/wk8/go-ordered-map/v2 v2.1.8 gopkg.in/yaml.v3 v3.0.1 ) @@ -19,6 +21,5 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect golang.org/x/net v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 0956595..e1d9f63 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -69,8 +71,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/pb33f/libopenapi v0.16.13 h1:uR/W3Rit/yxRWG5DWal26PdEnEq4mu/3cYjbkK6LHm0= -github.com/pb33f/libopenapi v0.16.13/go.mod h1:8/lZGTZmxybpTPOggS6LefdrYvsQ5kbirD364TceyQo= github.com/pb33f/libopenapi v0.16.14 h1:NyyYWAhNuuzVO/PM690tKUaljfL5nZY77w1Kx4kiKIg= github.com/pb33f/libopenapi v0.16.14/go.mod h1:8/lZGTZmxybpTPOggS6LefdrYvsQ5kbirD364TceyQo= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -101,8 +101,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -122,16 +120,16 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/jsonschema/convert-config.jsonschema b/jsonschema/convert-config.jsonschema new file mode 100644 index 0000000..586e911 --- /dev/null +++ b/jsonschema/convert-config.jsonschema @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/hasura/ndc-rest-schema/command/convert-config", + "$ref": "#/$defs/ConvertConfig", + "$defs": { + "ConvertConfig": { + "properties": { + "file": { + "type": "string" + }, + "spec": { + "$ref": "#/$defs/SchemaSpecType" + }, + "methodAlias": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "trimPrefix": { + "type": "string" + }, + "envPrefix": { + "type": "string" + }, + "pure": { + "type": "boolean" + }, + "strict": { + "type": "boolean" + }, + "patchBefore": { + "items": { + "$ref": "#/$defs/PatchConfig" + }, + "type": "array" + }, + "patchAfter": { + "items": { + "$ref": "#/$defs/PatchConfig" + }, + "type": "array" + }, + "allowedContentTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "output": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "file" + ] + }, + "PatchConfig": { + "properties": { + "path": { + "type": "string" + }, + "strategy": { + "type": "string", + "enum": [ + "merge", + "json6902" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "path", + "strategy" + ] + }, + "SchemaSpecType": { + "type": "string", + "enum": [ + "oas3", + "oas2", + "openapi3", + "openapi2", + "ndc" + ] + } + } +} \ No newline at end of file diff --git a/jsonschema/generator.go b/jsonschema/generator.go new file mode 100644 index 0000000..0386772 --- /dev/null +++ b/jsonschema/generator.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/hasura/ndc-rest-schema/command" + "github.com/hasura/ndc-rest-schema/schema" + "github.com/invopop/jsonschema" +) + +func main() { + if err := jsonSchemaConvertConfig(); err != nil { + panic(fmt.Errorf("failed to write jsonschema for ConvertConfig: %s", err)) + } + if err := jsonSchemaNdcRESTSchema(); err != nil { + panic(fmt.Errorf("failed to write jsonschema for NDCRestSchema: %s", err)) + } +} + +func jsonSchemaConvertConfig() error { + reflectSchema := jsonschema.Reflect(&command.ConvertConfig{}) + schemaBytes, err := json.MarshalIndent(reflectSchema, "", " ") + if err != nil { + return err + } + + return os.WriteFile("convert-config.jsonschema", schemaBytes, 0644) +} + +func jsonSchemaNdcRESTSchema() error { + reflectSchema := jsonschema.Reflect(&schema.NDCRestSchema{}) + schemaBytes, err := json.MarshalIndent(reflectSchema, "", " ") + if err != nil { + return err + } + + return os.WriteFile("ndc-rest-schema.jsonschema", schemaBytes, 0644) +} diff --git a/jsonschema/ndc-rest-schema.jsonschema b/jsonschema/ndc-rest-schema.jsonschema new file mode 100644 index 0000000..0d4658b --- /dev/null +++ b/jsonschema/ndc-rest-schema.jsonschema @@ -0,0 +1,792 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/hasura/ndc-rest-schema/schema/ndc-rest-schema", + "$ref": "#/$defs/NDCRestSchema", + "$defs": { + "AggregateFunctionDefinition": { + "properties": { + "result_type": { + "$ref": "#/$defs/Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "result_type" + ] + }, + "ArgumentInfo": { + "properties": { + "description": { + "type": "string" + }, + "type": { + "$ref": "#/$defs/Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ] + }, + "AuthSecurities": { + "items": { + "$ref": "#/$defs/AuthSecurity" + }, + "type": "array" + }, + "AuthSecurity": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "CollectionInfo": { + "properties": { + "arguments": { + "$ref": "#/$defs/CollectionInfoArguments" + }, + "description": { + "type": "string" + }, + "foreign_keys": { + "$ref": "#/$defs/CollectionInfoForeignKeys" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uniqueness_constraints": { + "$ref": "#/$defs/CollectionInfoUniquenessConstraints" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "arguments", + "foreign_keys", + "name", + "type", + "uniqueness_constraints" + ] + }, + "CollectionInfoArguments": { + "additionalProperties": { + "$ref": "#/$defs/ArgumentInfo" + }, + "type": "object" + }, + "CollectionInfoForeignKeys": { + "additionalProperties": { + "$ref": "#/$defs/ForeignKeyConstraint" + }, + "type": "object" + }, + "CollectionInfoUniquenessConstraints": { + "additionalProperties": { + "$ref": "#/$defs/UniquenessConstraint" + }, + "type": "object" + }, + "ComparisonOperatorDefinition": { + "type": "object" + }, + "EncodingObject": { + "properties": { + "style": { + "$ref": "#/$defs/ParameterEncodingStyle" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean" + }, + "contentType": { + "items": { + "type": "string" + }, + "type": "array" + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/RequestParameter" + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object" + }, + "EnvInt": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "EnvInts": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "integer" + }, + "type": "array" + } + ] + }, + "EnvString": { + "type": "string" + }, + "ForeignKeyConstraint": { + "properties": { + "column_mapping": { + "$ref": "#/$defs/ForeignKeyConstraintColumnMapping" + }, + "foreign_collection": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "column_mapping", + "foreign_collection" + ] + }, + "ForeignKeyConstraintColumnMapping": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "FunctionInfoArguments": { + "additionalProperties": { + "$ref": "#/$defs/ArgumentInfo" + }, + "type": "object" + }, + "NDCRestSchema": { + "properties": { + "settings": { + "$ref": "#/$defs/NDCRestSettings" + }, + "collections": { + "items": { + "$ref": "#/$defs/CollectionInfo" + }, + "type": "array" + }, + "functions": { + "items": { + "$ref": "#/$defs/RESTFunctionInfo" + }, + "type": "array" + }, + "object_types": { + "$ref": "#/$defs/SchemaResponseObjectTypes" + }, + "procedures": { + "items": { + "$ref": "#/$defs/RESTProcedureInfo" + }, + "type": "array" + }, + "scalar_types": { + "$ref": "#/$defs/SchemaResponseScalarTypes" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "collections", + "functions", + "object_types", + "procedures", + "scalar_types" + ] + }, + "NDCRestSettings": { + "properties": { + "servers": { + "items": { + "$ref": "#/$defs/ServerConfig" + }, + "type": "array" + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/EnvString" + }, + "type": "object" + }, + "timeout": { + "$ref": "#/$defs/EnvInt" + }, + "retry": { + "$ref": "#/$defs/RetryPolicySetting" + }, + "securitySchemes": { + "additionalProperties": { + "$ref": "#/$defs/SecurityScheme" + }, + "type": "object" + }, + "security": { + "$ref": "#/$defs/AuthSecurities" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "servers" + ] + }, + "ObjectField": { + "properties": { + "arguments": { + "$ref": "#/$defs/ObjectFieldArguments" + }, + "description": { + "type": "string" + }, + "type": { + "$ref": "#/$defs/Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ] + }, + "ObjectFieldArguments": { + "additionalProperties": { + "$ref": "#/$defs/ArgumentInfo" + }, + "type": "object" + }, + "ObjectType": { + "properties": { + "description": { + "type": "string" + }, + "fields": { + "$ref": "#/$defs/ObjectTypeFields" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "fields" + ] + }, + "ObjectTypeFields": { + "additionalProperties": { + "$ref": "#/$defs/ObjectField" + }, + "type": "object" + }, + "ParameterEncodingStyle": { + "type": "string", + "enum": [ + "simple", + "label", + "matrix", + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "ParameterLocation": { + "type": "string", + "enum": [ + "query", + "header", + "path", + "cookie", + "body", + "formData" + ] + }, + "ProcedureInfoArguments": { + "additionalProperties": { + "$ref": "#/$defs/ArgumentInfo" + }, + "type": "object" + }, + "RESTFunctionInfo": { + "properties": { + "request": { + "$ref": "#/$defs/Request" + }, + "arguments": { + "$ref": "#/$defs/FunctionInfoArguments" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "result_type": { + "$ref": "#/$defs/Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "request", + "arguments", + "name", + "result_type" + ] + }, + "RESTProcedureInfo": { + "properties": { + "request": { + "$ref": "#/$defs/Request" + }, + "arguments": { + "$ref": "#/$defs/ProcedureInfoArguments" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "result_type": { + "$ref": "#/$defs/Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "request", + "arguments", + "name", + "result_type" + ] + }, + "Request": { + "properties": { + "url": { + "type": "string" + }, + "method": { + "type": "string", + "enum": [ + "get", + "post", + "put", + "patch", + "delete" + ] + }, + "type": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/EnvString" + }, + "type": "object" + }, + "parameters": { + "items": { + "$ref": "#/$defs/RequestParameter" + }, + "type": "array" + }, + "security": { + "$ref": "#/$defs/AuthSecurities" + }, + "timeout": { + "type": "integer" + }, + "servers": { + "items": { + "$ref": "#/$defs/ServerConfig" + }, + "type": "array" + }, + "requestBody": { + "$ref": "#/$defs/RequestBody" + }, + "response": { + "$ref": "#/$defs/Response" + }, + "retry": { + "$ref": "#/$defs/RetryPolicy" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "response" + ] + }, + "RequestBody": { + "properties": { + "contentType": { + "type": "string" + }, + "schema": { + "$ref": "#/$defs/TypeSchema" + }, + "encoding": { + "additionalProperties": { + "$ref": "#/$defs/EncodingObject" + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RequestParameter": { + "properties": { + "style": { + "$ref": "#/$defs/ParameterEncodingStyle" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean" + }, + "contentType": { + "items": { + "type": "string" + }, + "type": "array" + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/RequestParameter" + }, + "type": "object" + }, + "name": { + "type": "string" + }, + "argumentName": { + "type": "string" + }, + "in": { + "$ref": "#/$defs/ParameterLocation" + }, + "schema": { + "$ref": "#/$defs/TypeSchema" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Response": { + "properties": { + "contentType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "contentType" + ] + }, + "RetryPolicy": { + "properties": { + "times": { + "type": "integer" + }, + "delay": { + "type": "integer" + }, + "httpStatus": { + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RetryPolicySetting": { + "properties": { + "times": { + "$ref": "#/$defs/EnvInt" + }, + "delay": { + "$ref": "#/$defs/EnvInt" + }, + "httpStatus": { + "$ref": "#/$defs/EnvInts" + } + }, + "additionalProperties": false, + "type": "object" + }, + "ScalarType": { + "properties": { + "aggregate_functions": { + "$ref": "#/$defs/ScalarTypeAggregateFunctions" + }, + "comparison_operators": { + "additionalProperties": { + "$ref": "#/$defs/ComparisonOperatorDefinition" + }, + "type": "object" + }, + "representation": { + "$ref": "#/$defs/TypeRepresentation" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "aggregate_functions", + "comparison_operators" + ] + }, + "ScalarTypeAggregateFunctions": { + "additionalProperties": { + "$ref": "#/$defs/AggregateFunctionDefinition" + }, + "type": "object" + }, + "SchemaResponseObjectTypes": { + "additionalProperties": { + "$ref": "#/$defs/ObjectType" + }, + "type": "object" + }, + "SchemaResponseScalarTypes": { + "additionalProperties": { + "$ref": "#/$defs/ScalarType" + }, + "type": "object" + }, + "SecurityScheme": { + "oneOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "value": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "value", + "in", + "name" + ] + }, + { + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "value": { + "type": "string" + }, + "header": { + "type": "string" + }, + "scheme": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "value", + "header", + "scheme" + ] + }, + { + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "additionalProperties": true, + "type": "object" + } + }, + "type": "object", + "required": [ + "type", + "flows" + ] + }, + { + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ] + } + ] + }, + "ServerConfig": { + "properties": { + "url": { + "$ref": "#/$defs/EnvString" + }, + "id": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/EnvString" + }, + "type": "object" + }, + "timeout": { + "$ref": "#/$defs/EnvInt" + }, + "retry": { + "$ref": "#/$defs/RetryPolicySetting" + }, + "securitySchemes": { + "additionalProperties": { + "$ref": "#/$defs/SecurityScheme" + }, + "type": "object" + }, + "security": { + "$ref": "#/$defs/AuthSecurities" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url" + ] + }, + "Type": { + "type": "object" + }, + "TypeRepresentation": { + "type": "object" + }, + "TypeSchema": { + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "nullable": { + "type": "boolean" + }, + "maximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "maxLength": { + "type": "integer" + }, + "minLength": { + "type": "integer" + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "items": { + "$ref": "#/$defs/TypeSchema" + }, + "properties": { + "additionalProperties": { + "$ref": "#/$defs/TypeSchema" + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ] + }, + "UniquenessConstraint": { + "properties": { + "unique_columns": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "unique_columns" + ] + } + } +} \ No newline at end of file diff --git a/openapi/testdata/jsonplaceholder/expected.json b/openapi/testdata/jsonplaceholder/expected.json index 6595b2f..22e5593 100644 --- a/openapi/testdata/jsonplaceholder/expected.json +++ b/openapi/testdata/jsonplaceholder/expected.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/openapi/testdata/onesignal/expected-patch.json b/openapi/testdata/onesignal/expected-patch.json index 8f3f149..73bd94a 100644 --- a/openapi/testdata/onesignal/expected-patch.json +++ b/openapi/testdata/onesignal/expected-patch.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/openapi/testdata/onesignal/expected.json b/openapi/testdata/onesignal/expected.json index 0f9e41c..6586555 100644 --- a/openapi/testdata/onesignal/expected.json +++ b/openapi/testdata/onesignal/expected.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/openapi/testdata/openai/expected.json b/openapi/testdata/openai/expected.json index eee4831..30df2ad 100644 --- a/openapi/testdata/openai/expected.json +++ b/openapi/testdata/openai/expected.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/openapi/testdata/petstore2/expected.json b/openapi/testdata/petstore2/expected.json index ba4efd9..13f2ce2 100644 --- a/openapi/testdata/petstore2/expected.json +++ b/openapi/testdata/petstore2/expected.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/openapi/testdata/petstore3/expected.json b/openapi/testdata/petstore3/expected.json index f9aa586..08698da 100644 --- a/openapi/testdata/petstore3/expected.json +++ b/openapi/testdata/petstore3/expected.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", "settings": { "servers": [ { diff --git a/schema/auth.go b/schema/auth.go index 133601d..7cfaf02 100644 --- a/schema/auth.go +++ b/schema/auth.go @@ -5,6 +5,9 @@ import ( "errors" "fmt" "slices" + + "github.com/invopop/jsonschema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) // SecuritySchemeType represents the authentication scheme enum @@ -24,6 +27,14 @@ var securityScheme_enums = []SecuritySchemeType{ OpenIDConnectScheme, } +// JSONSchema is used to generate a custom jsonschema +func (j SecuritySchemeType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(securityScheme_enums), + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *SecuritySchemeType) UnmarshalJSON(b []byte) error { var rawResult string @@ -60,6 +71,14 @@ const ( var apiKeyLocation_enums = []APIKeyLocation{APIKeyInHeader, APIKeyInQuery, APIKeyInCookie} +// JSONSchema is used to generate a custom jsonschema +func (j APIKeyLocation) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(apiKeyLocation_enums), + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *APIKeyLocation) UnmarshalJSON(b []byte) error { var rawResult string @@ -98,6 +117,81 @@ type SecurityScheme struct { *OpenIDConfig `yaml:",inline"` } +// JSONSchema is used to generate a custom jsonschema +func (j SecurityScheme) JSONSchema() *jsonschema.Schema { + apiKeySchema := orderedmap.New[string, *jsonschema.Schema]() + apiKeySchema.Set("type", &jsonschema.Schema{ + Type: "string", + Enum: []any{APIKeyScheme}, + }) + apiKeySchema.Set("value", &jsonschema.Schema{ + Type: "string", + }) + apiKeySchema.Set("in", (APIKeyLocation("")).JSONSchema()) + apiKeySchema.Set("name", &jsonschema.Schema{ + Type: "string", + }) + + httpAuthSchema := orderedmap.New[string, *jsonschema.Schema]() + httpAuthSchema.Set("type", &jsonschema.Schema{ + Type: "string", + Enum: []any{HTTPAuthScheme}, + }) + httpAuthSchema.Set("value", &jsonschema.Schema{ + Type: "string", + }) + httpAuthSchema.Set("header", &jsonschema.Schema{ + Type: "string", + }) + httpAuthSchema.Set("scheme", &jsonschema.Schema{ + Type: "string", + }) + + oauth2Schema := orderedmap.New[string, *jsonschema.Schema]() + oauth2Schema.Set("type", &jsonschema.Schema{ + Type: "string", + Enum: []any{OAuth2Scheme}, + }) + oauth2Schema.Set("flows", &jsonschema.Schema{ + Type: "object", + AdditionalProperties: &jsonschema.Schema{}, + }) + + oidcSchema := orderedmap.New[string, *jsonschema.Schema]() + oidcSchema.Set("type", &jsonschema.Schema{ + Type: "string", + Enum: []any{OpenIDConnectScheme}, + }) + oidcSchema.Set("openIdConnectUrl", &jsonschema.Schema{ + Type: "string", + }) + + return &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + { + Type: "object", + Required: []string{"type", "value", "in", "name"}, + Properties: apiKeySchema, + }, + { + Type: "object", + Properties: httpAuthSchema, + Required: []string{"type", "value", "header", "scheme"}, + }, + { + Type: "object", + Properties: oauth2Schema, + Required: []string{"type", "flows"}, + }, + { + Type: "object", + Properties: oidcSchema, + Required: []string{"type", "openIdConnectUrl"}, + }, + }, + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *SecurityScheme) UnmarshalJSON(b []byte) error { type Plain SecurityScheme diff --git a/schema/enum.go b/schema/enum.go index 341674f..c2b1d5a 100644 --- a/schema/enum.go +++ b/schema/enum.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "slices" + + "github.com/invopop/jsonschema" ) // SchemaSpecType represents the spec enum of schema @@ -17,7 +19,15 @@ const ( NDCSpec SchemaSpecType = "ndc" ) -var schemaSpecType_enums = []SchemaSpecType{OpenAPIv3Spec, OpenAPIv2Spec, NDCSpec} +var schemaSpecType_enums = []SchemaSpecType{OAS3Spec, OAS2Spec, OpenAPIv3Spec, OpenAPIv2Spec, NDCSpec} + +// JSONSchema is used to generate a custom jsonschema +func (j SchemaSpecType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(schemaSpecType_enums), + } +} // UnmarshalJSON implements json.Unmarshaler. func (j *SchemaSpecType) UnmarshalJSON(b []byte) error { @@ -88,6 +98,14 @@ const ( var schemaFileFormat_enums = []SchemaFileFormat{SchemaFileYAML, SchemaFileJSON} +// JSONSchema is used to generate a custom jsonschema +func (j SchemaFileFormat) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(schemaFileFormat_enums), + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *SchemaFileFormat) UnmarshalJSON(b []byte) error { var rawResult string @@ -135,6 +153,14 @@ const ( var parameterLocation_enums = []ParameterLocation{InQuery, InHeader, InPath, InCookie, InBody, InFormData} +// JSONSchema is used to generate a custom jsonschema +func (j ParameterLocation) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(parameterLocation_enums), + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *ParameterLocation) UnmarshalJSON(b []byte) error { var rawResult string @@ -260,6 +286,14 @@ var parameterEncodingStyle_enums = []ParameterEncodingStyle{ EncodingStyleDeepObject, } +// JSONSchema is used to generate a custom jsonschema +func (j ParameterEncodingStyle) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: toAnySlice(parameterEncodingStyle_enums), + } +} + // UnmarshalJSON implements json.Unmarshaler. func (j *ParameterEncodingStyle) UnmarshalJSON(b []byte) error { var rawResult string diff --git a/schema/enum_test.go b/schema/enum_test.go new file mode 100644 index 0000000..6c1a403 --- /dev/null +++ b/schema/enum_test.go @@ -0,0 +1,74 @@ +package schema + +import ( + "encoding/json" + "fmt" + "testing" +) + +func TestSchemaSpecType(t *testing.T) { + rawValue := "oas2" + var got SchemaSpecType + if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, rawValue)), &got); err != nil { + t.Fatalf(err.Error()) + } + if got != SchemaSpecType(rawValue) { + t.Fatalf("expected %s, got: %s", rawValue, got) + } + if got.JSONSchema().Type != "string" { + t.Fatalf("expected string, got: %s", got.JSONSchema().Type) + } +} + +func TestRequestType(t *testing.T) { + rawValue := "rest" + var got RequestType + if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, rawValue)), &got); err != nil { + t.Fatalf(err.Error()) + } + if got != RequestType(rawValue) { + t.Fatalf("expected %s, got: %s", rawValue, got) + } +} + +func TestSchemaFileFormat(t *testing.T) { + rawValue := "yaml" + var got SchemaFileFormat + if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, rawValue)), &got); err != nil { + t.Fatalf(err.Error()) + } + if got != SchemaFileFormat(rawValue) { + t.Fatalf("expected %s, got: %s", rawValue, got) + } + if got.JSONSchema().Type != "string" { + t.Fatalf("expected string, got: %s", got.JSONSchema().Type) + } +} + +func TestParameterLocation(t *testing.T) { + rawValue := "cookie" + var got ParameterLocation + if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, rawValue)), &got); err != nil { + t.Fatalf(err.Error()) + } + if got != ParameterLocation(rawValue) { + t.Fatalf("expected %s, got: %s", rawValue, got) + } + if got.JSONSchema().Type != "string" { + t.Fatalf("expected string, got: %s", got.JSONSchema().Type) + } +} + +func TestParameterEncodingStyle(t *testing.T) { + rawValue := "matrix" + var got ParameterEncodingStyle + if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, rawValue)), &got); err != nil { + t.Fatalf(err.Error()) + } + if got != ParameterEncodingStyle(rawValue) { + t.Fatalf("expected %s, got: %s", rawValue, got) + } + if got.JSONSchema().Type != "string" { + t.Fatalf("expected string, got: %s", got.JSONSchema().Type) + } +} diff --git a/schema/env.go b/schema/env.go index f3004c0..8f9daf9 100644 --- a/schema/env.go +++ b/schema/env.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/invopop/jsonschema" "gopkg.in/yaml.v3" ) @@ -159,6 +160,13 @@ type EnvString struct { EnvTemplate } +// JSONSchema is used to generate a custom jsonschema +func (j EnvString) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + } +} + // WithValue returns a new EnvString instance with new value func (j EnvString) WithValue(value string) *EnvString { j.value = &value @@ -277,6 +285,16 @@ func NewEnvIntTemplate(template EnvTemplate) *EnvInt { } } +// JSONSchema is used to generate a custom jsonschema +func (j EnvInt) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + {Type: "integer"}, + {Type: "string"}, + }, + } +} + // WithValue returns a new EnvInt instance with new value func (j EnvInt) WithValue(value int64) *EnvInt { j.value = &value @@ -409,6 +427,16 @@ func NewEnvIntsTemplate(template EnvTemplate) *EnvInts { } } +// JSONSchema is used to generate a custom jsonschema +func (j EnvInts) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + {Type: "string"}, + {Type: "array", Items: &jsonschema.Schema{Type: "integer"}}, + }, + } +} + // WithValue returns a new EnvInts instance with new value func (j EnvInts) WithValue(value []int64) *EnvInts { j.value = value diff --git a/schema/schema.go b/schema/schema.go index 1d5f6d0..fded6d7 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -10,7 +10,8 @@ import ( // // [NDC schema]: https://github.com/hasura/ndc-sdk-go/blob/1d3339db29e13a170aa8be5ff7fae8394cba0e49/schema/schema.generated.go#L887 type NDCRestSchema struct { - Settings *NDCRestSettings `json:"settings,omitempty" yaml:"settings,omitempty" mapstructure:"settings"` + SchemaRef string `json:"$schema,omitempty" yaml:"$schema,omitempty" mapstructure:"$schema"` + Settings *NDCRestSettings `json:"settings,omitempty" yaml:"settings,omitempty" mapstructure:"settings"` // Collections which are available for queries Collections []schema.CollectionInfo `json:"collections" yaml:"collections" mapstructure:"collections"` @@ -32,6 +33,7 @@ type NDCRestSchema struct { // NewNDCRestSchema creates a NDCRestSchema instance func NewNDCRestSchema() *NDCRestSchema { return &NDCRestSchema{ + SchemaRef: "https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/ndc-rest-schema.jsonschema", Settings: &NDCRestSettings{}, Collections: []schema.CollectionInfo{}, Functions: []*RESTFunctionInfo{}, @@ -68,7 +70,7 @@ type Response struct { // Request represents the HTTP request information of the webhook type Request struct { URL string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url"` - Method string `json:"method,omitempty" yaml:"method,omitempty" mapstructure:"method"` + Method string `json:"method,omitempty" yaml:"method,omitempty" mapstructure:"method" jsonschema:"enum=get,enum=post,enum=put,enum=patch,enum=delete"` Type RequestType `json:"type,omitempty" yaml:"type,omitempty" mapstructure:"type"` Headers map[string]EnvString `json:"headers,omitempty" yaml:"headers,omitempty" mapstructure:"headers"` Parameters []RequestParameter `json:"parameters,omitempty" yaml:"parameters,omitempty" mapstructure:"parameters"` @@ -235,3 +237,11 @@ func (j *RESTProcedureInfo) UnmarshalJSON(b []byte) error { func toPtr[V any](value V) *V { return &value } + +func toAnySlice[T any](values []T) []any { + results := make([]any, len(values)) + for i, v := range values { + results[i] = v + } + return results +} diff --git a/utils/patch.go b/utils/patch.go index 7de4d14..364091c 100644 --- a/utils/patch.go +++ b/utils/patch.go @@ -28,7 +28,7 @@ const ( // PatchConfig the configuration for JSON patch type PatchConfig struct { Path string `json:"path" yaml:"path"` - Strategy PatchStrategy `json:"strategy" yaml:"strategy"` + Strategy PatchStrategy `json:"strategy" yaml:"strategy" jsonschema:"enum=merge,enum=json6902"` } // ApplyPatchToRestSchema applies JSON patches to NDC rest schema and validate the output