diff --git a/examples/streaming/README.md b/examples/streaming/README.md new file mode 100644 index 000000000..adf763a5a --- /dev/null +++ b/examples/streaming/README.md @@ -0,0 +1,10 @@ +OpenAPI Code Generation Example - Streaming +------------------------------------------- + +This directory contains an example server using our code generator which implements +a simple streaming API with a single endpoint. + +This is the structure: +- `sse.yaml`: Contains the OpenAPI 3.0 specification +- `stdhttp/`: Contains the written and generated code for the server using the standard http package + diff --git a/examples/streaming/sse.yaml b/examples/streaming/sse.yaml new file mode 100644 index 000000000..0aedfc0f8 --- /dev/null +++ b/examples/streaming/sse.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.0 +info: + title: Simple SSE Service + version: 1.0.0 +paths: + /: + get: + summary: Server-Sent Events Stream + description: Provides a stream of JSON documents containing a timestamp and sequence number. + operationId: getStream + responses: + 200: + description: SSE Stream + content: + text/event-stream: + schema: + type: object + properties: + time: + type: string + format: date-time + description: Timestamp of the event. + sequence: + type: integer + description: Sequence number of the event. + example: + time: "2023-11-20T10:30:00Z" + sequence: 1 \ No newline at end of file diff --git a/examples/streaming/stdhttp/Makefile b/examples/streaming/stdhttp/Makefile new file mode 100644 index 000000000..66f60e4a2 --- /dev/null +++ b/examples/streaming/stdhttp/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f2 -d. <<< $(GOVER)") + +define execute-if-go-122 +@{ \ +if [[ 22 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.22, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./... --out-format=colored-line-number --timeout=5m) + +generate: + $(call execute-if-go-122,go generate ./...) + +test: + $(call execute-if-go-122,go test -cover ./...) + +tidy: + $(call execute-if-go-122,go mod tidy) + +tidy-ci: + $(call execute-if-go-122,tidied -verbose) diff --git a/examples/streaming/stdhttp/go.mod b/examples/streaming/stdhttp/go.mod new file mode 100644 index 000000000..398cc799d --- /dev/null +++ b/examples/streaming/stdhttp/go.mod @@ -0,0 +1,29 @@ +module github.com/oapi-codegen/oapi-codegen/v2/examples/streaming/stdhttp + +go 1.21.13 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ + +require ( + github.com/getkin/kin-openapi v0.127.0 + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 + github.com/oapi-codegen/runtime v1.1.1 +) + +require ( + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/streaming/stdhttp/go.sum b/examples/streaming/stdhttp/go.sum new file mode 100644 index 000000000..efcdb40c3 --- /dev/null +++ b/examples/streaming/stdhttp/go.sum @@ -0,0 +1,170 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= +github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/streaming/stdhttp/main.go b/examples/streaming/stdhttp/main.go new file mode 100644 index 000000000..908b77999 --- /dev/null +++ b/examples/streaming/stdhttp/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "context" + "github.com/oapi-codegen/oapi-codegen/v2/examples/streaming/stdhttp/sse" + "log/slog" + "os" + "os/signal" + "syscall" +) + +func main() { + server := sse.NewServer() + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + err := server.Run(ctx) + if err != nil { + slog.Error("server run failed", "error:", err) + os.Exit(1) + } +} diff --git a/examples/streaming/stdhttp/sse/cfg.yaml b/examples/streaming/stdhttp/sse/cfg.yaml new file mode 100644 index 000000000..14ddeb0c2 --- /dev/null +++ b/examples/streaming/stdhttp/sse/cfg.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: sse +output: streaming.gen.go +generate: + std-http-server: true + strict-server: true + models: true + embedded-spec: true diff --git a/examples/streaming/stdhttp/sse/generate.go b/examples/streaming/stdhttp/sse/generate.go new file mode 100644 index 000000000..22de84621 --- /dev/null +++ b/examples/streaming/stdhttp/sse/generate.go @@ -0,0 +1,3 @@ +package sse + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml ../../sse.yaml diff --git a/examples/streaming/stdhttp/sse/impl.go b/examples/streaming/stdhttp/sse/impl.go new file mode 100644 index 000000000..381881177 --- /dev/null +++ b/examples/streaming/stdhttp/sse/impl.go @@ -0,0 +1,89 @@ +package sse + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "time" +) + +type Server struct { + httpServer *http.Server +} + +func NewServer() *Server { + s := &Server{} + strictHandler := NewStrictHandler(s, nil) + handler := Handler(strictHandler) + s.httpServer = &http.Server{ + Handler: handler, + Addr: ":8080", + } + return s +} +func (s Server) Run(ctx context.Context) error { + go func() { + <-ctx.Done() + slog.Warn("context cancelled, shutting down") + ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second) + err := s.httpServer.Shutdown(ctxTimeout) + cancel() + if err != nil { + slog.Error("httpServer.Shutdown() error", "error", err) + } + }() + + err := s.httpServer.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("httpServer.ListenAndServe() error: %w", err) + } + return nil +} + +type SObject struct { + Time time.Time `json:"time"` + Sequence int `json:"sequence"` +} + +// GetStream handles GET / and will stream a JSON object every second. +func (Server) GetStream(ctx context.Context, _ GetStreamRequestObject) (GetStreamResponseObject, error) { + r, w := io.Pipe() // creates a pipe so that we can write to the response body asynchronously + go func() { + defer w.Close() + seq := 1 + ticker := time.NewTicker(time.Second) + for { + select { + case <-ctx.Done(): + slog.Info("request context done, closing stream") + return + case <-ticker.C: + content := getContent(seq) + if _, err := w.Write(content); err != nil { + return + } + if _, err := w.Write([]byte("\n")); err != nil { + return + } + seq++ + } + } + }() + return GetStream200TexteventStreamResponse{ + Body: r, + ContentLength: 0, + }, nil +} + +func getContent(seq int) []byte { + streamObject := SObject{ + Time: time.Now(), + Sequence: seq, + } + bytes, _ := json.Marshal(streamObject) + return bytes +} diff --git a/examples/streaming/stdhttp/sse/streaming.gen.go b/examples/streaming/stdhttp/sse/streaming.gen.go new file mode 100644 index 000000000..6d8a7b069 --- /dev/null +++ b/examples/streaming/stdhttp/sse/streaming.gen.go @@ -0,0 +1,371 @@ +//go:build go1.22 + +// Package sse provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package sse + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Server-Sent Events Stream + // (GET /) + GetStream(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetStream operation middleware +func (siw *ServerInterfaceWrapper) GetStream(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetStream(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{}) +} + +// ServeMux is an abstraction of http.ServeMux. +type ServeMux interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +type StdHTTPServerOptions struct { + BaseURL string + BaseRouter ServeMux + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, m ServeMux) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseRouter: m, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseURL: baseURL, + BaseRouter: m, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { + m := options.BaseRouter + + if m == nil { + m = http.NewServeMux() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("GET "+options.BaseURL+"/", wrapper.GetStream) + + return m +} + +type GetStreamRequestObject struct { +} + +type GetStreamResponseObject interface { + VisitGetStreamResponse(w http.ResponseWriter) error +} + +type GetStream200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetStream200TexteventStreamResponse) VisitGetStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Server-Sent Events Stream + // (GET /) + GetStream(ctx context.Context, request GetStreamRequestObject) (GetStreamResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetStream operation middleware +func (sh *strictHandler) GetStream(w http.ResponseWriter, r *http.Request) { + var request GetStreamRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetStream(ctx, request.(GetStreamRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetStream") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetStreamResponseObject); ok { + if err := validResponse.VisitGetStreamResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/1ySwY6bMBCGX8WaM2RNcvN9VbWHthJ76s0LfxJX67FrT9CuIt69GqOmWi4YDcw33/xw", + "p8DnRO5OEuQN5GgMMb/BjOOzGVGWMIE6WlBqSEyOhoM9WFo7ShnscyBHp1bqKHu5VkU96eUC0WNGnUrI", + "snX/LGkJM6rxpkqBjyadzbfxx3czp+kWwVLNlFh84MAX442EiCo+ZuN5NhV/buAJhm/xFeVATaN4pX+d", + "ydEXyNi41FFBzYkrmtLRWj2UDW5ignd5wgKWflPRIt69bq+3/2aRGzpSDXJ0tMdTPwz90b4M1p2ss/aX", + "ZlGnK6LXrlxUSMI29T9jH8T4eRONQa4wzUfXko+sAwMLLig6Y1PYc14e+ewJ51SiF3I0e0Hfuh/YKiXw", + "hdb1UUmvvzEJrVrameqPsAWkD+stRl8+2gplQelHsJjnpX26x3vr+jcAAP//32AL/1kCAAA=", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/examples/streaming/stdhttp/tools/tools.go b/examples/streaming/stdhttp/tools/tools.go new file mode 100644 index 000000000..8615cb4c5 --- /dev/null +++ b/examples/streaming/stdhttp/tools/tools.go @@ -0,0 +1,8 @@ +//go:build tools +// +build tools + +package tools + +import ( + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +) diff --git a/internal/test/strict-server/chi/server.gen.go b/internal/test/strict-server/chi/server.gen.go index c8c7a0924..b00beb43f 100644 --- a/internal/test/strict-server/chi/server.gen.go +++ b/internal/test/strict-server/chi/server.gen.go @@ -45,6 +45,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(w http.ResponseWriter, r *http.Request) + // (GET /streaming) + StreamingExample(w http.ResponseWriter, r *http.Request) + // (POST /text) TextExample(w http.ResponseWriter, r *http.Request) @@ -98,6 +101,11 @@ func (_ Unimplemented) ReusableResponses(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusNotImplemented) } +// (GET /streaming) +func (_ Unimplemented) StreamingExample(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // (POST /text) func (_ Unimplemented) TextExample(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) @@ -232,6 +240,20 @@ func (siw *ServerInterfaceWrapper) ReusableResponses(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } +// StreamingExample operation middleware +func (siw *ServerInterfaceWrapper) StreamingExample(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamingExample(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // TextExample operation middleware func (siw *ServerInterfaceWrapper) TextExample(w http.ResponseWriter, r *http.Request) { @@ -496,6 +518,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/streaming", wrapper.StreamingExample) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/text", wrapper.TextExample) }) @@ -684,8 +709,10 @@ func (response MultipleRequestAndResponseTypes200ImagepngResponse) VisitMultiple if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type MultipleRequestAndResponseTypes200MultipartResponse func(writer *multipart.Writer) error @@ -769,6 +796,57 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(w http.ResponseWriter) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -826,8 +904,10 @@ func (response UnknownExample200Videomp4Response) VisitUnknownExampleResponse(w if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnknownExample400Response = BadrequestResponse @@ -871,8 +951,10 @@ func (response UnspecifiedContentType200VideoResponse) VisitUnspecifiedContentTy if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnspecifiedContentType400Response = BadrequestResponse @@ -1069,6 +1151,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1336,6 +1421,30 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques } } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(w http.ResponseWriter, r *http.Request) { + var request StreamingExampleRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx, request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // TextExample operation middleware func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { var request TextExampleRequestObject @@ -1524,24 +1633,24 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/chi/server.go b/internal/test/strict-server/chi/server.go index c135a2a44..14397b6a5 100644 --- a/internal/test/strict-server/chi/server.go +++ b/internal/test/strict-server/chi/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -142,3 +143,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/client/client.gen.go b/internal/test/strict-server/client/client.gen.go index 5bf1dffb3..9ff295f62 100644 --- a/internal/test/strict-server/client/client.gen.go +++ b/internal/test/strict-server/client/client.gen.go @@ -173,6 +173,9 @@ type ClientInterface interface { ReusableResponses(ctx context.Context, body ReusableResponsesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // StreamingExample request + StreamingExample(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // TextExampleWithBody request with any body TextExampleWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -332,6 +335,18 @@ func (c *Client) ReusableResponses(ctx context.Context, body ReusableResponsesJS return c.Client.Do(req) } +func (c *Client) StreamingExample(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStreamingExampleRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) TextExampleWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewTextExampleRequestWithBody(c.Server, contentType, body) if err != nil { @@ -682,6 +697,33 @@ func NewReusableResponsesRequestWithBody(server string, contentType string, body return req, nil } +// NewStreamingExampleRequest generates requests for StreamingExample +func NewStreamingExampleRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/streaming") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewTextExampleRequestWithTextBody calls the generic TextExample builder with text/plain body func NewTextExampleRequestWithTextBody(server string, body TextExampleTextRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -991,6 +1033,9 @@ type ClientWithResponsesInterface interface { ReusableResponsesWithResponse(ctx context.Context, body ReusableResponsesJSONRequestBody, reqEditors ...RequestEditorFn) (*ReusableResponsesResponse, error) + // StreamingExampleWithResponse request + StreamingExampleWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StreamingExampleResponse, error) + // TextExampleWithBodyWithResponse request with any body TextExampleWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TextExampleResponse, error) @@ -1147,6 +1192,27 @@ func (r ReusableResponsesResponse) StatusCode() int { return 0 } +type StreamingExampleResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r StreamingExampleResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StreamingExampleResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type TextExampleResponse struct { Body []byte HTTPResponse *http.Response @@ -1373,6 +1439,15 @@ func (c *ClientWithResponses) ReusableResponsesWithResponse(ctx context.Context, return ParseReusableResponsesResponse(rsp) } +// StreamingExampleWithResponse request returning *StreamingExampleResponse +func (c *ClientWithResponses) StreamingExampleWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StreamingExampleResponse, error) { + rsp, err := c.StreamingExample(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseStreamingExampleResponse(rsp) +} + // TextExampleWithBodyWithResponse request with arbitrary body returning *TextExampleResponse func (c *ClientWithResponses) TextExampleWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TextExampleResponse, error) { rsp, err := c.TextExampleWithBody(ctx, contentType, body, reqEditors...) @@ -1588,6 +1663,22 @@ func ParseReusableResponsesResponse(rsp *http.Response) (*ReusableResponsesRespo return response, nil } +// ParseStreamingExampleResponse parses an HTTP response from a StreamingExampleWithResponse call +func ParseStreamingExampleResponse(rsp *http.Response) (*StreamingExampleResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StreamingExampleResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseTextExampleResponse parses an HTTP response from a TextExampleWithResponse call func ParseTextExampleResponse(rsp *http.Response) (*TextExampleResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/test/strict-server/echo/server.gen.go b/internal/test/strict-server/echo/server.gen.go index f07dfa5e4..4a79aa443 100644 --- a/internal/test/strict-server/echo/server.gen.go +++ b/internal/test/strict-server/echo/server.gen.go @@ -45,6 +45,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx echo.Context) error + // (GET /streaming) + StreamingExample(ctx echo.Context) error + // (POST /text) TextExample(ctx echo.Context) error @@ -130,6 +133,15 @@ func (w *ServerInterfaceWrapper) ReusableResponses(ctx echo.Context) error { return err } +// StreamingExample converts echo context to params. +func (w *ServerInterfaceWrapper) StreamingExample(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.StreamingExample(ctx) + return err +} + // TextExample converts echo context to params. func (w *ServerInterfaceWrapper) TextExample(ctx echo.Context) error { var err error @@ -255,6 +267,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) router.GET(baseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.POST(baseURL+"/reusable-responses", wrapper.ReusableResponses) + router.GET(baseURL+"/streaming", wrapper.StreamingExample) router.POST(baseURL+"/text", wrapper.TextExample) router.POST(baseURL+"/unknown", wrapper.UnknownExample) router.POST(baseURL+"/unspecified-content-type", wrapper.UnspecifiedContentType) @@ -430,8 +443,10 @@ func (response MultipleRequestAndResponseTypes200ImagepngResponse) VisitMultiple if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type MultipleRequestAndResponseTypes200MultipartResponse func(writer *multipart.Writer) error @@ -515,6 +530,57 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(w http.ResponseWriter) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -572,8 +638,10 @@ func (response UnknownExample200Videomp4Response) VisitUnknownExampleResponse(w if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnknownExample400Response = BadrequestResponse @@ -617,8 +685,10 @@ func (response UnspecifiedContentType200VideoResponse) VisitUnspecifiedContentTy if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnspecifiedContentType400Response = BadrequestResponse @@ -815,6 +885,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1049,6 +1122,29 @@ func (sh *strictHandler) ReusableResponses(ctx echo.Context) error { return nil } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(ctx echo.Context) error { + var request StreamingExampleRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx.Request().Context(), request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + return validResponse.VisitStreamingExampleResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // TextExample operation middleware func (sh *strictHandler) TextExample(ctx echo.Context) error { var request TextExampleRequestObject @@ -1227,24 +1323,24 @@ func (sh *strictHandler) UnionExample(ctx echo.Context) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/echo/server.go b/internal/test/strict-server/echo/server.go index c135a2a44..14397b6a5 100644 --- a/internal/test/strict-server/echo/server.go +++ b/internal/test/strict-server/echo/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -142,3 +143,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/fiber/server.gen.go b/internal/test/strict-server/fiber/server.gen.go index b15d923fd..1d9d2eb7f 100644 --- a/internal/test/strict-server/fiber/server.gen.go +++ b/internal/test/strict-server/fiber/server.gen.go @@ -44,6 +44,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(c *fiber.Ctx) error + // (GET /streaming) + StreamingExample(c *fiber.Ctx) error + // (POST /text) TextExample(c *fiber.Ctx) error @@ -116,6 +119,12 @@ func (siw *ServerInterfaceWrapper) ReusableResponses(c *fiber.Ctx) error { return siw.Handler.ReusableResponses(c) } +// StreamingExample operation middleware +func (siw *ServerInterfaceWrapper) StreamingExample(c *fiber.Ctx) error { + + return siw.Handler.StreamingExample(c) +} + // TextExample operation middleware func (siw *ServerInterfaceWrapper) TextExample(c *fiber.Ctx) error { @@ -221,6 +230,8 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Post(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) + router.Get(options.BaseURL+"/streaming", wrapper.StreamingExample) + router.Post(options.BaseURL+"/text", wrapper.TextExample) router.Post(options.BaseURL+"/unknown", wrapper.UnknownExample) @@ -486,6 +497,32 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(ctx *fiber.Ctx) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + ctx.Status(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) + return err +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -786,6 +823,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1019,6 +1059,31 @@ func (sh *strictHandler) ReusableResponses(ctx *fiber.Ctx) error { return nil } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(ctx *fiber.Ctx) error { + var request StreamingExampleRequestObject + + handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx.UserContext(), request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(ctx, request) + + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(ctx); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // TextExample operation middleware func (sh *strictHandler) TextExample(ctx *fiber.Ctx) error { var request TextExampleRequestObject @@ -1202,24 +1267,24 @@ func (sh *strictHandler) UnionExample(ctx *fiber.Ctx) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/fiber/server.go b/internal/test/strict-server/fiber/server.go index c135a2a44..14397b6a5 100644 --- a/internal/test/strict-server/fiber/server.go +++ b/internal/test/strict-server/fiber/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -142,3 +143,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/gin/server.gen.go b/internal/test/strict-server/gin/server.gen.go index 21f9f3276..7e5f0305c 100644 --- a/internal/test/strict-server/gin/server.gen.go +++ b/internal/test/strict-server/gin/server.gen.go @@ -45,6 +45,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(c *gin.Context) + // (GET /streaming) + StreamingExample(c *gin.Context) + // (POST /text) TextExample(c *gin.Context) @@ -162,6 +165,19 @@ func (siw *ServerInterfaceWrapper) ReusableResponses(c *gin.Context) { siw.Handler.ReusableResponses(c) } +// StreamingExample operation middleware +func (siw *ServerInterfaceWrapper) StreamingExample(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.StreamingExample(c) +} + // TextExample operation middleware func (siw *ServerInterfaceWrapper) TextExample(c *gin.Context) { @@ -321,6 +337,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.POST(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) router.GET(options.BaseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.POST(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) + router.GET(options.BaseURL+"/streaming", wrapper.StreamingExample) router.POST(options.BaseURL+"/text", wrapper.TextExample) router.POST(options.BaseURL+"/unknown", wrapper.UnknownExample) router.POST(options.BaseURL+"/unspecified-content-type", wrapper.UnspecifiedContentType) @@ -495,8 +512,10 @@ func (response MultipleRequestAndResponseTypes200ImagepngResponse) VisitMultiple if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type MultipleRequestAndResponseTypes200MultipartResponse func(writer *multipart.Writer) error @@ -580,6 +599,57 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(w http.ResponseWriter) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -637,8 +707,10 @@ func (response UnknownExample200Videomp4Response) VisitUnknownExampleResponse(w if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnknownExample400Response = BadrequestResponse @@ -682,8 +754,10 @@ func (response UnspecifiedContentType200VideoResponse) VisitUnspecifiedContentTy if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnspecifiedContentType400Response = BadrequestResponse @@ -880,6 +954,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1139,6 +1216,31 @@ func (sh *strictHandler) ReusableResponses(ctx *gin.Context) { } } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(ctx *gin.Context) { + var request StreamingExampleRequestObject + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx, request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + // TextExample operation middleware func (sh *strictHandler) TextExample(ctx *gin.Context) { var request TextExampleRequestObject @@ -1335,24 +1437,24 @@ func (sh *strictHandler) UnionExample(ctx *gin.Context) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/gin/server.go b/internal/test/strict-server/gin/server.go index c135a2a44..14397b6a5 100644 --- a/internal/test/strict-server/gin/server.go +++ b/internal/test/strict-server/gin/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -142,3 +143,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/gorilla/server.gen.go b/internal/test/strict-server/gorilla/server.gen.go index c6b29393d..43c472bc9 100644 --- a/internal/test/strict-server/gorilla/server.gen.go +++ b/internal/test/strict-server/gorilla/server.gen.go @@ -45,6 +45,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(w http.ResponseWriter, r *http.Request) + // (GET /streaming) + StreamingExample(w http.ResponseWriter, r *http.Request) + // (POST /text) TextExample(w http.ResponseWriter, r *http.Request) @@ -168,6 +171,20 @@ func (siw *ServerInterfaceWrapper) ReusableResponses(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } +// StreamingExample operation middleware +func (siw *ServerInterfaceWrapper) StreamingExample(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamingExample(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // TextExample operation middleware func (siw *ServerInterfaceWrapper) TextExample(w http.ResponseWriter, r *http.Request) { @@ -426,6 +443,8 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H r.HandleFunc(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses).Methods("POST") + r.HandleFunc(options.BaseURL+"/streaming", wrapper.StreamingExample).Methods("GET") + r.HandleFunc(options.BaseURL+"/text", wrapper.TextExample).Methods("POST") r.HandleFunc(options.BaseURL+"/unknown", wrapper.UnknownExample).Methods("POST") @@ -607,8 +626,10 @@ func (response MultipleRequestAndResponseTypes200ImagepngResponse) VisitMultiple if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type MultipleRequestAndResponseTypes200MultipartResponse func(writer *multipart.Writer) error @@ -692,6 +713,57 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(w http.ResponseWriter) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -749,8 +821,10 @@ func (response UnknownExample200Videomp4Response) VisitUnknownExampleResponse(w if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnknownExample400Response = BadrequestResponse @@ -794,8 +868,10 @@ func (response UnspecifiedContentType200VideoResponse) VisitUnspecifiedContentTy if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnspecifiedContentType400Response = BadrequestResponse @@ -992,6 +1068,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1259,6 +1338,30 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques } } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(w http.ResponseWriter, r *http.Request) { + var request StreamingExampleRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx, request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // TextExample operation middleware func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { var request TextExampleRequestObject @@ -1447,24 +1550,24 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/gorilla/server.go b/internal/test/strict-server/gorilla/server.go index 5c5d7c9e7..41051b395 100644 --- a/internal/test/strict-server/gorilla/server.go +++ b/internal/test/strict-server/gorilla/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -118,3 +119,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/iris/server.gen.go b/internal/test/strict-server/iris/server.gen.go index 7f224b3b3..c6e9672ca 100644 --- a/internal/test/strict-server/iris/server.gen.go +++ b/internal/test/strict-server/iris/server.gen.go @@ -45,6 +45,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx iris.Context) + // (GET /streaming) + StreamingExample(ctx iris.Context) + // (POST /text) TextExample(ctx iris.Context) @@ -125,6 +128,13 @@ func (w *ServerInterfaceWrapper) ReusableResponses(ctx iris.Context) { w.Handler.ReusableResponses(ctx) } +// StreamingExample converts iris context to params. +func (w *ServerInterfaceWrapper) StreamingExample(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.StreamingExample(ctx) +} + // TextExample converts iris context to params. func (w *ServerInterfaceWrapper) TextExample(ctx iris.Context) { @@ -240,6 +250,7 @@ func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, o router.Post(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) router.Get(options.BaseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.Post(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) + router.Get(options.BaseURL+"/streaming", wrapper.StreamingExample) router.Post(options.BaseURL+"/text", wrapper.TextExample) router.Post(options.BaseURL+"/unknown", wrapper.UnknownExample) router.Post(options.BaseURL+"/unspecified-content-type", wrapper.UnspecifiedContentType) @@ -501,6 +512,32 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(ctx iris.Context) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + ctx.ResponseWriter().Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + ctx.StatusCode(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(ctx.ResponseWriter(), response.Body) + return err +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -801,6 +838,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1069,6 +1109,33 @@ func (sh *strictHandler) ReusableResponses(ctx iris.Context) { } } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(ctx iris.Context) { + var request StreamingExampleRequestObject + + handler := func(ctx iris.Context, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx, request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(ctx); err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else if response != nil { + ctx.Writef("Unexpected response type: %T", response) + return + } +} + // TextExample operation middleware func (sh *strictHandler) TextExample(ctx iris.Context) { var request TextExampleRequestObject @@ -1275,24 +1342,24 @@ func (sh *strictHandler) UnionExample(ctx iris.Context) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/iris/server.go b/internal/test/strict-server/iris/server.go index c0ab4c770..415a3deee 100644 --- a/internal/test/strict-server/iris/server.go +++ b/internal/test/strict-server/iris/server.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct{} @@ -141,3 +142,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/stdhttp/server.gen.go b/internal/test/strict-server/stdhttp/server.gen.go index 28c36a7ac..3a035c476 100644 --- a/internal/test/strict-server/stdhttp/server.gen.go +++ b/internal/test/strict-server/stdhttp/server.gen.go @@ -46,6 +46,9 @@ type ServerInterface interface { // (POST /reusable-responses) ReusableResponses(w http.ResponseWriter, r *http.Request) + // (GET /streaming) + StreamingExample(w http.ResponseWriter, r *http.Request) + // (POST /text) TextExample(w http.ResponseWriter, r *http.Request) @@ -169,6 +172,20 @@ func (siw *ServerInterfaceWrapper) ReusableResponses(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } +// StreamingExample operation middleware +func (siw *ServerInterfaceWrapper) StreamingExample(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamingExample(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // TextExample operation middleware func (siw *ServerInterfaceWrapper) TextExample(w http.ResponseWriter, r *http.Request) { @@ -428,6 +445,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc("POST "+options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) m.HandleFunc("GET "+options.BaseURL+"/reserved-go-keyword-parameters/{type}", wrapper.ReservedGoKeywordParameters) m.HandleFunc("POST "+options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) + m.HandleFunc("GET "+options.BaseURL+"/streaming", wrapper.StreamingExample) m.HandleFunc("POST "+options.BaseURL+"/text", wrapper.TextExample) m.HandleFunc("POST "+options.BaseURL+"/unknown", wrapper.UnknownExample) m.HandleFunc("POST "+options.BaseURL+"/unspecified-content-type", wrapper.UnspecifiedContentType) @@ -604,8 +622,10 @@ func (response MultipleRequestAndResponseTypes200ImagepngResponse) VisitMultiple if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type MultipleRequestAndResponseTypes200MultipartResponse func(writer *multipart.Writer) error @@ -689,6 +709,57 @@ func (response ReusableResponsesdefaultResponse) VisitReusableResponsesResponse( return nil } +type StreamingExampleRequestObject struct { +} + +type StreamingExampleResponseObject interface { + VisitStreamingExampleResponse(w http.ResponseWriter) error +} + +type StreamingExample200TexteventStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response StreamingExample200TexteventStreamResponse) VisitStreamingExampleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + +} + type TextExampleRequestObject struct { Body *TextExampleTextRequestBody } @@ -746,8 +817,10 @@ func (response UnknownExample200Videomp4Response) VisitUnknownExampleResponse(w if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnknownExample400Response = BadrequestResponse @@ -791,8 +864,10 @@ func (response UnspecifiedContentType200VideoResponse) VisitUnspecifiedContentTy if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + _, err := io.Copy(w, response.Body) return err + } type UnspecifiedContentType400Response = BadrequestResponse @@ -989,6 +1064,9 @@ type StrictServerInterface interface { // (POST /reusable-responses) ReusableResponses(ctx context.Context, request ReusableResponsesRequestObject) (ReusableResponsesResponseObject, error) + // (GET /streaming) + StreamingExample(ctx context.Context, request StreamingExampleRequestObject) (StreamingExampleResponseObject, error) + // (POST /text) TextExample(ctx context.Context, request TextExampleRequestObject) (TextExampleResponseObject, error) @@ -1256,6 +1334,30 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques } } +// StreamingExample operation middleware +func (sh *strictHandler) StreamingExample(w http.ResponseWriter, r *http.Request) { + var request StreamingExampleRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StreamingExample(ctx, request.(StreamingExampleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamingExample") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StreamingExampleResponseObject); ok { + if err := validResponse.VisitStreamingExampleResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // TextExample operation middleware func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { var request TextExampleRequestObject @@ -1444,24 +1546,24 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5Q0bccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQeph0YqU", + "6NHJ9MbHYnfxfbuLxc54ZkprNGhyPJ1xBGeNdtC8DIVE+KcCR/5NgstQWVJG85S/E3LQ/ptHHKFyYlhA", + "t9zLZ0YT6GapsLZQmfBLk0/Or59xl42hFP7pR4Scp/yHZOlKEv66BJ5FaQvg8/k8euHB3Uce8TEICdh4", + "Gx6v1nXT1AJPuSNUesS9kiB23SumNMEI0Fvzoq0TXqDzI51xi8YCkgoYTURRQb+l9osZfoKMwg6Uzs0m", + "lrdGk1DaManyHBA0sRY85nU45iprDRJINpwybyEj5gAngDzipMg7xu9Xv7PWYccjPgF0wdDVxeXFpefL", + "WNDCKp7yt82niFtB42ZDC4Ks6eP9t/u7P5lyTFRkSkEqE0UxZaVANxYFSKY0Ge9ilZG74I0lbIj/Vbar", + "37dQ+qhpAuidkdNjBEwTlyvhfH15eaK4nEf8Jhjr07FwKllJsEZNLqqiB/NH/VmbWjNANNjuLCmrgpQV", + "SKtcraP9RyeyC+QLfUlusIylIHEk1A9l6dzAxwiFIJA7EDAIkvvxsKL+qCx8i52zctDW4946dT82tWNj", + "UzMyTIIoWK1ozLqFLwqs0kwwp/SoANY5FfWSWUB77P2s5aDdy4PXcfR6Fq1peY7ruo6bBKqwAJ0Z+XUU", + "RlyVYgSJ1aP15V63IJ7y4ZR8yG4ecAdK5IgTPFNiC6H09tP7RCX9f6QPltghXRGarkTGIxN/hmltUMZW", + "oCiBAF0y89bnXvEIelL5r4Uky4RmQ2BalCCZyAmQfTCsVek2UnbQ2v1gPgaRpaqm5Vm8pH/PuIekaYN4", + "xL0BngZUQl4r9KQTVhBtge3pi/H5TQR0aIZmO14z1V8GuxK1gA4hd74k9jHXg1+wNFiROE/Ttj3iNq4f", + "pziDHCGIUoVk7g3c+06CLRx7ifBCZLU1+HIAwQQ0xcGD/WrJlrjyml/vZR7geac+8oC1/NTFat8IqMLH", + "1zFrV+0C21ceDTugOFESTFLamwMEyklAdRYylSuQcbuLOPj2Wo27NTpDoPWezt9RtSG2UOavzjQGFhCI", + "mDOsBlZWjpgVzjFFTVksVLh+y81cfVx6dhssPSzPh22svjkSp2/OxejN5dX+S94eOW7WerNX8nHw+/sg", + "s+8Q4mBN4J4t7OHsnimd/a0rXpnS9afwL0Fg2aRkoCa+xdOSIVCFGiSbKNFNljZys1WwpLWvuQtuLNu7", + "bmK4T4cXbdV1zaNtU8Wn73jmdcxZ7KnitNJq2+zz0f9m7aXg5dmgjP6PTjZFQYBakJrAT4e5Em9qMRru", + "8ibTXrAc7Wjh6fuLqnnEwzA+lKAKC18niGyaJGGIf+FqMRoBXiiTCKs8Cv8GAAD//7RwfACRGQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/stdhttp/server.go b/internal/test/strict-server/stdhttp/server.go index a142a6b81..31632a3e4 100644 --- a/internal/test/strict-server/stdhttp/server.go +++ b/internal/test/strict-server/stdhttp/server.go @@ -10,6 +10,7 @@ import ( "encoding/json" "io" "mime/multipart" + "time" ) type StrictServer struct { @@ -144,3 +145,29 @@ func (s StrictServer) UnionExample(ctx context.Context, request UnionExampleRequ }, }, nil } + +func (s StrictServer) StreamingExample(ctx context.Context, _ StreamingExampleRequestObject) (StreamingExampleResponseObject, error) { + r, w := io.Pipe() + go func() { + defer w.Close() + _, err := w.Write([]byte("first write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("second write\n")) + if err != nil { + panic(err) + } + time.Sleep(time.Millisecond * 10) + _, err = w.Write([]byte("third write\n")) + if err != nil { + panic(err) + } + + }() + return StreamingExample200TexteventStreamResponse{ + ContentLength: 0, + Body: r, + }, nil +} diff --git a/internal/test/strict-server/stdhttp/std_strict_test.go b/internal/test/strict-server/stdhttp/std_strict_test.go index 40d362a54..a6001f40f 100644 --- a/internal/test/strict-server/stdhttp/std_strict_test.go +++ b/internal/test/strict-server/stdhttp/std_strict_test.go @@ -5,29 +5,51 @@ package api import ( "bytes" "encoding/json" + "fmt" + clientAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/client" + "github.com/oapi-codegen/runtime" + "github.com/oapi-codegen/testutil" + "github.com/stretchr/testify/assert" "io" "mime" "mime/multipart" "net/http" + "net/http/httptest" "net/url" "strings" "testing" - - "github.com/stretchr/testify/assert" - - clientAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/client" - "github.com/oapi-codegen/runtime" - "github.com/oapi-codegen/testutil" + "time" ) func TestStdHTTPServer(t *testing.T) { server := StrictServer{} strictHandler := NewStrictHandler(server, nil) m := http.NewServeMux() - HandlerFromMux(strictHandler, m) + _ = HandlerFromMux(strictHandler, m) testImpl(t, m) } +func TestStreaming(t *testing.T) { + server := StrictServer{} + strictHandler := NewStrictHandler(server, nil) + m := http.NewServeMux() + handler := HandlerFromMux(strictHandler, m) + + // create a request with a context that will be canceled after 3 second + req := httptest.NewRequest(http.MethodGet, "/streaming", nil) + rr := newStreamResponseWriter() + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "text/event-stream", rr.Header().Get("Content-Type")) + assert.Equal(t, 3, len(rr.ops)) + for i := 1; i < len(rr.ops); i++ { // make sure there is at least 10 ms between the writes + assert.True(t, rr.ops[i].ts-rr.ops[i-1].ts >= 10*time.Millisecond) + } + assert.Equal(t, "first write\n", string(rr.ops[0].bytes)) + assert.Equal(t, "second write\n", string(rr.ops[1].bytes)) + assert.Equal(t, "third write\n", string(rr.ops[2].bytes)) +} + func testImpl(t *testing.T, handler http.Handler) { t.Run("JSONExample", func(t *testing.T) { value := "123" @@ -226,4 +248,11 @@ func testImpl(t *testing.T, handler http.Handler) { assert.NoError(t, err) assert.Equal(t, requestBody, responseBody) }) + t.Run("StreamingResponse", func(t *testing.T) { + // the /streaming endpoint will issue two writes with 100ms between them. + rr := testutil.NewRequest().Get("/streaming").GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "text/event-stream", rr.Header().Get("Content-Type")) + fmt.Println("body length", len(rr.Body.String())) + }) } diff --git a/internal/test/strict-server/stdhttp/streamrecorder.go b/internal/test/strict-server/stdhttp/streamrecorder.go new file mode 100644 index 000000000..cf10d8e5e --- /dev/null +++ b/internal/test/strict-server/stdhttp/streamrecorder.go @@ -0,0 +1,52 @@ +package api + +import ( + "net/http" + "time" +) + +// Chunk represents a piece of data with its associated timing +type writtenChunk struct { + bytes []byte + ts time.Duration +} + +// Custom ResponseWriter with writtenChunk tracking and timing +type streamRecordWriter struct { + ops []writtenChunk + startTime time.Time + headers http.Header + Code int +} + +func newStreamResponseWriter() *streamRecordWriter { + return &streamRecordWriter{ + startTime: time.Now(), + ops: make([]writtenChunk, 0), + headers: make(http.Header), + Code: 0, + } +} + +func (crw *streamRecordWriter) Write(data []byte) (int, error) { + // copy the data to avoid the slice being modified if the caller modifies it + dataCopy := make([]byte, len(data)) + copy(dataCopy, data) + newChunk := writtenChunk{ + bytes: dataCopy, + ts: time.Since(crw.startTime), + } + crw.ops = append(crw.ops, newChunk) + return len(data), nil +} + +func (crw *streamRecordWriter) Header() http.Header { + return crw.headers +} + +func (crw *streamRecordWriter) WriteHeader(statusCode int) { + crw.Code = statusCode +} + +func (crw *streamRecordWriter) Flush() { +} diff --git a/internal/test/strict-server/strict-schema.yaml b/internal/test/strict-server/strict-schema.yaml index a0b6a6e05..110187e7b 100644 --- a/internal/test/strict-server/strict-schema.yaml +++ b/internal/test/strict-server/strict-schema.yaml @@ -249,21 +249,21 @@ paths: description: Unknown error /reserved-go-keyword-parameters/{type}: get: - operationId: ReservedGoKeywordParameters - description: Parameters can be named after Go keywords - parameters: - - name: type - in: path - required: true - schema: + operationId: ReservedGoKeywordParameters + description: Parameters can be named after Go keywords + parameters: + - name: type + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + text/plain: + schema: type: string - responses: - 200: - description: OK - content: - text/plain: - schema: - type: string /with-union: post: operationId: UnionExample @@ -287,8 +287,8 @@ paths: application/json: schema: oneOf: - - type: string - - $ref: "#/components/schemas/example" + - type: string + - $ref: "#/components/schemas/example" application/alternative+json: schema: $ref: "#/components/schemas/example" @@ -296,6 +296,18 @@ paths: $ref: "#/components/responses/badrequest" default: description: Unknown error + /streaming: + get: + operationId: StreamingExample + description: Streaming response + responses: + 200: + description: OK + content: + text/event-stream: + schema: + type: string + format: byte components: responses: badrequest: diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index e19936d6a..7f44a26d9 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -105,8 +105,36 @@ if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } + {{if eq .ContentType "text/event-stream"}} + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + _, writeErr := w.Write(buf[:n]) + if writeErr != nil { + return writeErr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } + {{else}} _, err := io.Copy(w, response.Body) return err + {{end}} {{end}}{{/* if eq .NameTag "JSON" */ -}} } {{end}}