Skip to content

Commit a72086b

Browse files
committed
feat: remove schema resource and add default schema
1 parent 3530eb0 commit a72086b

File tree

9 files changed

+249
-280
lines changed

9 files changed

+249
-280
lines changed

plugins/jsonschema_validator/jsonschema/jsonschema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type JSONSchema struct {
1515
func New(schemaDef []byte) *JSONSchema {
1616
return &JSONSchema{
1717
schemaDef: string(schemaDef),
18-
hex: utils.Hash256(string(schemaDef)),
18+
hex: utils.Sha256(string(schemaDef)),
1919
}
2020
}
2121

plugins/jsonschema_validator/plugin.go

Lines changed: 64 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,72 +6,22 @@ import (
66
"errors"
77
"fmt"
88
"github.com/getkin/kin-openapi/openapi3"
9-
lru "github.com/hashicorp/golang-lru/v2"
109
"github.com/webhookx-io/webhookx/pkg/errs"
10+
"github.com/webhookx-io/webhookx/pkg/http/response"
1111
"github.com/webhookx-io/webhookx/pkg/plugin"
12+
"github.com/webhookx-io/webhookx/pkg/types"
1213
"github.com/webhookx-io/webhookx/plugins/jsonschema_validator/jsonschema"
1314
"github.com/webhookx-io/webhookx/utils"
14-
"io"
15-
"net/http"
16-
"os"
17-
"time"
1815
)
1916

2017
type Config struct {
21-
Schemas map[string]*SchemaResource `json:"schemas" validate:"dive,required"`
18+
DraftVersion int `json:"draft_version" validate:"required,oneof=6"`
19+
DefaultSchema string `json:"default_schema" validate:"omitempty,json,max=1048576"`
20+
Schemas map[string]*Schema `json:"schemas" validate:"dive"`
2221
}
2322

24-
type EventTypeSchema struct {
25-
EventType string `json:"event_type" validate:"required,max=100"`
26-
JSONSchema string `json:"jsonschema" validate:"required,jsonschema,max=1048576"`
27-
}
28-
29-
type SchemaResource struct {
30-
JSONString string `json:"json" validate:"omitempty,json,max=1048576"`
31-
File string `json:"file" validate:"omitempty,file"`
32-
URL string `json:"url" validate:"omitempty,url"`
33-
}
34-
35-
var cache, _ = lru.New[string, []byte](128)
36-
37-
func (s *SchemaResource) Resource() ([]byte, string, error) {
38-
// priority: json > file > url
39-
if s.JSONString != "" {
40-
return []byte(s.JSONString), "json", nil
41-
}
42-
if s.File != "" {
43-
bytes, ok := cache.Get(s.File)
44-
if ok {
45-
return bytes, "file", nil
46-
}
47-
bytes, err := os.ReadFile(s.File)
48-
if err != nil {
49-
return nil, "file", fmt.Errorf("failed to read schema: %w", err)
50-
}
51-
cache.Add(s.File, bytes)
52-
return bytes, "file", nil
53-
}
54-
if s.URL != "" {
55-
bytes, ok := cache.Get(s.URL)
56-
if ok {
57-
return bytes, "url", nil
58-
}
59-
client := &http.Client{
60-
Timeout: time.Second * 2,
61-
}
62-
resp, err := client.Get(s.URL)
63-
if err != nil {
64-
return nil, "url", fmt.Errorf("failed to fetch schema: %w", err)
65-
}
66-
defer func() { _ = resp.Body.Close() }()
67-
body, err := io.ReadAll(resp.Body)
68-
if err != nil {
69-
return nil, "url", fmt.Errorf("failed to read schema from response: %w", err)
70-
}
71-
cache.Add(s.URL, body)
72-
return body, "url", nil
73-
}
74-
return nil, "json", errors.New("no schema defined")
23+
type Schema struct {
24+
Schema string `json:"schema" validate:"omitempty,json,max=1048576"`
7525
}
7626

7727
type SchemaValidatorPlugin struct {
@@ -90,47 +40,62 @@ func New(config []byte) (plugin.Plugin, error) {
9040
return p, nil
9141
}
9242

43+
func unmarshalAndValidateSchema(schema string) (*openapi3.Schema, error) {
44+
openapiSchema := &openapi3.Schema{}
45+
err := openapiSchema.UnmarshalJSON([]byte(schema))
46+
if err != nil {
47+
return nil, fmt.Errorf("value must be a valid jsonschema")
48+
}
49+
err = openapiSchema.Validate(context.Background(), openapi3.EnableSchemaFormatValidation())
50+
if err != nil {
51+
return openapiSchema, err
52+
}
53+
return openapiSchema, nil
54+
}
55+
9356
func (p *SchemaValidatorPlugin) ValidateConfig() error {
9457
err := utils.Validate(p.Config)
9558
if err != nil {
9659
return err
9760
}
9861

9962
e := errs.NewValidateError(errors.New("request validation"))
100-
for event, schema := range p.Config.Schemas {
101-
field := fmt.Sprintf("schemas[%s]", event)
102-
if schema == nil {
103-
e.Fields[field] = fmt.Errorf("schema is empty")
104-
return e
105-
}
106-
schemaBytes, invalidField, err := schema.Resource()
63+
64+
var defaultErr error
65+
if p.Config.DefaultSchema != "" {
66+
_, err := unmarshalAndValidateSchema(p.Config.DefaultSchema)
10767
if err != nil {
108-
e.Fields[field] = map[string]string{
109-
invalidField: err.Error(),
68+
defaultErr = err
69+
e.Fields = map[string]interface{}{
70+
"default_schema": err.Error(),
11071
}
111-
return e
11272
}
113-
openapiSchema := &openapi3.Schema{}
114-
err = openapiSchema.UnmarshalJSON(schemaBytes)
115-
if err != nil {
116-
e.Fields[field] = map[string]string{
117-
invalidField: "the content must be a valid json string",
73+
}
74+
75+
for event, schema := range p.Config.Schemas {
76+
field := fmt.Sprintf("schemas[%s]", event)
77+
if schema == nil || schema.Schema == "" {
78+
if defaultErr != nil {
79+
e.Fields[field] = map[string]string{
80+
"schema": "invalid due to reusing the default_schema definition",
81+
}
11882
}
119-
return e
120-
}
121-
err = openapiSchema.Validate(context.Background(), openapi3.EnableSchemaFormatValidation())
122-
if err != nil {
123-
e.Fields[field] = map[string]string{
124-
invalidField: fmt.Sprintf("invalid jsonschema: %v", err),
83+
} else {
84+
_, err = unmarshalAndValidateSchema(schema.Schema)
85+
if err != nil {
86+
e.Fields[field] = map[string]string{
87+
"schema": err.Error(),
88+
}
12589
}
126-
return e
12790
}
12891
}
92+
if len(e.Fields) > 0 {
93+
return e
94+
}
12995
return nil
13096
}
13197

13298
func (p *SchemaValidatorPlugin) ExecuteInbound(inbound *plugin.Inbound) (res plugin.InboundResult, err error) {
133-
// parse body to get event type
13499
var event map[string]any
135100
body := inbound.RawBody
136101
if err = json.Unmarshal(body, &event); err != nil {
@@ -149,24 +114,34 @@ func (p *SchemaValidatorPlugin) ExecuteInbound(inbound *plugin.Inbound) (res plu
149114
return
150115
}
151116

152-
schemaResource, ok := p.Config.Schemas[eventType]
153-
if !ok || schemaResource == nil {
117+
schema, ok := p.Config.Schemas[eventType]
118+
if !ok {
154119
res.Payload = body
155120
return
156121
}
157-
158-
bytes, _, err := schemaResource.Resource()
159-
if err != nil {
160-
return
122+
if schema == nil || schema.Schema == "" {
123+
if p.Config.DefaultSchema == "" {
124+
res.Payload = body
125+
return
126+
}
127+
schema = &Schema{
128+
Schema: p.Config.DefaultSchema,
129+
}
161130
}
162-
validator := jsonschema.New(bytes)
163-
err = validator.Validate(&jsonschema.ValidatorContext{
131+
132+
validator := jsonschema.New([]byte(schema.Schema))
133+
e := validator.Validate(&jsonschema.ValidatorContext{
164134
HTTPRequest: &jsonschema.HTTPRequest{
165135
R: inbound.Request,
166136
Data: data.(map[string]any),
167137
},
168138
})
169-
if err != nil {
139+
if e != nil {
140+
response.JSON(inbound.Response, 400, types.ErrorResponse{
141+
Message: "Request Validation",
142+
Error: e,
143+
})
144+
res.Terminated = true
170145
return
171146
}
172147
res.Payload = body

proxy/gateway.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/webhookx-io/webhookx/dispatcher"
1515
"github.com/webhookx-io/webhookx/eventbus"
1616
"github.com/webhookx-io/webhookx/mcache"
17-
"github.com/webhookx-io/webhookx/pkg/errs"
1817
"github.com/webhookx-io/webhookx/pkg/http/response"
1918
"github.com/webhookx-io/webhookx/pkg/loglimiter"
2019
"github.com/webhookx-io/webhookx/pkg/metrics"
@@ -257,14 +256,6 @@ func (gw *Gateway) handle(w http.ResponseWriter, r *http.Request) bool {
257256
RawBody: body,
258257
})
259258
if err != nil {
260-
if validateErr, ok := err.(*errs.ValidateError); ok {
261-
response.JSON(w, 400, types.ErrorResponse{
262-
Message: "Request Validation",
263-
Error: validateErr,
264-
})
265-
return false
266-
}
267-
268259
gw.log.Errorf("failed to execute plugin: %v", err)
269260
response.JSON(w, 500, types.ErrorResponse{Message: "internal error"})
270261
return false

test/cmd/admin_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ var _ = Describe("admin", Ordered, func() {
101101
assert.Equal(GinkgoT(), `{"function": "function handle() {}"}`, string(plugins[0].Config))
102102
assert.Equal(GinkgoT(), `jsonschema-validator`, plugins[1].Name)
103103
assert.Equal(GinkgoT(), true, plugins[1].Enabled)
104-
assert.Equal(GinkgoT(), `{"schemas": {"charge.succeeded": {"url": "", "file": "", "json": "{\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" },\n \"amount\": { \"type\": \"integer\", \"minimum\": 1 },\n \"currency\": { \"type\": \"string\", \"minLength\": 3, \"maxLength\": 6 }\n },\n \"required\": [\"id\", \"amount\", \"currency\"]\n}\n"}}}`, string(plugins[1].Config))
104+
105+
assert.Equal(GinkgoT(), `{"schemas": {"charge.succeeded": {"schema": "{\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" },\n \"amount\": { \"type\": \"integer\", \"minimum\": 1 },\n \"currency\": { \"type\": \"string\", \"minLength\": 3, \"maxLength\": 6 }\n },\n \"required\": [\"id\", \"amount\", \"currency\"]\n}\n"}}, "draft_version": 6, "default_schema": "{\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" }\n },\n \"required\": [\"id\"]\n}\n"}`, string(plugins[1].Config))
105106
})
106107

107108
It("entities not defined in the declarative configuration should be deleted", func() {

test/declarative/declarative_test.go

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,38 @@ endpoints:
4141
- name: foo
4242
`
4343

44-
invalidSourcePluginJSONSchemaJSONYAML = `
44+
invalidSourcePluginJSONSchemaConfigYAML = `
4545
sources:
4646
- name: default-source
4747
path: /
4848
methods: ["POST"]
4949
plugins:
5050
- name: "jsonschema-validator"
5151
config:
52+
default_schema: |
53+
%s
5254
schemas:
5355
charge.succeed:
54-
json: '%s'
56+
schema: |
57+
%s
5558
`
5659

57-
invalidSourcePluginJSONSchemaFileYAML = `
58-
sources:
59-
- name: default-source
60-
path: /
61-
methods: ["POST"]
62-
plugins:
63-
- name: "jsonschema-validator"
64-
config:
65-
schemas:
66-
charge.succeed:
67-
file: "%s"
68-
`
69-
invalidSourcePluginJSONSchemaURLYAML = `
60+
invalidSourcePluginJSONSchemaJSONYAML = `
7061
sources:
7162
- name: default-source
7263
path: /
7364
methods: ["POST"]
7465
plugins:
7566
- name: "jsonschema-validator"
7667
config:
68+
draft_version: 6
69+
default_schema: |
70+
%s
7771
schemas:
7872
charge.succeed:
79-
url: "http://localhost/charge.succeed.json"
73+
schema: |
74+
%s
75+
reuse.default_schema:
8076
`
8177
)
8278

@@ -143,59 +139,30 @@ var _ = Describe("Declarative", Ordered, func() {
143139
assert.Equal(GinkgoT(), `{"message":"Request Validation","error":{"message":"request validation","fields":{"name":"unknown plugin name 'foo'"}}}`, string(resp.Body()))
144140
})
145141

146-
It("should return 400 for invalid jsonschema-validator plugin config json string", func() {
147-
resp, err := adminClient.R().
148-
SetBody(fmt.Sprintf(invalidSourcePluginJSONSchemaJSONYAML, "invalid jsonstring")).
149-
Post("/workspaces/default/config/sync")
150-
assert.Nil(GinkgoT(), err)
151-
assert.Equal(GinkgoT(), 400, resp.StatusCode())
152-
assert.Equal(GinkgoT(),
153-
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"schemas[charge.succeed]":{"json":"value must be a valid json string"}}}}}`,
154-
string(resp.Body()))
155-
})
156-
157-
It("should return 400 for invalid jsonschema-validator plugin config jsonschema", func() {
158-
resp, err := adminClient.R().
159-
SetBody(fmt.Sprintf(invalidSourcePluginJSONSchemaJSONYAML, `{"type":"invalidObject"}`)).
160-
Post("/workspaces/default/config/sync")
161-
assert.Nil(GinkgoT(), err)
162-
assert.Equal(GinkgoT(), 400, resp.StatusCode())
163-
assert.Equal(GinkgoT(),
164-
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"schemas[charge.succeed]":{"json":"invalid jsonschema: unsupported 'type' value \"invalidObject\""}}}}}`,
165-
string(resp.Body()))
166-
})
167-
168-
It("should return 400 for invalid jsonschema-validator plugin config file", func() {
142+
It("should return 400 for invalid jsonschema-validator plugin config", func() {
169143
resp, err := adminClient.R().
170-
SetBody(fmt.Sprintf(invalidSourcePluginJSONSchemaFileYAML, "./notexist.json")).
144+
SetBody(
145+
fmt.Sprintf(invalidSourcePluginJSONSchemaConfigYAML, "invalid jsonschema", "invalid jsonschema"),
146+
).
171147
Post("/workspaces/default/config/sync")
172148

173149
assert.Nil(GinkgoT(), err)
174150
assert.Equal(GinkgoT(), 400, resp.StatusCode())
175151
assert.Equal(GinkgoT(),
176-
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"schemas[charge.succeed]":{"file":"value must be a valid exist file"}}}}}`,
177-
string(resp.Body()))
178-
})
179-
180-
It("should return 400 for invalid jsonschema-validator config file content", func() {
181-
resp, err := adminClient.R().
182-
SetBody(fmt.Sprintf(invalidSourcePluginJSONSchemaFileYAML, "../fixtures/jsonschema/invalid.json")).
183-
Post("/workspaces/default/config/sync")
184-
assert.Nil(GinkgoT(), err)
185-
assert.Equal(GinkgoT(), 400, resp.StatusCode())
186-
assert.Equal(GinkgoT(),
187-
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"schemas[charge.succeed]":{"file":"the content must be a valid json string"}}}}}`,
152+
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"default_schema":"value must be a valid json string","draft_version":"required field missing","schemas[charge.succeed]":{"schema":"value must be a valid json string"}}}}}`,
188153
string(resp.Body()))
189154
})
190155

191-
It("should return 400 for invalid source plugin config url", func() {
156+
It("should return 400 for invalid jsonschema-validator plugin config jsonschema string", func() {
192157
resp, err := adminClient.R().
193-
SetBody(invalidSourcePluginJSONSchemaURLYAML).
158+
SetBody(fmt.Sprintf(invalidSourcePluginJSONSchemaJSONYAML,
159+
`{"type": "invlidObject","properties": {"id": { "type": "string"}}}`,
160+
`{"type": "object","properties": {"id": { "type": "number", "format":"invalid"}}}`)).
194161
Post("/workspaces/default/config/sync")
195162
assert.Nil(GinkgoT(), err)
196163
assert.Equal(GinkgoT(), 400, resp.StatusCode())
197164
assert.Equal(GinkgoT(),
198-
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"schemas[charge.succeed]":{"url":"failed to fetch schema: Get \"http://localhost/charge.succeed.json\": dial tcp [::1]:80: connect: connection refused"}}}}}`,
165+
`{"message":"Request Validation","error":{"message":"request validation","fields":{"config":{"default_schema":"unsupported 'type' value \"invlidObject\"","schemas[charge.succeed]":{"schema":"unsupported 'format' value \"invalid\""},"schemas[reuse.default_schema]":{"schema":"invalid due to reusing the default_schema definition"}}}}}`,
199166
string(resp.Body()))
200167
})
201168
})

test/fixtures/webhookx.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,18 @@ sources:
3939
function: "function handle() {}"
4040
- name: "jsonschema-validator"
4141
config:
42+
draft_version: 6
43+
default_schema: |
44+
{
45+
"type": "object",
46+
"properties": {
47+
"id": { "type": "string" }
48+
},
49+
"required": ["id"]
50+
}
4251
schemas:
4352
charge.succeeded:
44-
#file: "../fixtures/jsonschema/charge.succeed.json"
45-
#url: "https://raw.githubusercontent.com/cchenggit/webhookx/refs/heads/feat/plugin-jsonschema-validator/test/fixtures/jsonschema/charge.succeed.json"
46-
json: |
53+
schema: |
4754
{
4855
"type": "object",
4956
"properties": {

0 commit comments

Comments
 (0)