diff --git a/README.md b/README.md index 438247d..ccf6fb0 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,17 @@ pkg/petstore/ codegen.yml # Code generation settings context.yml # Custom values for mock data generate.go # go:generate directive - gen.go # Generated handler (do not edit) - service.go # Custom logic (optional overrides) + gen.go # Generated types and handlers (do not edit) + register.go # Generated, wires the service into mockzilla (do not edit) + service.go # Written once, yours to edit: custom logic per operation ``` +Code is generated by [mockzilla-codegen](https://github.com/mockzilla/mockzilla-codegen). +`setup/codegen.yml` is its config file. See its +[configuration reference](https://github.com/mockzilla/mockzilla-codegen/blob/main/docs/config.md) +and the [mockzilla codegen docs](https://github.com/mockzilla/mockzilla/blob/main/docs/config/codegen.md). +In a method of `service.go`, return `nil, nil` to answer with the generated mock response. + ## API Explorer UI The built-in UI is available at `/` (configurable in `resources/data/app.yml`). diff --git a/go.mod b/go.mod index 95a4415..4e6c730 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,13 @@ module github.com/mockzilla/mockzilla-codegen-template -go 1.26.8 +go 1.26.9 require ( - github.com/doordash-oss/oapi-codegen-dd/v3 v3.75.18 github.com/go-chi/chi/v5 v5.3.2 - github.com/go-playground/validator/v10 v10.30.4 github.com/joho/godotenv v1.5.1 github.com/lmittmann/tint v1.1.3 - github.com/mockzilla/mockzilla/v2 v2.10.0 + github.com/mockzilla/mockzilla-codegen v1.0.2 + github.com/mockzilla/mockzilla/v2 v2.11.0 ) require ( @@ -18,15 +17,10 @@ require ( github.com/caarlos0/env/v11 v11.4.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/iancoleman/strcase v0.3.0 // indirect github.com/jaswdr/faker/v2 v2.9.1 // indirect - github.com/leodido/go-urn v1.5.0 // indirect github.com/pb33f/jsonpath v0.8.2 // indirect github.com/pb33f/libopenapi v0.38.7 // indirect github.com/pb33f/libopenapi-validator v0.13.7 // indirect @@ -34,11 +28,7 @@ require ( github.com/redis/go-redis/v9 v9.13.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.56.0 // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/text v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index 520a96c..e80ac32 100644 --- a/go.sum +++ b/go.sum @@ -17,14 +17,11 @@ github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vaui github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/doordash-oss/oapi-codegen-dd/v3 v3.75.18 h1:lCJLaKz6fItFo8htsVV6wgMS3txLN+ypBHGG8aV694g= -github.com/doordash-oss/oapi-codegen-dd/v3 v3.75.18/go.mod h1:6muR3yI4LRf8KO0awHMhLwPM92bN7hUCgv1RRt3peus= github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= @@ -35,20 +32,14 @@ github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiN github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w= github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk= -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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jaswdr/faker/v2 v2.9.1 h1:J0Rjqb2/FquZnoZplzkGVL5LmhNkeIpvsSMoJKzn+8E= github.com/jaswdr/faker/v2 v2.9.1/go.mod h1:jZq+qzNQr8/P+5fHd9t3txe2GNPnthrTfohtnJ7B+68= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -57,8 +48,10 @@ github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/mockzilla/mockzilla/v2 v2.10.0 h1:1hnF2C5udP8oBFqzAo4Q3zCOwvpOvggRUR/WcJgJqz0= -github.com/mockzilla/mockzilla/v2 v2.10.0/go.mod h1:xDjmQstM5AcMUquDKP3K3cpJhxE+M0nmM+qNBb0UXTE= +github.com/mockzilla/mockzilla-codegen v1.0.2 h1:CRMcpMikIk0mFTo/ICTDtzitd1A/Bgsr550dp889c/4= +github.com/mockzilla/mockzilla-codegen v1.0.2/go.mod h1:CTSIfBgX/LmnOmxBUjKla8XHtZDdx/58Y5Rbh6iZ6v8= +github.com/mockzilla/mockzilla/v2 v2.11.0 h1:qUTdaB3f3JMthvfiHAkP/b9OCuAWbIpeelJl86ivpQQ= +github.com/mockzilla/mockzilla/v2 v2.11.0/go.mod h1:3BpxRz9HzOwjVcVksKokgPCsPFVVszVsA74I5po1DI0= github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= github.com/pb33f/libopenapi v0.38.7 h1:Q2jfgRPdnU38WW8wQvrX2HEPGiqsxj01PX1BHmAEihc= @@ -69,7 +62,6 @@ github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pb33f/testify v0.1.0 h1:g48/HDU/jn2COspS4nM0scptxiKTJ4DnbX/4ehK6IZ8= github.com/pb33f/testify v0.1.0/go.mod h1:nq283P/jJ8hXMmdhAqfj7BJIz0y+6IOHj9q0044rKt4= -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/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg= github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= @@ -77,35 +69,35 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -114,8 +106,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= 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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -127,16 +119,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/pkg/hello_world/gen.go b/pkg/hello_world/gen.go index dfa451e..ade4032 100644 --- a/pkg/hello_world/gen.go +++ b/pkg/hello_world/gen.go @@ -1,629 +1,326 @@ -// Code generated by oapi-codegen. DO NOT EDIT. +// Code generated by mockzilla-codegen. DO NOT EDIT. package hello_world import ( "context" - "embed" "encoding/json" "errors" - "fmt" - "io" - "log/slog" "net/http" - "sync" - - "github.com/doordash-oss/oapi-codegen-dd/v3/pkg/runtime" - "github.com/go-chi/chi/v5" - "github.com/go-playground/validator/v10" - "github.com/mockzilla/mockzilla/v2/pkg/api" - "github.com/mockzilla/mockzilla/v2/pkg/config" - "github.com/mockzilla/mockzilla/v2/pkg/db" - "github.com/mockzilla/mockzilla/v2/pkg/factory" - "github.com/mockzilla/mockzilla/v2/pkg/generator" - "github.com/mockzilla/mockzilla/v2/pkg/loader" - "github.com/mockzilla/mockzilla/v2/pkg/schema" - "github.com/mockzilla/mockzilla/v2/pkg/typedef" + chi "github.com/go-chi/chi/v5" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime/httpserver" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime/validation" ) -// OapiErrorKind represents the type of error that occurred during request processing. -type OapiErrorKind int +// Fails to compile when the runtime does not match the generator that wrote this file. +const _ = runtime.SupportsGeneratorV1 -const ( - // OapiErrorKindParse indicates a parameter parsing error (invalid path/query/header parameter). - OapiErrorKindParse OapiErrorKind = iota - - // OapiErrorKindDecode indicates a request body decoding error (invalid JSON, form data, etc.). - OapiErrorKindDecode - - // OapiErrorKindValidation indicates a request validation error (failed schema validation). - OapiErrorKindValidation - - // OapiErrorKindService indicates a service/business logic error returned by the service implementation. - OapiErrorKindService -) - -// OapiHandlerError represents an error that occurred during request handling (parse, decode, validation). -// When no typed error response is configured in the OpenAPI spec, this error type is used. -// Custom error handlers can type-assert to this type to access error details. -type OapiHandlerError struct { - Kind OapiErrorKind - OperationID string - Message string - ParamName string - ParamLocation string - - // Err is the error that caused this handler error. - Err error `json:"-"` +type PostHelloQuery struct { + Q *string `json:"q,omitempty"` } -func (e OapiHandlerError) Error() string { - return e.Message -} +type PostHelloRequestBody map[string]any -// Unwrap returns the underlying error, enabling errors.Is and errors.As. -func (e OapiHandlerError) Unwrap() error { - return e.Err +type PostHelloResponse200 struct { + Hellow *string `json:"hellow,omitempty"` } -// OapiErrorResponse is the default JSON error response structure used by OapiDefaultErrorHandler. -type OapiErrorResponse struct { - Error string `json:"error"` - OperationID string `json:"operation_id,omitempty"` - ParamName string `json:"param_name,omitempty"` - ParamLocation string `json:"param_location,omitempty"` +// ServiceInterface is what the generated handlers call. Implement it with the business logic. +type ServiceInterface interface { + // PostHello handles POST /hello. + PostHello(ctx context.Context, opts *PostHelloServiceRequestOptions) (*PostHelloResponseData, error) } -// OapiErrorHandler handles errors that occur during request processing. -// Implement this interface to customize error responses, logging, and metrics. -type OapiErrorHandler interface { - // HandleError writes an error response to w with the given status code. - // The err is either an OapiHandlerError (for parse/decode/validation errors) - // or a typed error matching the OpenAPI spec's error response schema. - HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) +// PostHelloServiceRequestOptions is what PostHello receives. +type PostHelloServiceRequestOptions struct { + Query *PostHelloQuery + // Body sent as application/json. + Body PostHelloRequestBody + // GenerateResponse makes a mock response that satisfies the spec. + GenerateResponse func() (*PostHelloResponseData, error) + RawRequest *http.Request } -// OapiDefaultErrorHandler provides the default error handling behavior. -// It writes JSON error responses. For OapiHandlerError, it uses OapiErrorResponse. -// For typed errors (from OpenAPI spec), it encodes them directly. -type OapiDefaultErrorHandler struct{} - -// HandleError implements OapiErrorHandler with default JSON error responses. -func (h *OapiDefaultErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) +// Validate checks the parameters and the body against the constraints of the spec. +func (o *PostHelloServiceRequestOptions) Validate() error { + return nil +} - if handlerErr, ok := err.(OapiHandlerError); ok { - _ = json.NewEncoder(w).Encode(OapiErrorResponse{ - Error: handlerErr.Message, - OperationID: handlerErr.OperationID, - ParamName: handlerErr.ParamName, - ParamLocation: handlerErr.ParamLocation, - }) - return - } +// PostHelloResponseData is what PostHello returns. +type PostHelloResponseData struct { + Status int + Headers http.Header + Body any - // Typed error from OpenAPI spec - encode directly - _ = json.NewEncoder(w).Encode(err) + contentType string } -// ServiceInterface defines the service interface for business logic. -type ServiceInterface interface { - PostHello(ctx context.Context, opts *PostHelloServiceRequestOptions) (*PostHelloResponseData, error) +// NewPostHelloResponseData returns the 200 response with its application/json body. +func NewPostHelloResponseData(body *PostHelloResponse200) *PostHelloResponseData { + return &PostHelloResponseData{Status: 200, Body: body, contentType: "application/json"} } -// HTTPAdapter adapts the ServiceInterface to HTTP handlers. -// This struct is generated and should not be modified. -type HTTPAdapter struct { - svc ServiceInterface - errHandler OapiErrorHandler - jsonBodyDecoder runtime.JSONBodyDecoderFunc +// WithStatus sets the status code. +func (r *PostHelloResponseData) WithStatus(code int) *PostHelloResponseData { + r.Status = code + return r } -// NewHTTPAdapter creates a new HTTPAdapter wrapping the given service. -// If errHandler is nil, OapiDefaultErrorHandler is used. -func NewHTTPAdapter(svc ServiceInterface, errHandler OapiErrorHandler) *HTTPAdapter { - if errHandler == nil { - errHandler = &OapiDefaultErrorHandler{} - } - return &HTTPAdapter{svc: svc, errHandler: errHandler, jsonBodyDecoder: runtime.DecodeJSONBody} +// WithHeaders sets the headers. +func (r *PostHelloResponseData) WithHeaders(h http.Header) *PostHelloResponseData { + r.Headers = h + return r } -// PostHello handles POST /hello -func (a *HTTPAdapter) PostHello(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - opts := &PostHelloServiceRequestOptions{} - opts.RawRequest = r - - // Parse query parameters - queryParams := &PostHelloQuery{} - query := r.URL.Query() - if queryParamQStr := query.Get("q"); queryParamQStr != "" { - queryParamQ := queryParamQStr - queryParams.Q = &queryParamQ - } - opts.Query = queryParams - // Parse request body - defer r.Body.Close() - var body PostHelloBody - switch err := a.jsonBodyDecoder(r.Body, &body); { - case errors.Is(err, runtime.ErrRequestBodyEmpty): - // requestBody is optional, so a request carrying none leaves opts.Body - // nil rather than failing. Decoding into the zero value instead would - // hand the validator a body nobody sent, and fail on its required fields. - case err != nil: - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindDecode, - OperationID: "PostHello", - Message: err.Error(), - Err: err, - }) - return - default: - opts.Body = &body - } - // Validate request - if err := opts.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "PostHello", - Message: err.Error(), - Err: err, - }) - return - } - - // Call business logic - resp, err := a.svc.PostHello(ctx, opts) - if err != nil { - code := http.StatusInternalServerError - if resp != nil && resp.Status != 0 { - code = resp.Status - } - a.errHandler.HandleError(w, r, code, err) - return - } +// StatusCode returns the status. +func (r *PostHelloResponseData) StatusCode() int { + return r.Status +} - // Validate response - if resp != nil && resp.Body != nil { - if v, ok := any(resp.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusInternalServerError, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "PostHello", - Message: fmt.Sprintf("response validation failed: %v", err), - Err: err, - }) - return - } - } - } +// Header returns the headers. +func (r *PostHelloResponseData) Header() http.Header { + return r.Headers +} - // Apply custom headers from response - if resp != nil && resp.Headers != nil { - for k, v := range resp.Headers { - for _, val := range v { - w.Header().Add(k, val) - } - } - } +// Payload returns the body. +func (r *PostHelloResponseData) Payload() any { + return r.Body +} - // Determine status code - status := 200 - if resp != nil && resp.Status != 0 { - status = resp.Status - } - if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", "application/json") - } - w.WriteHeader(status) - if resp != nil { - if payload := resp.Payload(); payload != nil { - _ = json.NewEncoder(w).Encode(payload) - } - } +// ContentType returns the media type of the body, empty for the default of its Go type. +func (r *PostHelloResponseData) ContentType() string { + return r.contentType } -// RouterOption is a function that configures the router. -type RouterOption func(*routerConfig) +// The error types the handlers use, as the runtime declares them. +type ( + ErrorKind = httpserver.ErrorKind + HandlerError = httpserver.HandlerError + ErrorHandler = httpserver.ErrorHandler + ErrorHandlerFunc = httpserver.ErrorHandlerFunc + DefaultErrorHandler = httpserver.DefaultErrorHandler +) -type routerConfig struct { - middlewares []func(http.Handler) http.Handler - errHandler OapiErrorHandler -} +// The kinds of HandlerError. +const ( + ErrorParse = httpserver.ErrorParse + ErrorDecode = httpserver.ErrorDecode + ErrorValidation = httpserver.ErrorValidation + ErrorService = httpserver.ErrorService + ErrorResponse = httpserver.ErrorResponse +) -// WithMiddleware adds middleware to the router. -func WithMiddleware(mw func(http.Handler) http.Handler) RouterOption { - return func(cfg *routerConfig) { - cfg.middlewares = append(cfg.middlewares, mw) - } +var bodyPresence = runtime.Presence{ + IsChecked: true, } -// WithErrorHandler sets a custom error handler for the router. -// If not set, OapiOapiDefaultErrorHandler is used. -func WithErrorHandler(h OapiErrorHandler) RouterOption { - return func(cfg *routerConfig) { - cfg.errHandler = h - } +// ServerOptions is what the adapter and the router are set up with. +type ServerOptions struct { + Router any + Middleware []func(http.Handler) http.Handler + OperationMiddleware []func(http.Handler) http.Handler + ErrorHandler httpserver.ErrorHandler + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) + MultipartMaxMemory int64 + Presence runtime.PresenceChecker } -// ServiceErrorHandler answers the failures raised before the service is reached: -// parameter parsing, body decoding and request validation. Those never return -// through the service, so no spec-declared error type describes them and -// error-mapping does not reach them. A service whose API has an error shape of -// its own replaces this at init. -var ServiceErrorHandler OapiErrorHandler = &OapiDefaultErrorHandler{} +// ServerOption sets one field of ServerOptions. +type ServerOption func(*ServerOptions) -// NewRouter creates a new chi.Router with the given service implementation. -func NewRouter(svc ServiceInterface, opts ...RouterOption) chi.Router { - cfg := &routerConfig{} - for _, opt := range opts { - opt(cfg) +// NewServerOptions applies opts to the defaults. +func NewServerOptions(opts ...ServerOption) *ServerOptions { + o := &ServerOptions{ + ErrorHandler: httpserver.DefaultErrorHandler{}, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, + MultipartMaxMemory: 33554432, + Presence: bodyPresence, } - - r := chi.NewRouter() - r.Use(api.ContextReplacementsMiddleware) - for _, mw := range cfg.middlewares { - r.Use(mw) + for _, opt := range opts { + opt(o) } - adapter := NewHTTPAdapter(svc, cfg.errHandler) - r.Method("POST", "/hello", http.HandlerFunc(adapter.PostHello)) - - return r + return o } -// ============================================================================ -// mockzilla Service Registration -// ============================================================================ - -//go:embed setup/config.yml -var configSrc []byte - -//go:embed setup/openapi.* -var openapiSpecFS embed.FS - -//go:embed setup/context.yml -var contextSrc []byte - -var cfg *config.ServiceConfig - -func init() { - var err error - cfg, err = config.NewServiceConfigFromBytes(configSrc) - if err != nil { - slog.Error("Failed to parse service config", "error", err) - return +// WithMiddleware wraps the routes with mw, outermost first; on a new router, unknown paths too. +func WithMiddleware(mw ...func(http.Handler) http.Handler) ServerOption { + return func(o *ServerOptions) { + o.Middleware = append(o.Middleware, mw...) } - loader.Register(cfg.Name, RegisterAPIRouter) } -// Register registers the service with the central router. -func RegisterAPIRouter(router *api.Router) { - serviceName := cfg.Name - - // Read OpenAPI spec from embedded FS - openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) - if err != nil { - slog.Error(fmt.Sprintf("Failed to read OpenAPI spec for %s", serviceName), - "error", err, - "service", serviceName, - ) - return +// WithOperationMiddleware wraps each operation with mw, once its name is on the context. +func WithOperationMiddleware(mw ...func(http.Handler) http.Handler) ServerOption { + return func(o *ServerOptions) { + o.OperationMiddleware = append(o.OperationMiddleware, mw...) } +} - registry, err := typedef.NewRegistry(openapiSpec, typedef.RegistryOptions{ - SpecOptions: cfg.SpecOptions, - // The served request path reads converted operations only, so the - // parsed document does not need to stay resident. - ReleaseDocument: true, - }) - if err != nil { - slog.Error(fmt.Sprintf("Failed to create registry for %s", serviceName), - "error", err, - "service", serviceName, - ) - return +// WithErrorHandler sets what writes the response of a failed request. +func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { + return func(o *ServerOptions) { + o.ErrorHandler = h } +} - // Create the generator with service contexts - orderedCtx := router.ServiceContext(serviceName, contextSrc) - gen, err := generator.NewGenerator(orderedCtx, router.GetContexts()) - if err != nil { - slog.Error(fmt.Sprintf("Failed to create generator for %s", serviceName), - "error", err, - "service", serviceName, - ) - return +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { + return func(o *ServerOptions) { + o.JSONDecoder = decode } - - // Register with mockzilla using handler factory - router.RegisterHTTPHandler(cfg, func(serviceDB db.DB) api.Handler { - userSvc := newService(&api.ServiceParams{ - AppConfig: router.Config(), - ServiceConfig: cfg, - DB: serviceDB, - }) - genSvc := &generatorService{service: userSvc, generator: gen, registry: registry} - return newServiceHandler(genSvc, gen, registry) - }) - - slog.Info(fmt.Sprintf("Registered %s service", serviceName), - "service", serviceName, - ) } -// ============================================================================ -// Service Handler -// ============================================================================ - -// serviceHandler wraps the chi router and service to implement api.Handler. -type serviceHandler struct { - router chi.Router - service ServiceInterface - gen generator.Generate - registry typedef.OperationRegistry +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } } -// newServiceHandler creates a new serviceHandler. -func newServiceHandler(svc ServiceInterface, gen generator.Generate, registry typedef.OperationRegistry) api.Handler { - return &serviceHandler{ - router: NewRouter(svc, WithErrorHandler(ServiceErrorHandler)), - service: svc, - gen: gen, - registry: registry, +// WithMultipartMaxMemory sets how much of a multipart form stays in memory. +func WithMultipartMaxMemory(n int64) ServerOption { + return func(o *ServerOptions) { + o.MultipartMaxMemory = n } } -func (h *serviceHandler) Routes() api.RouteDescriptions { - routes := h.router.Routes() - descriptions := make(api.RouteDescriptions, 0, len(routes)) - for _, route := range routes { - for method := range route.Handlers { - descriptions = append(descriptions, &api.RouteDescription{ - Method: method, - Path: route.Pattern, - }) - } +// WithPresence sets what checks the keys of request bodies and fills their defaults. +func WithPresence(p runtime.PresenceChecker) ServerOption { + return func(o *ServerOptions) { + o.Presence = p } - return descriptions } -func (h *serviceHandler) RegisterRoutes(router chi.Router) { - router.Mount("/", h.router) +// HTTPAdapter answers HTTP requests by calling the service. +type HTTPAdapter struct { + svc ServiceInterface + opts *ServerOptions + operations httpserver.Chain } -func (h *serviceHandler) Generate(w http.ResponseWriter, r *http.Request) { - var req api.GenerateRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - message := err.Error() - if errors.Is(err, io.EOF) { - message = "request body is empty or incomplete" - } - slog.Error("Failed to decode request", "error", err) - http.Error(w, message, http.StatusBadRequest) - return - } - - op := h.registry.FindOperation(req.Path, req.Method) - res := h.gen.Request(&req, op, req.Context) - api.NewJSONResponse(w).Send(res) +type responseData interface { + StatusCode() int + Header() http.Header + Payload() any + ContentType() string } -// ============================================================================ -// Generator Service (fallback to mock responses) -// ============================================================================ - -// generatorService implements ServiceInterface with generator fallback. -// It delegates to the user's service first; if that returns nil, it generates a mock response. -type generatorService struct { - service *service - generator generator.Generate - registry typedef.OperationRegistry +// NewHTTPAdapter returns the adapter of svc. +func NewHTTPAdapter(svc ServiceInterface, opts ...ServerOption) *HTTPAdapter { + o := NewServerOptions(opts...) + return &HTTPAdapter{svc: svc, opts: o, operations: httpserver.NewChain(o.OperationMiddleware...)} } -// Ensure generatorService implements ServiceInterface. -var _ ServiceInterface = (*generatorService)(nil) +// PostHello handles POST /hello. +func (a *HTTPAdapter) PostHello(w http.ResponseWriter, r *http.Request) { + a.operations.Serve(w, r, "PostHello", a.servePostHello) +} -// PostHello handles POST /hello -func (s *generatorService) PostHello(ctx context.Context, opts *PostHelloServiceRequestOptions) (*PostHelloResponseData, error) { - // Inject GenerateResponse so user service can call it - opts.GenerateResponse = func() (*PostHelloResponseData, error) { - respSchema := s.registry.GetResponseSchema("/hello", "POST") - if respSchema == nil { - return NewPostHelloResponseData(nil), nil +func (a *HTTPAdapter) servePostHello(w http.ResponseWriter, r *http.Request) { + opts := &PostHelloServiceRequestOptions{RawRequest: r} + query := runtime.ParseQuery(r.URL.RawQuery) + opts.Query = &PostHelloQuery{} + if err := runtime.DecodeQuery(query, runtime.Param{Name: "q", Style: runtime.StyleForm, IsExplode: true, IsRequired: false, IsJSON: false}, &opts.Query.Q); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorParse, OperationID: "PostHello", ParamName: "q", ParamLocation: "query", Err: err}) + return + } + switch contentType := runtime.ContentType(r.Header); contentType { + case "application/json": + body, err := a.opts.Presence.JSON(r.Body, runtime.Prop{}) + if err != nil { + a.failBody(w, r, "PostHello", err) + return } - res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(api.RequestFromGoContext(ctx))) - var body PostHelloResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err + if err = runtime.DecodeJSON(body, &opts.Body, false, a.opts.JSONDecoder); err != nil { + a.failDecode(w, r, "PostHello", err) + return } - return NewPostHelloResponseData(&body).WithHeaders(res.Headers), nil + case "": + default: + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorDecode, OperationID: "PostHello", Status: http.StatusUnsupportedMediaType, Err: runtime.ContentTypeError(contentType)}) + return } - - // Call user's service - if resp, err := s.service.PostHello(ctx, opts); resp != nil || err != nil { - return resp, err + if err := opts.Validate(); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorValidation, OperationID: "PostHello", Err: err}) + return } - // Fallback to generator - return opts.GenerateResponse() -} - -// ============================================================================ -// Factory (programmatic request/response generation) -// ============================================================================ - -var ( - defaultFactory *factory.Factory - defaultFactoryOnce sync.Once - defaultFactoryErr error -) - -// NewFactory creates a Factory pre-configured with this service's OpenAPI spec, -// spec options, and context. -// Use it to generate mock requests and responses programmatically without running the server. -func NewFactory(opts ...factory.FactoryOption) (*factory.Factory, error) { - openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) + res, err := a.svc.PostHello(r.Context(), opts) if err != nil { - return nil, err - } - - allOpts := []factory.FactoryOption{ - factory.WithServiceContext(contextSrc), + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "PostHello", Err: err}) + return } - if cfg != nil && cfg.SpecOptions != nil { - allOpts = append(allOpts, factory.WithSpecOptions(cfg.SpecOptions)) + if res == nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "PostHello", Err: httpserver.ErrNoResponse}) + return } - allOpts = append(allOpts, opts...) - return factory.NewFactory(openapiSpec, allOpts...) + a.write(w, r, "PostHello", res) } -// GetFactory returns a singleton Factory instance. -// The factory is initialized on first call and reused for subsequent calls. -func GetFactory() (*factory.Factory, error) { - defaultFactoryOnce.Do(func() { - defaultFactory, defaultFactoryErr = NewFactory() - }) - return defaultFactory, defaultFactoryErr +func (a *HTTPAdapter) fail(w http.ResponseWriter, r *http.Request, err *httpserver.HandlerError) { + // A response that failed to write leaves its media type, which is not the error's. + w.Header().Del("Content-Type") + a.opts.ErrorHandler.HandleError(w, r, err.StatusCode(), err) } -// GeneratePostHelloResponse generates a mock response for POST /hello. -// ctx is an optional replacement context for controlling generated values. -func GeneratePostHelloResponse(ctx map[string]any) (*PostHelloResponseData, error) { - f, err := GetFactory() - if err != nil { - return nil, err - } - res, err := f.Response("/hello", "POST", ctx) - if err != nil { - return nil, err - } - var body PostHelloResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewPostHelloResponseData(&body).WithHeaders(res.Headers), nil +func (a *HTTPAdapter) failDecode(w http.ResponseWriter, r *http.Request, id string, err error) { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorDecode, OperationID: id, Err: err}) } -// GeneratePostHelloResponseBody generates a mock response body for POST /hello. -// Returns just the typed body without headers or status. -// ctx is an optional replacement context for controlling generated values. -func GeneratePostHelloResponseBody(ctx map[string]any) (*PostHelloResponse, error) { - res, err := GeneratePostHelloResponse(ctx) - if err != nil { - return nil, err +func (a *HTTPAdapter) failBody(w http.ResponseWriter, r *http.Request, id string, err error) { + kind := httpserver.ErrorDecode + if validation.Failed(err) { + kind = httpserver.ErrorValidation } - return res.Body, nil + a.fail(w, r, &httpserver.HandlerError{Kind: kind, OperationID: id, Err: err}) } -// GeneratePostHelloRequest generates a mock request for POST /hello. -// ctx is an optional replacement context for controlling generated values. -func GeneratePostHelloRequest(ctx map[string]any) (schema.GeneratedRequest, error) { - f, err := GetFactory() - if err != nil { - return schema.GeneratedRequest{}, err +func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, res responseData) { + if err := httpserver.ValidateResponse(res.Payload()); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) + return } - return f.Request("/hello", "POST", ctx) -} - -// GeneratePostHelloRequestBody generates a typed mock request body for POST /hello. -// ctx is an optional replacement context for controlling generated values. -func GeneratePostHelloRequestBody(ctx map[string]any) (*PostHelloBody, error) { - req, err := GeneratePostHelloRequest(ctx) - if err != nil { - return nil, err + if res.ContentType() != "" { + w.Header().Set("Content-Type", res.ContentType()) } - var body PostHelloBody - if err := json.Unmarshal(req.Body, &body); err != nil { - return nil, err + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) + switch { + case errors.Is(err, runtime.ErrContentType): + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) + case err != nil: + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: id, Err: err}) } - return &body, nil -} - -type PostHelloBody map[string]any - -type PostHelloQuery struct { - Q *string `json:"q,omitempty"` -} - -// PostHelloResponseData wraps the success response with optional headers and status override. -type PostHelloResponseData struct { - Body *PostHelloResponse - Headers http.Header - Status int // 0 = use default (200) } -// NewPostHelloResponseData creates a new PostHelloResponseData with the given body. -func NewPostHelloResponseData(body *PostHelloResponse) *PostHelloResponseData { - return &PostHelloResponseData{Body: body} -} - -// WithHeaders sets custom headers on the response. -func (r *PostHelloResponseData) WithHeaders(h http.Header) *PostHelloResponseData { - r.Headers = h - return r -} - -// WithStatus overrides the default status code. -func (r *PostHelloResponseData) WithStatus(code int) *PostHelloResponseData { - r.Status = code - return r -} - -// Payload returns the value to be JSON-encoded as the response body. Override -// this template to customize the encoded shape (e.g. to wrap it in an envelope) -// without needing to change the adapter template. -func (r *PostHelloResponseData) Payload() any { - if r.Body == nil { - return nil +// WithRouter registers the routes on r instead of a new router. +func WithRouter(r chi.Router) ServerOption { + return func(o *ServerOptions) { + o.Router = r } - return r.Body -} - -type PostHelloResponse struct { - Hellow *string `json:"hellow,omitempty"` -} - -// PostHelloServiceRequestOptions holds all parameters for the PostHello operation. -type PostHelloServiceRequestOptions struct { - Query *PostHelloQuery - Body *PostHelloBody - // RawRequest provides access to the underlying HTTP request for custom content type handling. - RawRequest *http.Request - // GenerateResponse generates a sample response with random data satisfying the OpenAPI schema. - GenerateResponse func() (*PostHelloResponseData, error) } -// Validate validates all the fields in the options. -func (o *PostHelloServiceRequestOptions) Validate() error { - var errors runtime.ValidationErrors - - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) - } - } +// NewRouter registers every operation on a chi router. +func NewRouter(svc ServiceInterface, opts ...ServerOption) chi.Router { + o := NewServerOptions(opts...) + a := NewHTTPAdapter(svc, opts...) + register := func(r chi.Router) { + r.Post("/hello", a.PostHello) } - if o.Body != nil { - if v, ok := any(o.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("Body", err) - } - } - } - if len(errors) == 0 { - return nil + router, _ := o.Router.(chi.Router) + if router == nil { + router = chi.NewRouter() + router.Use(o.Middleware...) + register(router) + return router } - - return errors -} - -var typesValidator *validator.Validate - -func init() { - typesValidator = validator.New(validator.WithRequiredStructEnabled()) - runtime.RegisterCustomTypeFunc(typesValidator) + router.Group(func(r chi.Router) { + r.Use(o.Middleware...) + register(r) + }) + return router } diff --git a/pkg/hello_world/register.go b/pkg/hello_world/register.go new file mode 100644 index 0000000..2481a1b --- /dev/null +++ b/pkg/hello_world/register.go @@ -0,0 +1,253 @@ +// Code generated by mockzilla-codegen. DO NOT EDIT. + +package hello_world + +import ( + "context" + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "sync" + + chi "github.com/go-chi/chi/v5" + "github.com/mockzilla/mockzilla/v2/pkg/api" + "github.com/mockzilla/mockzilla/v2/pkg/config" + "github.com/mockzilla/mockzilla/v2/pkg/db" + "github.com/mockzilla/mockzilla/v2/pkg/factory" + "github.com/mockzilla/mockzilla/v2/pkg/generator" + "github.com/mockzilla/mockzilla/v2/pkg/loader" + "github.com/mockzilla/mockzilla/v2/pkg/schema" + "github.com/mockzilla/mockzilla/v2/pkg/typedef" +) + +//go:embed setup/config.yml +var configSrc []byte + +//go:embed setup/openapi.* +var openapiSpecFS embed.FS + +//go:embed setup/context.yml +var contextSrc []byte + +// ServiceErrorHandler answers the requests that fail before the service is reached. +var ServiceErrorHandler ErrorHandler = DefaultErrorHandler{} + +// cfg is set before any init function runs, so the package's other files can read it in theirs. +var cfg, cfgErr = config.NewServiceConfigFromBytes(configSrc) + +func init() { + if cfgErr != nil { + slog.Error("Failed to parse service config", "error", cfgErr) + return + } + loader.Register(cfg.Name, RegisterAPIRouter) +} + +// RegisterAPIRouter registers the service with the central router. +func RegisterAPIRouter(router *api.Router) { + serviceName := cfg.Name + + openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) + if err != nil { + slog.Error(fmt.Sprintf("Failed to read OpenAPI spec for %s", serviceName), "error", err, "service", serviceName) + return + } + + registry, err := typedef.NewRegistry(openapiSpec, typedef.RegistryOptions{ + SpecOptions: cfg.SpecOptions, + ReleaseDocument: true, + }) + if err != nil { + slog.Error(fmt.Sprintf("Failed to create registry for %s", serviceName), "error", err, "service", serviceName) + return + } + + orderedCtx := router.ServiceContext(serviceName, contextSrc) + gen, err := generator.NewGenerator(orderedCtx, router.GetContexts()) + if err != nil { + slog.Error(fmt.Sprintf("Failed to create generator for %s", serviceName), "error", err, "service", serviceName) + return + } + + router.RegisterHTTPHandler(cfg, func(serviceDB db.DB) api.Handler { + userSvc := newService(&api.ServiceParams{ + AppConfig: router.Config(), + ServiceConfig: cfg, + DB: serviceDB, + }) + genSvc := &generatorService{service: userSvc, generator: gen, registry: registry} + return newServiceHandler(genSvc, gen, registry) + }) + + slog.Info(fmt.Sprintf("Registered %s service", serviceName), "service", serviceName) +} + +type serviceHandler struct { + router chi.Router + gen generator.Generate + registry typedef.OperationRegistry +} + +func newServiceHandler(svc ServiceInterface, gen generator.Generate, registry typedef.OperationRegistry) api.Handler { + return &serviceHandler{ + router: NewRouter(svc, + WithMiddleware(api.ContextReplacementsMiddleware), + WithErrorHandler(returnedErrors{next: ServiceErrorHandler}), + ), + gen: gen, + registry: registry, + } +} + +func (h *serviceHandler) Routes() api.RouteDescriptions { + routes := h.router.Routes() + descriptions := make(api.RouteDescriptions, 0, len(routes)) + for _, route := range routes { + for method := range route.Handlers { + descriptions = append(descriptions, &api.RouteDescription{Method: method, Path: route.Pattern}) + } + } + return descriptions +} + +func (h *serviceHandler) RegisterRoutes(router chi.Router) { + router.Mount("/", h.router) +} + +func (h *serviceHandler) Generate(w http.ResponseWriter, r *http.Request) { + var req api.GenerateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + message := err.Error() + if errors.Is(err, io.EOF) { + message = "request body is empty or incomplete" + } + slog.Error("Failed to decode request", "error", err) + http.Error(w, message, http.StatusBadRequest) + return + } + + op := h.registry.FindOperation(req.Path, req.Method) + res := h.gen.Request(&req, op, req.Context) + api.NewJSONResponse(w).Send(res) +} + +// returnedError is an error the service returned, with the status of the response it returned too. +// It has no Unwrap, so the adapter cannot answer an error type of the spec before returnedErrors. +type returnedError struct { + status int + err error +} + +func (e *returnedError) Error() string { + return e.err.Error() +} + +// returnedErrors hands next an error the service returned as it was returned, with the status of +// its response, so a service answers an error with the status it sets. +type returnedErrors struct { + next ErrorHandler +} + +func (h returnedErrors) HandleError(w http.ResponseWriter, r *http.Request, status int, err error) { + var returned *returnedError + if errors.As(err, &returned) { + if returned.status != 0 { + status = returned.status + } + err = returned.err + } + h.next.HandleError(w, r, status, err) +} + +// generatorService calls the service first and makes a mock response when it returns nil, nil. +type generatorService struct { + service *service + generator generator.Generate + registry typedef.OperationRegistry +} + +var _ ServiceInterface = (*generatorService)(nil) + +func (s *generatorService) PostHello(ctx context.Context, opts *PostHelloServiceRequestOptions) (*PostHelloResponseData, error) { + opts.GenerateResponse = func() (*PostHelloResponseData, error) { + respSchema := s.registry.GetResponseSchema("/hello", "POST") + res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(opts.RawRequest)) + var body PostHelloResponse200 + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewPostHelloResponseData(&body).WithHeaders(res.Headers), nil + } + + resp, err := s.service.PostHello(ctx, opts) + if err != nil { + status := 0 + if resp != nil { + status = resp.Status + } + return nil, &returnedError{status: status, err: err} + } + if resp != nil { + return resp, nil + } + return opts.GenerateResponse() +} + +var ( + defaultFactory *factory.Factory + defaultFactoryOnce sync.Once + defaultFactoryErr error +) + +// NewFactory returns a Factory with the spec, spec options and context of this service. +func NewFactory(opts ...factory.FactoryOption) (*factory.Factory, error) { + openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) + if err != nil { + return nil, err + } + + allOpts := []factory.FactoryOption{factory.WithServiceContext(contextSrc)} + if cfg != nil && cfg.SpecOptions != nil { + allOpts = append(allOpts, factory.WithSpecOptions(cfg.SpecOptions)) + } + allOpts = append(allOpts, opts...) + return factory.NewFactory(openapiSpec, allOpts...) +} + +// GetFactory returns the Factory of this service, made on the first call. +func GetFactory() (*factory.Factory, error) { + defaultFactoryOnce.Do(func() { + defaultFactory, defaultFactoryErr = NewFactory() + }) + return defaultFactory, defaultFactoryErr +} + +// GeneratePostHelloResponse makes a mock response for POST /hello. +func GeneratePostHelloResponse(ctx map[string]any) (*PostHelloResponseData, error) { + f, err := GetFactory() + if err != nil { + return nil, err + } + res, err := f.Response("/hello", "POST", ctx) + if err != nil { + return nil, err + } + var body PostHelloResponse200 + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewPostHelloResponseData(&body).WithHeaders(res.Headers), nil +} + +// GeneratePostHelloRequest makes a mock request for POST /hello. +func GeneratePostHelloRequest(ctx map[string]any) (schema.GeneratedRequest, error) { + f, err := GetFactory() + if err != nil { + return schema.GeneratedRequest{}, err + } + return f.Request("/hello", "POST", ctx) +} diff --git a/pkg/hello_world/service.go b/pkg/hello_world/service.go index 5da6214..b141612 100644 --- a/pkg/hello_world/service.go +++ b/pkg/hello_world/service.go @@ -1,6 +1,5 @@ -// Package hello_world This file is generated ONCE as a starting point and will NOT be overwritten. -// Modify it freely to add your business logic. -// To regenerate, delete this file or set generate.handler.output.overwrite: true in config. +// Code generated by mockzilla-codegen. DO NOT EDIT. + package hello_world import ( @@ -22,9 +21,8 @@ func newService(params *api.ServiceParams) *service { return &service{params: params} } -// PostHello handles POST /hello +// PostHello handles POST /hello. func (s *service) PostHello(ctx context.Context, opts *PostHelloServiceRequestOptions) (*PostHelloResponseData, error) { - // TODO: Implement your business logic here. // Return nil, nil to use the generated mock response. return nil, nil } diff --git a/pkg/hello_world/setup/codegen.yml b/pkg/hello_world/setup/codegen.yml index 0203472..ca6ae86 100644 --- a/pkg/hello_world/setup/codegen.yml +++ b/pkg/hello_world/setup/codegen.yml @@ -1,22 +1,18 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/doordash-oss/oapi-codegen-dd/main/configuration-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/mockzilla/mockzilla-codegen/main/config.schema.json package: hello_world -output: - use-single-file: true - skip-fmt: true - directory: ../ -generate: - omit-description: true - default-int-type: int64 - always-prefix-enum-values: true +models: + int-type: int64 + descriptions: false + validation: + response: true +server: + framework: chi validation: + request: true response: true - handler: - kind: chi - output: - overwrite: true - service: {} - # middleware: {} - validation: - request: true - response: true -error-mapping: + scaffold: + service: ../service.go + # middleware: ../middleware.go + overwrite: true +output: + file: ../gen.go diff --git a/pkg/petstore/gen.go b/pkg/petstore/gen.go index 7a779e2..a70dcca 100644 --- a/pkg/petstore/gen.go +++ b/pkg/petstore/gen.go @@ -1,1303 +1,646 @@ -// Code generated by oapi-codegen. DO NOT EDIT. +// Code generated by mockzilla-codegen. DO NOT EDIT. package petstore import ( "context" - "embed" "encoding/json" "errors" - "fmt" - "io" - "log/slog" "net/http" - "sync" - - "github.com/doordash-oss/oapi-codegen-dd/v3/pkg/runtime" - "github.com/go-chi/chi/v5" - "github.com/go-playground/validator/v10" - "github.com/mockzilla/mockzilla/v2/pkg/api" - "github.com/mockzilla/mockzilla/v2/pkg/config" - "github.com/mockzilla/mockzilla/v2/pkg/db" - "github.com/mockzilla/mockzilla/v2/pkg/factory" - "github.com/mockzilla/mockzilla/v2/pkg/generator" - "github.com/mockzilla/mockzilla/v2/pkg/loader" - "github.com/mockzilla/mockzilla/v2/pkg/schema" - "github.com/mockzilla/mockzilla/v2/pkg/typedef" + chi "github.com/go-chi/chi/v5" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime/httpserver" + "github.com/mockzilla/mockzilla-codegen/pkg/runtime/validation" ) -// OapiErrorKind represents the type of error that occurred during request processing. -type OapiErrorKind int +// Fails to compile when the runtime does not match the generator that wrote this file. +const _ = runtime.SupportsGeneratorV1 -const ( - // OapiErrorKindParse indicates a parameter parsing error (invalid path/query/header parameter). - OapiErrorKindParse OapiErrorKind = iota - - // OapiErrorKindDecode indicates a request body decoding error (invalid JSON, form data, etc.). - OapiErrorKindDecode - - // OapiErrorKindValidation indicates a request validation error (failed schema validation). - OapiErrorKindValidation - - // OapiErrorKindService indicates a service/business logic error returned by the service implementation. - OapiErrorKindService -) - -// OapiHandlerError represents an error that occurred during request handling (parse, decode, validation). -// When no typed error response is configured in the OpenAPI spec, this error type is used. -// Custom error handlers can type-assert to this type to access error details. -type OapiHandlerError struct { - Kind OapiErrorKind - OperationID string - Message string - ParamName string - ParamLocation string - - // Err is the error that caused this handler error. - Err error `json:"-"` +type Pet struct { + Name string `json:"name"` + Tag *string `json:"tag,omitempty"` + ID int64 `json:"id"` } -func (e OapiHandlerError) Error() string { - return e.Message +type NewPet struct { + Name string `json:"name"` + Tag *string `json:"tag,omitempty"` } -// Unwrap returns the underlying error, enabling errors.Is and errors.As. -func (e OapiHandlerError) Unwrap() error { - return e.Err +type Error struct { + Code int32 `json:"code"` + Message string `json:"message"` } -// OapiErrorResponse is the default JSON error response structure used by OapiDefaultErrorHandler. -type OapiErrorResponse struct { - Error string `json:"error"` - OperationID string `json:"operation_id,omitempty"` - ParamName string `json:"param_name,omitempty"` - ParamLocation string `json:"param_location,omitempty"` +type FindPetsQuery struct { + Tags []string `json:"tags,omitzero"` + Limit *int32 `json:"limit,omitempty"` } -// OapiErrorHandler handles errors that occur during request processing. -// Implement this interface to customize error responses, logging, and metrics. -type OapiErrorHandler interface { - // HandleError writes an error response to w with the given status code. - // The err is either an OapiHandlerError (for parse/decode/validation errors) - // or a typed error matching the OpenAPI spec's error response schema. - HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) +type FindPetByIDPathParams struct { + ID int64 `json:"id"` } -// OapiDefaultErrorHandler provides the default error handling behavior. -// It writes JSON error responses. For OapiHandlerError, it uses OapiErrorResponse. -// For typed errors (from OpenAPI spec), it encodes them directly. -type OapiDefaultErrorHandler struct{} - -// HandleError implements OapiErrorHandler with default JSON error responses. -func (h *OapiDefaultErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - - if handlerErr, ok := err.(OapiHandlerError); ok { - _ = json.NewEncoder(w).Encode(OapiErrorResponse{ - Error: handlerErr.Message, - OperationID: handlerErr.OperationID, - ParamName: handlerErr.ParamName, - ParamLocation: handlerErr.ParamLocation, - }) - return - } - - // Typed error from OpenAPI spec - encode directly - _ = json.NewEncoder(w).Encode(err) +type DeletePetPathParams struct { + ID int64 `json:"id"` } -// ServiceInterface defines the service interface for business logic. +type FindPetsResponse200 []Pet + +// ServiceInterface is what the generated handlers call. Implement it with the business logic. type ServiceInterface interface { + // FindPets handles GET /pets. FindPets(ctx context.Context, opts *FindPetsServiceRequestOptions) (*FindPetsResponseData, error) - + // AddPet handles POST /pets. AddPet(ctx context.Context, opts *AddPetServiceRequestOptions) (*AddPetResponseData, error) - + // FindPetByID handles GET /pets/{id}. FindPetByID(ctx context.Context, opts *FindPetByIDServiceRequestOptions) (*FindPetByIDResponseData, error) - + // DeletePet handles DELETE /pets/{id}. DeletePet(ctx context.Context, opts *DeletePetServiceRequestOptions) (*DeletePetResponseData, error) } -// HTTPAdapter adapts the ServiceInterface to HTTP handlers. -// This struct is generated and should not be modified. -type HTTPAdapter struct { - svc ServiceInterface - errHandler OapiErrorHandler - jsonBodyDecoder runtime.JSONBodyDecoderFunc -} - -// NewHTTPAdapter creates a new HTTPAdapter wrapping the given service. -// If errHandler is nil, OapiDefaultErrorHandler is used. -func NewHTTPAdapter(svc ServiceInterface, errHandler OapiErrorHandler) *HTTPAdapter { - if errHandler == nil { - errHandler = &OapiDefaultErrorHandler{} - } - return &HTTPAdapter{svc: svc, errHandler: errHandler, jsonBodyDecoder: runtime.DecodeJSONBody} -} - -// FindPets handles GET /pets -func (a *HTTPAdapter) FindPets(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - opts := &FindPetsServiceRequestOptions{} - opts.RawRequest = r - - // Parse query parameters - queryParams := &FindPetsQuery{} - query := r.URL.Query() - - if values, ok := query["tags"]; ok { - queryParams.Tags = values - } - if queryParamLimitStr := query.Get("limit"); queryParamLimitStr != "" { - queryParamLimit, err := runtime.ParseString[int32](queryParamLimitStr, "int32") - if err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindParse, - OperationID: "FindPets", - Message: err.Error(), - ParamName: "limit", - ParamLocation: "query", - Err: err, - }) - return - } - queryParams.Limit = &queryParamLimit - } - opts.Query = queryParams - // Validate request - if err := opts.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "FindPets", - Message: err.Error(), - Err: err, - }) - return - } - - // Call business logic - resp, err := a.svc.FindPets(ctx, opts) - if err != nil { - code := http.StatusInternalServerError - if resp != nil && resp.Status != 0 { - code = resp.Status - } - a.errHandler.HandleError(w, r, code, err) - return - } - - // Validate response - if resp != nil && resp.Body != nil { - if v, ok := any(resp.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusInternalServerError, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "FindPets", - Message: fmt.Sprintf("response validation failed: %v", err), - Err: err, - }) - return - } - } - } - - // Apply custom headers from response - if resp != nil && resp.Headers != nil { - for k, v := range resp.Headers { - for _, val := range v { - w.Header().Add(k, val) - } - } - } - - // Determine status code - status := 200 - if resp != nil && resp.Status != 0 { - status = resp.Status - } - if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", "application/json") - } - w.WriteHeader(status) - if resp != nil { - if payload := resp.Payload(); payload != nil { - _ = json.NewEncoder(w).Encode(payload) - } - } -} - -// AddPet handles POST /pets -func (a *HTTPAdapter) AddPet(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - opts := &AddPetServiceRequestOptions{} - opts.RawRequest = r - - // Parse request body - defer r.Body.Close() - var body AddPetBody - if err := a.jsonBodyDecoder(r.Body, &body); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindDecode, - OperationID: "AddPet", - Message: err.Error(), - Err: err, - }) - return - } - opts.Body = &body - // Validate request - if err := opts.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "AddPet", - Message: err.Error(), - Err: err, - }) - return - } - - // Call business logic - resp, err := a.svc.AddPet(ctx, opts) - if err != nil { - code := http.StatusInternalServerError - if resp != nil && resp.Status != 0 { - code = resp.Status - } - a.errHandler.HandleError(w, r, code, err) - return - } - - // Validate response - if resp != nil && resp.Body != nil { - if v, ok := any(resp.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusInternalServerError, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "AddPet", - Message: fmt.Sprintf("response validation failed: %v", err), - Err: err, - }) - return - } - } - } - - // Apply custom headers from response - if resp != nil && resp.Headers != nil { - for k, v := range resp.Headers { - for _, val := range v { - w.Header().Add(k, val) - } - } - } - - // Determine status code - status := 200 - if resp != nil && resp.Status != 0 { - status = resp.Status - } - if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", "application/json") - } - w.WriteHeader(status) - if resp != nil { - if payload := resp.Payload(); payload != nil { - _ = json.NewEncoder(w).Encode(payload) - } - } +// FindPetsServiceRequestOptions is what FindPets receives. +type FindPetsServiceRequestOptions struct { + Query *FindPetsQuery + // GenerateResponse makes a mock response that satisfies the spec. + GenerateResponse func() (*FindPetsResponseData, error) + RawRequest *http.Request } -// FindPetByID handles GET /pets/{id} -func (a *HTTPAdapter) FindPetByID(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - opts := &FindPetByIDServiceRequestOptions{} - opts.RawRequest = r - - // Parse path parameters - pathParams := &FindPetByIDPath{} - pathParamIDStr := chi.URLParam(r, "id") - - pathParamID, err := runtime.ParseString[int64](pathParamIDStr, "int64") - if err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindParse, - OperationID: "FindPetByID", - Message: err.Error(), - ParamName: "id", - ParamLocation: "path", - Err: err, - }) - return - } - pathParams.ID = pathParamID - opts.PathParams = pathParams - // Validate request - if err := opts.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "FindPetByID", - Message: err.Error(), - Err: err, - }) - return - } - - // Call business logic - resp, err := a.svc.FindPetByID(ctx, opts) - if err != nil { - code := http.StatusInternalServerError - if resp != nil && resp.Status != 0 { - code = resp.Status - } - a.errHandler.HandleError(w, r, code, err) - return - } - - // Validate response - if resp != nil && resp.Body != nil { - if v, ok := any(resp.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusInternalServerError, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "FindPetByID", - Message: fmt.Sprintf("response validation failed: %v", err), - Err: err, - }) - return - } - } - } - - // Apply custom headers from response - if resp != nil && resp.Headers != nil { - for k, v := range resp.Headers { - for _, val := range v { - w.Header().Add(k, val) - } - } - } - - // Determine status code - status := 200 - if resp != nil && resp.Status != 0 { - status = resp.Status - } - if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", "application/json") - } - w.WriteHeader(status) - if resp != nil { - if payload := resp.Payload(); payload != nil { - _ = json.NewEncoder(w).Encode(payload) - } - } +// Validate checks the parameters and the body against the constraints of the spec. +func (o *FindPetsServiceRequestOptions) Validate() error { + return nil } -// DeletePet handles DELETE /pets/{id} -func (a *HTTPAdapter) DeletePet(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - opts := &DeletePetServiceRequestOptions{} - opts.RawRequest = r - - // Parse path parameters - pathParams := &DeletePetPath{} - pathParamIDStr := chi.URLParam(r, "id") - - pathParamID, err := runtime.ParseString[int64](pathParamIDStr, "int64") - if err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindParse, - OperationID: "DeletePet", - Message: err.Error(), - ParamName: "id", - ParamLocation: "path", - Err: err, - }) - return - } - pathParams.ID = pathParamID - opts.PathParams = pathParams - // Validate request - if err := opts.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusBadRequest, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "DeletePet", - Message: err.Error(), - Err: err, - }) - return - } - - // Call business logic - resp, err := a.svc.DeletePet(ctx, opts) - if err != nil { - code := http.StatusInternalServerError - if resp != nil && resp.Status != 0 { - code = resp.Status - } - a.errHandler.HandleError(w, r, code, err) - return - } - - // Validate response - if resp != nil && resp.Body != nil { - if v, ok := any(resp.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - a.errHandler.HandleError(w, r, http.StatusInternalServerError, OapiHandlerError{ - Kind: OapiErrorKindValidation, - OperationID: "DeletePet", - Message: fmt.Sprintf("response validation failed: %v", err), - Err: err, - }) - return - } - } - } - - // Apply custom headers from response - if resp != nil && resp.Headers != nil { - for k, v := range resp.Headers { - for _, val := range v { - w.Header().Add(k, val) - } - } - } +// FindPetsResponseData is what FindPets returns. +type FindPetsResponseData struct { + Status int + Headers http.Header + Body any - // Determine status code - status := 204 - if resp != nil && resp.Status != 0 { - status = resp.Status - } - w.WriteHeader(status) + contentType string } -// RouterOption is a function that configures the router. -type RouterOption func(*routerConfig) - -type routerConfig struct { - middlewares []func(http.Handler) http.Handler - errHandler OapiErrorHandler +// NewFindPetsResponseData200 returns the 200 response with its application/json body. +func NewFindPetsResponseData200(body FindPetsResponse200) *FindPetsResponseData { + return &FindPetsResponseData{Status: 200, Body: body, contentType: "application/json"} } -// WithMiddleware adds middleware to the router. -func WithMiddleware(mw func(http.Handler) http.Handler) RouterOption { - return func(cfg *routerConfig) { - cfg.middlewares = append(cfg.middlewares, mw) - } +// NewFindPetsResponseDataDefault returns the default response with its application/json body. +func NewFindPetsResponseDataDefault(status int, body *Error) *FindPetsResponseData { + return &FindPetsResponseData{Status: status, Body: body, contentType: "application/json"} } -// WithErrorHandler sets a custom error handler for the router. -// If not set, OapiOapiDefaultErrorHandler is used. -func WithErrorHandler(h OapiErrorHandler) RouterOption { - return func(cfg *routerConfig) { - cfg.errHandler = h - } +// WithStatus sets the status code. +func (r *FindPetsResponseData) WithStatus(code int) *FindPetsResponseData { + r.Status = code + return r } -// ServiceErrorHandler answers the failures raised before the service is reached: -// parameter parsing, body decoding and request validation. Those never return -// through the service, so no spec-declared error type describes them and -// error-mapping does not reach them. A service whose API has an error shape of -// its own replaces this at init. -var ServiceErrorHandler OapiErrorHandler = &OapiDefaultErrorHandler{} - -// NewRouter creates a new chi.Router with the given service implementation. -func NewRouter(svc ServiceInterface, opts ...RouterOption) chi.Router { - cfg := &routerConfig{} - for _, opt := range opts { - opt(cfg) - } - - r := chi.NewRouter() - r.Use(api.ContextReplacementsMiddleware) - for _, mw := range cfg.middlewares { - r.Use(mw) - } - adapter := NewHTTPAdapter(svc, cfg.errHandler) - r.Method("GET", "/pets", http.HandlerFunc(adapter.FindPets)) - r.Method("POST", "/pets", http.HandlerFunc(adapter.AddPet)) - r.Method("GET", "/pets/{id}", http.HandlerFunc(adapter.FindPetByID)) - r.Method("DELETE", "/pets/{id}", http.HandlerFunc(adapter.DeletePet)) - +// WithHeaders sets the headers. +func (r *FindPetsResponseData) WithHeaders(h http.Header) *FindPetsResponseData { + r.Headers = h return r } -// ============================================================================ -// mockzilla Service Registration -// ============================================================================ - -//go:embed setup/config.yml -var configSrc []byte - -//go:embed setup/openapi.* -var openapiSpecFS embed.FS - -//go:embed setup/context.yml -var contextSrc []byte - -var cfg *config.ServiceConfig - -func init() { - var err error - cfg, err = config.NewServiceConfigFromBytes(configSrc) - if err != nil { - slog.Error("Failed to parse service config", "error", err) - return - } - loader.Register(cfg.Name, RegisterAPIRouter) +// StatusCode returns the status. +func (r *FindPetsResponseData) StatusCode() int { + return r.Status } -// Register registers the service with the central router. -func RegisterAPIRouter(router *api.Router) { - serviceName := cfg.Name - - // Read OpenAPI spec from embedded FS - openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) - if err != nil { - slog.Error(fmt.Sprintf("Failed to read OpenAPI spec for %s", serviceName), - "error", err, - "service", serviceName, - ) - return - } - - registry, err := typedef.NewRegistry(openapiSpec, typedef.RegistryOptions{ - SpecOptions: cfg.SpecOptions, - // The served request path reads converted operations only, so the - // parsed document does not need to stay resident. - ReleaseDocument: true, - }) - if err != nil { - slog.Error(fmt.Sprintf("Failed to create registry for %s", serviceName), - "error", err, - "service", serviceName, - ) - return - } - - // Create the generator with service contexts - orderedCtx := router.ServiceContext(serviceName, contextSrc) - gen, err := generator.NewGenerator(orderedCtx, router.GetContexts()) - if err != nil { - slog.Error(fmt.Sprintf("Failed to create generator for %s", serviceName), - "error", err, - "service", serviceName, - ) - return - } - - // Register with mockzilla using handler factory - router.RegisterHTTPHandler(cfg, func(serviceDB db.DB) api.Handler { - userSvc := newService(&api.ServiceParams{ - AppConfig: router.Config(), - ServiceConfig: cfg, - DB: serviceDB, - }) - genSvc := &generatorService{service: userSvc, generator: gen, registry: registry} - return newServiceHandler(genSvc, gen, registry) - }) - - slog.Info(fmt.Sprintf("Registered %s service", serviceName), - "service", serviceName, - ) +// Header returns the headers. +func (r *FindPetsResponseData) Header() http.Header { + return r.Headers } -// ============================================================================ -// Service Handler -// ============================================================================ - -// serviceHandler wraps the chi router and service to implement api.Handler. -type serviceHandler struct { - router chi.Router - service ServiceInterface - gen generator.Generate - registry typedef.OperationRegistry +// Payload returns the body. +func (r *FindPetsResponseData) Payload() any { + return r.Body } -// newServiceHandler creates a new serviceHandler. -func newServiceHandler(svc ServiceInterface, gen generator.Generate, registry typedef.OperationRegistry) api.Handler { - return &serviceHandler{ - router: NewRouter(svc, WithErrorHandler(ServiceErrorHandler)), - service: svc, - gen: gen, - registry: registry, - } +// ContentType returns the media type of the body, empty for the default of its Go type. +func (r *FindPetsResponseData) ContentType() string { + return r.contentType } -func (h *serviceHandler) Routes() api.RouteDescriptions { - routes := h.router.Routes() - descriptions := make(api.RouteDescriptions, 0, len(routes)) - for _, route := range routes { - for method := range route.Handlers { - descriptions = append(descriptions, &api.RouteDescription{ - Method: method, - Path: route.Pattern, - }) - } - } - return descriptions +// AddPetServiceRequestOptions is what AddPet receives. +type AddPetServiceRequestOptions struct { + // Body sent as application/json. + Body *NewPet + // GenerateResponse makes a mock response that satisfies the spec. + GenerateResponse func() (*AddPetResponseData, error) + RawRequest *http.Request } -func (h *serviceHandler) RegisterRoutes(router chi.Router) { - router.Mount("/", h.router) +// Validate checks the parameters and the body against the constraints of the spec. +func (o *AddPetServiceRequestOptions) Validate() error { + return nil } -func (h *serviceHandler) Generate(w http.ResponseWriter, r *http.Request) { - var req api.GenerateRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - message := err.Error() - if errors.Is(err, io.EOF) { - message = "request body is empty or incomplete" - } - slog.Error("Failed to decode request", "error", err) - http.Error(w, message, http.StatusBadRequest) - return - } +// AddPetResponseData is what AddPet returns. +type AddPetResponseData struct { + Status int + Headers http.Header + Body any - op := h.registry.FindOperation(req.Path, req.Method) - res := h.gen.Request(&req, op, req.Context) - api.NewJSONResponse(w).Send(res) + contentType string } -// ============================================================================ -// Generator Service (fallback to mock responses) -// ============================================================================ - -// generatorService implements ServiceInterface with generator fallback. -// It delegates to the user's service first; if that returns nil, it generates a mock response. -type generatorService struct { - service *service - generator generator.Generate - registry typedef.OperationRegistry +// NewAddPetResponseData200 returns the 200 response with its application/json body. +func NewAddPetResponseData200(body *Pet) *AddPetResponseData { + return &AddPetResponseData{Status: 200, Body: body, contentType: "application/json"} } -// Ensure generatorService implements ServiceInterface. -var _ ServiceInterface = (*generatorService)(nil) - -// FindPets handles GET /pets -func (s *generatorService) FindPets(ctx context.Context, opts *FindPetsServiceRequestOptions) (*FindPetsResponseData, error) { - // Inject GenerateResponse so user service can call it - opts.GenerateResponse = func() (*FindPetsResponseData, error) { - respSchema := s.registry.GetResponseSchema("/pets", "GET") - if respSchema == nil { - return NewFindPetsResponseData(nil), nil - } - res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(api.RequestFromGoContext(ctx))) - var body FindPetsResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewFindPetsResponseData(&body).WithHeaders(res.Headers), nil - } - - // Call user's service - if resp, err := s.service.FindPets(ctx, opts); resp != nil || err != nil { - return resp, err - } - - // Fallback to generator - return opts.GenerateResponse() +// NewAddPetResponseDataDefault returns the default response with its application/json body. +func NewAddPetResponseDataDefault(status int, body *Error) *AddPetResponseData { + return &AddPetResponseData{Status: status, Body: body, contentType: "application/json"} } -// AddPet handles POST /pets -func (s *generatorService) AddPet(ctx context.Context, opts *AddPetServiceRequestOptions) (*AddPetResponseData, error) { - // Inject GenerateResponse so user service can call it - opts.GenerateResponse = func() (*AddPetResponseData, error) { - respSchema := s.registry.GetResponseSchema("/pets", "POST") - if respSchema == nil { - return NewAddPetResponseData(nil), nil - } - res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(api.RequestFromGoContext(ctx))) - var body AddPetResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewAddPetResponseData(&body).WithHeaders(res.Headers), nil - } - - // Call user's service - if resp, err := s.service.AddPet(ctx, opts); resp != nil || err != nil { - return resp, err - } - - // Fallback to generator - return opts.GenerateResponse() +// WithStatus sets the status code. +func (r *AddPetResponseData) WithStatus(code int) *AddPetResponseData { + r.Status = code + return r } -// FindPetByID handles GET /pets/{id} -func (s *generatorService) FindPetByID(ctx context.Context, opts *FindPetByIDServiceRequestOptions) (*FindPetByIDResponseData, error) { - // Inject GenerateResponse so user service can call it - opts.GenerateResponse = func() (*FindPetByIDResponseData, error) { - respSchema := s.registry.GetResponseSchema("/pets/{id}", "GET") - if respSchema == nil { - return NewFindPetByIDResponseData(nil), nil - } - res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(api.RequestFromGoContext(ctx))) - var body FindPetByIDResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewFindPetByIDResponseData(&body).WithHeaders(res.Headers), nil - } - - // Call user's service - if resp, err := s.service.FindPetByID(ctx, opts); resp != nil || err != nil { - return resp, err - } - - // Fallback to generator - return opts.GenerateResponse() +// WithHeaders sets the headers. +func (r *AddPetResponseData) WithHeaders(h http.Header) *AddPetResponseData { + r.Headers = h + return r } -// DeletePet handles DELETE /pets/{id} -func (s *generatorService) DeletePet(ctx context.Context, opts *DeletePetServiceRequestOptions) (*DeletePetResponseData, error) { - // Inject GenerateResponse so user service can call it - opts.GenerateResponse = func() (*DeletePetResponseData, error) { - respSchema := s.registry.GetResponseSchema("/pets/{id}", "DELETE") - if respSchema == nil { - return NewDeletePetResponseData(nil), nil - } - res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(api.RequestFromGoContext(ctx))) - return NewDeletePetResponseData(nil).WithHeaders(res.Headers), nil - } - - // Call user's service - if resp, err := s.service.DeletePet(ctx, opts); resp != nil || err != nil { - return resp, err - } - - // Fallback to generator - return opts.GenerateResponse() +// StatusCode returns the status. +func (r *AddPetResponseData) StatusCode() int { + return r.Status } -// ============================================================================ -// Factory (programmatic request/response generation) -// ============================================================================ - -var ( - defaultFactory *factory.Factory - defaultFactoryOnce sync.Once - defaultFactoryErr error -) - -// NewFactory creates a Factory pre-configured with this service's OpenAPI spec, -// spec options, and context. -// Use it to generate mock requests and responses programmatically without running the server. -func NewFactory(opts ...factory.FactoryOption) (*factory.Factory, error) { - openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) - if err != nil { - return nil, err - } - - allOpts := []factory.FactoryOption{ - factory.WithServiceContext(contextSrc), - } - if cfg != nil && cfg.SpecOptions != nil { - allOpts = append(allOpts, factory.WithSpecOptions(cfg.SpecOptions)) - } - allOpts = append(allOpts, opts...) - return factory.NewFactory(openapiSpec, allOpts...) +// Header returns the headers. +func (r *AddPetResponseData) Header() http.Header { + return r.Headers } -// GetFactory returns a singleton Factory instance. -// The factory is initialized on first call and reused for subsequent calls. -func GetFactory() (*factory.Factory, error) { - defaultFactoryOnce.Do(func() { - defaultFactory, defaultFactoryErr = NewFactory() - }) - return defaultFactory, defaultFactoryErr +// Payload returns the body. +func (r *AddPetResponseData) Payload() any { + return r.Body } -// GenerateFindPetsResponse generates a mock response for GET /pets. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetsResponse(ctx map[string]any) (*FindPetsResponseData, error) { - f, err := GetFactory() - if err != nil { - return nil, err - } - res, err := f.Response("/pets", "GET", ctx) - if err != nil { - return nil, err - } - var body FindPetsResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewFindPetsResponseData(&body).WithHeaders(res.Headers), nil +// ContentType returns the media type of the body, empty for the default of its Go type. +func (r *AddPetResponseData) ContentType() string { + return r.contentType } -// GenerateFindPetsResponseBody generates a mock response body for GET /pets. -// Returns just the typed body without headers or status. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetsResponseBody(ctx map[string]any) (*FindPetsResponse, error) { - res, err := GenerateFindPetsResponse(ctx) - if err != nil { - return nil, err - } - return res.Body, nil +// FindPetByIDServiceRequestOptions is what FindPetByID receives. +type FindPetByIDServiceRequestOptions struct { + PathParams *FindPetByIDPathParams + // GenerateResponse makes a mock response that satisfies the spec. + GenerateResponse func() (*FindPetByIDResponseData, error) + RawRequest *http.Request } -// GenerateFindPetsRequest generates a mock request for GET /pets. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetsRequest(ctx map[string]any) (schema.GeneratedRequest, error) { - f, err := GetFactory() - if err != nil { - return schema.GeneratedRequest{}, err - } - return f.Request("/pets", "GET", ctx) +// Validate checks the parameters and the body against the constraints of the spec. +func (o *FindPetByIDServiceRequestOptions) Validate() error { + return nil } -// GenerateAddPetResponse generates a mock response for POST /pets. -// ctx is an optional replacement context for controlling generated values. -func GenerateAddPetResponse(ctx map[string]any) (*AddPetResponseData, error) { - f, err := GetFactory() - if err != nil { - return nil, err - } - res, err := f.Response("/pets", "POST", ctx) - if err != nil { - return nil, err - } - var body AddPetResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewAddPetResponseData(&body).WithHeaders(res.Headers), nil -} +// FindPetByIDResponseData is what FindPetByID returns. +type FindPetByIDResponseData struct { + Status int + Headers http.Header + Body any -// GenerateAddPetResponseBody generates a mock response body for POST /pets. -// Returns just the typed body without headers or status. -// ctx is an optional replacement context for controlling generated values. -func GenerateAddPetResponseBody(ctx map[string]any) (*AddPetResponse, error) { - res, err := GenerateAddPetResponse(ctx) - if err != nil { - return nil, err - } - return res.Body, nil + contentType string } -// GenerateAddPetRequest generates a mock request for POST /pets. -// ctx is an optional replacement context for controlling generated values. -func GenerateAddPetRequest(ctx map[string]any) (schema.GeneratedRequest, error) { - f, err := GetFactory() - if err != nil { - return schema.GeneratedRequest{}, err - } - return f.Request("/pets", "POST", ctx) +// NewFindPetByIDResponseData200 returns the 200 response with its application/json body. +func NewFindPetByIDResponseData200(body *Pet) *FindPetByIDResponseData { + return &FindPetByIDResponseData{Status: 200, Body: body, contentType: "application/json"} } -// GenerateAddPetRequestBody generates a typed mock request body for POST /pets. -// ctx is an optional replacement context for controlling generated values. -func GenerateAddPetRequestBody(ctx map[string]any) (*AddPetBody, error) { - req, err := GenerateAddPetRequest(ctx) - if err != nil { - return nil, err - } - var body AddPetBody - if err := json.Unmarshal(req.Body, &body); err != nil { - return nil, err - } - return &body, nil +// NewFindPetByIDResponseDataDefault returns the default response with its application/json body. +func NewFindPetByIDResponseDataDefault(status int, body *Error) *FindPetByIDResponseData { + return &FindPetByIDResponseData{Status: status, Body: body, contentType: "application/json"} } -// GenerateFindPetByIDResponse generates a mock response for GET /pets/{id}. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetByIDResponse(ctx map[string]any) (*FindPetByIDResponseData, error) { - f, err := GetFactory() - if err != nil { - return nil, err - } - res, err := f.Response("/pets/{id}", "GET", ctx) - if err != nil { - return nil, err - } - var body FindPetByIDResponse - if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { - return nil, err - } - return NewFindPetByIDResponseData(&body).WithHeaders(res.Headers), nil +// WithStatus sets the status code. +func (r *FindPetByIDResponseData) WithStatus(code int) *FindPetByIDResponseData { + r.Status = code + return r } -// GenerateFindPetByIDResponseBody generates a mock response body for GET /pets/{id}. -// Returns just the typed body without headers or status. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetByIDResponseBody(ctx map[string]any) (*FindPetByIDResponse, error) { - res, err := GenerateFindPetByIDResponse(ctx) - if err != nil { - return nil, err - } - return res.Body, nil +// WithHeaders sets the headers. +func (r *FindPetByIDResponseData) WithHeaders(h http.Header) *FindPetByIDResponseData { + r.Headers = h + return r } -// GenerateFindPetByIDRequest generates a mock request for GET /pets/{id}. -// ctx is an optional replacement context for controlling generated values. -func GenerateFindPetByIDRequest(ctx map[string]any) (schema.GeneratedRequest, error) { - f, err := GetFactory() - if err != nil { - return schema.GeneratedRequest{}, err - } - return f.Request("/pets/{id}", "GET", ctx) +// StatusCode returns the status. +func (r *FindPetByIDResponseData) StatusCode() int { + return r.Status } -// GenerateDeletePetResponse generates a mock response for DELETE /pets/{id}. -// ctx is an optional replacement context for controlling generated values. -func GenerateDeletePetResponse(ctx map[string]any) (*DeletePetResponseData, error) { - f, err := GetFactory() - if err != nil { - return nil, err - } - res, err := f.Response("/pets/{id}", "DELETE", ctx) - if err != nil { - return nil, err - } - return NewDeletePetResponseData(nil).WithHeaders(res.Headers), nil +// Header returns the headers. +func (r *FindPetByIDResponseData) Header() http.Header { + return r.Headers } -// GenerateDeletePetResponseBody generates a mock response body for DELETE /pets/{id}. -// Returns just the typed body without headers or status. -// ctx is an optional replacement context for controlling generated values. -func GenerateDeletePetResponseBody(ctx map[string]any) error { - _, err := GenerateDeletePetResponse(ctx) - return err +// Payload returns the body. +func (r *FindPetByIDResponseData) Payload() any { + return r.Body } -// GenerateDeletePetRequest generates a mock request for DELETE /pets/{id}. -// ctx is an optional replacement context for controlling generated values. -func GenerateDeletePetRequest(ctx map[string]any) (schema.GeneratedRequest, error) { - f, err := GetFactory() - if err != nil { - return schema.GeneratedRequest{}, err - } - return f.Request("/pets/{id}", "DELETE", ctx) +// ContentType returns the media type of the body, empty for the default of its Go type. +func (r *FindPetByIDResponseData) ContentType() string { + return r.contentType } -type FindPetByIDPath struct { - ID int64 `json:"id"` +// DeletePetServiceRequestOptions is what DeletePet receives. +type DeletePetServiceRequestOptions struct { + PathParams *DeletePetPathParams + // GenerateResponse makes a mock response that satisfies the spec. + GenerateResponse func() (*DeletePetResponseData, error) + RawRequest *http.Request } -type DeletePetPath struct { - ID int64 `json:"id"` +// Validate checks the parameters and the body against the constraints of the spec. +func (o *DeletePetServiceRequestOptions) Validate() error { + return nil } -type AddPetBody = NewPet +// DeletePetResponseData is what DeletePet returns. +type DeletePetResponseData struct { + Status int + Headers http.Header + Body any -type FindPetsQuery struct { - Tags []string `json:"tags,omitempty"` - Limit *int32 `json:"limit,omitempty"` + contentType string } -// FindPetsResponseData wraps the success response with optional headers and status override. -type FindPetsResponseData struct { - Body *FindPetsResponse - Headers http.Header - Status int // 0 = use default (200) +// NewDeletePetResponseData204 returns the 204 response. +func NewDeletePetResponseData204() *DeletePetResponseData { + return &DeletePetResponseData{Status: 204, contentType: ""} } -// NewFindPetsResponseData creates a new FindPetsResponseData with the given body. -func NewFindPetsResponseData(body *FindPetsResponse) *FindPetsResponseData { - return &FindPetsResponseData{Body: body} +// NewDeletePetResponseDataDefault returns the default response with its application/json body. +func NewDeletePetResponseDataDefault(status int, body *Error) *DeletePetResponseData { + return &DeletePetResponseData{Status: status, Body: body, contentType: "application/json"} } -// WithHeaders sets custom headers on the response. -func (r *FindPetsResponseData) WithHeaders(h http.Header) *FindPetsResponseData { - r.Headers = h +// WithStatus sets the status code. +func (r *DeletePetResponseData) WithStatus(code int) *DeletePetResponseData { + r.Status = code return r } -// WithStatus overrides the default status code. -func (r *FindPetsResponseData) WithStatus(code int) *FindPetsResponseData { - r.Status = code +// WithHeaders sets the headers. +func (r *DeletePetResponseData) WithHeaders(h http.Header) *DeletePetResponseData { + r.Headers = h return r } -// Payload returns the value to be JSON-encoded as the response body. Override -// this template to customize the encoded shape (e.g. to wrap it in an envelope) -// without needing to change the adapter template. -func (r *FindPetsResponseData) Payload() any { - if r.Body == nil { - return nil - } - return r.Body +// StatusCode returns the status. +func (r *DeletePetResponseData) StatusCode() int { + return r.Status } -// AddPetResponseData wraps the success response with optional headers and status override. -type AddPetResponseData struct { - Body *AddPetResponse - Headers http.Header - Status int // 0 = use default (200) +// Header returns the headers. +func (r *DeletePetResponseData) Header() http.Header { + return r.Headers } -// NewAddPetResponseData creates a new AddPetResponseData with the given body. -func NewAddPetResponseData(body *AddPetResponse) *AddPetResponseData { - return &AddPetResponseData{Body: body} +// Payload returns the body. +func (r *DeletePetResponseData) Payload() any { + return r.Body } -// WithHeaders sets custom headers on the response. -func (r *AddPetResponseData) WithHeaders(h http.Header) *AddPetResponseData { - r.Headers = h - return r +// ContentType returns the media type of the body, empty for the default of its Go type. +func (r *DeletePetResponseData) ContentType() string { + return r.contentType } -// WithStatus overrides the default status code. -func (r *AddPetResponseData) WithStatus(code int) *AddPetResponseData { - r.Status = code - return r -} +// The error types the handlers use, as the runtime declares them. +type ( + ErrorKind = httpserver.ErrorKind + HandlerError = httpserver.HandlerError + ErrorHandler = httpserver.ErrorHandler + ErrorHandlerFunc = httpserver.ErrorHandlerFunc + DefaultErrorHandler = httpserver.DefaultErrorHandler +) -// Payload returns the value to be JSON-encoded as the response body. Override -// this template to customize the encoded shape (e.g. to wrap it in an envelope) -// without needing to change the adapter template. -func (r *AddPetResponseData) Payload() any { - if r.Body == nil { - return nil +// The kinds of HandlerError. +const ( + ErrorParse = httpserver.ErrorParse + ErrorDecode = httpserver.ErrorDecode + ErrorValidation = httpserver.ErrorValidation + ErrorService = httpserver.ErrorService + ErrorResponse = httpserver.ErrorResponse +) + +var bodyPresence = runtime.Presence{ + IsChecked: true, + Objects: []runtime.Object{ + {Name: "NewPet", Props: []runtime.Prop{ + {Key: "name", IsRequired: true}, + {Key: "tag"}, + }}, + }, +} + +// ServerOptions is what the adapter and the router are set up with. +type ServerOptions struct { + Router any + Middleware []func(http.Handler) http.Handler + OperationMiddleware []func(http.Handler) http.Handler + ErrorHandler httpserver.ErrorHandler + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) + MultipartMaxMemory int64 + Presence runtime.PresenceChecker +} + +// ServerOption sets one field of ServerOptions. +type ServerOption func(*ServerOptions) + +// NewServerOptions applies opts to the defaults. +func NewServerOptions(opts ...ServerOption) *ServerOptions { + o := &ServerOptions{ + ErrorHandler: httpserver.DefaultErrorHandler{}, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, + MultipartMaxMemory: 33554432, + Presence: bodyPresence, } - return r.Body + for _, opt := range opts { + opt(o) + } + return o } -// FindPetByIDResponseData wraps the success response with optional headers and status override. -type FindPetByIDResponseData struct { - Body *FindPetByIDResponse - Headers http.Header - Status int // 0 = use default (200) +// WithMiddleware wraps the routes with mw, outermost first; on a new router, unknown paths too. +func WithMiddleware(mw ...func(http.Handler) http.Handler) ServerOption { + return func(o *ServerOptions) { + o.Middleware = append(o.Middleware, mw...) + } } -// NewFindPetByIDResponseData creates a new FindPetByIDResponseData with the given body. -func NewFindPetByIDResponseData(body *FindPetByIDResponse) *FindPetByIDResponseData { - return &FindPetByIDResponseData{Body: body} +// WithOperationMiddleware wraps each operation with mw, once its name is on the context. +func WithOperationMiddleware(mw ...func(http.Handler) http.Handler) ServerOption { + return func(o *ServerOptions) { + o.OperationMiddleware = append(o.OperationMiddleware, mw...) + } } -// WithHeaders sets custom headers on the response. -func (r *FindPetByIDResponseData) WithHeaders(h http.Header) *FindPetByIDResponseData { - r.Headers = h - return r +// WithErrorHandler sets what writes the response of a failed request. +func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { + return func(o *ServerOptions) { + o.ErrorHandler = h + } } -// WithStatus overrides the default status code. -func (r *FindPetByIDResponseData) WithStatus(code int) *FindPetByIDResponseData { - r.Status = code - return r +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { + return func(o *ServerOptions) { + o.JSONDecoder = decode + } } -// Payload returns the value to be JSON-encoded as the response body. Override -// this template to customize the encoded shape (e.g. to wrap it in an envelope) -// without needing to change the adapter template. -func (r *FindPetByIDResponseData) Payload() any { - if r.Body == nil { - return nil +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode } - return r.Body } -// DeletePetResponseData wraps the success response with optional headers and status override. -type DeletePetResponseData struct { - Body *struct{} - Headers http.Header - Status int // 0 = use default (204) +// WithMultipartMaxMemory sets how much of a multipart form stays in memory. +func WithMultipartMaxMemory(n int64) ServerOption { + return func(o *ServerOptions) { + o.MultipartMaxMemory = n + } } -// NewDeletePetResponseData creates a new DeletePetResponseData with the given body. -func NewDeletePetResponseData(body *struct{}) *DeletePetResponseData { - return &DeletePetResponseData{Body: body} +// WithPresence sets what checks the keys of request bodies and fills their defaults. +func WithPresence(p runtime.PresenceChecker) ServerOption { + return func(o *ServerOptions) { + o.Presence = p + } } -// WithHeaders sets custom headers on the response. -func (r *DeletePetResponseData) WithHeaders(h http.Header) *DeletePetResponseData { - r.Headers = h - return r +// HTTPAdapter answers HTTP requests by calling the service. +type HTTPAdapter struct { + svc ServiceInterface + opts *ServerOptions + operations httpserver.Chain } -// WithStatus overrides the default status code. -func (r *DeletePetResponseData) WithStatus(code int) *DeletePetResponseData { - r.Status = code - return r +type responseData interface { + StatusCode() int + Header() http.Header + Payload() any + ContentType() string } -// Payload returns the value to be JSON-encoded as the response body. Override -// this template to customize the encoded shape (e.g. to wrap it in an envelope) -// without needing to change the adapter template. -func (r *DeletePetResponseData) Payload() any { - if r.Body == nil { - return nil - } - return r.Body +// NewHTTPAdapter returns the adapter of svc. +func NewHTTPAdapter(svc ServiceInterface, opts ...ServerOption) *HTTPAdapter { + o := NewServerOptions(opts...) + return &HTTPAdapter{svc: svc, opts: o, operations: httpserver.NewChain(o.OperationMiddleware...)} } -type FindPetsResponse []Pet +// FindPets handles GET /pets. +func (a *HTTPAdapter) FindPets(w http.ResponseWriter, r *http.Request) { + a.operations.Serve(w, r, "FindPets", a.serveFindPets) +} -func (f FindPetsResponse) Validate() error { - if f == nil { - return nil +func (a *HTTPAdapter) serveFindPets(w http.ResponseWriter, r *http.Request) { + opts := &FindPetsServiceRequestOptions{RawRequest: r} + query := runtime.ParseQuery(r.URL.RawQuery) + opts.Query = &FindPetsQuery{} + if err := runtime.DecodeQuery(query, runtime.Param{Name: "tags", Style: runtime.StyleForm, IsExplode: true, IsRequired: false, IsJSON: false}, &opts.Query.Tags); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorParse, OperationID: "FindPets", ParamName: "tags", ParamLocation: "query", Err: err}) + return } - var errors runtime.ValidationErrors - for i, item := range f { - if v, ok := any(item).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append(fmt.Sprintf("[%d]", i), err) - } - } + if err := runtime.DecodeQuery(query, runtime.Param{Name: "limit", Style: runtime.StyleForm, IsExplode: true, IsRequired: false, IsJSON: false}, &opts.Query.Limit); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorParse, OperationID: "FindPets", ParamName: "limit", ParamLocation: "query", Err: err}) + return } - if len(errors) == 0 { - return nil + if err := opts.Validate(); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorValidation, OperationID: "FindPets", Err: err}) + return } - return errors -} - -type FindPetsErrorResponse = Error - -type AddPetResponse = Pet -type AddPetErrorResponse = Error - -type FindPetByIDResponse = Pet - -type FindPetByIDErrorResponse = Error - -type DeletePetErrorResponse = Error - -// FindPetsServiceRequestOptions holds all parameters for the FindPets operation. -type FindPetsServiceRequestOptions struct { - Query *FindPetsQuery - // RawRequest provides access to the underlying HTTP request for custom content type handling. - RawRequest *http.Request - // GenerateResponse generates a sample response with random data satisfying the OpenAPI schema. - GenerateResponse func() (*FindPetsResponseData, error) -} - -// Validate validates all the fields in the options. -func (o *FindPetsServiceRequestOptions) Validate() error { - var errors runtime.ValidationErrors - - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) - } - } + res, err := a.svc.FindPets(r.Context(), opts) + if err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "FindPets", Err: err}) + return } - if len(errors) == 0 { - return nil + if res == nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "FindPets", Err: httpserver.ErrNoResponse}) + return } - - return errors + a.write(w, r, "FindPets", res) } -// AddPetServiceRequestOptions holds all parameters for the AddPet operation. -type AddPetServiceRequestOptions struct { - Body *AddPetBody - // RawRequest provides access to the underlying HTTP request for custom content type handling. - RawRequest *http.Request - // GenerateResponse generates a sample response with random data satisfying the OpenAPI schema. - GenerateResponse func() (*AddPetResponseData, error) +// AddPet handles POST /pets. +func (a *HTTPAdapter) AddPet(w http.ResponseWriter, r *http.Request) { + a.operations.Serve(w, r, "AddPet", a.serveAddPet) } -// Validate validates all the fields in the options. -func (o *AddPetServiceRequestOptions) Validate() error { - var errors runtime.ValidationErrors - - if o.Body != nil { - if v, ok := any(o.Body).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("Body", err) - } +func (a *HTTPAdapter) serveAddPet(w http.ResponseWriter, r *http.Request) { + opts := &AddPetServiceRequestOptions{RawRequest: r} + switch contentType := runtime.ContentType(r.Header); contentType { + case "application/json": + body, err := a.opts.Presence.JSON(r.Body, runtime.Prop{Object: "NewPet"}) + if err != nil { + a.failBody(w, r, "AddPet", err) + return + } + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { + a.failDecode(w, r, "AddPet", err) + return } + case "": + a.failDecode(w, r, "AddPet", runtime.ErrBodyEmpty) + return + default: + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorDecode, OperationID: "AddPet", Status: http.StatusUnsupportedMediaType, Err: runtime.ContentTypeError(contentType)}) + return } - if len(errors) == 0 { - return nil + if err := opts.Validate(); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorValidation, OperationID: "AddPet", Err: err}) + return } - return errors + res, err := a.svc.AddPet(r.Context(), opts) + if err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "AddPet", Err: err}) + return + } + if res == nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "AddPet", Err: httpserver.ErrNoResponse}) + return + } + a.write(w, r, "AddPet", res) } -// FindPetByIDServiceRequestOptions holds all parameters for the FindPetByID operation. -type FindPetByIDServiceRequestOptions struct { - PathParams *FindPetByIDPath - // RawRequest provides access to the underlying HTTP request for custom content type handling. - RawRequest *http.Request - // GenerateResponse generates a sample response with random data satisfying the OpenAPI schema. - GenerateResponse func() (*FindPetByIDResponseData, error) +// FindPetByID handles GET /pets/{id}. +func (a *HTTPAdapter) FindPetByID(w http.ResponseWriter, r *http.Request) { + a.operations.Serve(w, r, "FindPetByID", a.serveFindPetByID) } -// Validate validates all the fields in the options. -func (o *FindPetByIDServiceRequestOptions) Validate() error { - var errors runtime.ValidationErrors - - if o.PathParams != nil { - if v, ok := any(o.PathParams).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("PathParams", err) - } - } +func (a *HTTPAdapter) serveFindPetByID(w http.ResponseWriter, r *http.Request) { + opts := &FindPetByIDServiceRequestOptions{RawRequest: r} + opts.PathParams = &FindPetByIDPathParams{} + if err := runtime.DecodePath(runtime.UnescapePath(r, chi.URLParam(r, "id")), runtime.Param{Name: "id", Style: runtime.StyleSimple, IsExplode: false, IsRequired: true, IsJSON: false}, &opts.PathParams.ID); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorParse, OperationID: "FindPetByID", ParamName: "id", ParamLocation: "path", Err: err}) + return } - if len(errors) == 0 { - return nil + if err := opts.Validate(); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorValidation, OperationID: "FindPetByID", Err: err}) + return } - return errors + res, err := a.svc.FindPetByID(r.Context(), opts) + if err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "FindPetByID", Err: err}) + return + } + if res == nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "FindPetByID", Err: httpserver.ErrNoResponse}) + return + } + a.write(w, r, "FindPetByID", res) } -// DeletePetServiceRequestOptions holds all parameters for the DeletePet operation. -type DeletePetServiceRequestOptions struct { - PathParams *DeletePetPath - // RawRequest provides access to the underlying HTTP request for custom content type handling. - RawRequest *http.Request - // GenerateResponse generates a sample response with random data satisfying the OpenAPI schema. - GenerateResponse func() (*DeletePetResponseData, error) +// DeletePet handles DELETE /pets/{id}. +func (a *HTTPAdapter) DeletePet(w http.ResponseWriter, r *http.Request) { + a.operations.Serve(w, r, "DeletePet", a.serveDeletePet) } -// Validate validates all the fields in the options. -func (o *DeletePetServiceRequestOptions) Validate() error { - var errors runtime.ValidationErrors - - if o.PathParams != nil { - if v, ok := any(o.PathParams).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append("PathParams", err) - } - } +func (a *HTTPAdapter) serveDeletePet(w http.ResponseWriter, r *http.Request) { + opts := &DeletePetServiceRequestOptions{RawRequest: r} + opts.PathParams = &DeletePetPathParams{} + if err := runtime.DecodePath(runtime.UnescapePath(r, chi.URLParam(r, "id")), runtime.Param{Name: "id", Style: runtime.StyleSimple, IsExplode: false, IsRequired: true, IsJSON: false}, &opts.PathParams.ID); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorParse, OperationID: "DeletePet", ParamName: "id", ParamLocation: "path", Err: err}) + return } - if len(errors) == 0 { - return nil + if err := opts.Validate(); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorValidation, OperationID: "DeletePet", Err: err}) + return } - return errors -} - -type Pet struct { - Name string `json:"name" validate:"required"` - Tag *string `json:"tag,omitempty"` - ID int64 `json:"id"` + res, err := a.svc.DeletePet(r.Context(), opts) + if err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "DeletePet", Err: err}) + return + } + if res == nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: "DeletePet", Err: httpserver.ErrNoResponse}) + return + } + a.write(w, r, "DeletePet", res) } -func (p Pet) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(p)) +func (a *HTTPAdapter) fail(w http.ResponseWriter, r *http.Request, err *httpserver.HandlerError) { + // A response that failed to write leaves its media type, which is not the error's. + w.Header().Del("Content-Type") + a.opts.ErrorHandler.HandleError(w, r, err.StatusCode(), err) } -type NewPet struct { - Name string `json:"name" validate:"required"` - Tag *string `json:"tag,omitempty"` +func (a *HTTPAdapter) failDecode(w http.ResponseWriter, r *http.Request, id string, err error) { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorDecode, OperationID: id, Err: err}) } -func (n NewPet) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(n)) -} - -type Error struct { - Code int32 `json:"code"` - Message string `json:"message" validate:"required"` +func (a *HTTPAdapter) failBody(w http.ResponseWriter, r *http.Request, id string, err error) { + kind := httpserver.ErrorDecode + if validation.Failed(err) { + kind = httpserver.ErrorValidation + } + a.fail(w, r, &httpserver.HandlerError{Kind: kind, OperationID: id, Err: err}) } -func (e Error) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(e)) +func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, res responseData) { + if err := httpserver.ValidateResponse(res.Payload()); err != nil { + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) + return + } + if res.ContentType() != "" { + w.Header().Set("Content-Type", res.ContentType()) + } + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) + switch { + case errors.Is(err, runtime.ErrContentType): + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) + case err != nil: + a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorService, OperationID: id, Err: err}) + } } -func (s Error) Error() string { - return "unmapped client error" +// WithRouter registers the routes on r instead of a new router. +func WithRouter(r chi.Router) ServerOption { + return func(o *ServerOptions) { + o.Router = r + } } -var typesValidator *validator.Validate +// NewRouter registers every operation on a chi router. +func NewRouter(svc ServiceInterface, opts ...ServerOption) chi.Router { + o := NewServerOptions(opts...) + a := NewHTTPAdapter(svc, opts...) + register := func(r chi.Router) { + r.Get("/pets", a.FindPets) + r.Post("/pets", a.AddPet) + r.Get("/pets/{id}", a.FindPetByID) + r.Delete("/pets/{id}", a.DeletePet) + } -func init() { - typesValidator = validator.New(validator.WithRequiredStructEnabled()) - runtime.RegisterCustomTypeFunc(typesValidator) + router, _ := o.Router.(chi.Router) + if router == nil { + router = chi.NewRouter() + router.Use(o.Middleware...) + register(router) + return router + } + router.Group(func(r chi.Router) { + r.Use(o.Middleware...) + register(r) + }) + return router } diff --git a/pkg/petstore/register.go b/pkg/petstore/register.go new file mode 100644 index 0000000..bacc1c9 --- /dev/null +++ b/pkg/petstore/register.go @@ -0,0 +1,411 @@ +// Code generated by mockzilla-codegen. DO NOT EDIT. + +package petstore + +import ( + "context" + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "sync" + + chi "github.com/go-chi/chi/v5" + "github.com/mockzilla/mockzilla/v2/pkg/api" + "github.com/mockzilla/mockzilla/v2/pkg/config" + "github.com/mockzilla/mockzilla/v2/pkg/db" + "github.com/mockzilla/mockzilla/v2/pkg/factory" + "github.com/mockzilla/mockzilla/v2/pkg/generator" + "github.com/mockzilla/mockzilla/v2/pkg/loader" + "github.com/mockzilla/mockzilla/v2/pkg/schema" + "github.com/mockzilla/mockzilla/v2/pkg/typedef" +) + +//go:embed setup/config.yml +var configSrc []byte + +//go:embed setup/openapi.* +var openapiSpecFS embed.FS + +//go:embed setup/context.yml +var contextSrc []byte + +// ServiceErrorHandler answers the requests that fail before the service is reached. +var ServiceErrorHandler ErrorHandler = DefaultErrorHandler{} + +// cfg is set before any init function runs, so the package's other files can read it in theirs. +var cfg, cfgErr = config.NewServiceConfigFromBytes(configSrc) + +func init() { + if cfgErr != nil { + slog.Error("Failed to parse service config", "error", cfgErr) + return + } + loader.Register(cfg.Name, RegisterAPIRouter) +} + +// RegisterAPIRouter registers the service with the central router. +func RegisterAPIRouter(router *api.Router) { + serviceName := cfg.Name + + openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) + if err != nil { + slog.Error(fmt.Sprintf("Failed to read OpenAPI spec for %s", serviceName), "error", err, "service", serviceName) + return + } + + registry, err := typedef.NewRegistry(openapiSpec, typedef.RegistryOptions{ + SpecOptions: cfg.SpecOptions, + ReleaseDocument: true, + }) + if err != nil { + slog.Error(fmt.Sprintf("Failed to create registry for %s", serviceName), "error", err, "service", serviceName) + return + } + + orderedCtx := router.ServiceContext(serviceName, contextSrc) + gen, err := generator.NewGenerator(orderedCtx, router.GetContexts()) + if err != nil { + slog.Error(fmt.Sprintf("Failed to create generator for %s", serviceName), "error", err, "service", serviceName) + return + } + + router.RegisterHTTPHandler(cfg, func(serviceDB db.DB) api.Handler { + userSvc := newService(&api.ServiceParams{ + AppConfig: router.Config(), + ServiceConfig: cfg, + DB: serviceDB, + }) + genSvc := &generatorService{service: userSvc, generator: gen, registry: registry} + return newServiceHandler(genSvc, gen, registry) + }) + + slog.Info(fmt.Sprintf("Registered %s service", serviceName), "service", serviceName) +} + +type serviceHandler struct { + router chi.Router + gen generator.Generate + registry typedef.OperationRegistry +} + +func newServiceHandler(svc ServiceInterface, gen generator.Generate, registry typedef.OperationRegistry) api.Handler { + return &serviceHandler{ + router: NewRouter(svc, + WithMiddleware(api.ContextReplacementsMiddleware), + WithErrorHandler(returnedErrors{next: ServiceErrorHandler}), + ), + gen: gen, + registry: registry, + } +} + +func (h *serviceHandler) Routes() api.RouteDescriptions { + routes := h.router.Routes() + descriptions := make(api.RouteDescriptions, 0, len(routes)) + for _, route := range routes { + for method := range route.Handlers { + descriptions = append(descriptions, &api.RouteDescription{Method: method, Path: route.Pattern}) + } + } + return descriptions +} + +func (h *serviceHandler) RegisterRoutes(router chi.Router) { + router.Mount("/", h.router) +} + +func (h *serviceHandler) Generate(w http.ResponseWriter, r *http.Request) { + var req api.GenerateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + message := err.Error() + if errors.Is(err, io.EOF) { + message = "request body is empty or incomplete" + } + slog.Error("Failed to decode request", "error", err) + http.Error(w, message, http.StatusBadRequest) + return + } + + op := h.registry.FindOperation(req.Path, req.Method) + res := h.gen.Request(&req, op, req.Context) + api.NewJSONResponse(w).Send(res) +} + +// returnedError is an error the service returned, with the status of the response it returned too. +// It has no Unwrap, so the adapter cannot answer an error type of the spec before returnedErrors. +type returnedError struct { + status int + err error +} + +func (e *returnedError) Error() string { + return e.err.Error() +} + +// returnedErrors hands next an error the service returned as it was returned, with the status of +// its response, so a service answers an error with the status it sets. +type returnedErrors struct { + next ErrorHandler +} + +func (h returnedErrors) HandleError(w http.ResponseWriter, r *http.Request, status int, err error) { + var returned *returnedError + if errors.As(err, &returned) { + if returned.status != 0 { + status = returned.status + } + err = returned.err + } + h.next.HandleError(w, r, status, err) +} + +// generatorService calls the service first and makes a mock response when it returns nil, nil. +type generatorService struct { + service *service + generator generator.Generate + registry typedef.OperationRegistry +} + +var _ ServiceInterface = (*generatorService)(nil) + +func (s *generatorService) FindPets(ctx context.Context, opts *FindPetsServiceRequestOptions) (*FindPetsResponseData, error) { + opts.GenerateResponse = func() (*FindPetsResponseData, error) { + respSchema := s.registry.GetResponseSchema("/pets", "GET") + res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(opts.RawRequest)) + var body FindPetsResponse200 + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewFindPetsResponseData200(body).WithHeaders(res.Headers), nil + } + + resp, err := s.service.FindPets(ctx, opts) + if err != nil { + status := 0 + if resp != nil { + status = resp.Status + } + return nil, &returnedError{status: status, err: err} + } + if resp != nil { + return resp, nil + } + return opts.GenerateResponse() +} + +func (s *generatorService) AddPet(ctx context.Context, opts *AddPetServiceRequestOptions) (*AddPetResponseData, error) { + opts.GenerateResponse = func() (*AddPetResponseData, error) { + respSchema := s.registry.GetResponseSchema("/pets", "POST") + res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(opts.RawRequest)) + var body Pet + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewAddPetResponseData200(&body).WithHeaders(res.Headers), nil + } + + resp, err := s.service.AddPet(ctx, opts) + if err != nil { + status := 0 + if resp != nil { + status = resp.Status + } + return nil, &returnedError{status: status, err: err} + } + if resp != nil { + return resp, nil + } + return opts.GenerateResponse() +} + +func (s *generatorService) FindPetByID(ctx context.Context, opts *FindPetByIDServiceRequestOptions) (*FindPetByIDResponseData, error) { + opts.GenerateResponse = func() (*FindPetByIDResponseData, error) { + respSchema := s.registry.GetResponseSchema("/pets/{id}", "GET") + res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(opts.RawRequest)) + var body Pet + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewFindPetByIDResponseData200(&body).WithHeaders(res.Headers), nil + } + + resp, err := s.service.FindPetByID(ctx, opts) + if err != nil { + status := 0 + if resp != nil { + status = resp.Status + } + return nil, &returnedError{status: status, err: err} + } + if resp != nil { + return resp, nil + } + return opts.GenerateResponse() +} + +func (s *generatorService) DeletePet(ctx context.Context, opts *DeletePetServiceRequestOptions) (*DeletePetResponseData, error) { + opts.GenerateResponse = func() (*DeletePetResponseData, error) { + respSchema := s.registry.GetResponseSchema("/pets/{id}", "DELETE") + res := s.generator.Response(respSchema, api.UserContextFromGoContext(ctx), generator.WithRequest(opts.RawRequest)) + return NewDeletePetResponseData204().WithHeaders(res.Headers), nil + } + + resp, err := s.service.DeletePet(ctx, opts) + if err != nil { + status := 0 + if resp != nil { + status = resp.Status + } + return nil, &returnedError{status: status, err: err} + } + if resp != nil { + return resp, nil + } + return opts.GenerateResponse() +} + +var ( + defaultFactory *factory.Factory + defaultFactoryOnce sync.Once + defaultFactoryErr error +) + +// NewFactory returns a Factory with the spec, spec options and context of this service. +func NewFactory(opts ...factory.FactoryOption) (*factory.Factory, error) { + openapiSpec, err := api.ReadEmbeddedSpec(openapiSpecFS) + if err != nil { + return nil, err + } + + allOpts := []factory.FactoryOption{factory.WithServiceContext(contextSrc)} + if cfg != nil && cfg.SpecOptions != nil { + allOpts = append(allOpts, factory.WithSpecOptions(cfg.SpecOptions)) + } + allOpts = append(allOpts, opts...) + return factory.NewFactory(openapiSpec, allOpts...) +} + +// GetFactory returns the Factory of this service, made on the first call. +func GetFactory() (*factory.Factory, error) { + defaultFactoryOnce.Do(func() { + defaultFactory, defaultFactoryErr = NewFactory() + }) + return defaultFactory, defaultFactoryErr +} + +// GenerateFindPetsResponse makes a mock response for GET /pets. +func GenerateFindPetsResponse(ctx map[string]any) (*FindPetsResponseData, error) { + f, err := GetFactory() + if err != nil { + return nil, err + } + res, err := f.Response("/pets", "GET", ctx) + if err != nil { + return nil, err + } + var body FindPetsResponse200 + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewFindPetsResponseData200(body).WithHeaders(res.Headers), nil +} + +// GenerateFindPetsRequest makes a mock request for GET /pets. +func GenerateFindPetsRequest(ctx map[string]any) (schema.GeneratedRequest, error) { + f, err := GetFactory() + if err != nil { + return schema.GeneratedRequest{}, err + } + return f.Request("/pets", "GET", ctx) +} + +// GenerateAddPetResponse makes a mock response for POST /pets. +func GenerateAddPetResponse(ctx map[string]any) (*AddPetResponseData, error) { + f, err := GetFactory() + if err != nil { + return nil, err + } + res, err := f.Response("/pets", "POST", ctx) + if err != nil { + return nil, err + } + var body Pet + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewAddPetResponseData200(&body).WithHeaders(res.Headers), nil +} + +// GenerateAddPetRequest makes a mock request for POST /pets. +func GenerateAddPetRequest(ctx map[string]any) (schema.GeneratedRequest, error) { + f, err := GetFactory() + if err != nil { + return schema.GeneratedRequest{}, err + } + return f.Request("/pets", "POST", ctx) +} + +// GenerateAddPetRequestBody makes a typed mock request body for POST /pets. +func GenerateAddPetRequestBody(ctx map[string]any) (*NewPet, error) { + req, err := GenerateAddPetRequest(ctx) + if err != nil { + return nil, err + } + body := new(NewPet) + if err := json.Unmarshal(req.Body, body); err != nil { + return nil, err + } + return body, nil +} + +// GenerateFindPetByIDResponse makes a mock response for GET /pets/{id}. +func GenerateFindPetByIDResponse(ctx map[string]any) (*FindPetByIDResponseData, error) { + f, err := GetFactory() + if err != nil { + return nil, err + } + res, err := f.Response("/pets/{id}", "GET", ctx) + if err != nil { + return nil, err + } + var body Pet + if err := api.UnmarshalResponseInto(res.Body, "application/json", &body); err != nil { + return nil, err + } + return NewFindPetByIDResponseData200(&body).WithHeaders(res.Headers), nil +} + +// GenerateFindPetByIDRequest makes a mock request for GET /pets/{id}. +func GenerateFindPetByIDRequest(ctx map[string]any) (schema.GeneratedRequest, error) { + f, err := GetFactory() + if err != nil { + return schema.GeneratedRequest{}, err + } + return f.Request("/pets/{id}", "GET", ctx) +} + +// GenerateDeletePetResponse makes a mock response for DELETE /pets/{id}. +func GenerateDeletePetResponse(ctx map[string]any) (*DeletePetResponseData, error) { + f, err := GetFactory() + if err != nil { + return nil, err + } + res, err := f.Response("/pets/{id}", "DELETE", ctx) + if err != nil { + return nil, err + } + return NewDeletePetResponseData204().WithHeaders(res.Headers), nil +} + +// GenerateDeletePetRequest makes a mock request for DELETE /pets/{id}. +func GenerateDeletePetRequest(ctx map[string]any) (schema.GeneratedRequest, error) { + f, err := GetFactory() + if err != nil { + return schema.GeneratedRequest{}, err + } + return f.Request("/pets/{id}", "DELETE", ctx) +} diff --git a/pkg/petstore/setup/codegen.yml b/pkg/petstore/setup/codegen.yml index c03bf7b..1c1d8cf 100644 --- a/pkg/petstore/setup/codegen.yml +++ b/pkg/petstore/setup/codegen.yml @@ -1,22 +1,17 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/doordash-oss/oapi-codegen-dd/main/configuration-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/mockzilla/mockzilla-codegen/main/config.schema.json package: petstore -output: - use-single-file: true - skip-fmt: true - directory: ../ -generate: - omit-description: true - default-int-type: int64 - always-prefix-enum-values: true +models: + int-type: int64 + descriptions: false + validation: + response: true +server: + framework: chi validation: + request: true response: true - handler: - kind: chi - output: - overwrite: false - service: {} - # middleware: {} - validation: - request: true - response: true -error-mapping: + scaffold: + service: ../service.go + # middleware: ../middleware.go +output: + file: ../gen.go