diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index bd4fb36..51fbc24 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -21,7 +21,7 @@ env: GO111MODULE: "on" CACHE_BENCHMARK: "off" # Enables benchmark result reuse between runs, may skew latency results. RUN_BASE_BENCHMARK: "on" # Runs benchmark for PR base in case benchmark result is missing. - GO_VERSION: 1.20.x + GO_VERSION: 1.21.x jobs: bench: runs-on: ubuntu-latest diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index ac6e92d..d2e4b61 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,13 +21,13 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.20.x + go-version: 1.21.x - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v3.4.0 + uses: golangci/golangci-lint-action@v3.7.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.51.2 + version: v1.54.1 # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml index 41767ba..97ebe87 100644 --- a/.github/workflows/gorelease.yml +++ b/.github/workflows/gorelease.yml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.20.x + GO_VERSION: 1.21.x jobs: gorelease: runs-on: ubuntu-latest diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 2817dcc..1f61fb8 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -15,13 +15,13 @@ concurrency: env: GO111MODULE: "on" RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing. - COV_GO_VERSION: 1.20.x # Version of Go to collect coverage + COV_GO_VERSION: 1.21.x # Version of Go to collect coverage TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents jobs: test: strategy: matrix: - go-version: [ 1.13.x, 1.19.x, 1.20.x ] + go-version: [ 1.13.x, 1.20.x, 1.21.x ] runs-on: ubuntu-latest steps: - name: Install Go stable @@ -88,14 +88,14 @@ jobs: id: annotate if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' run: | - curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz + curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.4.0/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz gocovdiff_hash=$(git hash-object ./gocovdiff) - [ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) + [ "$gocovdiff_hash" == "f191b45548bb65ec2c7d88909679a57116ff1ba1" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) git fetch origin master ${{ github.event.pull_request.base.sha }} - REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) + REP=$(./gocovdiff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) echo "${REP}" cat gha-unit.txt - DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") + DIFF=$(test -e unit-base.txt && ./gocovdiff -mod github.com/$GITHUB_REPOSITORY -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") TOTAL=$(cat delta-cov-unit.txt) echo "rep<> $GITHUB_OUTPUT && echo "$REP" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT echo "diff<> $GITHUB_OUTPUT && echo "$DIFF" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT diff --git a/.golangci.yml b/.golangci.yml index 3be2189..ffdb4db 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,6 +47,8 @@ linters: - deadcode - testableexamples - dupword + - depguard + - tagalign issues: exclude-use-default: false diff --git a/ADVANCED.md b/ADVANCED.md index c190f2e..df6fdfc 100644 --- a/ADVANCED.md +++ b/ADVANCED.md @@ -96,3 +96,51 @@ r.Method(http.MethodGet, "/docs/openapi.json", apiSchema) r.Mount("/docs", v3cdn.NewHandler(apiSchema.Reflector().Spec.Info.Title, "/docs/openapi.json", "/docs")) ``` + +## Request Control + +Input type may implement methods to customize request handling. + +```go +// LoadFromHTTPRequest takes full control of request decoding and validating and prevents automated request decoding. +LoadFromHTTPRequest(r *http.Request) error +``` + +```go +// SetRequest captures current *http.Request, but does not prevent automated request decoding. +// You can embed request.EmbeddedSetter into your structure to get access to the request. +SetRequest(r *http.Request) +``` + +## Response Control + +Output type may implement methods to customize response handling. + +```go +// NoContent controls whether status 204 should be used in response to current request. +NoContent() bool +``` + +```go +// SetupResponseHeader gives access to response headers of current request. +SetupResponseHeader(h http.Header) +``` + +```go +// ETag returns the Etag response header value for the current request. +ETag() string +``` + +```go +// SetWriter takes full control of http.ResponseWriter and prevents automated response handling. +SetWriter(w io.Writer) +``` + +```go +// HTTPStatus declares successful HTTP status for all requests. +HTTPStatus() int + +// ExpectedHTTPStatuses declares additional HTTP statuses for all requests. +// Only used for documentation purposes. +ExpectedHTTPStatuses() []int +``` \ No newline at end of file diff --git a/Makefile b/Makefile index 6a729aa..73ced0a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GOLANGCI_LINT_VERSION := "v1.51.1" # Optional configuration to pinpoint golangci-lint version. +#GOLANGCI_LINT_VERSION := "v1.54.1" # Optional configuration to pinpoint golangci-lint version. # The head of Makefile determines location of dev-go to include standard targets. GO ?= go diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json index 17b7961..bba76e9 100644 --- a/_examples/advanced/_testdata/openapi.json +++ b/_examples/advanced/_testdata/openapi.json @@ -28,6 +28,38 @@ "security":[{"User":[]}] } }, + "/dynamic-schema":{ + "get":{ + "summary":"Dynamic Request Schema", + "description":"This use case demonstrates documentation of types that are only known at runtime.", + "operationId":"_examples/advanced.dynamicSchema", + "parameters":[ + {"name":"bar","in":"query","schema":{"type":"string"}}, + {"name":"type","in":"query","schema":{"type":"string"}}, + {"name":"foo","in":"header","schema":{"enum":["123","456","789"],"type":"integer"}} + ], + "responses":{ + "200":{ + "description":"OK", + "headers":{"foo":{"style":"simple","schema":{"enum":["123","456","789"],"type":"integer"}}}, + "content":{"application/dummy+json":{"schema":{"$ref":"#/components/schemas/AdvancedDynamicOutput"}}} + }, + "400":{ + "description":"Bad Request", + "content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} + }, + "409":{ + "description":"Conflict", + "content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} + }, + "412":{ + "description":"Precondition Failed", + "content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} + } + }, + "x-forbid-unknown-query":true + } + }, "/error-response":{ "get":{ "summary":"Declare Expected Errors", @@ -460,6 +492,7 @@ "schemas":{ "AdvancedAnotherErr":{"type":"object","properties":{"foo":{"type":"integer"}}}, "AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}}, + "AdvancedDynamicOutput":{"type":"object","properties":{"bar":{"type":"string"},"status":{"type":"string"}}}, "AdvancedGzipPassThroughStruct":{ "type":"object", "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}} diff --git a/_examples/advanced/dynamic_schema.go b/_examples/advanced/dynamic_schema.go new file mode 100644 index 0000000..dd93142 --- /dev/null +++ b/_examples/advanced/dynamic_schema.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "net/http" + + "github.com/bool64/ctxd" + "github.com/swaggest/jsonschema-go" + "github.com/swaggest/rest/request" + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" +) + +type dynamicInput struct { + jsonschema.Struct + request.EmbeddedSetter + + // Type is a static field example. + Type string `query:"type"` +} + +type dynamicOutput struct { + // Embedded jsonschema.Struct exposes dynamic fields for documentation. + jsonschema.Struct + + jsonFields map[string]interface{} + headerFields map[string]string + + // Status is a static field example. + Status string `json:"status"` +} + +func (o dynamicOutput) SetupResponseHeader(h http.Header) { + for k, v := range o.headerFields { + h.Set(k, v) + } +} + +func (o dynamicOutput) MarshalJSON() ([]byte, error) { + if o.jsonFields == nil { + o.jsonFields = map[string]interface{}{} + } + + o.jsonFields["status"] = o.Status + + return json.Marshal(o.jsonFields) +} + +func dynamicSchema() usecase.Interactor { + dynIn := dynamicInput{} + dynIn.Struct.Fields = []jsonschema.Field{ + {Name: "Foo", Value: 123, Tag: `header:"foo" enum:"123,456,789"`}, + {Name: "Bar", Value: "abc", Tag: `query:"bar"`}, + } + + dynOut := dynamicOutput{} + dynOut.Struct.Fields = []jsonschema.Field{ + {Name: "Foo", Value: 123, Tag: `header:"foo" enum:"123,456,789"`}, + {Name: "Bar", Value: "abc", Tag: `json:"bar"`}, + } + + u := usecase.NewIOI(dynIn, dynOut, func(ctx context.Context, input, output interface{}) (err error) { + var ( + in = input.(dynamicInput) + out = output.(*dynamicOutput) + ) + + switch in.Type { + case "ok": + out.Status = "ok" + out.jsonFields = map[string]interface{}{ + "bar": in.Request().URL.Query().Get("bar"), + } + out.headerFields = map[string]string{ + "foo": in.Request().Header.Get("foo"), + } + case "invalid_argument": + return status.Wrap(errors.New("bad value for foo"), status.InvalidArgument) + case "conflict": + return status.Wrap(ctxd.NewError(ctx, "conflict", "foo", "bar"), + status.AlreadyExists) + } + + return nil + }) + + u.SetTitle("Dynamic Request Schema") + u.SetDescription("This use case demonstrates documentation of types that are only known at runtime.") + u.SetExpectedErrors(status.InvalidArgument, status.FailedPrecondition, status.AlreadyExists) + + return u +} diff --git a/_examples/advanced/dynamic_schema_test.go b/_examples/advanced/dynamic_schema_test.go new file mode 100644 index 0000000..7b555a4 --- /dev/null +++ b/_examples/advanced/dynamic_schema_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/swaggest/assertjson" +) + +func Test_dynamicOutput(t *testing.T) { + r := NewRouter() + + req := httptest.NewRequest(http.MethodGet, "/dynamic-schema?bar=ccc&type=ok", nil) + req.Header.Set("foo", "456") + + rw := httptest.NewRecorder() + r.ServeHTTP(rw, req) + + assertjson.Equal(t, []byte(`{"bar": "ccc","status": "ok"}`), rw.Body.Bytes()) + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "456", rw.Header().Get("foo")) +} diff --git a/_examples/advanced/router.go b/_examples/advanced/router.go index 5bc8e19..ccf9492 100644 --- a/_examples/advanced/router.go +++ b/_examples/advanced/router.go @@ -148,6 +148,7 @@ func NewRouter() http.Handler { s.Head("/gzip-pass-through", directGzip()) s.Get("/error-response", errorResponse()) + s.Get("/dynamic-schema", dynamicSchema()) // Security middlewares. // - sessMW is the actual request-level processor, diff --git a/_examples/go.mod b/_examples/go.mod index 6ca8c48..6ba38b6 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -6,7 +6,7 @@ replace github.com/swaggest/rest => ../ require ( github.com/bool64/ctxd v1.2.1 - github.com/bool64/dev v0.2.29 + github.com/bool64/dev v0.2.31 github.com/bool64/httpmock v0.1.13 github.com/bool64/httptestbench v0.1.4 github.com/gin-gonic/gin v1.9.1 @@ -16,7 +16,7 @@ require ( github.com/rs/cors v1.9.0 github.com/stretchr/testify v1.8.4 github.com/swaggest/assertjson v1.9.0 - github.com/swaggest/jsonschema-go v0.3.57 + github.com/swaggest/jsonschema-go v0.3.58 github.com/swaggest/openapi-go v0.2.39 github.com/swaggest/rest v0.0.0-00010101000000-000000000000 github.com/swaggest/swgui v1.7.2 diff --git a/_examples/go.sum b/_examples/go.sum index 1cda97c..5288935 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -4,8 +4,8 @@ github.com/bool64/ctxd v1.2.1 h1:hARFteq0zdn4bwfmxLhak3fXFuvtJVKDH2X29VV/2ls= github.com/bool64/ctxd v1.2.1/go.mod h1:ZG6QkeGVLTiUl2mxPpyHmFhDzFZCyocr9hluBV3LYuc= github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4= -github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= +github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo= github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E= github.com/bool64/httptestbench v0.1.4 h1:35f9RwWqcnQRXM+sA+GUhWVGSa6XEFlKpNSH9oYzOjI= @@ -104,8 +104,8 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY= github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg= -github.com/swaggest/jsonschema-go v0.3.57 h1:n6D/2K9557Yqn/NohXoszmjuN0Lp5n0DyHlVLRZXbOM= -github.com/swaggest/jsonschema-go v0.3.57/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik= +github.com/swaggest/jsonschema-go v0.3.58 h1:OPixN4HW9H3FTh9BSomH2i0bdJi3V646TfSihzt9QBc= +github.com/swaggest/jsonschema-go v0.3.58/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik= github.com/swaggest/openapi-go v0.2.39 h1:GfICsAAFnQuyxfywsGyCbPqDKeMXxots4N/9j6+qSCk= github.com/swaggest/openapi-go v0.2.39/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c= github.com/swaggest/refl v1.2.1 h1:1meX9NaXjM5lmb4kk4RP3OZsXFRke9B1EHAP/pCEKO0= diff --git a/go.mod b/go.mod index 9337ce9..1f27548 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/swaggest/rest go 1.17 require ( - github.com/bool64/dev v0.2.29 + github.com/bool64/dev v0.2.31 github.com/bool64/httpmock v0.1.13 github.com/bool64/shared v0.1.5 github.com/cespare/xxhash/v2 v2.2.0 @@ -13,7 +13,7 @@ require ( github.com/stretchr/testify v1.8.2 github.com/swaggest/assertjson v1.9.0 github.com/swaggest/form/v5 v5.1.1 - github.com/swaggest/jsonschema-go v0.3.57 + github.com/swaggest/jsonschema-go v0.3.58 github.com/swaggest/openapi-go v0.2.39 github.com/swaggest/refl v1.2.1 github.com/swaggest/usecase v1.2.1 diff --git a/go.sum b/go.sum index 24cf29d..472b54e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,9 @@ github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8 github.com/bool64/dev v0.2.24/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4= github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= +github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo= github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= @@ -76,8 +77,9 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY= github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg= -github.com/swaggest/jsonschema-go v0.3.57 h1:n6D/2K9557Yqn/NohXoszmjuN0Lp5n0DyHlVLRZXbOM= github.com/swaggest/jsonschema-go v0.3.57/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik= +github.com/swaggest/jsonschema-go v0.3.58 h1:OPixN4HW9H3FTh9BSomH2i0bdJi3V646TfSihzt9QBc= +github.com/swaggest/jsonschema-go v0.3.58/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik= github.com/swaggest/openapi-go v0.2.39 h1:GfICsAAFnQuyxfywsGyCbPqDKeMXxots4N/9j6+qSCk= github.com/swaggest/openapi-go v0.2.39/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c= github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU= diff --git a/gorillamux/collector_test.go b/gorillamux/collector_test.go index e6a3154..a61a48a 100644 --- a/gorillamux/collector_test.go +++ b/gorillamux/collector_test.go @@ -33,7 +33,7 @@ func newStructuredHandler(setup func(h *structuredHandler)) structuredHandler { return h } -func (s structuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {} +func (s structuredHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {} func TestOpenAPICollector_Walker(t *testing.T) { r := mux.NewRouter() diff --git a/nethttp/wrapper.go b/nethttp/wrapper.go index 1466fd2..f1da04f 100644 --- a/nethttp/wrapper.go +++ b/nethttp/wrapper.go @@ -6,7 +6,7 @@ type wrapperChecker struct { found bool } -func (*wrapperChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {} +func (*wrapperChecker) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {} // IsWrapperChecker is a hack to mark middleware as a handler wrapper. // See chirouter.Wrapper Wrap() documentation for more details on the difference. diff --git a/request/decoder.go b/request/decoder.go index 92ef627..d9f0a20 100644 --- a/request/decoder.go +++ b/request/decoder.go @@ -29,6 +29,21 @@ type ( valueDecoderFunc func(r *http.Request, v interface{}, validator rest.Validator) error ) +// EmbeddedSetter can capture *http.Resuest in your input structure. +type EmbeddedSetter struct { + r *http.Request +} + +// SetRequest implements Setter. +func (e *EmbeddedSetter) SetRequest(r *http.Request) { + e.r = r +} + +// Request is an accessor. +func (e *EmbeddedSetter) Request() *http.Request { + return e.r +} + func decodeValidate(d *form.Decoder, v interface{}, p url.Values, in rest.ParamIn, val rest.Validator) error { goValues := make(map[string]interface{}, len(p)) diff --git a/response/encoder.go b/response/encoder.go index 0754809..875fde7 100644 --- a/response/encoder.go +++ b/response/encoder.go @@ -28,12 +28,22 @@ type Encoder struct { skipRendering bool outputWithWriter bool unwrapInterface bool + + dynamicWithHeadersSetup bool + dynamicETagged bool + dynamicNoContent bool } type noContent interface { + // NoContent controls whether status 204 should be used in response to current request. NoContent() bool } +type outputWithHeadersSetup interface { + // SetupResponseHeader gives access to response headers of current request. + SetupResponseHeader(h http.Header) +} + // DefaultSuccessResponseContentType is a package-level variable set to // default success response content type. var DefaultSuccessResponseContentType = "application/json" @@ -56,7 +66,7 @@ func addressable(output interface{}) interface{} { func (h *Encoder) setupHeadersEncoder(output interface{}, ht *rest.HandlerTrait) { // Enable dynamic headers check in interface mode. - if h.unwrapInterface = reflect.ValueOf(output).Elem().Kind() == reflect.Interface; h.unwrapInterface { + if h.unwrapInterface { enc := form.NewEncoder() enc.SetMode(form.ModeExplicit) enc.SetTagName(string(rest.ParamInHeader)) @@ -102,7 +112,7 @@ func (h *Encoder) setupHeadersEncoder(output interface{}, ht *rest.HandlerTrait) func (h *Encoder) setupCookiesEncoder(output interface{}, ht *rest.HandlerTrait) { // Enable dynamic headers check in interface mode. - if h.unwrapInterface = reflect.ValueOf(output).Elem().Kind() == reflect.Interface; h.unwrapInterface { + if h.unwrapInterface { enc := form.NewEncoder() enc.SetMode(form.ModeExplicit) enc.SetTagName(string(rest.ParamInCookie)) @@ -171,6 +181,20 @@ func (h *Encoder) SetupOutput(output interface{}, ht *rest.HandlerTrait) { output = addressable(output) + h.unwrapInterface = reflect.ValueOf(output).Elem().Kind() == reflect.Interface + + if _, ok := output.(outputWithHeadersSetup); ok || h.unwrapInterface { + h.dynamicWithHeadersSetup = true + } + + if _, ok := output.(rest.ETagged); ok || h.unwrapInterface { + h.dynamicETagged = true + } + + if _, ok := output.(noContent); ok || h.unwrapInterface { + h.dynamicNoContent = true + } + h.setupHeadersEncoder(output, ht) h.setupCookiesEncoder(output, ht) @@ -331,10 +355,12 @@ func (h *Encoder) WriteSuccessfulResponse( output = reflect.ValueOf(output).Elem().Interface() } - if etagged, ok := output.(rest.ETagged); ok { - etag := etagged.ETag() - if etag != "" { - w.Header().Set("Etag", etag) + if h.dynamicETagged { + if etagged, ok := output.(rest.ETagged); ok { + etag := etagged.ETag() + if etag != "" { + w.Header().Set("Etag", etag) + } } } @@ -347,7 +373,7 @@ func (h *Encoder) WriteSuccessfulResponse( } skipRendering := h.skipRendering - if !skipRendering { + if !skipRendering && h.dynamicNoContent { if nc, ok := output.(noContent); ok { skipRendering = nc.NoContent() if skipRendering && ht.SuccessStatus == 0 { @@ -382,6 +408,12 @@ func (h *Encoder) writeError(err error, w http.ResponseWriter, r *http.Request, } func (h *Encoder) whiteHeader(w http.ResponseWriter, r *http.Request, output interface{}, ht rest.HandlerTrait) bool { + if h.dynamicWithHeadersSetup { + if sh, ok := output.(outputWithHeadersSetup); ok { + sh.SetupResponseHeader(w.Header()) + } + } + if h.outputHeadersEncoder == nil { return true }