diff --git a/openapi.go b/openapi.go index 5eebae99..23f15571 100644 --- a/openapi.go +++ b/openapi.go @@ -103,23 +103,14 @@ func (c *oaComponents) addSchema(t reflect.Type, mode schema.Mode, hint string, return c.addExistingSchema(s, name, generateSchemaField) } - //AddExistingSchema adds an existing schema instance under the given name. +// AddExistingSchema adds an existing schema instance under the given name. func (c *oaComponents) AddExistingSchema(s *schema.Schema, name string, generateSchemaField bool) string { return c.addExistingSchema(s, name, generateSchemaField) } func (c *oaComponents) addExistingSchema(s *schema.Schema, name string, generateSchemaField bool) string { - if generateSchemaField && s.Type == schema.TypeObject && s.Properties != nil { - if s.Properties["$schema"] == nil { - // Some editors allow you to place a $schema key which gives you rich - // validation and code completion support. Let's enable that by allowing - // a field here if it doesn't already exist in the model. - s.Properties["$schema"] = &schema.Schema{ - Type: schema.TypeString, - Format: "uri", - Description: "An optional URL to a JSON Schema document describing this resource", - } - } + if generateSchemaField { + s.AddSchemaField() } orig := name diff --git a/openapi_test.go b/openapi_test.go index 5b723674..5613b062 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -23,26 +23,26 @@ func TestComponentSchemas(t *testing.T) { } // Adding two different versions of the same component. - ref := components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeRead, "hint") + ref := components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeRead, "hint", true) assert.Equal(t, ref, "#/components/schemas/componentFoo") assert.NotNil(t, components.Schemas["componentFoo"]) - ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint") + ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint", true) assert.Equal(t, ref, "#/components/schemas/componentFoo2") assert.NotNil(t, components.Schemas["componentFoo2"]) // Re-adding the second should not create a third. - ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint") + ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint", true) assert.Equal(t, ref, "#/components/schemas/componentFoo2") assert.Nil(t, components.Schemas["componentFoo3"]) // Adding a list of pointers to a struct. - ref = components.AddSchema(reflect.TypeOf([]*componentBar{}), schema.ModeAll, "hint") + ref = components.AddSchema(reflect.TypeOf([]*componentBar{}), schema.ModeAll, "hint", true) assert.Equal(t, ref, "#/components/schemas/componentBarList") assert.NotNil(t, components.Schemas["componentBarList"]) // Adding an anonymous empty struct, should use the hint. - ref = components.AddSchema(reflect.TypeOf(struct{}{}), schema.ModeAll, "hint") + ref = components.AddSchema(reflect.TypeOf(struct{}{}), schema.ModeAll, "hint", true) assert.Equal(t, ref, "#/components/schemas/hint") assert.NotNil(t, components.Schemas["hint"]) } diff --git a/operation.go b/operation.go index 9ee58795..f273400f 100644 --- a/operation.go +++ b/operation.go @@ -240,6 +240,9 @@ func (o *Operation) Run(handler interface{}) { if o.requestSchema == nil { o.requestSchema, err = schema.GenerateWithMode(body.Type, schema.ModeWrite, nil) + if o.resource != nil && o.resource.router != nil && !o.resource.router.disableSchemaProperty { + o.requestSchema.AddSchemaField() + } if err != nil { panic(fmt.Errorf("unable to generate JSON schema: %w", err)) } diff --git a/router_test.go b/router_test.go index 2d76b384..642cf2b1 100644 --- a/router_test.go +++ b/router_test.go @@ -497,3 +497,52 @@ func TestDefaultResponse(t *testing.T) { // This should not panic and should return the 200 OK assert.Equal(t, http.StatusOK, w.Result().StatusCode) } + +func TestRoundTrip(t *testing.T) { + app := newTestRouter() + + // This should not crash. + resource := app.Resource("/") + + type Thing struct { + Name string `json:"name"` + } + + resource.Get("get-root", "docs", NewResponse(0, "").Model(Thing{})).Run(func(ctx Context) { + ctx.WriteModel(http.StatusOK, Thing{Name: "Test"}) + }) + + resource.Put("put-root", "", NewResponse(200, "")).Run(func(ctx Context, input struct { + Body Thing + }) { + // If we get here then all the validation passed okay! + assert.Equal(t, "Test", input.Body.Name) + }) + + w1 := httptest.NewRecorder() + req1, _ := http.NewRequest(http.MethodGet, "/openapi.json", nil) + app.ServeHTTP(w1, req1) + t.Log(w1.Body.String()) + + w1 = httptest.NewRecorder() + req1, _ = http.NewRequest(http.MethodGet, "/schemas/Thing.json", nil) + app.ServeHTTP(w1, req1) + t.Log(w1.Body.String()) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/", nil) + app.ServeHTTP(w, req) + + // This should not panic and should return the 200 OK + assert.Equal(t, http.StatusOK, w.Result().StatusCode) + + thing := w.Body + + w = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodPut, "/", thing) + app.ServeHTTP(w, req) + + t.Log(w.Body.String()) + + assert.Equal(t, http.StatusOK, w.Result().StatusCode) +} diff --git a/schema/schema.go b/schema/schema.go index 16fef29b..c993d43d 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -171,6 +171,24 @@ func (s *Schema) RemoveProperty(name string) { } } +// AddSchemaField adds a $schema field if none is present, allowing the +// resource to be self-descriptive and enabling editor features like +// code completion / suggestions as you type & inline linting / validation. +func (s *Schema) AddSchemaField() { + if s.Type == TypeObject && s.Properties != nil { + if s.Properties["$schema"] == nil { + // Some editors allow you to place a $schema key which gives you rich + // validation and code completion support. Let's enable that by allowing + // a field here if it doesn't already exist in the model. + s.Properties["$schema"] = &Schema{ + Type: TypeString, + Format: "uri", + Description: "An optional URL to a JSON Schema document describing this resource", + } + } + } +} + // Generate creates a JSON schema for a Go type. Struct field tags // can be used to provide additional metadata such as descriptions and // validation. diff --git a/schema/schema_test.go b/schema/schema_test.go index 5095a2b6..3f7fa167 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Example() { @@ -621,3 +622,11 @@ func TestExampleBadJSON(t *testing.T) { _, err := Generate(reflect.TypeOf(Foo{})) assert.Error(t, err) } + +func TestAddSchemaField(t *testing.T) { + dummy := struct{ Name string }{Name: "test"} + s, err := Generate(reflect.TypeOf(dummy)) + require.NoError(t, err) + s.AddSchemaField() + assert.NotNil(t, s.Properties["$schema"]) +}