diff --git a/_examples/advanced-generic-openapi31/_testdata/openapi.json b/_examples/advanced-generic-openapi31/_testdata/openapi.json index b539ff3..53628a7 100644 --- a/_examples/advanced-generic-openapi31/_testdata/openapi.json +++ b/_examples/advanced-generic-openapi31/_testdata/openapi.json @@ -305,7 +305,7 @@ }, { "name":"identity","in":"query","description":"JSON value in query", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJSONPayload"}}} + "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJSONPayload"}}} }, { "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, @@ -313,7 +313,7 @@ }, { "name":"in_cookie","in":"cookie","description":"UUID in cookie.", - "schema":{"$ref":"#/components/schemas/CookieUuidUUID","description":"UUID in cookie."} + "schema":{"$ref":"#/components/schemas/UuidUUID","description":"UUID in cookie."} }, { "name":"X-Header","in":"header","description":"Simple scalar value in header.", @@ -485,13 +485,12 @@ }, { "name":"json_filter","in":"query","description":"JSON object value in query.", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJsonFilter"}}} + "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonFilter"}}} }, { "name":"deep_object_filter","in":"query","description":"Deep object value in query params.", "schema":{ - "$ref":"#/components/schemas/QueryAdvancedDeepObjectFilter", - "description":"Deep object value in query params." + "$ref":"#/components/schemas/AdvancedDeepObjectFilter","description":"Deep object value in query params." }, "style":"deepObject","explode":true } @@ -692,6 +691,7 @@ "properties":{"id":{"minimum":100,"type":"integer"},"name":{"minLength":3,"type":"string"}},"type":"object" }, "AdvancedJSONMapPayload":{"additionalProperties":{"type":"number"},"type":"object"}, + "AdvancedJSONPayload":{"additionalProperties":false,"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"}, "AdvancedJSONPayloadType2":{"additionalProperties":false,"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"}, "AdvancedJSONPayloadType3":{"additionalProperties":false,"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"}, "AdvancedJSONSlicePayload":{"items":{"type":"integer"},"type":["array","null"]}, @@ -758,7 +758,6 @@ }, "type":"object" }, - "CookieUuidUUID":{"examples":["248df4b7-aa70-47b8-a036-33ac447e668d"],"format":"uuid","type":"string"}, "FormDataAdvancedForm":{"additionalProperties":false,"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"}, "FormDataAdvancedInputPort":{ "additionalProperties":false, @@ -794,13 +793,6 @@ }, "FormDataMultipartFile":{"format":"binary","type":["null","string"]}, "FormDataMultipartFileHeader":{"format":"binary","type":["null","string"]}, - "QueryAdvancedDeepObjectFilter":{ - "additionalProperties":false, - "properties":{"bar":{"minLength":3,"type":"string"},"baz":{"minLength":3,"type":["null","string"]}}, - "type":"object" - }, - "QueryAdvancedJSONPayload":{"additionalProperties":false,"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"}, - "QueryAdvancedJsonFilter":{"additionalProperties":false,"properties":{"foo":{"maxLength":5,"type":"string"}},"type":"object"}, "RestErrResponse":{ "properties":{ "code":{"description":"Application-specific error code.","type":"integer"}, @@ -810,7 +802,8 @@ }, "type":"object" }, - "TextprotoMIMEHeader":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"} + "TextprotoMIMEHeader":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"}, + "UuidUUID":{"examples":["248df4b7-aa70-47b8-a036-33ac447e668d"],"format":"uuid","type":"string"} }, "securitySchemes":{"User":{"description":"Session cookie.","type":"apiKey","name":"sessid","in":"cookie"}} } diff --git a/_examples/advanced-generic/_testdata/openapi.json b/_examples/advanced-generic/_testdata/openapi.json deleted file mode 100644 index b40ed84..0000000 --- a/_examples/advanced-generic/_testdata/openapi.json +++ /dev/null @@ -1,807 +0,0 @@ -{ - "openapi":"3.0.3", - "info":{"title":"Advanced Example","description":"This app showcases a variety of features.","version":"v1.2.3"}, - "paths":{ - "/deeper-with-session/one":{ - "get":{ - "tags":["Other"],"summary":"Dummy","operationId":"_examples/advanced-generic.dummy2", - "responses":{ - "204":{"description":"No Content"}, - "401":{ - "description":"Unauthorized", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestErrResponse"}}} - } - }, - "security":[{"User":[]}] - } - }, - "/deeper-with-session/two":{ - "get":{ - "tags":["Other"],"summary":"Dummy","operationId":"_examples/advanced-generic.dummy3", - "responses":{ - "204":{"description":"No Content"}, - "401":{ - "description":"Unauthorized", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestErrResponse"}}} - } - }, - "security":[{"User":[]}] - } - }, - "/error-response":{ - "get":{ - "tags":["Response"],"summary":"Declare Expected Errors", - "description":"This use case demonstrates documentation of expected errors.", - "operationId":"_examples/advanced-generic.errorResponse", - "parameters":[ - { - "name":"type","in":"query","required":true, - "schema":{"enum":["ok","invalid_argument","conflict"],"type":"string"} - } - ], - "responses":{ - "200":{ - "description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOkResp"}}} - }, - "400":{ - "description":"Bad Request", - "content":{ - "application/json":{ - "schema":{ - "anyOf":[ - {"$ref":"#/components/schemas/AdvancedCustomErr"}, - {"$ref":"#/components/schemas/AdvancedAnotherErr"} - ] - } - } - } - }, - "409":{ - "description":"Conflict", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} - }, - "412":{ - "description":"Precondition Failed", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/file-multi-upload":{ - "post":{ - "tags":["Request"],"summary":"Files Uploads With 'multipart/form-data'", - "operationId":"_examples/advanced-generic.fileMultiUploader", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"integer","description":"Simple scalar value in query."} - } - ], - "requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedUploadType2"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfoType2"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/file-upload":{ - "post":{ - "tags":["Request"],"summary":"File Upload With 'multipart/form-data'", - "operationId":"_examples/advanced-generic.fileUploader", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"integer","description":"Simple scalar value in query."} - } - ], - "requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedUpload"}}}}, - "responses":{ - "200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfo"}}}} - }, - "x-forbid-unknown-query":true - } - }, - "/form":{ - "post":{ - "tags":["Request"],"summary":"Request With Form", - "description":"The `form` field tag acts as `query` and `formData`, with priority on `formData`.\n\nIt is decoded with `http.Request.Form` values.", - "operationId":"_examples/advanced-generic.form", - "parameters":[ - {"name":"id","in":"query","schema":{"type":"integer"}}, - {"name":"name","in":"query","schema":{"type":"string"}} - ], - "requestBody":{ - "content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedForm"}}} - }, - "responses":{ - "200":{ - "description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutput"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/gzip-pass-through":{ - "get":{ - "tags":["Response"],"summary":"Direct Gzip","operationId":"_examples/advanced-generic.directGzip", - "parameters":[ - { - "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.", - "schema":{"type":"boolean","description":"Output plain structure instead of gzip container."} - }, - { - "name":"countItems","in":"query","description":"Invokes internal decoding of compressed data.", - "schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."} - } - ], - "responses":{ - "200":{ - "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}, - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedGzipPassThroughStruct"}}} - } - }, - "x-forbid-unknown-query":true - }, - "head":{ - "tags":["Response"],"summary":"Direct Gzip","operationId":"_examples/advanced-generic.directGzip2", - "parameters":[ - { - "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.", - "schema":{"type":"boolean","description":"Output plain structure instead of gzip container."} - }, - { - "name":"countItems","in":"query","description":"Invokes internal decoding of compressed data.", - "schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."} - } - ], - "responses":{"200":{"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}}}, - "x-forbid-unknown-query":true - } - }, - "/html-response/{id}":{ - "get":{ - "tags":["Response"],"summary":"Request With HTML Response", - "description":"Request with templated HTML response.","operationId":"_examples/advanced-generic.htmlResponse", - "parameters":[ - {"name":"filter","in":"query","schema":{"type":"string"}}, - {"name":"id","in":"path","required":true,"schema":{"type":"integer"}}, - {"name":"X-Header","in":"header","schema":{"type":"boolean"}} - ], - "responses":{ - "200":{ - "description":"OK","headers":{"X-Anti-Header":{"style":"simple","schema":{"type":"boolean"}}}, - "content":{"text/html":{"schema":{"type":"string"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/json-body-manual/{in-path}":{ - "post":{ - "tags":["Request"],"summary":"Request With JSON Body and manual decoder", - "description":"Request with JSON body and query/header/path params, response with JSON body and data from request.", - "operationId":"_examples/advanced-generic.jsonBodyManual", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"string","description":"Simple scalar value in query.","format":"date"} - }, - { - "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, - "schema":{"type":"string","description":"Simple scalar value in path"} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"type":"string","description":"Simple scalar value in header."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInputWithJSONType3"}}}}, - "responses":{ - "201":{ - "description":"Created", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType3"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/json-body-validation/{in-path}":{ - "post":{ - "tags":["Request","Response","Validation"],"summary":"Request With JSON Body and non-trivial validation", - "description":"Request with JSON body and query/header/path params, response with JSON body and data from request.", - "operationId":"_examples/advanced-generic.jsonBodyValidation", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"minimum":100,"type":"integer","description":"Simple scalar value in query."} - }, - { - "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, - "schema":{"minLength":3,"type":"string","description":"Simple scalar value in path"} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"minLength":3,"type":"string","description":"Simple scalar value in header."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInputWithJSONType4"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType4"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/json-body/{in-path}":{ - "post":{ - "tags":["Request"],"summary":"Request With JSON Body", - "description":"Request with JSON body and query/header/path params, response with JSON body and data from request.", - "operationId":"_examples/advanced-generic.jsonBody", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"string","description":"Simple scalar value in query.","format":"date"} - }, - { - "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, - "schema":{"type":"string","description":"Simple scalar value in path"} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"type":"string","description":"Simple scalar value in header."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInputWithJSONType2"}}}}, - "responses":{ - "201":{ - "description":"Created", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType2"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/json-map-body":{ - "post":{ - "tags":["Request"],"summary":"Request With JSON Map In Body", - "description":"Request with JSON object (map) body.","operationId":"_examples/advanced-generic.jsonMapBody", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"integer","description":"Simple scalar value in query."} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"type":"string","description":"Simple scalar value in header."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonMapReq"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutputType2"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/json-param/{in-path}":{ - "get":{ - "tags":["Request"],"summary":"Request With JSON Query Parameter", - "description":"Request with JSON body and query/header/path params, response with JSON body and data from request.", - "operationId":"_examples/advanced-generic.jsonParam", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"integer","description":"Simple scalar value in query."} - }, - { - "name":"identity","in":"query","description":"JSON value in query", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJSONPayload"}}} - }, - { - "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, - "schema":{"type":"string","description":"Simple scalar value in path"} - }, - { - "name":"in_cookie","in":"cookie","description":"UUID in cookie.", - "schema":{"$ref":"#/components/schemas/CookieUuidUUID"} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"type":"string","description":"Simple scalar value in header."} - } - ], - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSON"}}} - } - }, - "x-forbid-unknown-cookie":true,"x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/json-slice-body":{ - "post":{ - "tags":["Request"],"summary":"Request With JSON Array In Body", - "operationId":"_examples/advanced-generic.jsonSliceBody", - "parameters":[ - { - "name":"in_query","in":"query","description":"Simple scalar value in query.", - "schema":{"type":"integer","description":"Simple scalar value in query."} - }, - { - "name":"X-Header","in":"header","description":"Simple scalar value in header.", - "schema":{"type":"string","description":"Simple scalar value in header."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonSliceReq"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutput"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/no-validation":{ - "post":{ - "tags":["Request","Response"],"summary":"No Validation","description":"Input/Output without validation.", - "operationId":"_examples/advanced-generic.noValidation", - "parameters":[ - {"name":"q","in":"query","schema":{"type":"boolean"}}, - {"name":"X-Input","in":"header","schema":{"type":"integer"}} - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInputPortType3"}}}}, - "responses":{ - "200":{ - "description":"OK", - "headers":{ - "X-Output":{"style":"simple","schema":{"type":"integer"}}, - "X-Query":{"style":"simple","schema":{"type":"boolean"}} - }, - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType3"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/output-csv-writer":{ - "get":{ - "tags":["Response"],"summary":"Output With Stream Writer","description":"Output with stream writer.", - "operationId":"_examples/advanced-generic.outputCSVWriter", - "parameters":[ - { - "name":"If-None-Match","in":"header","description":"Content hash.", - "schema":{"type":"string","description":"Content hash."} - } - ], - "responses":{ - "200":{ - "description":"OK", - "headers":{ - "ETag":{ - "style":"simple","description":"Content hash.","schema":{"type":"string","description":"Content hash."} - }, - "X-Header":{ - "style":"simple","description":"Sample response header.", - "schema":{"type":"string","description":"Sample response header."} - } - }, - "content":{"text/csv":{"schema":{"type":"string"}}} - }, - "304":{"description":"Not Modified"}, - "500":{ - "description":"Internal Server Error", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} - } - } - } - }, - "/output-headers":{ - "get":{ - "tags":["Response"],"summary":"Output With Headers","description":"Output with headers.", - "operationId":"_examples/advanced-generic.outputHeaders", - "parameters":[ - { - "name":"X-foO","in":"header","description":"Reduced by 20 in response.","required":true, - "schema":{"minimum":10,"type":"integer","description":"Reduced by 20 in response."} - } - ], - "responses":{ - "200":{ - "description":"OK", - "headers":{ - "X-foO":{ - "style":"simple","description":"Reduced by 20 in response.","required":true, - "schema":{"minimum":10,"type":"integer","description":"Reduced by 20 in response."} - }, - "x-HeAdEr":{ - "style":"simple","description":"Sample response header.", - "schema":{"type":"string","description":"Sample response header."} - }, - "x-omit-empty":{ - "style":"simple","description":"Receives req value of X-Foo reduced by 30.", - "schema":{"type":"integer","description":"Receives req value of X-Foo reduced by 30."} - } - }, - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedHeaderOutput"}}} - }, - "500":{ - "description":"Internal Server Error", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}} - } - } - }, - "head":{ - "tags":["Response"],"summary":"Output With Headers","description":"Output with headers.", - "operationId":"_examples/advanced-generic.outputHeaders2", - "parameters":[ - { - "name":"X-foO","in":"header","description":"Reduced by 20 in response.","required":true, - "schema":{"minimum":10,"type":"integer","description":"Reduced by 20 in response."} - } - ], - "responses":{ - "200":{ - "description":"OK", - "headers":{ - "X-foO":{ - "style":"simple","description":"Reduced by 20 in response.","required":true, - "schema":{"minimum":10,"type":"integer","description":"Reduced by 20 in response."} - }, - "x-HeAdEr":{ - "style":"simple","description":"Sample response header.", - "schema":{"type":"string","description":"Sample response header."} - }, - "x-omit-empty":{ - "style":"simple","description":"Receives req value of X-Foo reduced by 30.", - "schema":{"type":"integer","description":"Receives req value of X-Foo reduced by 30."} - } - } - }, - "500":{"description":"Internal Server Error"} - } - } - }, - "/query-object":{ - "get":{ - "tags":["Request"],"summary":"Request With Object As Query Parameter", - "operationId":"_examples/advanced-generic.queryObject", - "parameters":[ - { - "name":"in_query","in":"query","description":"Object value in query.","style":"deepObject","explode":true, - "schema":{"type":"object","additionalProperties":{"type":"number"},"description":"Object value in query."} - }, - { - "name":"json_filter","in":"query","description":"JSON object value in query.", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJsonFilter"}}} - }, - { - "name":"deep_object_filter","in":"query","description":"Deep object value in query params.", - "style":"deepObject","explode":true,"schema":{"$ref":"#/components/schemas/QueryAdvancedDeepObjectFilter"} - } - ], - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputQueryObject"}}} - } - }, - "x-forbid-unknown-query":true - } - }, - "/req-resp-mapping":{ - "post":{ - "tags":["Request","Response"],"summary":"Request Response Mapping", - "description":"This use case has transport concerns fully decoupled with external req/resp mapping.", - "operationId":"reqRespMapping", - "parameters":[ - { - "name":"X-Header","in":"header","description":"Simple scalar value with sample validation.","required":true, - "schema":{"minLength":3,"type":"string","description":"Simple scalar value with sample validation."} - } - ], - "requestBody":{ - "content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedInputPort"}}} - }, - "responses":{ - "204":{ - "description":"No Content", - "headers":{ - "X-Value-1":{ - "style":"simple","description":"Simple scalar value with sample validation.","required":true, - "schema":{"minLength":3,"type":"string","description":"Simple scalar value with sample validation."} - }, - "X-Value-2":{ - "style":"simple","description":"Simple scalar value with sample validation.","required":true, - "schema":{"minimum":3,"type":"integer","description":"Simple scalar value with sample validation."} - } - } - } - } - } - }, - "/root-with-session":{ - "get":{ - "tags":["Other"],"summary":"Dummy","operationId":"_examples/advanced-generic.dummy", - "responses":{ - "204":{"description":"No Content"}, - "401":{ - "description":"Unauthorized", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestErrResponse"}}} - } - }, - "security":[{"User":[]}] - } - }, - "/text-req-body-ptr/{path}":{ - "post":{ - "tags":["Request"],"summary":"Request With Text Body (ptr input)", - "description":"This usecase allows direct access to original `*http.Request` while keeping automated decoding of parameters.", - "operationId":"_examples/advanced-generic.textReqBodyPtr", - "parameters":[ - {"name":"query","in":"query","schema":{"type":"integer"}}, - {"name":"path","in":"path","required":true,"schema":{"type":"string"}} - ], - "requestBody":{"content":{"text/csv":{"schema":{"type":"string"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputType3"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/text-req-body/{path}":{ - "post":{ - "tags":["Request"],"summary":"Request With Text Body", - "description":"This usecase allows direct access to original `*http.Request` while keeping automated decoding of parameters.", - "operationId":"_examples/advanced-generic.textReqBody", - "parameters":[ - {"name":"query","in":"query","schema":{"type":"integer"}}, - {"name":"path","in":"path","required":true,"schema":{"type":"string"}} - ], - "requestBody":{"content":{"text/csv":{"schema":{"type":"string"}}}}, - "responses":{ - "200":{ - "description":"OK", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputType2"}}} - } - }, - "x-forbid-unknown-path":true,"x-forbid-unknown-query":true - } - }, - "/validation":{ - "post":{ - "tags":["Request","Response","Validation"],"summary":"Validation", - "description":"Input/Output with validation. Custom annotation.", - "operationId":"_examples/advanced-generic.validation", - "parameters":[ - { - "name":"q","in":"query", - "description":"This parameter will bypass explicit validation as it does not have constraints.", - "schema":{ - "type":"boolean", - "description":"This parameter will bypass explicit validation as it does not have constraints." - } - }, - { - "name":"X-Input","in":"header","description":"Request minimum: 10, response maximum: 20.", - "schema":{"minimum":10,"type":"integer","description":"Request minimum: 10, response maximum: 20."} - } - ], - "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInputPortType2"}}}}, - "responses":{ - "200":{ - "description":"OK", - "headers":{ - "X-Output":{"style":"simple","schema":{"maximum":20,"type":"integer"}}, - "X-Query":{ - "style":"simple","description":"This header bypasses validation as it does not have constraints.", - "schema":{"type":"boolean","description":"This header bypasses validation as it does not have constraints."} - } - }, - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType2"}}} - } - }, - "x-forbid-unknown-query":true - } - } - }, - "components":{ - "schemas":{ - "AdvancedAnotherErr":{"type":"object","properties":{"foo":{"type":"integer"}}}, - "AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}}, - "AdvancedDeepObjectFilter":{ - "type":"object", - "properties":{"bar":{"minLength":3,"type":"string"},"baz":{"minLength":3,"type":"string","nullable":true}} - }, - "AdvancedGzipPassThroughStruct":{ - "type":"object", - "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}} - }, - "AdvancedHeaderOutput":{"type":"object","properties":{"inBody":{"type":"string","deprecated":true}}}, - "AdvancedInfo":{ - "type":"object", - "properties":{ - "filename":{"type":"string"},"header":{"$ref":"#/components/schemas/TextprotoMIMEHeader"}, - "inQuery":{"type":"integer"},"peek1":{"type":"string"},"peek2":{"type":"string"},"simple":{"type":"string"}, - "size":{"type":"integer"} - } - }, - "AdvancedInfoType2":{ - "type":"object", - "properties":{ - "filenames":{"type":"array","items":{"type":"string"},"nullable":true}, - "headers":{"type":"array","items":{"$ref":"#/components/schemas/TextprotoMIMEHeader"},"nullable":true}, - "inQuery":{"type":"integer"},"peeks1":{"type":"array","items":{"type":"string"},"nullable":true}, - "peeks2":{"type":"array","items":{"type":"string"},"nullable":true},"simple":{"type":"string"}, - "sizes":{"type":"array","items":{"type":"integer"},"nullable":true} - } - }, - "AdvancedInputPortType2":{ - "required":["data"],"type":"object", - "properties":{ - "data":{ - "type":"object", - "properties":{"value":{"minLength":3,"type":"string","description":"Request minLength: 3, response maxLength: 7"}}, - "additionalProperties":false - } - }, - "additionalProperties":false - }, - "AdvancedInputPortType3":{ - "type":"object", - "properties":{"data":{"type":"object","properties":{"value":{"type":"string"}},"additionalProperties":false}}, - "additionalProperties":false - }, - "AdvancedInputWithJSONType2":{ - "type":"object", - "properties":{ - "id":{"type":"integer"},"name":{"type":"string"}, - "namedStruct":{"allOf":[{"deprecated":true},{"$ref":"#/components/schemas/AdvancedJSONPayloadType2"}]} - }, - "additionalProperties":false - }, - "AdvancedInputWithJSONType3":{ - "type":"object", - "properties":{ - "id":{"type":"integer"},"name":{"type":"string"}, - "namedStruct":{"allOf":[{"deprecated":true},{"$ref":"#/components/schemas/AdvancedJSONPayloadType3"}]} - }, - "additionalProperties":false - }, - "AdvancedInputWithJSONType4":{ - "type":"object","properties":{"id":{"minimum":100,"type":"integer"},"name":{"minLength":3,"type":"string"}}, - "additionalProperties":false - }, - "AdvancedJSONMapPayload":{"type":"object","additionalProperties":{"type":"number"}}, - "AdvancedJSONPayloadType2":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, - "AdvancedJSONPayloadType3":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, - "AdvancedJSONSlicePayload":{"type":"array","items":{"type":"integer"},"nullable":true}, - "AdvancedJsonFilter":{"type":"object","properties":{"foo":{"maxLength":5,"type":"string"}}}, - "AdvancedJsonMapReq":{"type":"object","additionalProperties":{"type":"number"}}, - "AdvancedJsonOutput":{ - "type":"object", - "properties":{ - "data":{"$ref":"#/components/schemas/AdvancedJSONSlicePayload"},"inHeader":{"type":"string"}, - "inQuery":{"type":"integer"} - } - }, - "AdvancedJsonOutputType2":{ - "type":"object", - "properties":{ - "data":{"$ref":"#/components/schemas/AdvancedJSONMapPayload"},"inHeader":{"type":"string"}, - "inQuery":{"type":"integer"} - } - }, - "AdvancedJsonSliceReq":{"type":"array","items":{"type":"integer"}}, - "AdvancedOkResp":{"type":"object","properties":{"status":{"type":"string"}}}, - "AdvancedOutput":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}}}, - "AdvancedOutputPortType2":{ - "required":["data"],"type":"object", - "properties":{"data":{"type":"object","properties":{"value":{"maxLength":7,"type":"string"}}}} - }, - "AdvancedOutputPortType3":{"type":"object","properties":{"data":{"type":"object","properties":{"value":{"type":"string"}}}}}, - "AdvancedOutputQueryObject":{ - "type":"object", - "properties":{ - "deepObjectFilter":{"$ref":"#/components/schemas/AdvancedDeepObjectFilter"}, - "inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}, - "jsonFilter":{"$ref":"#/components/schemas/AdvancedJsonFilter"} - } - }, - "AdvancedOutputType2":{"type":"object","properties":{"path":{"type":"string"},"query":{"type":"integer"},"text":{"type":"string"}}}, - "AdvancedOutputType3":{"type":"object","properties":{"path":{"type":"string"},"query":{"type":"integer"},"text":{"type":"string"}}}, - "AdvancedOutputWithJSON":{ - "type":"object", - "properties":{ - "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"},"inQuery":{"type":"integer"}, - "name":{"type":"string"} - } - }, - "AdvancedOutputWithJSONType2":{ - "type":"object", - "properties":{ - "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"}, - "inQuery":{"type":"string","format":"date","deprecated":true},"name":{"type":"string"} - } - }, - "AdvancedOutputWithJSONType3":{ - "type":"object", - "properties":{ - "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"}, - "inQuery":{"type":"string","format":"date","deprecated":true},"name":{"type":"string"} - } - }, - "AdvancedOutputWithJSONType4":{ - "type":"object", - "properties":{ - "id":{"minimum":100,"type":"integer"},"inHeader":{"minLength":3,"type":"string"}, - "inPath":{"minLength":3,"type":"string"},"inQuery":{"minimum":3,"type":"integer"}, - "name":{"minLength":3,"type":"string"} - } - }, - "CookieUuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"}, - "FormDataAdvancedForm":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, - "FormDataAdvancedInputPort":{ - "required":["val2"],"type":"object", - "properties":{"val2":{"minimum":3,"type":"integer","description":"Simple scalar value with sample validation."}}, - "additionalProperties":false - }, - "FormDataAdvancedUpload":{ - "type":"object", - "properties":{ - "simple":{"type":"string","description":"Simple scalar value in body."}, - "upload1":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}, - "upload2":{"$ref":"#/components/schemas/FormDataMultipartFile"} - }, - "additionalProperties":false - }, - "FormDataAdvancedUploadType2":{ - "type":"object", - "properties":{ - "simple":{"type":"string","description":"Simple scalar value in body."}, - "uploads1":{ - "type":"array","items":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}, - "description":"Uploads with *multipart.FileHeader.","nullable":true - }, - "uploads2":{ - "type":"array","items":{"$ref":"#/components/schemas/FormDataMultipartFile"}, - "description":"Uploads with multipart.File.","nullable":true - } - }, - "additionalProperties":false - }, - "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true}, - "FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true}, - "QueryAdvancedDeepObjectFilter":{ - "type":"object", - "properties":{"bar":{"minLength":3,"type":"string"},"baz":{"minLength":3,"type":"string","nullable":true}}, - "additionalProperties":false - }, - "QueryAdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, - "QueryAdvancedJsonFilter":{"type":"object","properties":{"foo":{"maxLength":5,"type":"string"}},"additionalProperties":false}, - "RestErrResponse":{ - "type":"object", - "properties":{ - "code":{"type":"integer","description":"Application-specific error code."}, - "context":{"type":"object","additionalProperties":{},"description":"Application context."}, - "error":{"type":"string","description":"Error message."}, - "status":{"type":"string","description":"Status text."} - } - }, - "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}} - }, - "securitySchemes":{"User":{"type":"apiKey","name":"sessid","in":"cookie","description":"Session cookie."}} - } -} diff --git a/_examples/advanced-generic/dummy.go b/_examples/advanced-generic/dummy.go deleted file mode 100644 index f9d2db5..0000000 --- a/_examples/advanced-generic/dummy.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func dummy() usecase.Interactor { - u := usecase.NewInteractor(func(ctx context.Context, input struct{}, output *struct{}) error { - return nil - }) - u.SetTags("Other") - - return u -} diff --git a/_examples/advanced-generic/error_response.go b/_examples/advanced-generic/error_response.go deleted file mode 100644 index 2fa2352..0000000 --- a/_examples/advanced-generic/error_response.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "errors" - - "github.com/bool64/ctxd" - "github.com/swaggest/usecase" - "github.com/swaggest/usecase/status" -) - -type customErr struct { - Message string `json:"msg"` - Details map[string]interface{} `json:"details,omitempty"` -} - -func errorResponse() usecase.Interactor { - type errType struct { - Type string `query:"type" enum:"ok,invalid_argument,conflict" required:"true"` - } - - type okResp struct { - Status string `json:"status"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in errType, out *okResp) (err error) { - switch in.Type { - case "ok": - out.Status = "ok" - 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("Declare Expected Errors") - u.SetDescription("This use case demonstrates documentation of expected errors.") - u.SetExpectedErrors(status.InvalidArgument, anotherErr{}, status.FailedPrecondition, status.AlreadyExists) - u.SetTags("Response") - - return u -} - -// anotherErr is another custom error. -type anotherErr struct { - Foo int `json:"foo"` -} - -func (anotherErr) Error() string { - return "foo happened" -} diff --git a/_examples/advanced-generic/file_multi_upload.go b/_examples/advanced-generic/file_multi_upload.go deleted file mode 100644 index 006d5c0..0000000 --- a/_examples/advanced-generic/file_multi_upload.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "mime/multipart" - "net/textproto" - - "github.com/swaggest/usecase" -) - -func fileMultiUploader() usecase.Interactor { - type upload struct { - Simple string `formData:"simple" description:"Simple scalar value in body."` - Query int `query:"in_query" description:"Simple scalar value in query."` - Uploads1 []*multipart.FileHeader `formData:"uploads1" description:"Uploads with *multipart.FileHeader."` - Uploads2 []multipart.File `formData:"uploads2" description:"Uploads with multipart.File."` - } - - type info struct { - Filenames []string `json:"filenames"` - Headers []textproto.MIMEHeader `json:"headers"` - Sizes []int64 `json:"sizes"` - Upload1Peeks []string `json:"peeks1"` - Upload2Peeks []string `json:"peeks2"` - Simple string `json:"simple"` - Query int `json:"inQuery"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in upload, out *info) (err error) { - out.Query = in.Query - out.Simple = in.Simple - for _, o := range in.Uploads1 { - out.Filenames = append(out.Filenames, o.Filename) - out.Headers = append(out.Headers, o.Header) - out.Sizes = append(out.Sizes, o.Size) - - f, err := o.Open() - if err != nil { - return err - } - p := make([]byte, 100) - _, err = f.Read(p) - if err != nil { - return err - } - - out.Upload1Peeks = append(out.Upload1Peeks, string(p)) - - err = f.Close() - if err != nil { - return err - } - } - - for _, o := range in.Uploads2 { - p := make([]byte, 100) - _, err = o.Read(p) - if err != nil { - return err - } - - out.Upload2Peeks = append(out.Upload2Peeks, string(p)) - err = o.Close() - if err != nil { - return err - } - } - - return nil - }) - - u.SetTitle("Files Uploads With 'multipart/form-data'") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/file_upload.go b/_examples/advanced-generic/file_upload.go deleted file mode 100644 index be820a2..0000000 --- a/_examples/advanced-generic/file_upload.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "mime/multipart" - "net/textproto" - - "github.com/swaggest/usecase" -) - -func fileUploader() usecase.Interactor { - type upload struct { - Simple string `formData:"simple" description:"Simple scalar value in body."` - Query int `query:"in_query" description:"Simple scalar value in query."` - Upload1 *multipart.FileHeader `formData:"upload1" description:"Upload with *multipart.FileHeader."` - Upload2 multipart.File `formData:"upload2" description:"Upload with multipart.File."` - } - - type info struct { - Filename string `json:"filename"` - Header textproto.MIMEHeader `json:"header"` - Size int64 `json:"size"` - Upload1Peek string `json:"peek1"` - Upload2Peek string `json:"peek2"` - Simple string `json:"simple"` - Query int `json:"inQuery"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in upload, out *info) (err error) { - out.Query = in.Query - out.Simple = in.Simple - if in.Upload1 == nil { - return nil - } - - out.Filename = in.Upload1.Filename - out.Header = in.Upload1.Header - out.Size = in.Upload1.Size - - f, err := in.Upload1.Open() - if err != nil { - return err - } - - defer func() { - clErr := f.Close() - if clErr != nil && err == nil { - err = clErr - } - - clErr = in.Upload2.Close() - if clErr != nil && err == nil { - err = clErr - } - }() - - p := make([]byte, 100) - _, err = f.Read(p) - if err != nil { - return err - } - - out.Upload1Peek = string(p) - - p = make([]byte, 100) - _, err = in.Upload2.Read(p) - if err != nil { - return err - } - - out.Upload2Peek = string(p) - - return nil - }) - - u.SetTitle("File Upload With 'multipart/form-data'") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/form.go b/_examples/advanced-generic/form.go deleted file mode 100644 index ffdfa28..0000000 --- a/_examples/advanced-generic/form.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func form() usecase.Interactor { - type form struct { - ID int `form:"id"` - Name string `form:"name"` - } - - type output struct { - ID int `json:"id"` - Name string `json:"name"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in form, out *output) error { - out.ID = in.ID - out.Name = in.Name - - return nil - }) - - u.SetTitle("Request With Form") - u.SetDescription("The `form` field tag acts as `query` and `formData`, with priority on `formData`.\n\n" + - "It is decoded with `http.Request.Form` values.") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/gzip_pass_through.go b/_examples/advanced-generic/gzip_pass_through.go deleted file mode 100644 index f09d3b7..0000000 --- a/_examples/advanced-generic/gzip_pass_through.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/rest/gzip" - "github.com/swaggest/usecase" -) - -type gzipPassThroughInput struct { - PlainStruct bool `query:"plainStruct" description:"Output plain structure instead of gzip container."` - CountItems bool `query:"countItems" description:"Invokes internal decoding of compressed data."` -} - -// gzipPassThroughOutput defers data to an accessor function instead of using struct directly. -// This is necessary to allow containers that can data in binary wire-friendly format. -type gzipPassThroughOutput interface { - // Data should be accessed though an accessor to allow container interface. - gzipPassThroughStruct() gzipPassThroughStruct -} - -// gzipPassThroughStruct represents the actual structure that is held in the container -// and implements gzipPassThroughOutput to be directly useful in output. -type gzipPassThroughStruct struct { - Header string `header:"X-Header" json:"-"` - ID int `json:"id"` - Text []string `json:"text"` -} - -func (d gzipPassThroughStruct) gzipPassThroughStruct() gzipPassThroughStruct { - return d -} - -// gzipPassThroughContainer is wrapping gzip.JSONContainer and implements gzipPassThroughOutput. -type gzipPassThroughContainer struct { - Header string `header:"X-Header" json:"-"` - gzip.JSONContainer -} - -func (dc gzipPassThroughContainer) gzipPassThroughStruct() gzipPassThroughStruct { - var p gzipPassThroughStruct - - err := dc.UnpackJSON(&p) - if err != nil { - panic(err) - } - - return p -} - -func directGzip() usecase.Interactor { - // Prepare moderately big JSON, resulting JSON payload is ~67KB. - rawData := gzipPassThroughStruct{ - ID: 123, - } - for i := 0; i < 400; i++ { - rawData.Text = append(rawData.Text, "Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, "+ - "quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?") - } - - // Precompute compressed data container. Generally this step should be owned by a caching storage of data. - dataFromCache := gzipPassThroughContainer{} - - err := dataFromCache.PackJSON(rawData) - if err != nil { - panic(err) - } - - u := usecase.NewInteractor(func(ctx context.Context, in gzipPassThroughInput, out *gzipPassThroughOutput) error { - if in.PlainStruct { - o := rawData - o.Header = "cba" - *out = o - } else { - o := dataFromCache - o.Header = "abc" - *out = o - } - - // Imitating an internal read operation on data in container. - if in.CountItems { - _ = len((*out).gzipPassThroughStruct().Text) - } - - return nil - }) - u.SetTags("Response") - - return u -} diff --git a/_examples/advanced-generic/gzip_pass_through_test.go b/_examples/advanced-generic/gzip_pass_through_test.go deleted file mode 100644 index c50bb04..0000000 --- a/_examples/advanced-generic/gzip_pass_through_test.go +++ /dev/null @@ -1,154 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" -) - -func Test_directGzip(t *testing.T) { - r := NewRouter() - - req, err := http.NewRequest(http.MethodGet, "/gzip-pass-through", nil) - require.NoError(t, err) - - req.Header.Set("Accept-Encoding", "gzip") - - rw := httptest.NewRecorder() - - r.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "330epditz19z", rw.Header().Get("Etag")) - assert.Equal(t, "gzip", rw.Header().Get("Content-Encoding")) - assert.Equal(t, "abc", rw.Header().Get("X-Header")) - assert.Less(t, len(rw.Body.Bytes()), 500) -} - -func Test_noDirectGzip(t *testing.T) { - r := NewRouter() - - req, err := http.NewRequest(http.MethodGet, "/gzip-pass-through?plainStruct=1", nil) - require.NoError(t, err) - - req.Header.Set("Accept-Encoding", "gzip") - - rw := httptest.NewRecorder() - - r.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "", rw.Header().Get("Etag")) // No ETag for dynamic compression. - assert.Equal(t, "gzip", rw.Header().Get("Content-Encoding")) - assert.Equal(t, "cba", rw.Header().Get("X-Header")) - assert.Less(t, len(rw.Body.Bytes()), 1000) // Worse compression for better speed. -} - -func Test_directGzip_perf(t *testing.T) { - res := testing.Benchmark(Benchmark_directGzip) - - if httptestbench.RaceDetectorEnabled { - assert.Less(t, res.Extra["B:rcvd/op"], 700.0) - assert.Less(t, res.Extra["B:sent/op"], 104.0) - assert.Less(t, res.AllocsPerOp(), int64(60)) - assert.Less(t, res.AllocedBytesPerOp(), int64(8500)) - } else { - assert.Less(t, res.Extra["B:rcvd/op"], 700.0) - assert.Less(t, res.Extra["B:sent/op"], 104.0) - assert.Less(t, res.AllocsPerOp(), int64(45)) - assert.Less(t, res.AllocedBytesPerOp(), int64(4100)) - } -} - -// Direct gzip enabled. -// Benchmark_directGzip-4 48037 24474 ns/op 624 B:rcvd/op 103 B:sent/op 40860 rps 3499 B/op 36 allocs/op. -// Benchmark_directGzip-4 45792 26102 ns/op 624 B:rcvd/op 103 B:sent/op 38278 rps 3063 B/op 33 allocs/op. -func Benchmark_directGzip(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.Set("Accept-Encoding", "gzip") - req.SetRequestURI(srv.URL + "/gzip-pass-through") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -// Direct gzip enabled. -// Benchmark_directGzipHead-4 43804 26481 ns/op 168 B:rcvd/op 104 B:sent/op 37730 rps 3507 B/op 36 allocs/op. -// Benchmark_directGzipHead-4 45580 32286 ns/op 168 B:rcvd/op 104 B:sent/op 30963 rps 3093 B/op 33 allocs/op. -func Benchmark_directGzipHead(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodHead) - req.Header.Set("Accept-Encoding", "gzip") - req.SetRequestURI(srv.URL + "/gzip-pass-through") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -// Direct gzip disabled, payload is marshaled and compressed for every request. -// Benchmark_noDirectGzip-4 8031 136836 ns/op 1029 B:rcvd/op 117 B:sent/op 7308 rps 5382 B/op 41 allocs/op. -// Benchmark_noDirectGzip-4 7587 143294 ns/op 1029 B:rcvd/op 117 B:sent/op 6974 rps 4619 B/op 38 allocs/op. -// Benchmark_noDirectGzip-4 7825 157317 ns/op 1029 B:rcvd/op 117 B:sent/op 6357 rps 4655 B/op 40 allocs/op. -func Benchmark_noDirectGzip(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.Set("Accept-Encoding", "gzip") - req.SetRequestURI(srv.URL + "/gzip-pass-through?plainStruct=1") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -// Direct gzip enabled, payload is unmarshaled and decompressed for every request in usecase body. -// Unmarshaling large JSON payloads can be much more expensive than explicitly creating them from Go values. -// Benchmark_directGzip_decode-4 2018 499755 ns/op 624 B:rcvd/op 116 B:sent/op 2001 rps 403967 B/op 496 allocs/op. -// Benchmark_directGzip_decode-4 2085 526586 ns/op 624 B:rcvd/op 116 B:sent/op 1899 rps 403600 B/op 493 allocs/op. -func Benchmark_directGzip_decode(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.Set("Accept-Encoding", "gzip") - req.SetRequestURI(srv.URL + "/gzip-pass-through?countItems=1") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -// Direct gzip disabled. -// Benchmark_noDirectGzip_decode-4 7603 142173 ns/op 1029 B:rcvd/op 130 B:sent/op 7034 rps 5122 B/op 43 allocs/op. -// Benchmark_noDirectGzip_decode-4 5836 198000 ns/op 1029 B:rcvd/op 130 B:sent/op 5051 rps 5371 B/op 42 allocs/op. -func Benchmark_noDirectGzip_decode(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.Set("Accept-Encoding", "gzip") - req.SetRequestURI(srv.URL + "/gzip-pass-through?plainStruct=1&countItems=1") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} diff --git a/_examples/advanced-generic/html_response.go b/_examples/advanced-generic/html_response.go deleted file mode 100644 index bdf6242..0000000 --- a/_examples/advanced-generic/html_response.go +++ /dev/null @@ -1,70 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "html/template" - "io" - - "github.com/swaggest/usecase" -) - -type htmlResponseOutput struct { - ID int - Filter string - Title string - Items []string - AntiHeader bool `header:"X-Anti-Header"` - - writer io.Writer -} - -func (o *htmlResponseOutput) SetWriter(w io.Writer) { - o.writer = w -} - -func (o *htmlResponseOutput) Render(tmpl *template.Template) error { - return tmpl.Execute(o.writer, o) -} - -func htmlResponse() usecase.Interactor { - type htmlResponseInput struct { - ID int `path:"id"` - Filter string `query:"filter"` - Header bool `header:"X-Header"` - } - - const tpl = ` - - - - {{.Title}} - - - Next {{.Title}}
- {{range .Items}}
{{ . }}
{{else}}
no rows
{{end}} - -` - - tmpl, err := template.New("htmlResponse").Parse(tpl) - if err != nil { - panic(err) - } - - u := usecase.NewInteractor(func(ctx context.Context, in htmlResponseInput, out *htmlResponseOutput) (err error) { - out.AntiHeader = !in.Header - out.Filter = in.Filter - out.ID = in.ID + 1 - out.Title = "Foo" - out.Items = []string{"foo", "bar", "baz"} - - return out.Render(tmpl) - }) - - u.SetTitle("Request With HTML Response") - u.SetDescription("Request with templated HTML response.") - u.SetTags("Response") - - return u -} diff --git a/_examples/advanced-generic/html_response_test.go b/_examples/advanced-generic/html_response_test.go deleted file mode 100644 index dd65ebe..0000000 --- a/_examples/advanced-generic/html_response_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" -) - -func Test_htmlResponse(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - resp, err := http.Get(srv.URL + "/html-response/123?filter=feel") - require.NoError(t, err) - - assert.Equal(t, resp.StatusCode, http.StatusOK) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.Equal(t, "true", resp.Header.Get("X-Anti-Header")) - assert.Equal(t, "text/html", resp.Header.Get("Content-Type")) - assert.Equal(t, ` - - - - Foo - - - Next Foo
-
foo
bar
baz
- -`, string(body), string(body)) -} - -// Benchmark_htmlResponse-12 89209 12348 ns/op 0.3801 50%:ms 1.119 90%:ms 2.553 99%:ms 3.877 99.9%:ms 370.0 B:rcvd/op 108.0 B:sent/op 80973 rps 8279 B/op 144 allocs/op. -func Benchmark_htmlResponse(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.SetRequestURI(srv.URL + "/html-response/123?filter=feel") - req.Header.Set("X-Header", "true") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} diff --git a/_examples/advanced-generic/json_body.go b/_examples/advanced-generic/json_body.go deleted file mode 100644 index 3e923dc..0000000 --- a/_examples/advanced-generic/json_body.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/jsonschema-go" - "github.com/swaggest/usecase" -) - -func jsonBody() usecase.Interactor { - type JSONPayload struct { - ID int `json:"id"` - Name string `json:"name"` - } - - type inputWithJSON struct { - Header string `header:"X-Header" description:"Simple scalar value in header."` - Query jsonschema.Date `query:"in_query" description:"Simple scalar value in query."` - Path string `path:"in-path" description:"Simple scalar value in path"` - NamedStruct JSONPayload `json:"namedStruct" deprecated:"true"` - JSONPayload - } - - type outputWithJSON struct { - Header string `json:"inHeader"` - Query jsonschema.Date `json:"inQuery" deprecated:"true"` - Path string `json:"inPath"` - JSONPayload - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputWithJSON, out *outputWithJSON) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Path = in.Path - out.JSONPayload = in.JSONPayload - - return nil - }) - - u.SetTitle("Request With JSON Body") - u.SetDescription("Request with JSON body and query/header/path params, response with JSON body and data from request.") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/json_body_manual.go b/_examples/advanced-generic/json_body_manual.go deleted file mode 100644 index c16dd9f..0000000 --- a/_examples/advanced-generic/json_body_manual.go +++ /dev/null @@ -1,87 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/swaggest/jsonschema-go" - "github.com/swaggest/rest/request" - "github.com/swaggest/usecase" -) - -func jsonBodyManual() usecase.Interactor { - type outputWithJSON struct { - Header string `json:"inHeader"` - Query jsonschema.Date `json:"inQuery" deprecated:"true"` - Path string `json:"inPath"` - JSONPayload - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputWithJSON, out *outputWithJSON) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Path = in.Path - out.JSONPayload = in.JSONPayload - - return nil - }) - - u.SetTitle("Request With JSON Body and manual decoder") - u.SetDescription("Request with JSON body and query/header/path params, response with JSON body and data from request.") - u.SetTags("Request") - - return u -} - -type JSONPayload struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type inputWithJSON struct { - Header string `header:"X-Header" description:"Simple scalar value in header."` - Query jsonschema.Date `query:"in_query" description:"Simple scalar value in query."` - Path string `path:"in-path" description:"Simple scalar value in path"` - NamedStruct JSONPayload `json:"namedStruct" deprecated:"true"` - JSONPayload -} - -var _ request.Loader = &inputWithJSON{} - -func (i *inputWithJSON) LoadFromHTTPRequest(r *http.Request) (err error) { - defer func() { - if err := r.Body.Close(); err != nil { - log.Printf("failed to close request body: %s", err.Error()) - } - }() - - b, err := io.ReadAll(r.Body) - if err != nil { - return fmt.Errorf("failed to read request body: %w", err) - } - - if err = json.Unmarshal(b, i); err != nil { - return fmt.Errorf("failed to unmarshal request body: %w", err) - } - - i.Header = r.Header.Get("X-Header") - if err := i.Query.UnmarshalText([]byte(r.URL.Query().Get("in_query"))); err != nil { - return fmt.Errorf("failed to decode in_query %q: %w", r.URL.Query().Get("in_query"), err) - } - - if routeCtx := chi.RouteContext(r.Context()); routeCtx != nil { - i.Path = routeCtx.URLParam("in-path") - } else { - return errors.New("missing path params in context") - } - - return nil -} diff --git a/_examples/advanced-generic/json_body_manual_test.go b/_examples/advanced-generic/json_body_manual_test.go deleted file mode 100644 index 96f952f..0000000 --- a/_examples/advanced-generic/json_body_manual_test.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/valyala/fasthttp" -) - -// Benchmark_jsonBodyManual-12 125672 8542 ns/op 208.0 B:rcvd/op 195.0 B:sent/op 117048 rps 4523 B/op 49 allocs/op. -func Benchmark_jsonBodyManual(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/json-body-manual/abc?in_query=2006-01-02") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Header", "def") - req.SetBody([]byte(`{"id":321,"name":"Jane"}`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusCreated - }) -} diff --git a/_examples/advanced-generic/json_body_test.go b/_examples/advanced-generic/json_body_test.go deleted file mode 100644 index 55e9f0b..0000000 --- a/_examples/advanced-generic/json_body_test.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/valyala/fasthttp" -) - -// Benchmark_jsonBody-12 96762 12042 ns/op 208.0 B:rcvd/op 188.0 B:sent/op 83033 rps 10312 B/op 100 allocs/op. -func Benchmark_jsonBody(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/json-body/abc?in_query=2006-01-02") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Header", "def") - req.SetBody([]byte(`{"id":321,"name":"Jane"}`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusCreated - }) -} diff --git a/_examples/advanced-generic/json_body_validation.go b/_examples/advanced-generic/json_body_validation.go deleted file mode 100644 index 44dcd7c..0000000 --- a/_examples/advanced-generic/json_body_validation.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func jsonBodyValidation() usecase.Interactor { - type JSONPayload struct { - ID int `json:"id" minimum:"100"` - Name string `json:"name" minLength:"3"` - } - - type inputWithJSON struct { - Header string `header:"X-Header" description:"Simple scalar value in header." minLength:"3"` - Query int `query:"in_query" description:"Simple scalar value in query." minimum:"100"` - Path string `path:"in-path" description:"Simple scalar value in path" minLength:"3"` - JSONPayload - } - - type outputWithJSON struct { - Header string `json:"inHeader" minLength:"3"` - Query int `json:"inQuery" minimum:"3"` - Path string `json:"inPath" minLength:"3"` - JSONPayload - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputWithJSON, out *outputWithJSON) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Path = in.Path - out.JSONPayload = in.JSONPayload - - return nil - }) - - u.SetTitle("Request With JSON Body and non-trivial validation") - u.SetDescription("Request with JSON body and query/header/path params, response with JSON body and data from request.") - u.SetTags("Request", "Response", "Validation") - - return u -} diff --git a/_examples/advanced-generic/json_body_validation_test.go b/_examples/advanced-generic/json_body_validation_test.go deleted file mode 100644 index 69e7548..0000000 --- a/_examples/advanced-generic/json_body_validation_test.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/valyala/fasthttp" -) - -// Benchmark_jsonBodyValidation-4 19126 60170 ns/op 194 B:rcvd/op 192 B:sent/op 16620 rps 18363 B/op 144 allocs/op. -func Benchmark_jsonBodyValidation(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/json-body-validation/abc?in_query=123") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Header", "def") - req.SetBody([]byte(`{"id":321,"name":"Jane"}`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} diff --git a/_examples/advanced-generic/json_map_body.go b/_examples/advanced-generic/json_map_body.go deleted file mode 100644 index fdac060..0000000 --- a/_examples/advanced-generic/json_map_body.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "encoding/json" - - "github.com/swaggest/usecase" -) - -type JSONMapPayload map[string]float64 - -type jsonMapReq struct { - Header string `header:"X-Header" description:"Simple scalar value in header."` - Query int `query:"in_query" description:"Simple scalar value in query."` - JSONMapPayload -} - -func (j *jsonMapReq) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &j.JSONMapPayload) -} - -func jsonMapBody() usecase.Interactor { - type jsonOutput struct { - Header string `json:"inHeader"` - Query int `json:"inQuery"` - Data JSONMapPayload `json:"data"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in jsonMapReq, out *jsonOutput) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Data = in.JSONMapPayload - - return nil - }) - - u.SetTitle("Request With JSON Map In Body") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/json_param.go b/_examples/advanced-generic/json_param.go deleted file mode 100644 index 9a58205..0000000 --- a/_examples/advanced-generic/json_param.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/google/uuid" - "github.com/swaggest/usecase" -) - -func jsonParam() usecase.Interactor { - type JSONPayload struct { - ID int `json:"id"` - Name string `json:"name"` - } - - type inputWithJSON struct { - Header string `header:"X-Header" description:"Simple scalar value in header."` - Query int `query:"in_query" description:"Simple scalar value in query."` - Path string `path:"in-path" description:"Simple scalar value in path"` - Cookie uuid.UUID `cookie:"in_cookie" description:"UUID in cookie."` - Identity JSONPayload `query:"identity" description:"JSON value in query"` - } - - type outputWithJSON struct { - Header string `json:"inHeader"` - Query int `json:"inQuery"` - Path string `json:"inPath"` - JSONPayload - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputWithJSON, out *outputWithJSON) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Path = in.Path - out.JSONPayload = in.Identity - - return nil - }) - - u.SetTitle("Request With JSON Query Parameter") - u.SetDescription("Request with JSON body and query/header/path params, response with JSON body and data from request.") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/json_slice_body.go b/_examples/advanced-generic/json_slice_body.go deleted file mode 100644 index 13be45c..0000000 --- a/_examples/advanced-generic/json_slice_body.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "encoding/json" - - "github.com/swaggest/usecase" -) - -// JSONSlicePayload is an example non-scalar type without `json` tags. -type JSONSlicePayload []int - -type jsonSliceReq struct { - Header string `header:"X-Header" description:"Simple scalar value in header."` - Query int `query:"in_query" description:"Simple scalar value in query."` - JSONSlicePayload -} - -func (j *jsonSliceReq) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &j.JSONSlicePayload) -} - -func jsonSliceBody() usecase.Interactor { - type jsonOutput struct { - Header string `json:"inHeader"` - Query int `json:"inQuery"` - Data JSONSlicePayload `json:"data"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in jsonSliceReq, out *jsonOutput) (err error) { - out.Query = in.Query - out.Header = in.Header - out.Data = in.JSONSlicePayload - - return nil - }) - - u.SetTitle("Request With JSON Array In Body") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/main.go b/_examples/advanced-generic/main.go deleted file mode 100644 index 2821f54..0000000 --- a/_examples/advanced-generic/main.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "log" - "net/http" -) - -func main() { - log.Println("http://localhost:8011/docs") - if err := http.ListenAndServe("localhost:8011", NewRouter()); err != nil { - log.Fatal(err) - } -} diff --git a/_examples/advanced-generic/no_validation.go b/_examples/advanced-generic/no_validation.go deleted file mode 100644 index 8cc1b25..0000000 --- a/_examples/advanced-generic/no_validation.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func noValidation() usecase.Interactor { - type inputPort struct { - Header int `header:"X-Input"` - Query bool `query:"q"` - Data struct { - Value string `json:"value"` - } `json:"data"` - } - - type outputPort struct { - Header int `header:"X-Output" json:"-"` - AnotherHeader bool `header:"X-Query" json:"-"` - Data struct { - Value string `json:"value"` - } `json:"data"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputPort, out *outputPort) (err error) { - out.Header = in.Header - out.AnotherHeader = in.Query - out.Data.Value = in.Data.Value - - return nil - }) - - u.SetTitle("No Validation") - u.SetDescription("Input/Output without validation.") - u.SetTags("Request", "Response") - - return u -} diff --git a/_examples/advanced-generic/output_headers.go b/_examples/advanced-generic/output_headers.go deleted file mode 100644 index 41f2227..0000000 --- a/_examples/advanced-generic/output_headers.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" - "github.com/swaggest/usecase/status" -) - -func outputHeaders() usecase.Interactor { - type EmbeddedHeaders struct { - Foo int `header:"X-foO,omitempty" json:"-" minimum:"10" required:"true" description:"Reduced by 20 in response."` - } - - type headerOutput struct { - EmbeddedHeaders - Header string `header:"x-HeAdEr" json:"-" description:"Sample response header."` - OmitEmpty int `header:"x-omit-empty,omitempty" json:"-" description:"Receives req value of X-Foo reduced by 30."` - InBody string `json:"inBody" deprecated:"true"` - Cookie int `cookie:"coo,httponly,path:/foo" json:"-"` - } - - type headerInput struct { - EmbeddedHeaders - } - - u := usecase.NewInteractor(func(ctx context.Context, in headerInput, out *headerOutput) (err error) { - out.Header = "abc" - out.InBody = "def" - out.Cookie = 123 - out.Foo = in.Foo - 20 - out.OmitEmpty = in.Foo - 30 - - return nil - }) - - u.SetTitle("Output With Headers") - u.SetDescription("Output with headers.") - u.SetTags("Response") - u.SetExpectedErrors(status.Internal) - - return u -} diff --git a/_examples/advanced-generic/output_headers_test.go b/_examples/advanced-generic/output_headers_test.go deleted file mode 100644 index 929135f..0000000 --- a/_examples/advanced-generic/output_headers_test.go +++ /dev/null @@ -1,136 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/swaggest/assertjson" - "github.com/valyala/fasthttp" -) - -// Benchmark_outputHeaders-4 41424 27054 ns/op 154 B:rcvd/op 77.0 B:sent/op 36963 rps 3641 B/op 35 allocs/op. -func Benchmark_outputHeaders(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodGet) - req.SetRequestURI(srv.URL + "/output-headers") - req.Header.Set("X-Foo", "40") - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -func Test_outputHeaders(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - req, err := http.NewRequest(http.MethodGet, srv.URL+"/output-headers", nil) - require.NoError(t, err) - - req.Header.Set("x-FoO", "40") - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, resp.StatusCode, http.StatusOK) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.Equal(t, "abc", resp.Header.Get("X-Header")) - assert.Equal(t, "20", resp.Header.Get("X-Foo")) - assert.Equal(t, "10", resp.Header.Get("X-Omit-Empty")) - assert.Equal(t, []string{"coo=123; HttpOnly"}, resp.Header.Values("Set-Cookie")) - assertjson.Equal(t, []byte(`{"inBody":"def"}`), body) -} - -func Test_outputHeaders_invalidReq(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - req, err := http.NewRequest(http.MethodGet, srv.URL+"/output-headers", nil) - require.NoError(t, err) - - req.Header.Set("x-FoO", "5") - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assertjson.Equal(t, - []byte(`{"msg":"invalid argument: validation failed","details":{"header:X-Foo":["#: must be >= 10/1 but found 5"]}}`), - body, string(body)) -} - -func Test_outputHeaders_invalidResp(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - req, err := http.NewRequest(http.MethodGet, srv.URL+"/output-headers", nil) - require.NoError(t, err) - - req.Header.Set("x-FoO", "15") - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, resp.StatusCode, http.StatusInternalServerError) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assertjson.Equal(t, - []byte(`{"msg":"internal: bad response: validation failed","details":{"header:X-Foo":["#: must be >= 10/1 but found -5"]}}`), - body, string(body)) -} - -func Test_outputHeaders_omitempty(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - req, err := http.NewRequest(http.MethodGet, srv.URL+"/output-headers", nil) - require.NoError(t, err) - - req.Header.Set("x-FoO", "30") - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, resp.StatusCode, http.StatusOK) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.Equal(t, "abc", resp.Header.Get("X-Header")) - assert.Equal(t, "10", resp.Header.Get("X-Foo")) - assert.Equal(t, []string(nil), resp.Header.Values("X-Omit-Empty")) - assert.Equal(t, []string{"coo=123; HttpOnly"}, resp.Header.Values("Set-Cookie")) - assertjson.Equal(t, []byte(`{"inBody":"def"}`), body) -} diff --git a/_examples/advanced-generic/output_writer.go b/_examples/advanced-generic/output_writer.go deleted file mode 100644 index 61f88cc..0000000 --- a/_examples/advanced-generic/output_writer.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "encoding/csv" - "net/http" - - "github.com/swaggest/rest" - "github.com/swaggest/usecase" - "github.com/swaggest/usecase/status" -) - -func outputCSVWriter() usecase.Interactor { - type writerOutput struct { - Header string `header:"X-Header" description:"Sample response header."` - ContentHash string `header:"ETag" description:"Content hash."` - usecase.OutputWithEmbeddedWriter - } - - type writerInput struct { - ContentHash string `header:"If-None-Match" description:"Content hash."` - } - - u := usecase.NewInteractor(func(ctx context.Context, in writerInput, out *writerOutput) (err error) { - contentHash := "abc123" // Pretending this is an actual content hash. - - if in.ContentHash == contentHash { - return rest.HTTPCodeAsError(http.StatusNotModified) - } - - out.Header = "abc" - out.ContentHash = contentHash - - c := csv.NewWriter(out) - - return c.WriteAll([][]string{{"abc", "def", "hij"}, {"klm", "nop", "qrs"}}) - }) - - u.SetTitle("Output With Stream Writer") - u.SetDescription("Output with stream writer.") - u.SetExpectedErrors(status.Internal, rest.HTTPCodeAsError(http.StatusNotModified)) - u.SetTags("Response") - - return u -} diff --git a/_examples/advanced-generic/query_object.go b/_examples/advanced-generic/query_object.go deleted file mode 100644 index 148f404..0000000 --- a/_examples/advanced-generic/query_object.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func queryObject() usecase.Interactor { - type jsonFilter struct { - Foo string `json:"foo" maxLength:"5"` - } - - type deepObjectFilter struct { - Bar string `json:"bar" query:"bar" minLength:"3"` - Baz *string `json:"baz,omitempty" query:"baz" minLength:"3"` - } - - type inputQueryObject struct { - Query map[int]float64 `query:"in_query" description:"Object value in query."` - JSONFilter jsonFilter `query:"json_filter" description:"JSON object value in query."` - DeepObjectFilter deepObjectFilter `query:"deep_object_filter" description:"Deep object value in query params."` - } - - type outputQueryObject struct { - Query map[int]float64 `json:"inQuery"` - JSONFilter jsonFilter `json:"jsonFilter"` - DeepObjectFilter deepObjectFilter `json:"deepObjectFilter"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputQueryObject, out *outputQueryObject) (err error) { - out.Query = in.Query - out.JSONFilter = in.JSONFilter - out.DeepObjectFilter = in.DeepObjectFilter - - return nil - }) - - u.SetTitle("Request With Object As Query Parameter") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/query_object_test.go b/_examples/advanced-generic/query_object_test.go deleted file mode 100644 index cf95747..0000000 --- a/_examples/advanced-generic/query_object_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/swaggest/assertjson" -) - -func Test_queryObject(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - for _, tc := range []struct { - name string - url string - code int - resp string - }{ - { - name: "validation_failed_deep_object", - url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=sd`, - code: http.StatusBadRequest, - resp: `{ - "msg":"invalid argument: validation failed", - "details":{"query:deep_object_filter":["#/bar: length must be \u003e= 3, but got 2"]} - }`, - }, - { - name: "validation_failed_deep_object_2", - url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=asd&deep_object_filter[baz]=sd`, - code: http.StatusBadRequest, - resp: `{ - "msg":"invalid argument: validation failed", - "details":{"query:deep_object_filter":["#/baz: length must be \u003e= 3, but got 2"]} - }`, - }, - { - name: "validation_failed_json", - url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"string"}&deep_object_filter[bar]=asd`, - code: http.StatusBadRequest, - resp: `{ - "msg":"invalid argument: validation failed", - "details":{"query:json_filter":["#/foo: length must be \u003c= 5, but got 6"]} - }`, - }, - { - name: "ok", - url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=asd`, - code: http.StatusOK, - resp: `{ - "inQuery":{"1":0,"2":0,"3":0},"jsonFilter":{"foo":"strin"}, - "deepObjectFilter":{"bar":"asd"} - }`, - }, - } { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest( - http.MethodGet, - srv.URL+tc.url, - nil, - ) - require.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - assertjson.EqMarshal(t, tc.resp, json.RawMessage(body)) - assert.Equal(t, tc.code, resp.StatusCode) - }) - } -} diff --git a/_examples/advanced-generic/request_response_mapping.go b/_examples/advanced-generic/request_response_mapping.go deleted file mode 100644 index 39a5d7d..0000000 --- a/_examples/advanced-generic/request_response_mapping.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func reqRespMapping() usecase.Interactor { - type inputPort struct { - Val1 string `description:"Simple scalar value with sample validation." required:"true" minLength:"3"` - Val2 int `description:"Simple scalar value with sample validation." required:"true" minimum:"3"` - } - - type outputPort struct { - Val1 string `json:"-" description:"Simple scalar value with sample validation." required:"true" minLength:"3"` - Val2 int `json:"-" description:"Simple scalar value with sample validation." required:"true" minimum:"3"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputPort, out *outputPort) (err error) { - out.Val1 = in.Val1 - out.Val2 = in.Val2 - - return nil - }) - - u.SetTitle("Request Response Mapping") - u.SetName("reqRespMapping") - u.SetDescription("This use case has transport concerns fully decoupled with external req/resp mapping.") - u.SetTags("Request", "Response") - - return u -} diff --git a/_examples/advanced-generic/request_response_mapping_test.go b/_examples/advanced-generic/request_response_mapping_test.go deleted file mode 100644 index 0c85192..0000000 --- a/_examples/advanced-generic/request_response_mapping_test.go +++ /dev/null @@ -1,59 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "bytes" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" -) - -func Test_requestResponseMapping(t *testing.T) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - req, err := http.NewRequest(http.MethodPost, srv.URL+"/req-resp-mapping", - bytes.NewReader([]byte(`val2=3`))) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("X-Header", "abc") - - resp, err := http.DefaultTransport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, http.StatusNoContent, resp.StatusCode) - - body, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - assert.Equal(t, "", string(body)) - - assert.Equal(t, "abc", resp.Header.Get("X-Value-1")) - assert.Equal(t, "3", resp.Header.Get("X-Value-2")) -} - -func Benchmark_requestResponseMapping(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/req-resp-mapping") - req.Header.Set("X-Header", "abc") - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBody([]byte(`val2=3`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusNoContent - }) -} diff --git a/_examples/advanced-generic/request_text_body.go b/_examples/advanced-generic/request_text_body.go deleted file mode 100644 index 38b8240..0000000 --- a/_examples/advanced-generic/request_text_body.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "context" - "io" - "net/http" - - "github.com/swaggest/usecase" -) - -type textReqBodyInput struct { - Path string `path:"path"` - Query int `query:"query"` - text []byte - err error -} - -func (c *textReqBodyInput) SetRequest(r *http.Request) { - c.text, c.err = io.ReadAll(r.Body) - clErr := r.Body.Close() - - if c.err == nil { - c.err = clErr - } -} - -func textReqBody() usecase.Interactor { - type output struct { - Path string `json:"path"` - Query int `json:"query"` - Text string `json:"text"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in textReqBodyInput, out *output) (err error) { - out.Text = string(in.text) - out.Path = in.Path - out.Query = in.Query - - return nil - }) - - u.SetTitle("Request With Text Body") - u.SetDescription("This usecase allows direct access to original `*http.Request` while keeping automated decoding of parameters.") - u.SetTags("Request") - - return u -} - -func textReqBodyPtr() usecase.Interactor { - type output struct { - Path string `json:"path"` - Query int `json:"query"` - Text string `json:"text"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in *textReqBodyInput, out *output) (err error) { - out.Text = string(in.text) - out.Path = in.Path - out.Query = in.Query - - return nil - }) - - u.SetTitle("Request With Text Body (ptr input)") - u.SetDescription("This usecase allows direct access to original `*http.Request` while keeping automated decoding of parameters.") - u.SetTags("Request") - - return u -} diff --git a/_examples/advanced-generic/router.go b/_examples/advanced-generic/router.go deleted file mode 100644 index 87d93b4..0000000 --- a/_examples/advanced-generic/router.go +++ /dev/null @@ -1,238 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - "errors" - "log" - "net/http" - "reflect" - "strings" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/cors" - "github.com/swaggest/jsonschema-go" - oapi "github.com/swaggest/openapi-go" - "github.com/swaggest/openapi-go/openapi3" - "github.com/swaggest/rest" - "github.com/swaggest/rest/nethttp" - "github.com/swaggest/rest/response" - "github.com/swaggest/rest/response/gzip" - "github.com/swaggest/rest/web" - swgui "github.com/swaggest/swgui/v5emb" - "github.com/swaggest/usecase" -) - -func NewRouter() http.Handler { - s := web.NewService(openapi3.NewReflector()) - - s.OpenAPISchema().SetTitle("Advanced Example") - s.OpenAPISchema().SetDescription("This app showcases a variety of features.") - s.OpenAPISchema().SetVersion("v1.2.3") - - jsr := s.OpenAPIReflector().JSONSchemaReflector() - - jsr.DefaultOptions = append(jsr.DefaultOptions, jsonschema.InterceptDefName( - func(t reflect.Type, defaultDefName string) string { - return strings.ReplaceAll(defaultDefName, "Generic", "") - }, - )) - - // Usecase middlewares can be added to web.Service or chirouter.Wrapper. - s.Wrap(nethttp.UseCaseMiddlewares(usecase.MiddlewareFunc(func(next usecase.Interactor) usecase.Interactor { - var ( - hasName usecase.HasName - name = "unknown" - ) - - if usecase.As(next, &hasName) { - name = hasName.Name() - } - - return usecase.Interact(func(ctx context.Context, input, output interface{}) error { - err := next.Interact(ctx, input, output) - if err != nil && !errors.Is(err, rest.HTTPCodeAsError(http.StatusNotModified)) { - log.Printf("usecase %s request (%+v) failed: %v\n", name, input, err) - } - - return err - }) - }))) - - // An example of global schema override to disable additionalProperties for all object schemas. - jsr.DefaultOptions = append(jsr.DefaultOptions, - jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) { - // Allow unknown request headers and skip response. - if oc, ok := oapi.OperationCtx(params.Context); !params.Processed || !ok || - oc.IsProcessingResponse() || oc.ProcessingIn() == oapi.InHeader { - return false, nil - } - - schema := params.Schema - - if schema.HasType(jsonschema.Object) && len(schema.Properties) > 0 && schema.AdditionalProperties == nil { - schema.AdditionalProperties = (&jsonschema.SchemaOrBool{}).WithTypeBoolean(false) - } - - return false, nil - }), - ) - - // Create custom schema mapping for 3rd party type. - uuidDef := jsonschema.Schema{} - uuidDef.AddType(jsonschema.String) - uuidDef.WithFormat("uuid") - uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d") - jsr.AddTypeMapping(uuid.UUID{}, uuidDef) - - // When multiple structures can be returned with the same HTTP status code, it is possible to combine them into a - // single schema with such configuration. - s.OpenAPICollector.CombineErrors = "anyOf" - - s.Wrap( - // Example middleware to set up custom error responses and disable response validation for particular handlers. - func(handler http.Handler) http.Handler { - var h *nethttp.Handler - if nethttp.HandlerAs(handler, &h) { - h.MakeErrResp = func(ctx context.Context, err error) (int, interface{}) { - code, er := rest.Err(err) - - var ae anotherErr - - if errors.As(err, &ae) { - return http.StatusBadRequest, ae - } - - return code, customErr{ - Message: er.ErrorText, - Details: er.Context, - } - } - - var hr rest.HandlerWithRoute - if h.RespValidator != nil && - nethttp.HandlerAs(handler, &hr) { - if hr.RoutePattern() == "/json-body-manual/{in-path}" || hr.RoutePattern() == "/json-body/{in-path}" { - h.RespValidator = nil - } - } - } - - return handler - }, - - // Example middleware to set up CORS headers. - // See https://pkg.go.dev/github.com/rs/cors for more details. - cors.AllowAll().Handler, - - // Response validator setup. - // - // It might be a good idea to disable this middleware in production to save performance, - // but keep it enabled in dev/test/staging environments to catch logical issues. - response.ValidatorMiddleware(s.ResponseValidatorFactory), - gzip.Middleware, // Response compression with support for direct gzip pass through. - ) - - // Annotations can be used to alter documentation of operation identified by method and path. - s.OpenAPICollector.AnnotateOperation(http.MethodPost, "/validation", func(oc oapi.OperationContext) error { - o3, ok := oc.(openapi3.OperationExposer) - if !ok { - return nil - } - - op := o3.Operation() - - if op.Description != nil { - *op.Description = *op.Description + " Custom annotation." - } - - return nil - }) - - s.Get("/query-object", queryObject()) - s.Post("/form", form()) - - s.Post("/file-upload", fileUploader()) - s.Post("/file-multi-upload", fileMultiUploader()) - s.Get("/json-param/{in-path}", jsonParam()) - s.Post("/json-body/{in-path}", jsonBody(), - nethttp.SuccessStatus(http.StatusCreated)) - s.Post("/json-body-manual/{in-path}", jsonBodyManual(), - nethttp.SuccessStatus(http.StatusCreated)) - s.Post("/json-body-validation/{in-path}", jsonBodyValidation()) - s.Post("/json-slice-body", jsonSliceBody()) - - s.Post("/json-map-body", jsonMapBody(), - // Annotate operation to add post-processing if necessary. - nethttp.AnnotateOpenAPIOperation(func(oc oapi.OperationContext) error { - oc.SetDescription("Request with JSON object (map) body.") - - return nil - })) - - s.Get("/html-response/{id}", htmlResponse(), nethttp.SuccessfulResponseContentType("text/html")) - - s.Get("/output-headers", outputHeaders()) - s.Head("/output-headers", outputHeaders()) - s.Get("/output-csv-writer", outputCSVWriter(), - nethttp.SuccessfulResponseContentType("text/csv; charset=utf-8")) - - s.Post("/req-resp-mapping", reqRespMapping(), - nethttp.RequestMapping(new(struct { - Val1 string `header:"X-Header"` - Val2 int `formData:"val2"` - })), - nethttp.ResponseHeaderMapping(new(struct { - Val1 string `header:"X-Value-1"` - Val2 int `header:"X-Value-2"` - })), - ) - - s.Post("/validation", validation()) - s.Post("/no-validation", noValidation()) - - // Type mapping is necessary to pass interface as structure into documentation. - jsr.AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct)) - s.Get("/gzip-pass-through", directGzip()) - s.Head("/gzip-pass-through", directGzip()) - - s.Get("/error-response", errorResponse()) - s.Post("/text-req-body/{path}", textReqBody(), nethttp.RequestBodyContent("text/csv")) - s.Post("/text-req-body-ptr/{path}", textReqBodyPtr(), nethttp.RequestBodyContent("text/csv")) - - // Security middlewares. - // - sessMW is the actual request-level processor, - // - sessDoc is a handler-level wrapper to expose docs. - sessMW := func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if c, err := r.Cookie("sessid"); err == nil { - r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value)) - } - - handler.ServeHTTP(w, r) - }) - } - - sessDoc := nethttp.AuthMiddleware(s.OpenAPICollector, "User") - s.OpenAPISchema().SetAPIKeySecurity("User", "sessid", oapi.InCookie, "Session cookie.") - - // Security schema is configured for a single top-level route. - s.With(sessMW, sessDoc).Method(http.MethodGet, "/root-with-session", nethttp.NewHandler(dummy())) - - // Security schema is configured on a sub-router. - s.Route("/deeper-with-session", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(sessMW, sessDoc) - - r.Method(http.MethodGet, "/one", nethttp.NewHandler(dummy())) - r.Method(http.MethodGet, "/two", nethttp.NewHandler(dummy())) - }) - }) - - // Swagger UI endpoint at /docs. - s.Docs("/docs", swgui.New) - - return s -} diff --git a/_examples/advanced-generic/router_test.go b/_examples/advanced-generic/router_test.go deleted file mode 100644 index a581145..0000000 --- a/_examples/advanced-generic/router_test.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/swaggest/assertjson" -) - -func TestNewRouter(t *testing.T) { - r := NewRouter() - - req, err := http.NewRequest(http.MethodGet, "/docs/openapi.json", nil) - require.NoError(t, err) - - rw := httptest.NewRecorder() - - r.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) - - actualSchema, err := assertjson.MarshalIndentCompact(json.RawMessage(rw.Body.Bytes()), "", " ", 120) - require.NoError(t, err) - - expectedSchema, err := os.ReadFile("_testdata/openapi.json") - require.NoError(t, err) - - if !assertjson.Equal(t, expectedSchema, rw.Body.Bytes(), string(actualSchema)) { - require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", actualSchema, 0o600)) - } -} diff --git a/_examples/advanced-generic/validation.go b/_examples/advanced-generic/validation.go deleted file mode 100644 index f4e66c5..0000000 --- a/_examples/advanced-generic/validation.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "context" - - "github.com/swaggest/usecase" -) - -func validation() usecase.Interactor { - type inputPort struct { - Header int `header:"X-Input" minimum:"10" description:"Request minimum: 10, response maximum: 20."` - Query bool `query:"q" description:"This parameter will bypass explicit validation as it does not have constraints."` - Data struct { - Value string `json:"value" minLength:"3" description:"Request minLength: 3, response maxLength: 7"` - } `json:"data" required:"true"` - } - - type outputPort struct { - Header int `header:"X-Output" json:"-" maximum:"20"` - AnotherHeader bool `header:"X-Query" json:"-" description:"This header bypasses validation as it does not have constraints."` - Data struct { - Value string `json:"value" maxLength:"7"` - } `json:"data" required:"true"` - } - - u := usecase.NewInteractor(func(ctx context.Context, in inputPort, out *outputPort) (err error) { - out.Header = in.Header - out.AnotherHeader = in.Query - out.Data.Value = in.Data.Value - - return nil - }) - - u.SetTitle("Validation") - u.SetDescription("Input/Output with validation.") - u.SetTags("Request", "Response", "Validation") - - return u -} diff --git a/_examples/advanced-generic/validation_test.go b/_examples/advanced-generic/validation_test.go deleted file mode 100644 index 227ea0f..0000000 --- a/_examples/advanced-generic/validation_test.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build go1.18 - -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/bool64/httptestbench" - "github.com/valyala/fasthttp" -) - -// Benchmark_validation-4 18979 53012 ns/op 197 B:rcvd/op 170 B:sent/op 18861 rps 14817 B/op 131 allocs/op. -// Benchmark_validation-4 17665 58243 ns/op 177 B:rcvd/op 170 B:sent/op 17161 rps 16349 B/op 132 allocs/op. -func Benchmark_validation(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/validation?q=true") - req.Header.Set("X-Input", "12") - req.Header.Set("Content-Type", "application/json") - req.SetBody([]byte(`{"data":{"value":"abc"}}`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} - -func Benchmark_noValidation(b *testing.B) { - r := NewRouter() - - srv := httptest.NewServer(r) - defer srv.Close() - - httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) { - req.Header.SetMethod(http.MethodPost) - req.SetRequestURI(srv.URL + "/no-validation?q=true") - req.Header.Set("X-Input", "12") - req.Header.Set("Content-Type", "application/json") - req.SetBody([]byte(`{"data":{"value":"abc"}}`)) - }, func(i int, resp *fasthttp.Response) bool { - return resp.StatusCode() == http.StatusOK - }) -} diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json index 5bcc7e4..17b7961 100644 --- a/_examples/advanced/_testdata/openapi.json +++ b/_examples/advanced/_testdata/openapi.json @@ -237,7 +237,7 @@ }, { "name":"identity","in":"query","description":"JSON value in query", - "content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJSONPayload"}}} + "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJSONPayload"}}} }, { "name":"in-path","in":"path","description":"Simple scalar value in path","required":true, @@ -245,7 +245,7 @@ }, { "name":"in_cookie","in":"cookie","description":"UUID in cookie.", - "schema":{"$ref":"#/components/schemas/CookieUuidUUID"} + "schema":{"$ref":"#/components/schemas/UuidUUID"} }, { "name":"X-Header","in":"header","description":"Simple scalar value in header.", @@ -512,6 +512,7 @@ "additionalProperties":false }, "AdvancedJSONMapPayload":{"type":"object","additionalProperties":{"type":"number"}}, + "AdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, "AdvancedJSONPayloadType2":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, "AdvancedJSONSlicePayload":{"type":"array","items":{"type":"integer"},"nullable":true}, "AdvancedJsonMapReq":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}, @@ -562,7 +563,6 @@ "name":{"minLength":3,"type":"string"} } }, - "CookieUuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"}, "FormDataAdvancedInputPort":{ "required":["val2"],"type":"object", "properties":{"val2":{"minimum":3,"type":"integer","description":"Simple scalar value with sample validation."}}, @@ -594,7 +594,6 @@ }, "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true}, "FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true}, - "QueryAdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false}, "RestErrResponse":{ "type":"object", "properties":{ @@ -604,7 +603,8 @@ "status":{"type":"string","description":"Status text."} } }, - "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}} + "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}, + "UuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"} }, "securitySchemes":{"User":{"type":"apiKey","name":"sessid","in":"cookie","description":"Session cookie."}} } diff --git a/_examples/go.mod b/_examples/go.mod index 8f9ea40..b5a0a22 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -17,7 +17,7 @@ require ( 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/openapi-go v0.2.38 + github.com/swaggest/openapi-go v0.2.39 github.com/swaggest/rest v0.0.0-00010101000000-000000000000 github.com/swaggest/swgui v1.7.2 github.com/swaggest/usecase v1.2.1 diff --git a/_examples/go.sum b/_examples/go.sum index ab45a43..6ee6891 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -106,8 +106,8 @@ github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6c 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/openapi-go v0.2.38 h1:umFRZ2wg75eDAofQCMLfXf8eWBLuQNfU9jOjGxwFdng= -github.com/swaggest/openapi-go v0.2.38/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c= +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 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0= github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU= github.com/swaggest/swgui v1.7.2 h1:N5hMPCQ+bIedVJoQDNjFUn8BqtISQDwaqEa76VkvzLs= diff --git a/go.mod b/go.mod index 713a233..63e3f22 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( 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/openapi-go v0.2.38 + github.com/swaggest/openapi-go v0.2.39 github.com/swaggest/refl v1.2.0 github.com/swaggest/usecase v1.2.1 ) diff --git a/go.sum b/go.sum index a8336a8..9901f29 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,10 @@ github.com/swaggest/jsonschema-go v0.3.57 h1:n6D/2K9557Yqn/NohXoszmjuN0Lp5n0DyHl github.com/swaggest/jsonschema-go v0.3.57/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik= github.com/swaggest/openapi-go v0.2.38 h1:umFRZ2wg75eDAofQCMLfXf8eWBLuQNfU9jOjGxwFdng= github.com/swaggest/openapi-go v0.2.38/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c= +github.com/swaggest/openapi-go v0.2.39-0.20230808224522-65df89a56f9b h1:z6txhy0FqQLgaD8MbWe9Zn3CHNb41IMlPKzEUFQcBdQ= +github.com/swaggest/openapi-go v0.2.39-0.20230808224522-65df89a56f9b/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c= +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 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0= github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU= github.com/swaggest/usecase v1.2.1 h1:XYVdK9tK2KCPglTflUi7aWBrVwIyb58D5mvGWED7pNs= diff --git a/openapi/collector_test.go b/openapi/collector_test.go index 2ea83f3..3a3cc86 100644 --- a/openapi/collector_test.go +++ b/openapi/collector_test.go @@ -443,16 +443,14 @@ func TestCollector_Collect_queryObject(t *testing.T) { "name":"json_filter","in":"query", "description":"JSON object value in query.", "content":{ - "application/json":{ - "schema":{"$ref":"#/components/schemas/QueryOpenapiTestJsonFilter"} - } + "application/json":{"schema":{"$ref":"#/components/schemas/OpenapiTestJsonFilter"}} } }, { "name":"deep_object_filter","in":"query", "description":"Deep object value in query params.", "style":"deepObject","explode":true, - "schema":{"$ref":"#/components/schemas/QueryOpenapiTestDeepObjectFilter"} + "schema":{"$ref":"#/components/schemas/OpenapiTestDeepObjectFilter"} } ], "responses":{"204":{"description":"No Content"}} @@ -461,8 +459,8 @@ func TestCollector_Collect_queryObject(t *testing.T) { }, "components":{ "schemas":{ - "QueryOpenapiTestDeepObjectFilter":{"type":"object","properties":{"bar":{"type":"string"}}}, - "QueryOpenapiTestJsonFilter":{"type":"object","properties":{"foo":{"type":"string"}}} + "OpenapiTestDeepObjectFilter":{"type":"object","properties":{"bar":{"type":"string"}}}, + "OpenapiTestJsonFilter":{"type":"object","properties":{"foo":{"type":"string"}}} } } }`, c.SpecSchema())