├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── oapi-codegen │ ├── oapi-codegen.go │ └── oapi-codegen_test.go ├── examples ├── authenticated-api │ ├── README.md │ ├── api.yaml │ └── echo │ │ ├── api │ │ ├── api.gen.go │ │ ├── config.yaml │ │ └── doc.go │ │ ├── main.go │ │ └── server │ │ ├── fake_jws.go │ │ ├── jwt_authenticator.go │ │ ├── server.go │ │ └── server_test.go └── petstore-expanded │ ├── README.md │ ├── chi │ ├── api │ │ ├── cfg.yaml │ │ ├── petstore.gen.go │ │ └── petstore.go │ ├── petstore.go │ └── petstore_test.go │ ├── echo │ ├── api │ │ ├── petstore-server.gen.go │ │ ├── petstore-types.gen.go │ │ ├── petstore.go │ │ ├── server.cfg.yaml │ │ └── types.cfg.yaml │ ├── petstore.go │ └── petstore_test.go │ ├── gin │ ├── api │ │ ├── petstore-server.gen.go │ │ ├── petstore-types.gen.go │ │ ├── petstore.go │ │ ├── server.cfg.yaml │ │ └── types.cfg.yaml │ ├── petstore.go │ └── petstore_test.go │ ├── internal │ └── doc.go │ ├── petstore-client.gen.go │ └── petstore-expanded.yaml ├── go.mod ├── go.sum ├── internal └── test │ ├── all_of │ ├── config1.yaml │ ├── config2.yaml │ ├── doc.go │ ├── openapi.yaml │ ├── v1 │ │ └── openapi.gen.go │ └── v2 │ │ └── openapi.gen.go │ ├── client │ ├── client.gen.go │ ├── client.yaml │ ├── client_test.go │ └── doc.go │ ├── components │ ├── components.gen.go │ ├── components.yaml │ ├── components_test.go │ ├── config.yaml │ └── doc.go │ ├── externalref │ ├── doc.go │ ├── externalref.cfg.yaml │ ├── externalref.gen.go │ ├── imports_test.go │ ├── packageA │ │ ├── doc.go │ │ ├── externalref.gen.go │ │ └── spec.yaml │ ├── packageB │ │ ├── doc.go │ │ ├── externalref.gen.go │ │ └── spec.yaml │ └── spec.yaml │ ├── issues │ ├── issue-312 │ │ ├── doc.go │ │ ├── issue.gen.go │ │ ├── issue_test.go │ │ └── spec.yaml │ ├── issue-52 │ │ ├── doc.go │ │ ├── issue.gen.go │ │ ├── issue_test.go │ │ └── spec.yaml │ ├── issue-579 │ │ ├── gen.go │ │ ├── issue.gen.go │ │ ├── issue_test.go │ │ └── spec.yaml │ ├── issue-grab_import_names │ │ ├── doc.go │ │ ├── issue.gen.go │ │ ├── issue_test.go │ │ └── spec.yaml │ └── issue-illegal_enum_names │ │ ├── doc.go │ │ ├── issue.gen.go │ │ ├── issue_test.go │ │ └── spec.yaml │ ├── parameters │ ├── doc.go │ ├── parameters.gen.go │ ├── parameters.yaml │ └── parameters_test.go │ ├── schemas │ ├── doc.go │ ├── schemas.gen.go │ └── schemas.yaml │ ├── server │ ├── doc.go │ ├── server.gen.go │ ├── server_moq.gen.go │ └── server_test.go │ └── test-schema.yaml ├── pkg ├── chi-middleware │ ├── oapi_validate.go │ ├── oapi_validate_test.go │ └── test_spec.yaml ├── codegen │ ├── codegen.go │ ├── codegen_test.go │ ├── configuration.go │ ├── extension.go │ ├── extension_test.go │ ├── filter.go │ ├── filter_test.go │ ├── inline.go │ ├── merge_schemas.go │ ├── merge_schemas_v1.go │ ├── operations.go │ ├── operations_test.go │ ├── prune.go │ ├── prune_test.go │ ├── schema.go │ ├── template_helpers.go │ ├── templates │ │ ├── additional-properties.tmpl │ │ ├── chi │ │ │ ├── chi-handler.tmpl │ │ │ ├── chi-interface.tmpl │ │ │ └── chi-middleware.tmpl │ │ ├── client-with-responses.tmpl │ │ ├── client.tmpl │ │ ├── constants.tmpl │ │ ├── echo │ │ │ ├── echo-interface.tmpl │ │ │ ├── echo-register.tmpl │ │ │ └── echo-wrappers.tmpl │ │ ├── gin │ │ │ ├── gin-interface.tmpl │ │ │ ├── gin-register.tmpl │ │ │ └── gin-wrappers.tmpl │ │ ├── imports.tmpl │ │ ├── inline.tmpl │ │ ├── param-types.tmpl │ │ ├── request-bodies.tmpl │ │ └── typedef.tmpl │ ├── test_spec.yaml │ ├── utils.go │ └── utils_test.go ├── ecdsafile │ └── ecdsafile.go ├── gin-middleware │ ├── oapi_validate.go │ ├── oapi_validate_test.go │ └── test_spec.yaml ├── middleware │ ├── oapi_validate.go │ ├── oapi_validate_test.go │ └── test_spec.yaml ├── runtime │ ├── bind.go │ ├── bindparam.go │ ├── bindparam_test.go │ ├── bindstring.go │ ├── bindstring_test.go │ ├── deepobject.go │ ├── deepobject_test.go │ ├── styleparam.go │ └── styleparam_test.go ├── securityprovider │ ├── securityprovider.go │ └── securityprovider_test.go ├── testutil │ ├── request_helpers.go │ └── response_handlers.go ├── types │ ├── date.go │ ├── date_test.go │ ├── email.go │ ├── email_test.go │ ├── regexes.go │ ├── uuid.go │ └── uuid_test.go └── util │ ├── inputmapping.go │ ├── inputmapping_test.go │ └── loader.go └── tools └── tools.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [Makefile] 4 | indent_size = 8 5 | indent_style = tab 6 | 7 | [{go.mod,go.sum,*.go}] 8 | indent_size = 4 9 | indent_style = tab 10 | 11 | [{*.yml,*.yaml}] 12 | indent_size = 2 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.gen.go linguist-generated 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.16.x 4 | env: 5 | global: 6 | - GO111MODULE: "on" 7 | - CGO_ENABLED: "0" 8 | script: 9 | - make tidy 10 | - make generate 11 | - make test 12 | - git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] 13 | notifications: 14 | email: 15 | on_success: never 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "This is a helper makefile for oapi-codegen" 3 | @echo "Targets:" 4 | @echo " generate: regenerate all generated files" 5 | @echo " test: run all tests" 6 | @echo " gin_example generate gin example server code" 7 | @echo " tidy tidy go mod" 8 | 9 | generate: 10 | go generate ./... 11 | 12 | test: 13 | go test -cover ./... 14 | 15 | tidy: 16 | @echo "tidy..." 17 | go mod tidy 18 | -------------------------------------------------------------------------------- /cmd/oapi-codegen/oapi-codegen_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/algorand/oapi-codegen/pkg/util" 7 | ) 8 | 9 | func TestLoader(t *testing.T) { 10 | 11 | paths := []string{ 12 | "../../examples/petstore-expanded/petstore-expanded.yaml", 13 | "https://petstore3.swagger.io/api/v3/openapi.json", 14 | } 15 | 16 | for _, v := range paths { 17 | 18 | swagger, err := util.LoadSwagger(v) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | if swagger == nil || swagger.Info == nil || swagger.Info.Version == "" { 23 | t.Error("missing data") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/authenticated-api/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Authenticated API Example 5 | description: An example API which uses bearer token scopes and JWT auth 6 | paths: 7 | /things: 8 | get: 9 | operationId: listThings 10 | description: | 11 | Returns a list of things. Because this endpoint doesn't override the 12 | global security, it requires a JWT for authentication. 13 | responses: 14 | 200: 15 | description: a list of things 16 | content: 17 | application/json: 18 | schema: 19 | type: array 20 | items: 21 | $ref: '#/components/schemas/ThingWithID' 22 | post: 23 | operationId: addThing 24 | description: | 25 | Adds a thing to the list of things. This endpoints overrides the global 26 | security scheme and requires a `things:w` scope in order to perform a 27 | write. 28 | security: 29 | - BearerAuth: 30 | - "things:w" 31 | requestBody: 32 | description: A thing to insert. Returns the inserted thing with an ID 33 | required: true 34 | content: 35 | application/json: 36 | schema: 37 | $ref: '#/components/schemas/Thing' 38 | responses: 39 | 201: 40 | description: The inserted Thing with a unique ID 41 | content: 42 | application/json: 43 | schema: 44 | type: array 45 | items: 46 | $ref: '#/components/schemas/ThingWithID' 47 | components: 48 | schemas: 49 | Thing: 50 | properties: 51 | name: 52 | type: string 53 | required: 54 | - name 55 | ThingWithID: 56 | allOf: 57 | - $ref: '#/components/schemas/Thing' 58 | - properties: 59 | id: 60 | type: integer 61 | format: int64 62 | required: 63 | - id 64 | Error: 65 | required: 66 | - code 67 | - message 68 | properties: 69 | code: 70 | type: integer 71 | format: int32 72 | description: Error code 73 | message: 74 | type: string 75 | description: Error message 76 | securitySchemes: 77 | BearerAuth: 78 | type: http 79 | scheme: bearer 80 | bearerFormat: JWT 81 | security: 82 | - BearerAuth: [ ] 83 | 84 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/api/config.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | echo-server: true 4 | client: true 5 | models: true 6 | embedded-spec: true 7 | output: api.gen.go 8 | output-options: 9 | skip-prune: true 10 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/api/doc.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=config.yaml ../../api.yaml 4 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/algorand/oapi-codegen/examples/authenticated-api/echo/api" 9 | "github.com/algorand/oapi-codegen/examples/authenticated-api/echo/server" 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | ) 13 | 14 | func main() { 15 | var port = flag.Int("port", 8080, "port where to serve traffic") 16 | 17 | e := echo.New() 18 | 19 | // Create a fake authenticator. This allows us to issue tokens, and also 20 | // implements a validator to check their validity. 21 | fa, err := server.NewFakeAuthenticator() 22 | if err != nil { 23 | log.Fatalln("error creating authenticator:", err) 24 | } 25 | 26 | // Create middleware for validating tokens. 27 | mw, err := server.CreateMiddleware(fa) 28 | if err != nil { 29 | log.Fatalln("error creating middleware:", err) 30 | } 31 | e.Use(middleware.Logger()) 32 | e.Use(mw...) 33 | 34 | svr := server.NewServer() 35 | 36 | api.RegisterHandlers(e, svr) 37 | 38 | // We're going to print some useful things for interacting with this server. 39 | // This token allows access to any API's with no specific claims. 40 | readerJWS, err := fa.CreateJWSWithClaims([]string{}) 41 | if err != nil { 42 | log.Fatalln("error creating reader JWS:", err) 43 | } 44 | // This token allows access to API's with no scopes, and with the "things:w" claim. 45 | writerJWS, err := fa.CreateJWSWithClaims([]string{"things:w"}) 46 | if err != nil { 47 | log.Fatalln("error creating writer JWS:", err) 48 | } 49 | 50 | log.Println("Reader token", string(readerJWS)) 51 | log.Println("Writer token", string(writerJWS)) 52 | 53 | e.Logger.Fatal(e.Start(fmt.Sprintf("0.0.0.0:%d", *port))) 54 | } 55 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/server/fake_jws.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "fmt" 6 | 7 | "github.com/algorand/oapi-codegen/pkg/ecdsafile" 8 | "github.com/lestrrat-go/jwx/jwa" 9 | "github.com/lestrrat-go/jwx/jwk" 10 | "github.com/lestrrat-go/jwx/jws" 11 | "github.com/lestrrat-go/jwx/jwt" 12 | ) 13 | 14 | // PrivateKey is an ECDSA private key which was generated with the following 15 | // command: 16 | // openssl ecparam -name prime256v1 -genkey -noout -out ecprivatekey.pem 17 | // 18 | // We are using a hard coded key here in this example, but in real applications, 19 | // you would never do this. Your JWT signing key must never be in your application, 20 | // only the public key. 21 | const PrivateKey = `-----BEGIN EC PRIVATE KEY----- 22 | MHcCAQEEIN2dALnjdcZaIZg4QuA6Dw+kxiSW502kJfmBN3priIhPoAoGCCqGSM49 23 | AwEHoUQDQgAE4pPyvrB9ghqkT1Llk0A42lixkugFd/TBdOp6wf69O9Nndnp4+HcR 24 | s9SlG/8hjB2Hz42v4p3haKWv3uS1C6ahCQ== 25 | -----END EC PRIVATE KEY-----` 26 | 27 | const KeyID = `fake-key-id` 28 | const FakeIssuer = "fake-issuer" 29 | const FakeAudience = "example-users" 30 | const PermissionsClaim = "perm" 31 | 32 | type FakeAuthenticator struct { 33 | PrivateKey *ecdsa.PrivateKey 34 | KeySet jwk.Set 35 | } 36 | 37 | var _ JWSValidator = (*FakeAuthenticator)(nil) 38 | 39 | // NewFakeAuthenticator creates an authenticator example which uses a hard coded 40 | // ECDSA key to validate JWT's that it has signed itself. 41 | func NewFakeAuthenticator() (*FakeAuthenticator, error) { 42 | privKey, err := ecdsafile.LoadEcdsaPrivateKey([]byte(PrivateKey)) 43 | if err != nil { 44 | return nil, fmt.Errorf("loading PEM private key: %w", err) 45 | } 46 | 47 | set := jwk.NewSet() 48 | pubKey := jwk.NewECDSAPublicKey() 49 | 50 | err = pubKey.FromRaw(&privKey.PublicKey) 51 | if err != nil { 52 | return nil, fmt.Errorf("parsing jwk key: %w", err) 53 | } 54 | 55 | err = pubKey.Set(jwk.AlgorithmKey, jwa.ES256) 56 | if err != nil { 57 | return nil, fmt.Errorf("setting key algorithm: %w", err) 58 | } 59 | 60 | err = pubKey.Set(jwk.KeyIDKey, KeyID) 61 | if err != nil { 62 | return nil, fmt.Errorf("setting key ID: %w", err) 63 | } 64 | 65 | set.Add(pubKey) 66 | 67 | return &FakeAuthenticator{PrivateKey: privKey, KeySet: set}, nil 68 | } 69 | 70 | // ValidateJWS ensures that the critical JWT claims needed to ensure that we 71 | // trust the JWT are present and with the correct values. 72 | func (f *FakeAuthenticator) ValidateJWS(jwsString string) (jwt.Token, error) { 73 | return jwt.Parse([]byte(jwsString), jwt.WithKeySet(f.KeySet), 74 | jwt.WithAudience(FakeAudience), jwt.WithIssuer(FakeIssuer)) 75 | } 76 | 77 | // SignToken takes a JWT and signs it with our priviate key, returning a JWS. 78 | func (f *FakeAuthenticator) SignToken(t jwt.Token) ([]byte, error) { 79 | hdr := jws.NewHeaders() 80 | if err := hdr.Set(jws.AlgorithmKey, jwa.ES256); err != nil { 81 | return nil, fmt.Errorf("setting algorithm: %w", err) 82 | } 83 | if err := hdr.Set(jws.TypeKey, "JWT"); err != nil { 84 | return nil, fmt.Errorf("setting type: %w", err) 85 | } 86 | if err := hdr.Set(jws.KeyIDKey, KeyID); err != nil { 87 | return nil, fmt.Errorf("setting Key ID: %w", err) 88 | } 89 | return jwt.Sign(t, jwa.ES256, f.PrivateKey, jwt.WithHeaders(hdr)) 90 | } 91 | 92 | // CreateJWSWithClaims is a helper function to create JWT's with the specified 93 | // claims. 94 | func (f *FakeAuthenticator) CreateJWSWithClaims(claims []string) ([]byte, error) { 95 | t := jwt.New() 96 | err := t.Set(jwt.IssuerKey, FakeIssuer) 97 | if err != nil { 98 | return nil, fmt.Errorf("setting issuer: %w", err) 99 | } 100 | err = t.Set(jwt.AudienceKey, FakeAudience) 101 | if err != nil { 102 | return nil, fmt.Errorf("setting audience: %w", err) 103 | } 104 | err = t.Set(PermissionsClaim, claims) 105 | if err != nil { 106 | return nil, fmt.Errorf("setting permissions: %w", err) 107 | } 108 | return f.SignToken(t) 109 | } 110 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/server/jwt_authenticator.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/getkin/kin-openapi/openapi3filter" 11 | "github.com/lestrrat-go/jwx/jwt" 12 | ) 13 | 14 | // JWSValidator is used to validate JWS payloads and return a JWT if they're 15 | // valid 16 | type JWSValidator interface { 17 | ValidateJWS(jws string) (jwt.Token, error) 18 | } 19 | 20 | var ErrNoAuthHeader = errors.New("Authorization header is missing") 21 | var ErrInvalidAuthHeader = errors.New("Authorization header is malformed") 22 | var ErrClaimsInvalid = errors.New("Provided claims do not match expected scopes") 23 | 24 | // GetJWSFromRequest extracts a JWS string from an Authorization: Bearer header 25 | func GetJWSFromRequest(req *http.Request) (string, error) { 26 | authHdr := req.Header.Get("Authorization") 27 | // Check for the Authorization header. 28 | if authHdr == "" { 29 | return "", ErrNoAuthHeader 30 | } 31 | // We expect a header value of the form "Bearer ", with 1 space after 32 | // Bearer, per spec. 33 | prefix := "Bearer " 34 | if !strings.HasPrefix(authHdr, prefix) { 35 | return "", ErrInvalidAuthHeader 36 | } 37 | return strings.TrimPrefix(authHdr, prefix), nil 38 | } 39 | 40 | func NewAuthenticator(v JWSValidator) openapi3filter.AuthenticationFunc { 41 | return func(ctx context.Context, input *openapi3filter.AuthenticationInput) error { 42 | return Authenticate(v, ctx, input) 43 | } 44 | } 45 | 46 | // Authenticate uses the specified validator to ensure a JWT is valid, then makes 47 | // sure that the claims provided by the JWT match the scopes as required in the API. 48 | func Authenticate(v JWSValidator, ctx context.Context, input *openapi3filter.AuthenticationInput) error { 49 | // Our security scheme is named BearerAuth, ensure this is the case 50 | if input.SecuritySchemeName != "BearerAuth" { 51 | return fmt.Errorf("security scheme %s != 'BearerAuth'", input.SecuritySchemeName) 52 | } 53 | 54 | // Now, we need to get the JWS from the request, to match the request expectations 55 | // against request contents. 56 | jws, err := GetJWSFromRequest(input.RequestValidationInput.Request) 57 | if err != nil { 58 | return fmt.Errorf("getting jws: %w", err) 59 | } 60 | 61 | // if the JWS is valid, we have a JWT, which will contain a bunch of claims. 62 | token, err := v.ValidateJWS(jws) 63 | if err != nil { 64 | return fmt.Errorf("validating JWS: %w", err) 65 | } 66 | 67 | // We've got a valid token now, and we can look into its claims to see whether 68 | // they match. Every single scope must be present in the claims. 69 | err = CheckTokenClaims(input.Scopes, token) 70 | 71 | if err != nil { 72 | return fmt.Errorf("token claims don't match: %w", err) 73 | } 74 | return nil 75 | } 76 | 77 | // GetClaimsFromToken returns a list of claims from the token. We store these 78 | // as a list under the "perms" claim, short for permissions, to keep the token 79 | // shorter. 80 | func GetClaimsFromToken(t jwt.Token) ([]string, error) { 81 | rawPerms, found := t.Get(PermissionsClaim) 82 | if !found { 83 | // If the perms aren't found, it means that the token has none, but it has 84 | // passed signature validation by now, so it's a valid token, so we return 85 | // the empty list. 86 | return make([]string, 0), nil 87 | } 88 | 89 | // rawPerms will be an untyped JSON list, so we need to convert it to 90 | // a string list. 91 | rawList, ok := rawPerms.([]interface{}) 92 | if !ok { 93 | return nil, fmt.Errorf("'%s' claim is unexpected type'", PermissionsClaim) 94 | } 95 | 96 | claims := make([]string, len(rawList)) 97 | 98 | for i, rawClaim := range rawList { 99 | var ok bool 100 | claims[i], ok = rawClaim.(string) 101 | if !ok { 102 | return nil, fmt.Errorf("%s[%d] is not a string", PermissionsClaim, i) 103 | } 104 | } 105 | return claims, nil 106 | } 107 | 108 | func CheckTokenClaims(expectedClaims []string, t jwt.Token) error { 109 | claims, err := GetClaimsFromToken(t) 110 | if err != nil { 111 | return fmt.Errorf("getting claims from token: %w", err) 112 | } 113 | // Put the claims into a map, for quick access. 114 | claimsMap := make(map[string]bool, len(claims)) 115 | for _, c := range claims { 116 | claimsMap[c] = true 117 | } 118 | 119 | for _, e := range expectedClaims { 120 | if !claimsMap[e] { 121 | return ErrClaimsInvalid 122 | } 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sort" 7 | "sync" 8 | 9 | "github.com/getkin/kin-openapi/openapi3filter" 10 | "github.com/labstack/echo/v4" 11 | 12 | "github.com/algorand/oapi-codegen/examples/authenticated-api/echo/api" 13 | "github.com/algorand/oapi-codegen/pkg/middleware" 14 | ) 15 | 16 | type server struct { 17 | sync.RWMutex 18 | lastID int64 19 | things map[int64]api.Thing 20 | } 21 | 22 | func NewServer() *server { 23 | return &server{ 24 | lastID: 0, 25 | things: make(map[int64]api.Thing), 26 | } 27 | } 28 | 29 | func CreateMiddleware(v JWSValidator) ([]echo.MiddlewareFunc, error) { 30 | spec, err := api.GetSwagger() 31 | if err != nil { 32 | return nil, fmt.Errorf("loading spec: %w", err) 33 | } 34 | 35 | validator := middleware.OapiRequestValidatorWithOptions(spec, 36 | &middleware.Options{ 37 | Options: openapi3filter.Options{ 38 | AuthenticationFunc: NewAuthenticator(v), 39 | }, 40 | }) 41 | 42 | return []echo.MiddlewareFunc{validator}, nil 43 | } 44 | 45 | // Ensure that we implement the server interface 46 | var _ api.ServerInterface = (*server)(nil) 47 | 48 | func (s *server) ListThings(ctx echo.Context) error { 49 | // This handler will only be called when a valid JWT is presented for 50 | // access. 51 | s.RLock() 52 | 53 | thingKeys := make([]int64, 0, len(s.things)) 54 | for key := range s.things { 55 | thingKeys = append(thingKeys, key) 56 | } 57 | sort.Sort(int64s(thingKeys)) 58 | 59 | things := make([]api.ThingWithID, 0, len(s.things)) 60 | 61 | for _, key := range thingKeys { 62 | thing := s.things[key] 63 | things = append(things, api.ThingWithID{ 64 | Id: key, 65 | Name: thing.Name, 66 | }) 67 | } 68 | 69 | s.RUnlock() 70 | 71 | return ctx.JSON(http.StatusOK, things) 72 | } 73 | 74 | type int64s []int64 75 | 76 | func (in int64s) Len() int { 77 | return len(in) 78 | } 79 | 80 | func (in int64s) Less(i, j int) bool { 81 | return in[i] < in[j] 82 | } 83 | 84 | func (in int64s) Swap(i, j int) { 85 | in[i], in[j] = in[j], in[i] 86 | } 87 | 88 | var _ sort.Interface = (int64s)(nil) 89 | 90 | func (s *server) AddThing(ctx echo.Context) error { 91 | // This handler will only be called when the JWT is valid and the JWT contains 92 | // the scopes required. 93 | var thing api.Thing 94 | err := ctx.Bind(&thing) 95 | if err != nil { 96 | return returnError(ctx, http.StatusBadRequest, "could not bind request body") 97 | } 98 | 99 | s.Lock() 100 | defer s.Unlock() 101 | 102 | s.things[s.lastID] = thing 103 | thingWithId := api.ThingWithID{ 104 | Name: thing.Name, 105 | Id: s.lastID, 106 | } 107 | s.lastID++ 108 | 109 | return ctx.JSON(http.StatusCreated, thingWithId) 110 | } 111 | 112 | func returnError(ctx echo.Context, code int, message string) error { 113 | errResponse := api.Error{ 114 | Code: int32(code), 115 | Message: message, 116 | } 117 | return ctx.JSON(code, errResponse) 118 | } 119 | -------------------------------------------------------------------------------- /examples/authenticated-api/echo/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/algorand/oapi-codegen/examples/authenticated-api/echo/api" 8 | "github.com/algorand/oapi-codegen/pkg/testutil" 9 | "github.com/labstack/echo/v4" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestAPI(t *testing.T) { 15 | e := echo.New() 16 | s := NewServer() 17 | 18 | fa, err := NewFakeAuthenticator() 19 | require.NoError(t, err) 20 | 21 | mw, err := CreateMiddleware(fa) 22 | require.NoError(t, err) 23 | e.Use(mw...) 24 | api.RegisterHandlers(e, s) 25 | 26 | // Let's create a JWT with no scopes, which allows access to listing things. 27 | readerJWT, err := fa.CreateJWSWithClaims([]string{}) 28 | require.NoError(t, err) 29 | t.Logf("reader jwt: %s", string(readerJWT)) 30 | 31 | // Now, create a JWT with write permission. 32 | writerJWT, err := fa.CreateJWSWithClaims([]string{"things:w"}) 33 | require.NoError(t, err) 34 | t.Logf("writer jwt: %s", string(writerJWT)) 35 | 36 | // ListPets should return 403 forbidden without credentials 37 | response := testutil.NewRequest().Get("/things").Go(t, e) 38 | assert.Equal(t, http.StatusForbidden, response.Code()) 39 | 40 | // Using the writer JWT should allow us to insert a thing. 41 | response = testutil.NewRequest().Post("/things"). 42 | WithJWSAuth(string(writerJWT)). 43 | WithAcceptJson(). 44 | WithJsonBody(api.Thing{Name: "Thing 1"}).Go(t, e) 45 | require.Equal(t, http.StatusCreated, response.Code()) 46 | 47 | // Using the reader JWT should forbid inserting a thing. 48 | response = testutil.NewRequest().Post("/things"). 49 | WithJWSAuth(string(readerJWT)). 50 | WithAcceptJson(). 51 | WithJsonBody(api.Thing{Name: "Thing 2"}).Go(t, e) 52 | require.Equal(t, http.StatusForbidden, response.Code()) 53 | 54 | // Both JWT's should allow reading the list of things. 55 | jwts := []string{string(readerJWT), string(writerJWT)} 56 | for _, jwt := range jwts { 57 | response := testutil.NewRequest().Get("/things"). 58 | WithJWSAuth(string(jwt)). 59 | WithAcceptJson().Go(t, e) 60 | assert.Equal(t, http.StatusOK, response.Code()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/petstore-expanded/README.md: -------------------------------------------------------------------------------- 1 | OpenAPI Code Generation Example 2 | ------------------------------- 3 | 4 | This directory contains an example server using our code generator which implements 5 | the OpenAPI [petstore-expanded](https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml) 6 | example. 7 | 8 | This is the structure: 9 | - `api/`: Contains the OpenAPI 3.0 specification 10 | - `api/petstore/`: The generated code for our pet store handlers 11 | - `internal/`: Pet store handler implementation and unit tests 12 | - `cmd/`: Runnable server implementing the OpenAPI 3 spec. 13 | 14 | To generate the handler glue, run: 15 | 16 | go run cmd/oapi-codegen/oapi-codegen.go --package petstore examples/petstore-expanded/petstore-expanded.yaml > examples/petstore-expanded/petstore.gen.go 17 | -------------------------------------------------------------------------------- /examples/petstore-expanded/chi/api/cfg.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | chi-server: true 4 | models: true 5 | embedded-spec: true 6 | output: petstore.gen.go 7 | -------------------------------------------------------------------------------- /examples/petstore-expanded/chi/api/petstore.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=cfg.yaml ../../petstore-expanded.yaml 2 | 3 | package api 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | type PetStore struct { 13 | Pets map[int64]Pet 14 | NextId int64 15 | Lock sync.Mutex 16 | } 17 | 18 | // Make sure we conform to ServerInterface 19 | 20 | var _ ServerInterface = (*PetStore)(nil) 21 | 22 | func NewPetStore() *PetStore { 23 | return &PetStore{ 24 | Pets: make(map[int64]Pet), 25 | NextId: 1000, 26 | } 27 | } 28 | 29 | // This function wraps sending of an error in the Error format, and 30 | // handling the failure to marshal that. 31 | func sendPetstoreError(w http.ResponseWriter, code int, message string) { 32 | petErr := Error{ 33 | Code: int32(code), 34 | Message: message, 35 | } 36 | w.WriteHeader(code) 37 | json.NewEncoder(w).Encode(petErr) 38 | } 39 | 40 | // Here, we implement all of the handlers in the ServerInterface 41 | func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { 42 | p.Lock.Lock() 43 | defer p.Lock.Unlock() 44 | 45 | var result []Pet 46 | 47 | for _, pet := range p.Pets { 48 | if params.Tags != nil { 49 | // If we have tags, filter pets by tag 50 | for _, t := range *params.Tags { 51 | if pet.Tag != nil && (*pet.Tag == t) { 52 | result = append(result, pet) 53 | } 54 | } 55 | } else { 56 | // Add all pets if we're not filtering 57 | result = append(result, pet) 58 | } 59 | 60 | if params.Limit != nil { 61 | l := int(*params.Limit) 62 | if len(result) >= l { 63 | // We're at the limit 64 | break 65 | } 66 | } 67 | } 68 | 69 | w.WriteHeader(http.StatusOK) 70 | json.NewEncoder(w).Encode(result) 71 | } 72 | 73 | func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { 74 | // We expect a NewPet object in the request body. 75 | var newPet NewPet 76 | if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { 77 | sendPetstoreError(w, http.StatusBadRequest, "Invalid format for NewPet") 78 | return 79 | } 80 | 81 | // We now have a pet, let's add it to our "database". 82 | 83 | // We're always asynchronous, so lock unsafe operations below 84 | p.Lock.Lock() 85 | defer p.Lock.Unlock() 86 | 87 | // We handle pets, not NewPets, which have an additional ID field 88 | var pet Pet 89 | pet.Name = newPet.Name 90 | pet.Tag = newPet.Tag 91 | pet.Id = p.NextId 92 | p.NextId = p.NextId + 1 93 | 94 | // Insert into map 95 | p.Pets[pet.Id] = pet 96 | 97 | // Now, we have to return the NewPet 98 | w.WriteHeader(http.StatusCreated) 99 | json.NewEncoder(w).Encode(pet) 100 | } 101 | 102 | func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { 103 | p.Lock.Lock() 104 | defer p.Lock.Unlock() 105 | 106 | pet, found := p.Pets[id] 107 | if !found { 108 | sendPetstoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) 109 | return 110 | } 111 | 112 | w.WriteHeader(http.StatusOK) 113 | json.NewEncoder(w).Encode(pet) 114 | } 115 | 116 | func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { 117 | p.Lock.Lock() 118 | defer p.Lock.Unlock() 119 | 120 | _, found := p.Pets[id] 121 | if !found { 122 | sendPetstoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) 123 | return 124 | } 125 | delete(p.Pets, id) 126 | 127 | w.WriteHeader(http.StatusNoContent) 128 | } 129 | -------------------------------------------------------------------------------- /examples/petstore-expanded/chi/petstore.go: -------------------------------------------------------------------------------- 1 | // This is an example of implementing the Pet Store from the OpenAPI documentation 2 | // found at: 3 | // https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "github.com/go-chi/chi/v5" 15 | 16 | api "github.com/algorand/oapi-codegen/examples/petstore-expanded/chi/api" 17 | middleware "github.com/algorand/oapi-codegen/pkg/chi-middleware" 18 | ) 19 | 20 | func main() { 21 | var port = flag.Int("port", 8080, "Port for test HTTP server") 22 | flag.Parse() 23 | 24 | swagger, err := api.GetSwagger() 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) 27 | os.Exit(1) 28 | } 29 | 30 | // Clear out the servers array in the swagger spec, that skips validating 31 | // that server names match. We don't know how this thing will be run. 32 | swagger.Servers = nil 33 | 34 | // Create an instance of our handler which satisfies the generated interface 35 | petStore := api.NewPetStore() 36 | 37 | // This is how you set up a basic chi router 38 | r := chi.NewRouter() 39 | 40 | // Use our validation middleware to check all requests against the 41 | // OpenAPI schema. 42 | r.Use(middleware.OapiRequestValidator(swagger)) 43 | 44 | // We now register our petStore above as the handler for the interface 45 | api.HandlerFromMux(petStore, r) 46 | 47 | s := &http.Server{ 48 | Handler: r, 49 | Addr: fmt.Sprintf("0.0.0.0:%d", *port), 50 | } 51 | 52 | // And we serve HTTP until the world ends. 53 | log.Fatal(s.ListenAndServe()) 54 | } 55 | -------------------------------------------------------------------------------- /examples/petstore-expanded/chi/petstore_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/go-chi/chi/v5" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/algorand/oapi-codegen/examples/petstore-expanded/chi/api" 15 | middleware "github.com/algorand/oapi-codegen/pkg/chi-middleware" 16 | "github.com/algorand/oapi-codegen/pkg/testutil" 17 | ) 18 | 19 | func doGet(t *testing.T, mux *chi.Mux, url string) *httptest.ResponseRecorder { 20 | response := testutil.NewRequest().Get(url).WithAcceptJson().GoWithHTTPHandler(t, mux) 21 | return response.Recorder 22 | } 23 | 24 | func TestPetStore(t *testing.T) { 25 | var err error 26 | 27 | // Get the swagger description of our API 28 | swagger, err := api.GetSwagger() 29 | require.NoError(t, err) 30 | 31 | // Clear out the servers array in the swagger spec, that skips validating 32 | // that server names match. We don't know how this thing will be run. 33 | swagger.Servers = nil 34 | 35 | // This is how you set up a basic chi router 36 | r := chi.NewRouter() 37 | 38 | // Use our validation middleware to check all requests against the 39 | // OpenAPI schema. 40 | r.Use(middleware.OapiRequestValidator(swagger)) 41 | 42 | store := api.NewPetStore() 43 | api.HandlerFromMux(store, r) 44 | 45 | t.Run("Add pet", func(t *testing.T) { 46 | tag := "TagOfSpot" 47 | newPet := api.NewPet{ 48 | Name: "Spot", 49 | Tag: &tag, 50 | } 51 | 52 | rr := testutil.NewRequest().Post("/pets").WithJsonBody(newPet).GoWithHTTPHandler(t, r).Recorder 53 | assert.Equal(t, http.StatusCreated, rr.Code) 54 | 55 | var resultPet api.Pet 56 | err = json.NewDecoder(rr.Body).Decode(&resultPet) 57 | assert.NoError(t, err, "error unmarshaling response") 58 | assert.Equal(t, newPet.Name, resultPet.Name) 59 | assert.Equal(t, *newPet.Tag, *resultPet.Tag) 60 | }) 61 | 62 | t.Run("Find pet by ID", func(t *testing.T) { 63 | pet := api.Pet{ 64 | Id: 100, 65 | } 66 | 67 | store.Pets[pet.Id] = pet 68 | rr := doGet(t, r, fmt.Sprintf("/pets/%d", pet.Id)) 69 | 70 | var resultPet api.Pet 71 | err = json.NewDecoder(rr.Body).Decode(&resultPet) 72 | assert.NoError(t, err, "error getting pet") 73 | assert.Equal(t, pet, resultPet) 74 | }) 75 | 76 | t.Run("Pet not found", func(t *testing.T) { 77 | rr := doGet(t, r, "/pets/27179095781") 78 | assert.Equal(t, http.StatusNotFound, rr.Code) 79 | 80 | var petError api.Error 81 | err = json.NewDecoder(rr.Body).Decode(&petError) 82 | assert.NoError(t, err, "error getting response", err) 83 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 84 | }) 85 | 86 | t.Run("List all pets", func(t *testing.T) { 87 | store.Pets = map[int64]api.Pet{ 88 | 1: api.Pet{}, 89 | 2: api.Pet{}, 90 | } 91 | 92 | // Now, list all pets, we should have two 93 | rr := doGet(t, r, "/pets") 94 | assert.Equal(t, http.StatusOK, rr.Code) 95 | 96 | var petList []api.Pet 97 | err = json.NewDecoder(rr.Body).Decode(&petList) 98 | assert.NoError(t, err, "error getting response", err) 99 | assert.Equal(t, 2, len(petList)) 100 | }) 101 | 102 | t.Run("Filter pets by tag", func(t *testing.T) { 103 | tag := "TagOfFido" 104 | 105 | store.Pets = map[int64]api.Pet{ 106 | 1: { 107 | Tag: &tag, 108 | }, 109 | 2: {}, 110 | } 111 | 112 | // Filter pets by tag, we should have 1 113 | rr := doGet(t, r, "/pets?tags=TagOfFido") 114 | assert.Equal(t, http.StatusOK, rr.Code) 115 | 116 | var petList []api.Pet 117 | err = json.NewDecoder(rr.Body).Decode(&petList) 118 | assert.NoError(t, err, "error getting response", err) 119 | assert.Equal(t, 1, len(petList)) 120 | }) 121 | 122 | t.Run("Filter pets by tag", func(t *testing.T) { 123 | store.Pets = map[int64]api.Pet{ 124 | 1: api.Pet{}, 125 | 2: api.Pet{}, 126 | } 127 | 128 | // Filter pets by non existent tag, we should have 0 129 | rr := doGet(t, r, "/pets?tags=NotExists") 130 | assert.Equal(t, http.StatusOK, rr.Code) 131 | 132 | var petList []api.Pet 133 | err = json.NewDecoder(rr.Body).Decode(&petList) 134 | assert.NoError(t, err, "error getting response", err) 135 | assert.Equal(t, 0, len(petList)) 136 | }) 137 | 138 | t.Run("Delete pets", func(t *testing.T) { 139 | store.Pets = map[int64]api.Pet{ 140 | 1: api.Pet{}, 141 | 2: api.Pet{}, 142 | } 143 | 144 | // Let's delete non-existent pet 145 | rr := testutil.NewRequest().Delete("/pets/7").GoWithHTTPHandler(t, r).Recorder 146 | assert.Equal(t, http.StatusNotFound, rr.Code) 147 | 148 | var petError api.Error 149 | err = json.NewDecoder(rr.Body).Decode(&petError) 150 | assert.NoError(t, err, "error unmarshaling PetError") 151 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 152 | 153 | // Now, delete both real pets 154 | rr = testutil.NewRequest().Delete("/pets/1").GoWithHTTPHandler(t, r).Recorder 155 | assert.Equal(t, http.StatusNoContent, rr.Code) 156 | 157 | rr = testutil.NewRequest().Delete("/pets/2").GoWithHTTPHandler(t, r).Recorder 158 | assert.Equal(t, http.StatusNoContent, rr.Code) 159 | 160 | // Should have no pets left. 161 | var petList []api.Pet 162 | rr = doGet(t, r, "/pets") 163 | assert.Equal(t, http.StatusOK, rr.Code) 164 | err = json.NewDecoder(rr.Body).Decode(&petList) 165 | assert.NoError(t, err, "error getting response", err) 166 | assert.Equal(t, 0, len(petList)) 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/api/petstore-types.gen.go: -------------------------------------------------------------------------------- 1 | // Package api provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package api 5 | 6 | // Error defines model for Error. 7 | type Error struct { 8 | // Error code 9 | Code int32 `json:"code"` 10 | 11 | // Error message 12 | Message string `json:"message"` 13 | } 14 | 15 | // NewPet defines model for NewPet. 16 | type NewPet struct { 17 | // Name of the pet 18 | Name string `json:"name"` 19 | 20 | // Type of the pet 21 | Tag *string `json:"tag,omitempty"` 22 | } 23 | 24 | // Pet defines model for Pet. 25 | type Pet struct { 26 | // Unique id of the pet 27 | Id int64 `json:"id"` 28 | 29 | // Name of the pet 30 | Name string `json:"name"` 31 | 32 | // Type of the pet 33 | Tag *string `json:"tag,omitempty"` 34 | } 35 | 36 | // FindPetsParams defines parameters for FindPets. 37 | type FindPetsParams struct { 38 | // tags to filter by 39 | Tags *[]string `form:"tags,omitempty" json:"tags,omitempty"` 40 | 41 | // maximum number of results to return 42 | Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` 43 | } 44 | 45 | // AddPetJSONBody defines parameters for AddPet. 46 | type AddPetJSONBody = NewPet 47 | 48 | // AddPetJSONRequestBody defines body for AddPet for application/json ContentType. 49 | type AddPetJSONRequestBody = AddPetJSONBody 50 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/api/petstore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=types.cfg.yaml ../../petstore-expanded.yaml 16 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=server.cfg.yaml ../../petstore-expanded.yaml 17 | 18 | package api 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | "sync" 24 | 25 | "github.com/labstack/echo/v4" 26 | ) 27 | 28 | type PetStore struct { 29 | Pets map[int64]Pet 30 | NextId int64 31 | Lock sync.Mutex 32 | } 33 | 34 | func NewPetStore() *PetStore { 35 | return &PetStore{ 36 | Pets: make(map[int64]Pet), 37 | NextId: 1000, 38 | } 39 | } 40 | 41 | // This function wraps sending of an error in the Error format, and 42 | // handling the failure to marshal that. 43 | func sendPetstoreError(ctx echo.Context, code int, message string) error { 44 | petErr := Error{ 45 | Code: int32(code), 46 | Message: message, 47 | } 48 | err := ctx.JSON(code, petErr) 49 | return err 50 | } 51 | 52 | // Here, we implement all of the handlers in the ServerInterface 53 | func (p *PetStore) FindPets(ctx echo.Context, params FindPetsParams) error { 54 | p.Lock.Lock() 55 | defer p.Lock.Unlock() 56 | 57 | var result []Pet 58 | 59 | for _, pet := range p.Pets { 60 | if params.Tags != nil { 61 | // If we have tags, filter pets by tag 62 | for _, t := range *params.Tags { 63 | if pet.Tag != nil && (*pet.Tag == t) { 64 | result = append(result, pet) 65 | } 66 | } 67 | } else { 68 | // Add all pets if we're not filtering 69 | result = append(result, pet) 70 | } 71 | 72 | if params.Limit != nil { 73 | l := int(*params.Limit) 74 | if len(result) >= l { 75 | // We're at the limit 76 | break 77 | } 78 | } 79 | } 80 | return ctx.JSON(http.StatusOK, result) 81 | } 82 | 83 | func (p *PetStore) AddPet(ctx echo.Context) error { 84 | // We expect a NewPet object in the request body. 85 | var newPet NewPet 86 | err := ctx.Bind(&newPet) 87 | if err != nil { 88 | return sendPetstoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") 89 | } 90 | // We now have a pet, let's add it to our "database". 91 | 92 | // We're always asynchronous, so lock unsafe operations below 93 | p.Lock.Lock() 94 | defer p.Lock.Unlock() 95 | 96 | // We handle pets, not NewPets, which have an additional ID field 97 | var pet Pet 98 | pet.Name = newPet.Name 99 | pet.Tag = newPet.Tag 100 | pet.Id = p.NextId 101 | p.NextId = p.NextId + 1 102 | 103 | // Insert into map 104 | p.Pets[pet.Id] = pet 105 | 106 | // Now, we have to return the NewPet 107 | err = ctx.JSON(http.StatusCreated, pet) 108 | if err != nil { 109 | // Something really bad happened, tell Echo that our handler failed 110 | return err 111 | } 112 | 113 | // Return no error. This refers to the handler. Even if we return an HTTP 114 | // error, but everything else is working properly, tell Echo that we serviced 115 | // the error. We should only return errors from Echo handlers if the actual 116 | // servicing of the error on the infrastructure level failed. Returning an 117 | // HTTP/400 or HTTP/500 from here means Echo/HTTP are still working, so 118 | // return nil. 119 | return nil 120 | } 121 | 122 | func (p *PetStore) FindPetByID(ctx echo.Context, petId int64) error { 123 | p.Lock.Lock() 124 | defer p.Lock.Unlock() 125 | 126 | pet, found := p.Pets[petId] 127 | if !found { 128 | return sendPetstoreError(ctx, http.StatusNotFound, 129 | fmt.Sprintf("Could not find pet with ID %d", petId)) 130 | } 131 | return ctx.JSON(http.StatusOK, pet) 132 | } 133 | 134 | func (p *PetStore) DeletePet(ctx echo.Context, id int64) error { 135 | p.Lock.Lock() 136 | defer p.Lock.Unlock() 137 | 138 | _, found := p.Pets[id] 139 | if !found { 140 | return sendPetstoreError(ctx, http.StatusNotFound, 141 | fmt.Sprintf("Could not find pet with ID %d", id)) 142 | } 143 | delete(p.Pets, id) 144 | return ctx.NoContent(http.StatusNoContent) 145 | } 146 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/api/server.cfg.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | echo-server: true 4 | embedded-spec: true 5 | output: petstore-server.gen.go 6 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/api/types.cfg.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | models: true 4 | output: petstore-types.gen.go 5 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/petstore.go: -------------------------------------------------------------------------------- 1 | // This is an example of implementing the Pet Store from the OpenAPI documentation 2 | // found at: 3 | // https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml 4 | // 5 | // The code under api/petstore/ has been generated from that specification. 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "os" 12 | 13 | "github.com/algorand/oapi-codegen/examples/petstore-expanded/echo/api" 14 | "github.com/algorand/oapi-codegen/pkg/middleware" 15 | "github.com/labstack/echo/v4" 16 | echomiddleware "github.com/labstack/echo/v4/middleware" 17 | ) 18 | 19 | func main() { 20 | var port = flag.Int("port", 8080, "Port for test HTTP server") 21 | flag.Parse() 22 | 23 | swagger, err := api.GetSwagger() 24 | if err != nil { 25 | fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) 26 | os.Exit(1) 27 | } 28 | 29 | // Clear out the servers array in the swagger spec, that skips validating 30 | // that server names match. We don't know how this thing will be run. 31 | swagger.Servers = nil 32 | 33 | // Create an instance of our handler which satisfies the generated interface 34 | petStore := api.NewPetStore() 35 | 36 | // This is how you set up a basic Echo router 37 | e := echo.New() 38 | // Log all requests 39 | e.Use(echomiddleware.Logger()) 40 | // Use our validation middleware to check all requests against the 41 | // OpenAPI schema. 42 | e.Use(middleware.OapiRequestValidator(swagger)) 43 | 44 | // We now register our petStore above as the handler for the interface 45 | api.RegisterHandlers(e, petStore) 46 | 47 | // And we serve HTTP until the world ends. 48 | e.Logger.Fatal(e.Start(fmt.Sprintf("0.0.0.0:%d", *port))) 49 | } 50 | -------------------------------------------------------------------------------- /examples/petstore-expanded/echo/petstore_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "testing" 21 | 22 | "github.com/labstack/echo/v4" 23 | echo_middleware "github.com/labstack/echo/v4/middleware" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | 27 | "github.com/algorand/oapi-codegen/examples/petstore-expanded/echo/api" 28 | "github.com/algorand/oapi-codegen/pkg/middleware" 29 | "github.com/algorand/oapi-codegen/pkg/testutil" 30 | ) 31 | 32 | func TestPetStore(t *testing.T) { 33 | var err error 34 | // Here, we Initialize echo 35 | e := echo.New() 36 | 37 | // Now, we create our empty pet store 38 | store := api.NewPetStore() 39 | 40 | // Get the swagger description of our API 41 | swagger, err := api.GetSwagger() 42 | require.NoError(t, err) 43 | 44 | // This disables swagger server name validation. It seems to work poorly, 45 | // and requires our test server to be in that list. 46 | swagger.Servers = nil 47 | 48 | // Validate requests against the OpenAPI spec 49 | e.Use(middleware.OapiRequestValidator(swagger)) 50 | 51 | // Log requests 52 | e.Use(echo_middleware.Logger()) 53 | 54 | // We register the autogenerated boilerplate and bind our PetStore to this 55 | // echo router. 56 | api.RegisterHandlers(e, store) 57 | 58 | // At this point, we can start sending simulated Http requests, and record 59 | // the HTTP responses to check for validity. This exercises every part of 60 | // the stack except the well-tested HTTP system in Go, which there is no 61 | // point for us to test. 62 | tag := "TagOfSpot" 63 | newPet := api.NewPet{ 64 | Name: "Spot", 65 | Tag: &tag, 66 | } 67 | result := testutil.NewRequest().Post("/pets").WithJsonBody(newPet).Go(t, e) 68 | // We expect 201 code on successful pet insertion 69 | assert.Equal(t, http.StatusCreated, result.Code()) 70 | 71 | // We should have gotten a response from the server with the new pet. Make 72 | // sure that its fields match. 73 | var resultPet api.Pet 74 | err = result.UnmarshalBodyToObject(&resultPet) 75 | assert.NoError(t, err, "error unmarshaling response") 76 | assert.Equal(t, newPet.Name, resultPet.Name) 77 | assert.Equal(t, *newPet.Tag, *resultPet.Tag) 78 | 79 | // This is the Id of the pet we inserted. 80 | petId := resultPet.Id 81 | 82 | // Test the getter function. 83 | result = testutil.NewRequest().Get(fmt.Sprintf("/pets/%d", petId)).WithAcceptJson().Go(t, e) 84 | var resultPet2 api.Pet 85 | err = result.UnmarshalBodyToObject(&resultPet2) 86 | assert.NoError(t, err, "error getting pet") 87 | assert.Equal(t, resultPet, resultPet2) 88 | 89 | // We should get a 404 on invalid ID 90 | result = testutil.NewRequest().Get("/pets/27179095781").WithAcceptJson().Go(t, e) 91 | assert.Equal(t, http.StatusNotFound, result.Code()) 92 | var petError api.Error 93 | err = result.UnmarshalBodyToObject(&petError) 94 | assert.NoError(t, err, "error getting response", err) 95 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 96 | 97 | // Let's insert another pet for subsequent tests. 98 | tag = "TagOfFido" 99 | newPet = api.NewPet{ 100 | Name: "Fido", 101 | Tag: &tag, 102 | } 103 | result = testutil.NewRequest().Post("/pets").WithJsonBody(newPet).Go(t, e) 104 | // We expect 201 code on successful pet insertion 105 | assert.Equal(t, http.StatusCreated, result.Code()) 106 | // We should have gotten a response from the server with the new pet. Make 107 | // sure that its fields match. 108 | err = result.UnmarshalBodyToObject(&resultPet) 109 | assert.NoError(t, err, "error unmarshaling response") 110 | petId2 := resultPet.Id 111 | 112 | // Now, list all pets, we should have two 113 | result = testutil.NewRequest().Get("/pets").WithAcceptJson().Go(t, e) 114 | assert.Equal(t, http.StatusOK, result.Code()) 115 | var petList []api.Pet 116 | err = result.UnmarshalBodyToObject(&petList) 117 | assert.NoError(t, err, "error getting response", err) 118 | assert.Equal(t, 2, len(petList)) 119 | 120 | // Filter pets by tag, we should have 1 121 | petList = nil 122 | result = testutil.NewRequest().Get("/pets?tags=TagOfFido").WithAcceptJson().Go(t, e) 123 | assert.Equal(t, http.StatusOK, result.Code()) 124 | err = result.UnmarshalBodyToObject(&petList) 125 | assert.NoError(t, err, "error getting response", err) 126 | assert.Equal(t, 1, len(petList)) 127 | 128 | // Filter pets by non existent tag, we should have 0 129 | petList = nil 130 | result = testutil.NewRequest().Get("/pets?tags=NotExists").WithAcceptJson().Go(t, e) 131 | assert.Equal(t, http.StatusOK, result.Code()) 132 | err = result.UnmarshalBodyToObject(&petList) 133 | assert.NoError(t, err, "error getting response", err) 134 | assert.Equal(t, 0, len(petList)) 135 | 136 | // Let's delete non-existent pet 137 | result = testutil.NewRequest().Delete("/pets/7").Go(t, e) 138 | assert.Equal(t, http.StatusNotFound, result.Code()) 139 | err = result.UnmarshalBodyToObject(&petError) 140 | assert.NoError(t, err, "error unmarshaling PetError") 141 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 142 | 143 | // Now, delete both real pets 144 | result = testutil.NewRequest().Delete(fmt.Sprintf("/pets/%d", petId)).Go(t, e) 145 | assert.Equal(t, http.StatusNoContent, result.Code()) 146 | result = testutil.NewRequest().Delete(fmt.Sprintf("/pets/%d", petId2)).Go(t, e) 147 | assert.Equal(t, http.StatusNoContent, result.Code()) 148 | 149 | // Should have no pets left. 150 | petList = nil 151 | result = testutil.NewRequest().Get("/pets").WithAcceptJson().Go(t, e) 152 | assert.Equal(t, http.StatusOK, result.Code()) 153 | err = result.UnmarshalBodyToObject(&petList) 154 | assert.NoError(t, err, "error getting response", err) 155 | assert.Equal(t, 0, len(petList)) 156 | } 157 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/api/petstore-types.gen.go: -------------------------------------------------------------------------------- 1 | // Package api provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package api 5 | 6 | // Error defines model for Error. 7 | type Error struct { 8 | // Error code 9 | Code int32 `json:"code"` 10 | 11 | // Error message 12 | Message string `json:"message"` 13 | } 14 | 15 | // NewPet defines model for NewPet. 16 | type NewPet struct { 17 | // Name of the pet 18 | Name string `json:"name"` 19 | 20 | // Type of the pet 21 | Tag *string `json:"tag,omitempty"` 22 | } 23 | 24 | // Pet defines model for Pet. 25 | type Pet struct { 26 | // Unique id of the pet 27 | Id int64 `json:"id"` 28 | 29 | // Name of the pet 30 | Name string `json:"name"` 31 | 32 | // Type of the pet 33 | Tag *string `json:"tag,omitempty"` 34 | } 35 | 36 | // FindPetsParams defines parameters for FindPets. 37 | type FindPetsParams struct { 38 | // tags to filter by 39 | Tags *[]string `form:"tags,omitempty" json:"tags,omitempty"` 40 | 41 | // maximum number of results to return 42 | Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` 43 | } 44 | 45 | // AddPetJSONBody defines parameters for AddPet. 46 | type AddPetJSONBody = NewPet 47 | 48 | // AddPetJSONRequestBody defines body for AddPet for application/json ContentType. 49 | type AddPetJSONRequestBody = AddPetJSONBody 50 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/api/petstore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=types.cfg.yaml ../../petstore-expanded.yaml 16 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=server.cfg.yaml ../../petstore-expanded.yaml 17 | 18 | package api 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | "sync" 24 | 25 | "github.com/gin-gonic/gin" 26 | ) 27 | 28 | type PetStore struct { 29 | Pets map[int64]Pet 30 | NextId int64 31 | Lock sync.Mutex 32 | } 33 | 34 | func NewPetStore() *PetStore { 35 | return &PetStore{ 36 | Pets: make(map[int64]Pet), 37 | NextId: 1000, 38 | } 39 | } 40 | 41 | // This function wraps sending of an error in the Error format, and 42 | // handling the failure to marshal that. 43 | func sendPetstoreError(c *gin.Context, code int, message string) { 44 | petErr := Error{ 45 | Code: int32(code), 46 | Message: message, 47 | } 48 | c.JSON(code, petErr) 49 | } 50 | 51 | // Here, we implement all of the handlers in the ServerInterface 52 | func (p *PetStore) FindPets(c *gin.Context, params FindPetsParams) { 53 | p.Lock.Lock() 54 | defer p.Lock.Unlock() 55 | 56 | var result []Pet 57 | 58 | for _, pet := range p.Pets { 59 | if params.Tags != nil { 60 | // If we have tags, filter pets by tag 61 | for _, t := range *params.Tags { 62 | if pet.Tag != nil && (*pet.Tag == t) { 63 | result = append(result, pet) 64 | } 65 | } 66 | } else { 67 | // Add all pets if we're not filtering 68 | result = append(result, pet) 69 | } 70 | 71 | if params.Limit != nil { 72 | l := int(*params.Limit) 73 | if len(result) >= l { 74 | // We're at the limit 75 | break 76 | } 77 | } 78 | } 79 | c.JSON(http.StatusOK, result) 80 | } 81 | 82 | func (p *PetStore) AddPet(c *gin.Context) { 83 | // We expect a NewPet object in the request body. 84 | var newPet NewPet 85 | err := c.Bind(&newPet) 86 | if err != nil { 87 | sendPetstoreError(c, http.StatusBadRequest, "Invalid format for NewPet") 88 | return 89 | } 90 | // We now have a pet, let's add it to our "database". 91 | 92 | // We're always asynchronous, so lock unsafe operations below 93 | p.Lock.Lock() 94 | defer p.Lock.Unlock() 95 | 96 | // We handle pets, not NewPets, which have an additional ID field 97 | var pet Pet 98 | pet.Name = newPet.Name 99 | pet.Tag = newPet.Tag 100 | pet.Id = p.NextId 101 | p.NextId = p.NextId + 1 102 | 103 | // Insert into map 104 | p.Pets[pet.Id] = pet 105 | 106 | // Now, we have to return the NewPet 107 | c.JSON(http.StatusCreated, pet) 108 | return 109 | } 110 | 111 | func (p *PetStore) FindPetByID(c *gin.Context, petId int64) { 112 | p.Lock.Lock() 113 | defer p.Lock.Unlock() 114 | 115 | pet, found := p.Pets[petId] 116 | if !found { 117 | sendPetstoreError(c, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", petId)) 118 | return 119 | } 120 | c.JSON(http.StatusOK, pet) 121 | } 122 | 123 | func (p *PetStore) DeletePet(c *gin.Context, id int64) { 124 | p.Lock.Lock() 125 | defer p.Lock.Unlock() 126 | 127 | _, found := p.Pets[id] 128 | if !found { 129 | sendPetstoreError(c, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) 130 | } 131 | delete(p.Pets, id) 132 | c.Status(http.StatusNoContent) 133 | } 134 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/api/server.cfg.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | gin-server: true 4 | embedded-spec: true 5 | output: petstore-server.gen.go 6 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/api/types.cfg.yaml: -------------------------------------------------------------------------------- 1 | package: api 2 | generate: 3 | models: true 4 | output: petstore-types.gen.go 5 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/petstore.go: -------------------------------------------------------------------------------- 1 | // This is an example of implementing the Pet Store from the OpenAPI documentation 2 | // found at: 3 | // https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "github.com/gin-gonic/gin" 15 | 16 | "github.com/algorand/oapi-codegen/examples/petstore-expanded/gin/api" 17 | middleware "github.com/algorand/oapi-codegen/pkg/gin-middleware" 18 | ) 19 | 20 | func NewGinPetServer(petStore *api.PetStore, port int) *http.Server { 21 | swagger, err := api.GetSwagger() 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) 24 | os.Exit(1) 25 | } 26 | 27 | // Clear out the servers array in the swagger spec, that skips validating 28 | // that server names match. We don't know how this thing will be run. 29 | swagger.Servers = nil 30 | 31 | // This is how you set up a basic chi router 32 | r := gin.Default() 33 | 34 | // Use our validation middleware to check all requests against the 35 | // OpenAPI schema. 36 | r.Use(middleware.OapiRequestValidator(swagger)) 37 | 38 | // We now register our petStore above as the handler for the interface 39 | r = api.RegisterHandlers(r, petStore) 40 | 41 | s := &http.Server{ 42 | Handler: r, 43 | Addr: fmt.Sprintf("0.0.0.0:%d", port), 44 | } 45 | return s 46 | } 47 | 48 | func main() { 49 | var port = flag.Int("port", 8080, "Port for test HTTP server") 50 | flag.Parse() 51 | // Create an instance of our handler which satisfies the generated interface 52 | petStore := api.NewPetStore() 53 | s := NewGinPetServer(petStore, *port) 54 | // And we serve HTTP until the world ends. 55 | log.Fatal(s.ListenAndServe()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/petstore-expanded/gin/petstore_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/algorand/oapi-codegen/examples/petstore-expanded/gin/api" 13 | "github.com/algorand/oapi-codegen/pkg/testutil" 14 | ) 15 | 16 | func doGet(t *testing.T, handler http.Handler, url string) *httptest.ResponseRecorder { 17 | response := testutil.NewRequest().Get(url).WithAcceptJson().GoWithHTTPHandler(t, handler) 18 | return response.Recorder 19 | } 20 | 21 | func TestPetStore(t *testing.T) { 22 | var err error 23 | store := api.NewPetStore() 24 | ginPetServer := NewGinPetServer(store, 8080) 25 | r := ginPetServer.Handler 26 | 27 | t.Run("Add pet", func(t *testing.T) { 28 | tag := "TagOfSpot" 29 | newPet := api.NewPet{ 30 | Name: "Spot", 31 | Tag: &tag, 32 | } 33 | 34 | rr := testutil.NewRequest().Post("/pets").WithJsonBody(newPet).GoWithHTTPHandler(t, r).Recorder 35 | assert.Equal(t, http.StatusCreated, rr.Code) 36 | 37 | var resultPet api.Pet 38 | err = json.NewDecoder(rr.Body).Decode(&resultPet) 39 | assert.NoError(t, err, "error unmarshaling response") 40 | assert.Equal(t, newPet.Name, resultPet.Name) 41 | assert.Equal(t, *newPet.Tag, *resultPet.Tag) 42 | }) 43 | 44 | t.Run("Find pet by ID", func(t *testing.T) { 45 | pet := api.Pet{ 46 | Id: 100, 47 | } 48 | 49 | store.Pets[pet.Id] = pet 50 | rr := doGet(t, r, fmt.Sprintf("/pets/%d", pet.Id)) 51 | 52 | var resultPet api.Pet 53 | err = json.NewDecoder(rr.Body).Decode(&resultPet) 54 | assert.NoError(t, err, "error getting pet") 55 | assert.Equal(t, pet, resultPet) 56 | }) 57 | 58 | t.Run("Pet not found", func(t *testing.T) { 59 | rr := doGet(t, r, "/pets/27179095781") 60 | assert.Equal(t, http.StatusNotFound, rr.Code) 61 | 62 | var petError api.Error 63 | err = json.NewDecoder(rr.Body).Decode(&petError) 64 | assert.NoError(t, err, "error getting response", err) 65 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 66 | }) 67 | 68 | t.Run("List all pets", func(t *testing.T) { 69 | store.Pets = map[int64]api.Pet{1: {}, 2: {}} 70 | 71 | // Now, list all pets, we should have two 72 | rr := doGet(t, r, "/pets") 73 | assert.Equal(t, http.StatusOK, rr.Code) 74 | 75 | var petList []api.Pet 76 | err = json.NewDecoder(rr.Body).Decode(&petList) 77 | assert.NoError(t, err, "error getting response", err) 78 | assert.Equal(t, 2, len(petList)) 79 | }) 80 | 81 | t.Run("Filter pets by tag", func(t *testing.T) { 82 | tag := "TagOfFido" 83 | 84 | store.Pets = map[int64]api.Pet{ 85 | 1: { 86 | Tag: &tag, 87 | }, 88 | 2: {}, 89 | } 90 | 91 | // Filter pets by tag, we should have 1 92 | rr := doGet(t, r, "/pets?tags=TagOfFido") 93 | assert.Equal(t, http.StatusOK, rr.Code) 94 | 95 | var petList []api.Pet 96 | err = json.NewDecoder(rr.Body).Decode(&petList) 97 | assert.NoError(t, err, "error getting response", err) 98 | assert.Equal(t, 1, len(petList)) 99 | }) 100 | 101 | t.Run("Filter pets by tag", func(t *testing.T) { 102 | store.Pets = map[int64]api.Pet{1: {}, 2: {}} 103 | 104 | // Filter pets by non existent tag, we should have 0 105 | rr := doGet(t, r, "/pets?tags=NotExists") 106 | assert.Equal(t, http.StatusOK, rr.Code) 107 | 108 | var petList []api.Pet 109 | err = json.NewDecoder(rr.Body).Decode(&petList) 110 | assert.NoError(t, err, "error getting response", err) 111 | assert.Equal(t, 0, len(petList)) 112 | }) 113 | 114 | t.Run("Delete pets", func(t *testing.T) { 115 | store.Pets = map[int64]api.Pet{1: {}, 2: {}} 116 | 117 | // Let's delete non-existent pet 118 | rr := testutil.NewRequest().Delete("/pets/7").GoWithHTTPHandler(t, r).Recorder 119 | assert.Equal(t, http.StatusNotFound, rr.Code) 120 | 121 | var petError api.Error 122 | err = json.NewDecoder(rr.Body).Decode(&petError) 123 | assert.NoError(t, err, "error unmarshaling PetError") 124 | assert.Equal(t, int32(http.StatusNotFound), petError.Code) 125 | 126 | // Now, delete both real pets 127 | rr = testutil.NewRequest().Delete("/pets/1").GoWithHTTPHandler(t, r).Recorder 128 | assert.Equal(t, http.StatusNoContent, rr.Code) 129 | 130 | rr = testutil.NewRequest().Delete("/pets/2").GoWithHTTPHandler(t, r).Recorder 131 | assert.Equal(t, http.StatusNoContent, rr.Code) 132 | 133 | // Should have no pets left. 134 | var petList []api.Pet 135 | rr = doGet(t, r, "/pets") 136 | assert.Equal(t, http.StatusOK, rr.Code) 137 | err = json.NewDecoder(rr.Body).Decode(&petList) 138 | assert.NoError(t, err, "error getting response", err) 139 | assert.Equal(t, 0, len(petList)) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /examples/petstore-expanded/internal/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package internal 15 | 16 | // This directory contains the OpenAPI 3.0 specification which defines our 17 | // server. The file petstore.gen.go is automatically generated from the schema 18 | 19 | // Run oapi-codegen to regenerate the petstore boilerplate 20 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=petstore --generate types,client -o ../petstore-client.gen.go ../petstore-expanded.yaml 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/algorand/oapi-codegen 2 | 3 | require ( 4 | github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c 5 | github.com/getkin/kin-openapi v0.94.0 6 | github.com/gin-gonic/gin v1.7.7 7 | github.com/go-chi/chi/v5 v5.0.7 8 | github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 9 | github.com/google/uuid v1.3.0 10 | github.com/labstack/echo/v4 v4.7.2 11 | github.com/lestrrat-go/jwx v1.2.24 12 | github.com/matryer/moq v0.2.7 13 | github.com/stretchr/testify v1.7.1 14 | golang.org/x/tools v0.1.10 15 | gopkg.in/yaml.v2 v2.4.0 16 | ) 17 | 18 | go 1.16 19 | -------------------------------------------------------------------------------- /internal/test/all_of/config1.yaml: -------------------------------------------------------------------------------- 1 | output: 2 | v1/openapi.gen.go 3 | package: v1 4 | generate: 5 | - types 6 | - skip-prune 7 | - spec 8 | compatibility: 9 | old-merge-schemas: true 10 | -------------------------------------------------------------------------------- /internal/test/all_of/config2.yaml: -------------------------------------------------------------------------------- 1 | output: 2 | v2/openapi.gen.go 3 | package: v2 4 | generate: 5 | - types 6 | - skip-prune 7 | - spec 8 | -------------------------------------------------------------------------------- /internal/test/all_of/doc.go: -------------------------------------------------------------------------------- 1 | package all_of 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --config=config1.yaml openapi.yaml 4 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --config=config2.yaml openapi.yaml 5 | -------------------------------------------------------------------------------- /internal/test/all_of/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | version: 1.0.0 4 | title: Tests AllOf composition 5 | paths: 6 | /placeholder: 7 | get: 8 | operationId: placeholder 9 | description: | 10 | Validators want at least one path, so this makes them happy. 11 | responses: 12 | default: 13 | description: placeholder 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/PersonWithID" 18 | components: 19 | schemas: 20 | PersonProperties: 21 | type: object 22 | description: | 23 | These are fields that specify a person. They are all optional, and 24 | would be used by an `Edit` style API endpoint, where each is optional. 25 | properties: 26 | FirstName: 27 | type: string 28 | LastName: 29 | type: string 30 | GovernmentIDNumber: 31 | type: integer 32 | format: int64 33 | Person: 34 | type: object 35 | description: | 36 | This is a person, with mandatory first and last name, but optional ID 37 | number. This would be returned by a `Get` style API. We merge the person 38 | properties with another Schema which only provides required fields. 39 | allOf: 40 | - $ref: "#/components/schemas/PersonProperties" 41 | - required: [FirstName, LastName] 42 | PersonWithID: 43 | type: object 44 | description: | 45 | This is a person record as returned from a Create endpoint. It contains 46 | all the fields of a Person, with an additional resource UUID. 47 | allOf: 48 | - $ref: "#/components/schemas/Person" 49 | - properties: 50 | ID: 51 | type: integer 52 | format: int64 53 | required: [ ID ] 54 | -------------------------------------------------------------------------------- /internal/test/all_of/v1/openapi.gen.go: -------------------------------------------------------------------------------- 1 | // Package v1 provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | "github.com/getkin/kin-openapi/openapi3" 16 | ) 17 | 18 | // Person defines model for Person. 19 | type Person struct { 20 | // Embedded struct due to allOf(#/components/schemas/PersonProperties) 21 | PersonProperties `yaml:",inline"` 22 | // Embedded fields due to inline allOf schema 23 | } 24 | 25 | // These are fields that specify a person. They are all optional, and 26 | // would be used by an `Edit` style API endpoint, where each is optional. 27 | type PersonProperties struct { 28 | FirstName *string `json:"FirstName,omitempty"` 29 | GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` 30 | LastName *string `json:"LastName,omitempty"` 31 | } 32 | 33 | // PersonWithID defines model for PersonWithID. 34 | type PersonWithID struct { 35 | // Embedded struct due to allOf(#/components/schemas/Person) 36 | Person `yaml:",inline"` 37 | // Embedded fields due to inline allOf schema 38 | ID int64 `json:"ID"` 39 | } 40 | 41 | // Base64 encoded, gzipped, json marshaled Swagger object 42 | var swaggerSpec = []string{ 43 | 44 | "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", 45 | "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", 46 | "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", 47 | "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", 48 | "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", 49 | "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", 50 | "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", 51 | "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", 52 | "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", 53 | "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", 54 | "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", 55 | } 56 | 57 | // GetSwagger returns the content of the embedded swagger specification file 58 | // or error if failed to decode 59 | func decodeSpec() ([]byte, error) { 60 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 61 | if err != nil { 62 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 63 | } 64 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 65 | if err != nil { 66 | return nil, fmt.Errorf("error decompressing spec: %s", err) 67 | } 68 | var buf bytes.Buffer 69 | _, err = buf.ReadFrom(zr) 70 | if err != nil { 71 | return nil, fmt.Errorf("error decompressing spec: %s", err) 72 | } 73 | 74 | return buf.Bytes(), nil 75 | } 76 | 77 | var rawSpec = decodeSpecCached() 78 | 79 | // a naive cached of a decoded swagger spec 80 | func decodeSpecCached() func() ([]byte, error) { 81 | data, err := decodeSpec() 82 | return func() ([]byte, error) { 83 | return data, err 84 | } 85 | } 86 | 87 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 88 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 89 | var res = make(map[string]func() ([]byte, error)) 90 | if len(pathToFile) > 0 { 91 | res[pathToFile] = rawSpec 92 | } 93 | 94 | return res 95 | } 96 | 97 | // GetSwagger returns the Swagger specification corresponding to the generated code 98 | // in this file. The external references of Swagger specification are resolved. 99 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 100 | // Externally referenced files must be embedded in the corresponding golang packages. 101 | // Urls can be supported but this task was out of the scope. 102 | func GetSwagger() (swagger *openapi3.T, err error) { 103 | var resolvePath = PathToRawSpec("") 104 | 105 | loader := openapi3.NewLoader() 106 | loader.IsExternalRefsAllowed = true 107 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 108 | var pathToFile = url.String() 109 | pathToFile = path.Clean(pathToFile) 110 | getSpec, ok := resolvePath[pathToFile] 111 | if !ok { 112 | err1 := fmt.Errorf("path not found: %s", pathToFile) 113 | return nil, err1 114 | } 115 | return getSpec() 116 | } 117 | var specData []byte 118 | specData, err = rawSpec() 119 | if err != nil { 120 | return 121 | } 122 | swagger, err = loader.LoadFromData(specData) 123 | if err != nil { 124 | return 125 | } 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /internal/test/all_of/v2/openapi.gen.go: -------------------------------------------------------------------------------- 1 | // Package v2 provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package v2 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | "github.com/getkin/kin-openapi/openapi3" 16 | ) 17 | 18 | // Person defines model for Person. 19 | type Person struct { 20 | FirstName string `json:"FirstName"` 21 | GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` 22 | LastName string `json:"LastName"` 23 | } 24 | 25 | // These are fields that specify a person. They are all optional, and 26 | // would be used by an `Edit` style API endpoint, where each is optional. 27 | type PersonProperties struct { 28 | FirstName *string `json:"FirstName,omitempty"` 29 | GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` 30 | LastName *string `json:"LastName,omitempty"` 31 | } 32 | 33 | // PersonWithID defines model for PersonWithID. 34 | type PersonWithID struct { 35 | FirstName string `json:"FirstName"` 36 | GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` 37 | ID int64 `json:"ID"` 38 | LastName string `json:"LastName"` 39 | } 40 | 41 | // Base64 encoded, gzipped, json marshaled Swagger object 42 | var swaggerSpec = []string{ 43 | 44 | "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", 45 | "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", 46 | "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", 47 | "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", 48 | "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", 49 | "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", 50 | "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", 51 | "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", 52 | "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", 53 | "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", 54 | "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", 55 | } 56 | 57 | // GetSwagger returns the content of the embedded swagger specification file 58 | // or error if failed to decode 59 | func decodeSpec() ([]byte, error) { 60 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 61 | if err != nil { 62 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 63 | } 64 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 65 | if err != nil { 66 | return nil, fmt.Errorf("error decompressing spec: %s", err) 67 | } 68 | var buf bytes.Buffer 69 | _, err = buf.ReadFrom(zr) 70 | if err != nil { 71 | return nil, fmt.Errorf("error decompressing spec: %s", err) 72 | } 73 | 74 | return buf.Bytes(), nil 75 | } 76 | 77 | var rawSpec = decodeSpecCached() 78 | 79 | // a naive cached of a decoded swagger spec 80 | func decodeSpecCached() func() ([]byte, error) { 81 | data, err := decodeSpec() 82 | return func() ([]byte, error) { 83 | return data, err 84 | } 85 | } 86 | 87 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 88 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 89 | var res = make(map[string]func() ([]byte, error)) 90 | if len(pathToFile) > 0 { 91 | res[pathToFile] = rawSpec 92 | } 93 | 94 | return res 95 | } 96 | 97 | // GetSwagger returns the Swagger specification corresponding to the generated code 98 | // in this file. The external references of Swagger specification are resolved. 99 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 100 | // Externally referenced files must be embedded in the corresponding golang packages. 101 | // Urls can be supported but this task was out of the scope. 102 | func GetSwagger() (swagger *openapi3.T, err error) { 103 | var resolvePath = PathToRawSpec("") 104 | 105 | loader := openapi3.NewLoader() 106 | loader.IsExternalRefsAllowed = true 107 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 108 | var pathToFile = url.String() 109 | pathToFile = path.Clean(pathToFile) 110 | getSpec, ok := resolvePath[pathToFile] 111 | if !ok { 112 | err1 := fmt.Errorf("path not found: %s", pathToFile) 113 | return nil, err1 114 | } 115 | return getSpec() 116 | } 117 | var specData []byte 118 | specData, err = rawSpec() 119 | if err != nil { 120 | return 121 | } 122 | swagger, err = loader.LoadFromData(specData) 123 | if err != nil { 124 | return 125 | } 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /internal/test/client/client.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | version: 1.0.0 4 | title: Test Server 5 | license: 6 | name: MIT 7 | description: | 8 | This tests whether the Client and ClientWithResponses are generated correctly 9 | paths: 10 | /with_json_response: 11 | get: 12 | operationId: GetJson 13 | security: 14 | - OpenId: [json.read, json.admin] 15 | responses: 16 | 200: 17 | application/json: 18 | schema: 19 | $ref: '#/components/schemas/SchemaObject' 20 | /with_trailing_slash/: 21 | get: 22 | operationId: GetJsonWithTrailingSlash 23 | security: 24 | - OpenId: [json.read, json.admin] 25 | responses: 26 | 200: 27 | application/json: 28 | schema: 29 | $ref: '#/components/schemas/SchemaObject' 30 | /with_other_response: 31 | get: 32 | operationId: GetOther 33 | responses: 34 | 200: 35 | application/octet-stream: 36 | schema: 37 | type: string 38 | format: binary 39 | /with_both_responses: 40 | get: 41 | operationId: GetBoth 42 | responses: 43 | 200: 44 | application/json: 45 | schema: 46 | $ref: '#/components/schemas/SchemaObject' 47 | application/octet-stream: 48 | schema: 49 | type: string 50 | format: binary 51 | /with_json_body: 52 | post: 53 | operationId: PostJson 54 | requestBody: 55 | required: true 56 | content: 57 | application/json: 58 | schema: 59 | $ref: '#/components/schemas/SchemaObject' 60 | /with_other_body: 61 | post: 62 | operationId: PostOther 63 | requestBody: 64 | required: true 65 | content: 66 | application/octet-stream: 67 | schema: 68 | type: string 69 | format: binary 70 | /with_both_bodies: 71 | post: 72 | operationId: PostBoth 73 | requestBody: 74 | required: true 75 | content: 76 | application/octet-stream: 77 | schema: 78 | type: string 79 | format: binary 80 | application/json: 81 | schema: 82 | $ref: '#/components/schemas/SchemaObject' 83 | components: 84 | schemas: 85 | SchemaObject: 86 | properties: 87 | role: 88 | type: string 89 | firstName: 90 | type: string 91 | required: 92 | - role 93 | - firstName 94 | -------------------------------------------------------------------------------- /internal/test/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTemp(t *testing.T) { 10 | 11 | var ( 12 | withTrailingSlash string = "https://my-api.com/some-base-url/v1/" 13 | withoutTrailingSlash string = "https://my-api.com/some-base-url/v1" 14 | ) 15 | 16 | client1, err := NewClient( 17 | withTrailingSlash, 18 | ) 19 | assert.NoError(t, err) 20 | 21 | client2, err := NewClient( 22 | withoutTrailingSlash, 23 | ) 24 | assert.NoError(t, err) 25 | 26 | client3, err := NewClient( 27 | "", 28 | WithBaseURL(withTrailingSlash), 29 | ) 30 | assert.NoError(t, err) 31 | 32 | client4, err := NewClient( 33 | "", 34 | WithBaseURL(withoutTrailingSlash), 35 | ) 36 | assert.NoError(t, err) 37 | 38 | expectedURL := withTrailingSlash 39 | 40 | assert.Equal(t, expectedURL, client1.Server) 41 | assert.Equal(t, expectedURL, client2.Server) 42 | assert.Equal(t, expectedURL, client3.Server) 43 | assert.Equal(t, expectedURL, client4.Server) 44 | } 45 | -------------------------------------------------------------------------------- /internal/test/client/doc.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=client -o client.gen.go client.yaml 4 | -------------------------------------------------------------------------------- /internal/test/components/components_test.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func assertJsonEqual(t *testing.T, j1 []byte, j2 []byte) { 11 | var v1, v2 interface{} 12 | 13 | err := json.Unmarshal(j1, &v1) 14 | assert.NoError(t, err) 15 | 16 | err = json.Unmarshal(j2, &v2) 17 | assert.NoError(t, err) 18 | 19 | assert.EqualValues(t, v1, v2) 20 | } 21 | 22 | func TestRawJSON(t *testing.T) { 23 | // Check raw json unmarshaling 24 | const buf = `{"name":"bob","value1":{"present":true}}` 25 | var dst ObjectWithJsonField 26 | err := json.Unmarshal([]byte(buf), &dst) 27 | assert.NoError(t, err) 28 | 29 | buf2, err := json.Marshal(dst) 30 | assert.NoError(t, err) 31 | 32 | assertJsonEqual(t, []byte(buf), buf2) 33 | 34 | } 35 | 36 | func TestAdditionalProperties(t *testing.T) { 37 | buf := `{"name": "bob", "id": 5, "optional":"yes", "additional": 42}` 38 | var dst AdditionalPropertiesObject1 39 | err := json.Unmarshal([]byte(buf), &dst) 40 | assert.NoError(t, err) 41 | assert.Equal(t, "bob", dst.Name) 42 | assert.Equal(t, 5, dst.Id) 43 | assert.Equal(t, "yes", *dst.Optional) 44 | additional, found := dst.Get("additional") 45 | assert.True(t, found) 46 | assert.Equal(t, 42, additional) 47 | 48 | obj4 := AdditionalPropertiesObject4{ 49 | Name: "bob", 50 | } 51 | obj4.Set("add1", "hi") 52 | obj4.Set("add2", 7) 53 | 54 | foo, found := obj4.Get("add1") 55 | assert.True(t, found) 56 | assert.EqualValues(t, "hi", foo) 57 | foo, found = obj4.Get("add2") 58 | assert.True(t, found) 59 | assert.EqualValues(t, 7, foo) 60 | 61 | // test that additionalProperties that reference a schema work when unmarshalling 62 | bossSchema := SchemaObject{ 63 | FirstName: "bob", 64 | Role: "warehouse manager", 65 | } 66 | 67 | buf2 := `{"boss": { "firstName": "bob", "role": "warehouse manager" }, "employee": { "firstName": "kevin", "role": "warehouse"}}` 68 | var obj5 AdditionalPropertiesObject5 69 | err = json.Unmarshal([]byte(buf2), &obj5) 70 | assert.NoError(t, err) 71 | assert.Equal(t, bossSchema, obj5.AdditionalProperties["boss"]) 72 | } 73 | -------------------------------------------------------------------------------- /internal/test/components/config.yaml: -------------------------------------------------------------------------------- 1 | output: 2 | components.gen.go 3 | package: components 4 | generate: 5 | - types 6 | - skip-prune 7 | - spec 8 | -------------------------------------------------------------------------------- /internal/test/components/doc.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --config=config.yaml components.yaml 4 | -------------------------------------------------------------------------------- /internal/test/externalref/doc.go: -------------------------------------------------------------------------------- 1 | package externalref 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --config=externalref.cfg.yaml spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/externalref/externalref.cfg.yaml: -------------------------------------------------------------------------------- 1 | package: externalref 2 | generate: 3 | models: true 4 | embedded-spec: true 5 | import-mapping: 6 | ./packageA/spec.yaml: github.com/algorand/oapi-codegen/internal/test/externalref/packageA 7 | ./packageB/spec.yaml: github.com/algorand/oapi-codegen/internal/test/externalref/packageB 8 | output: externalref.gen.go 9 | output-options: 10 | 11 | skip-prune: true 12 | -------------------------------------------------------------------------------- /internal/test/externalref/externalref.gen.go: -------------------------------------------------------------------------------- 1 | // Package externalref provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package externalref 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | externalRef0 "github.com/algorand/oapi-codegen/internal/test/externalref/packageA" 16 | externalRef1 "github.com/algorand/oapi-codegen/internal/test/externalref/packageB" 17 | "github.com/getkin/kin-openapi/openapi3" 18 | ) 19 | 20 | // Container defines model for Container. 21 | type Container struct { 22 | ObjectA *externalRef0.ObjectA `json:"object_a,omitempty"` 23 | ObjectB *externalRef1.ObjectB `json:"object_b,omitempty"` 24 | } 25 | 26 | // Base64 encoded, gzipped, json marshaled Swagger object 27 | var swaggerSpec = []string{ 28 | 29 | "H4sIAAAAAAAC/4zPQcrCMBAF4Lu8/1+GpOAuu9YDeASZhqmNtsmQBEFK7i4pihsXZvUeYb5hNri4Sgwc", 30 | "SobdkN3MK+3xGEMhHzi1IikKp+J5/4rjlV05U8v/iSdYaCPkbnTh3mRhpx+0Ln/mg5uXbE77bI+q3sz4", 31 | "jRl+YgbU9hR8mGJjii8LwwIKd07Zx9BK2yUcSDwsDrrTHRSEytyuqfUZAAD//0P8tE0FAQAA", 32 | } 33 | 34 | // GetSwagger returns the content of the embedded swagger specification file 35 | // or error if failed to decode 36 | func decodeSpec() ([]byte, error) { 37 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 38 | if err != nil { 39 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 40 | } 41 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 42 | if err != nil { 43 | return nil, fmt.Errorf("error decompressing spec: %s", err) 44 | } 45 | var buf bytes.Buffer 46 | _, err = buf.ReadFrom(zr) 47 | if err != nil { 48 | return nil, fmt.Errorf("error decompressing spec: %s", err) 49 | } 50 | 51 | return buf.Bytes(), nil 52 | } 53 | 54 | var rawSpec = decodeSpecCached() 55 | 56 | // a naive cached of a decoded swagger spec 57 | func decodeSpecCached() func() ([]byte, error) { 58 | data, err := decodeSpec() 59 | return func() ([]byte, error) { 60 | return data, err 61 | } 62 | } 63 | 64 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 65 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 66 | var res = make(map[string]func() ([]byte, error)) 67 | if len(pathToFile) > 0 { 68 | res[pathToFile] = rawSpec 69 | } 70 | 71 | pathPrefix := path.Dir(pathToFile) 72 | 73 | for rawPath, rawFunc := range externalRef0.PathToRawSpec(path.Join(pathPrefix, "./packageA/spec.yaml")) { 74 | if _, ok := res[rawPath]; ok { 75 | // it is not possible to compare functions in golang, so always overwrite the old value 76 | } 77 | res[rawPath] = rawFunc 78 | } 79 | for rawPath, rawFunc := range externalRef1.PathToRawSpec(path.Join(pathPrefix, "./packageB/spec.yaml")) { 80 | if _, ok := res[rawPath]; ok { 81 | // it is not possible to compare functions in golang, so always overwrite the old value 82 | } 83 | res[rawPath] = rawFunc 84 | } 85 | return res 86 | } 87 | 88 | // GetSwagger returns the Swagger specification corresponding to the generated code 89 | // in this file. The external references of Swagger specification are resolved. 90 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 91 | // Externally referenced files must be embedded in the corresponding golang packages. 92 | // Urls can be supported but this task was out of the scope. 93 | func GetSwagger() (swagger *openapi3.T, err error) { 94 | var resolvePath = PathToRawSpec("") 95 | 96 | loader := openapi3.NewLoader() 97 | loader.IsExternalRefsAllowed = true 98 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 99 | var pathToFile = url.String() 100 | pathToFile = path.Clean(pathToFile) 101 | getSpec, ok := resolvePath[pathToFile] 102 | if !ok { 103 | err1 := fmt.Errorf("path not found: %s", pathToFile) 104 | return nil, err1 105 | } 106 | return getSpec() 107 | } 108 | var specData []byte 109 | specData, err = rawSpec() 110 | if err != nil { 111 | return 112 | } 113 | swagger, err = loader.LoadFromData(specData) 114 | if err != nil { 115 | return 116 | } 117 | return 118 | } 119 | -------------------------------------------------------------------------------- /internal/test/externalref/imports_test.go: -------------------------------------------------------------------------------- 1 | package externalref 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/algorand/oapi-codegen/internal/test/externalref/packageA" 9 | "github.com/algorand/oapi-codegen/internal/test/externalref/packageB" 10 | ) 11 | 12 | func TestParameters(t *testing.T) { 13 | b := &packageB.ObjectB{} 14 | _ = Container{ 15 | ObjectA: &packageA.ObjectA{ObjectB: b}, 16 | ObjectB: b, 17 | } 18 | } 19 | 20 | func TestGetSwagger(t *testing.T) { 21 | _, err := packageB.GetSwagger() 22 | require.Nil(t, err) 23 | 24 | _, err = packageA.GetSwagger() 25 | require.Nil(t, err) 26 | 27 | _, err = GetSwagger() 28 | require.Nil(t, err) 29 | } 30 | -------------------------------------------------------------------------------- /internal/test/externalref/packageA/doc.go: -------------------------------------------------------------------------------- 1 | package packageA 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style -generate types,skip-prune,spec --package=packageA -o externalref.gen.go --import-mapping=../packageB/spec.yaml:github.com/algorand/oapi-codegen/internal/test/externalref/packageB spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/externalref/packageA/externalref.gen.go: -------------------------------------------------------------------------------- 1 | // Package packageA provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package packageA 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | externalRef0 "github.com/algorand/oapi-codegen/internal/test/externalref/packageB" 16 | "github.com/getkin/kin-openapi/openapi3" 17 | ) 18 | 19 | // ObjectA defines model for ObjectA. 20 | type ObjectA struct { 21 | Name *string `json:"name,omitempty"` 22 | ObjectB *externalRef0.ObjectB `json:"object_b,omitempty"` 23 | } 24 | 25 | // Base64 encoded, gzipped, json marshaled Swagger object 26 | var swaggerSpec = []string{ 27 | 28 | "H4sIAAAAAAAC/0SNQarDMAwF7/L+X5p4711zgR6hKEZJ3MaysN1FCLl7sVOoNhoYHnPAp6hJWGqBO1D8", 29 | "ypE63qcn+3prqDkp5xq4C6HI7dddGQ6l5iALToPUF4+pyf/MMxyGwSr5Fy082qLsh53i9md/Ufst2is3", 30 | "4mxnEGROcPLeNoOkLKQBDjBQqmu5zPkJAAD//0utOZO+AAAA", 31 | } 32 | 33 | // GetSwagger returns the content of the embedded swagger specification file 34 | // or error if failed to decode 35 | func decodeSpec() ([]byte, error) { 36 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 37 | if err != nil { 38 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 39 | } 40 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 41 | if err != nil { 42 | return nil, fmt.Errorf("error decompressing spec: %s", err) 43 | } 44 | var buf bytes.Buffer 45 | _, err = buf.ReadFrom(zr) 46 | if err != nil { 47 | return nil, fmt.Errorf("error decompressing spec: %s", err) 48 | } 49 | 50 | return buf.Bytes(), nil 51 | } 52 | 53 | var rawSpec = decodeSpecCached() 54 | 55 | // a naive cached of a decoded swagger spec 56 | func decodeSpecCached() func() ([]byte, error) { 57 | data, err := decodeSpec() 58 | return func() ([]byte, error) { 59 | return data, err 60 | } 61 | } 62 | 63 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 64 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 65 | var res = make(map[string]func() ([]byte, error)) 66 | if len(pathToFile) > 0 { 67 | res[pathToFile] = rawSpec 68 | } 69 | 70 | pathPrefix := path.Dir(pathToFile) 71 | 72 | for rawPath, rawFunc := range externalRef0.PathToRawSpec(path.Join(pathPrefix, "../packageB/spec.yaml")) { 73 | if _, ok := res[rawPath]; ok { 74 | // it is not possible to compare functions in golang, so always overwrite the old value 75 | } 76 | res[rawPath] = rawFunc 77 | } 78 | return res 79 | } 80 | 81 | // GetSwagger returns the Swagger specification corresponding to the generated code 82 | // in this file. The external references of Swagger specification are resolved. 83 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 84 | // Externally referenced files must be embedded in the corresponding golang packages. 85 | // Urls can be supported but this task was out of the scope. 86 | func GetSwagger() (swagger *openapi3.T, err error) { 87 | var resolvePath = PathToRawSpec("") 88 | 89 | loader := openapi3.NewLoader() 90 | loader.IsExternalRefsAllowed = true 91 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 92 | var pathToFile = url.String() 93 | pathToFile = path.Clean(pathToFile) 94 | getSpec, ok := resolvePath[pathToFile] 95 | if !ok { 96 | err1 := fmt.Errorf("path not found: %s", pathToFile) 97 | return nil, err1 98 | } 99 | return getSpec() 100 | } 101 | var specData []byte 102 | specData, err = rawSpec() 103 | if err != nil { 104 | return 105 | } 106 | swagger, err = loader.LoadFromData(specData) 107 | if err != nil { 108 | return 109 | } 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /internal/test/externalref/packageA/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | ObjectA: 4 | properties: 5 | name: 6 | type: string 7 | object_b: 8 | $ref: ../packageB/spec.yaml#/components/schemas/ObjectB -------------------------------------------------------------------------------- /internal/test/externalref/packageB/doc.go: -------------------------------------------------------------------------------- 1 | package packageB 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style -generate types,skip-prune,spec --package=packageB -o externalref.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/externalref/packageB/externalref.gen.go: -------------------------------------------------------------------------------- 1 | // Package packageB provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package packageB 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | "github.com/getkin/kin-openapi/openapi3" 16 | ) 17 | 18 | // ObjectB defines model for ObjectB. 19 | type ObjectB struct { 20 | Name *string `json:"name,omitempty"` 21 | } 22 | 23 | // Base64 encoded, gzipped, json marshaled Swagger object 24 | var swaggerSpec = []string{ 25 | 26 | "H4sIAAAAAAAC/yTJwQ0CMQxE0V7mnApypAFqCNHAGm1sKzYHtNreUZa5zJfegW7DTakZqAeibxztyvvj", 27 | "zZ63lT7NOVN4gbbB9fl1oiJyir5wrhWIPg1VP/teYE5tLqhAgbfc4i/nLwAA//8neaWPdgAAAA==", 28 | } 29 | 30 | // GetSwagger returns the content of the embedded swagger specification file 31 | // or error if failed to decode 32 | func decodeSpec() ([]byte, error) { 33 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 34 | if err != nil { 35 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 36 | } 37 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 38 | if err != nil { 39 | return nil, fmt.Errorf("error decompressing spec: %s", err) 40 | } 41 | var buf bytes.Buffer 42 | _, err = buf.ReadFrom(zr) 43 | if err != nil { 44 | return nil, fmt.Errorf("error decompressing spec: %s", err) 45 | } 46 | 47 | return buf.Bytes(), nil 48 | } 49 | 50 | var rawSpec = decodeSpecCached() 51 | 52 | // a naive cached of a decoded swagger spec 53 | func decodeSpecCached() func() ([]byte, error) { 54 | data, err := decodeSpec() 55 | return func() ([]byte, error) { 56 | return data, err 57 | } 58 | } 59 | 60 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 61 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 62 | var res = make(map[string]func() ([]byte, error)) 63 | if len(pathToFile) > 0 { 64 | res[pathToFile] = rawSpec 65 | } 66 | 67 | return res 68 | } 69 | 70 | // GetSwagger returns the Swagger specification corresponding to the generated code 71 | // in this file. The external references of Swagger specification are resolved. 72 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 73 | // Externally referenced files must be embedded in the corresponding golang packages. 74 | // Urls can be supported but this task was out of the scope. 75 | func GetSwagger() (swagger *openapi3.T, err error) { 76 | var resolvePath = PathToRawSpec("") 77 | 78 | loader := openapi3.NewLoader() 79 | loader.IsExternalRefsAllowed = true 80 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 81 | var pathToFile = url.String() 82 | pathToFile = path.Clean(pathToFile) 83 | getSpec, ok := resolvePath[pathToFile] 84 | if !ok { 85 | err1 := fmt.Errorf("path not found: %s", pathToFile) 86 | return nil, err1 87 | } 88 | return getSpec() 89 | } 90 | var specData []byte 91 | specData, err = rawSpec() 92 | if err != nil { 93 | return 94 | } 95 | swagger, err = loader.LoadFromData(specData) 96 | if err != nil { 97 | return 98 | } 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /internal/test/externalref/packageB/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | ObjectB: 4 | properties: 5 | name: 6 | type: string 7 | -------------------------------------------------------------------------------- /internal/test/externalref/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: {} 3 | paths: {} 4 | components: 5 | schemas: 6 | Container: 7 | properties: 8 | object_a: 9 | $ref: ./packageA/spec.yaml#/components/schemas/ObjectA 10 | object_b: 11 | $ref: ./packageB/spec.yaml#/components/schemas/ObjectB 12 | -------------------------------------------------------------------------------- /internal/test/issues/issue-312/doc.go: -------------------------------------------------------------------------------- 1 | package issue_312 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=issue_312 -o issue.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/issues/issue-312/issue_test.go: -------------------------------------------------------------------------------- 1 | package issue_312 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const hostname = "http://host" 17 | 18 | func TestClient_WhenPathHasColon_RequestHasCorrectPath(t *testing.T) { 19 | doer := &HTTPRequestDoerMock{} 20 | client, _ := NewClientWithResponses(hostname, WithHTTPClient(doer)) 21 | _ = client 22 | 23 | doer.On("Do", mock.Anything).Return(nil, errors.New("something went wrong")).Run(func(args mock.Arguments) { 24 | req, ok := args.Get(0).(*http.Request) 25 | assert.True(t, ok) 26 | assert.Equal(t, "http://host/pets:validate", req.URL.String()) 27 | }) 28 | 29 | client.ValidatePetsWithResponse(context.Background(), ValidatePetsJSONRequestBody{ 30 | Names: []string{"fido"}, 31 | }) 32 | doer.AssertExpectations(t) 33 | } 34 | 35 | func TestClient_WhenPathHasId_RequestHasCorrectPath(t *testing.T) { 36 | doer := &HTTPRequestDoerMock{} 37 | client, _ := NewClientWithResponses(hostname, WithHTTPClient(doer)) 38 | _ = client 39 | 40 | doer.On("Do", mock.Anything).Return(nil, errors.New("something went wrong")).Run(func(args mock.Arguments) { 41 | req, ok := args.Get(0).(*http.Request) 42 | assert.True(t, ok) 43 | assert.Equal(t, "/pets/id", req.URL.Path) 44 | }) 45 | petID := "id" 46 | client.GetPetWithResponse(context.Background(), petID) 47 | doer.AssertExpectations(t) 48 | } 49 | 50 | func TestClient_WhenPathHasIdContainingReservedCharacter_RequestHasCorrectPath(t *testing.T) { 51 | doer := &HTTPRequestDoerMock{} 52 | client, _ := NewClientWithResponses(hostname, WithHTTPClient(doer)) 53 | _ = client 54 | 55 | doer.On("Do", mock.Anything).Return(nil, errors.New("something went wrong")).Run(func(args mock.Arguments) { 56 | req, ok := args.Get(0).(*http.Request) 57 | assert.True(t, ok) 58 | assert.Equal(t, "http://host/pets/id1%2Fid2", req.URL.String()) 59 | }) 60 | petID := "id1/id2" 61 | client.GetPetWithResponse(context.Background(), petID) 62 | doer.AssertExpectations(t) 63 | } 64 | 65 | func TestClient_ServerUnescapesEscapedArg(t *testing.T) { 66 | 67 | e := echo.New() 68 | m := &MockClient{} 69 | RegisterHandlers(e, m) 70 | 71 | svr := httptest.NewServer(e) 72 | defer svr.Close() 73 | 74 | // We'll make a function in the mock client which records the value of 75 | // the petId variable 76 | receivedPetID := "" 77 | m.getPet = func(ctx echo.Context, petId string) error { 78 | receivedPetID = petId 79 | return ctx.NoContent(http.StatusOK) 80 | } 81 | 82 | client, err := NewClientWithResponses(svr.URL) 83 | require.NoError(t, err) 84 | 85 | petID := "id1/id2" 86 | response, err := client.GetPetWithResponse(context.Background(), petID) 87 | require.NoError(t, err) 88 | require.Equal(t, http.StatusOK, response.StatusCode()) 89 | assert.Equal(t, petID, receivedPetID) 90 | } 91 | 92 | // HTTPRequestDoerMock mocks the interface HttpRequestDoerMock. 93 | type HTTPRequestDoerMock struct { 94 | mock.Mock 95 | } 96 | 97 | func (m *HTTPRequestDoerMock) Do(req *http.Request) (*http.Response, error) { 98 | args := m.Called(req) 99 | if args.Get(0) == nil { 100 | return nil, args.Error(1) 101 | } 102 | return args.Get(0).(*http.Response), args.Error(1) 103 | } 104 | 105 | // An implementation of the server interface which helps us check server 106 | // expectations for funky paths and parameters. 107 | type MockClient struct { 108 | getPet func(ctx echo.Context, petId string) error 109 | validatePets func(ctx echo.Context) error 110 | } 111 | 112 | func (m *MockClient) GetPet(ctx echo.Context, petId string) error { 113 | if m.getPet != nil { 114 | return m.getPet(ctx, petId) 115 | } 116 | return ctx.NoContent(http.StatusNotImplemented) 117 | } 118 | 119 | func (m *MockClient) ValidatePets(ctx echo.Context) error { 120 | if m.validatePets != nil { 121 | return m.validatePets(ctx) 122 | } 123 | return ctx.NoContent(http.StatusNotImplemented) 124 | } 125 | -------------------------------------------------------------------------------- /internal/test/issues/issue-312/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Issue 312 test 5 | description: Checks proper escaping of parameters 6 | paths: 7 | /pets:validate: 8 | post: 9 | summary: Validate pets 10 | description: Validate pets 11 | operationId: validatePets 12 | requestBody: 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | $ref: '#/components/schemas/PetNames' 18 | responses: 19 | '200': 20 | description: valid pets 21 | content: 22 | application/json: 23 | schema: 24 | type: array 25 | items: 26 | $ref: '#/components/schemas/Pet' 27 | default: 28 | description: unexpected error 29 | content: 30 | application/json: 31 | schema: 32 | $ref: '#/components/schemas/Error' 33 | /pets/{petId}: 34 | get: 35 | summary: Get pet given identifier. 36 | operationId: getPet 37 | parameters: 38 | - name: petId 39 | in: path 40 | required: true 41 | schema: 42 | type: string 43 | responses: 44 | '200': 45 | description: valid pet 46 | content: 47 | application/json: 48 | schema: 49 | $ref: '#/components/schemas/Pet' 50 | components: 51 | schemas: 52 | Pet: 53 | type: object 54 | required: 55 | - name 56 | properties: 57 | name: 58 | type: string 59 | description: The name of the pet. 60 | 61 | PetNames: 62 | type: object 63 | required: 64 | - names 65 | properties: 66 | names: 67 | type: array 68 | description: The names of the pets. 69 | items: 70 | type: string 71 | 72 | Error: 73 | required: 74 | - code 75 | - message 76 | properties: 77 | code: 78 | type: integer 79 | format: int32 80 | description: Error code 81 | message: 82 | type: string 83 | description: Error message 84 | -------------------------------------------------------------------------------- /internal/test/issues/issue-52/doc.go: -------------------------------------------------------------------------------- 1 | package issue_52 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=issue_52 -o issue.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/issues/issue-52/issue_test.go: -------------------------------------------------------------------------------- 1 | package issue_52 2 | 3 | import ( 4 | _ "embed" 5 | "testing" 6 | 7 | "github.com/getkin/kin-openapi/openapi3" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/algorand/oapi-codegen/pkg/codegen" 11 | ) 12 | 13 | //go:embed spec.yaml 14 | var spec []byte 15 | 16 | func TestIssue(t *testing.T) { 17 | swagger, err := openapi3.NewLoader().LoadFromData(spec) 18 | require.NoError(t, err) 19 | 20 | opts := codegen.Configuration{ 21 | PackageName: "issue_52", 22 | Generate: codegen.GenerateOptions{ 23 | EchoServer: true, 24 | Client: true, 25 | Models: true, 26 | EmbeddedSpec: true, 27 | }, 28 | } 29 | 30 | _, err = codegen.Generate(swagger, opts) 31 | require.NoError(t, err) 32 | } 33 | -------------------------------------------------------------------------------- /internal/test/issues/issue-52/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | version: '0.0.1' 4 | title: example 5 | description: | 6 | Make sure that recursive types are handled properly 7 | paths: 8 | /example: 9 | get: 10 | operationId: exampleGet 11 | responses: 12 | '200': 13 | description: "OK" 14 | content: 15 | 'application/json': 16 | schema: 17 | $ref: '#/components/schemas/Document' 18 | components: 19 | schemas: 20 | Document: 21 | type: object 22 | properties: 23 | fields: 24 | type: object 25 | additionalProperties: 26 | $ref: '#/components/schemas/Value' 27 | Value: 28 | type: object 29 | properties: 30 | stringValue: 31 | type: string 32 | arrayValue: 33 | $ref: '#/components/schemas/ArrayValue' 34 | ArrayValue: 35 | type: array 36 | items: 37 | $ref: '#/components/schemas/Value' 38 | -------------------------------------------------------------------------------- /internal/test/issues/issue-579/gen.go: -------------------------------------------------------------------------------- 1 | package issue_579 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=issue_579 --generate=types,skip-prune --alias-types -o issue.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/issues/issue-579/issue.gen.go: -------------------------------------------------------------------------------- 1 | // Package issue_579 provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package issue_579 5 | 6 | import ( 7 | openapi_types "github.com/algorand/oapi-codegen/pkg/types" 8 | ) 9 | 10 | // AliasedDate defines model for AliasedDate. 11 | type AliasedDate = openapi_types.Date 12 | 13 | // Pet defines model for Pet. 14 | type Pet struct { 15 | Born *AliasedDate `json:"born,omitempty"` 16 | BornAt *openapi_types.Date `json:"born_at,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/test/issues/issue-579/issue_test.go: -------------------------------------------------------------------------------- 1 | package issue_579 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAliasedDate(t *testing.T) { 11 | pet := Pet{} 12 | err := json.Unmarshal([]byte(`{"born": "2022-05-19", "born_at": "2022-05-20"}`), &pet) 13 | require.NoError(t, err) 14 | } 15 | -------------------------------------------------------------------------------- /internal/test/issues/issue-579/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Issue 579 test 5 | paths: 6 | /placeholder: 7 | get: 8 | responses: 9 | 200: 10 | description: placeholder 11 | content: 12 | text/plain: 13 | schema: 14 | type: string 15 | components: 16 | schemas: 17 | Pet: 18 | type: object 19 | properties: 20 | born: 21 | $ref: "#/components/schemas/AliasedDate" 22 | born_at: 23 | type: string 24 | format: date 25 | AliasedDate: 26 | type: string 27 | format: date 28 | -------------------------------------------------------------------------------- /internal/test/issues/issue-grab_import_names/doc.go: -------------------------------------------------------------------------------- 1 | package grab_import_names 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=grab_import_names -o issue.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/issues/issue-grab_import_names/issue_test.go: -------------------------------------------------------------------------------- 1 | package grab_import_names 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/algorand/oapi-codegen/pkg/codegen" 10 | ) 11 | 12 | func TestLineComments(t *testing.T) { 13 | swagger, err := openapi3.NewLoader().LoadFromFile("spec.yaml") 14 | require.NoError(t, err) 15 | 16 | opts := codegen.Configuration{ 17 | PackageName: "grab_import_names", 18 | Generate: codegen.GenerateOptions{ 19 | EchoServer: true, 20 | Client: true, 21 | Models: true, 22 | EmbeddedSpec: true, 23 | }, 24 | } 25 | 26 | code, err := codegen.Generate(swagger, opts) 27 | require.NoError(t, err) 28 | require.NotContains(t, code, `"openapi_types"`) 29 | } 30 | -------------------------------------------------------------------------------- /internal/test/issues/issue-grab_import_names/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | 3 | info: 4 | title: ... 5 | version: 0.0.0 6 | 7 | paths: 8 | /foo: 9 | get: 10 | parameters: 11 | - name: Foo 12 | in: header 13 | description: base64. bytes. chi. context. echo. errors. fmt. gzip. http. io. ioutil. json. openapi3. 14 | schema: 15 | type: string 16 | required: false 17 | - name: Bar 18 | in: header 19 | description: openapi_types. path. runtime. strings. time.Duration time.Time url. xml. yaml. 20 | schema: 21 | type: string 22 | required: false 23 | description: ... 24 | responses: 25 | 200: 26 | description: ... 27 | content: 28 | application/json: 29 | schema: 30 | type: string -------------------------------------------------------------------------------- /internal/test/issues/issue-illegal_enum_names/doc.go: -------------------------------------------------------------------------------- 1 | package illegal_enum_names 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=illegal_enum_names -o issue.gen.go spec.yaml 4 | -------------------------------------------------------------------------------- /internal/test/issues/issue-illegal_enum_names/issue_test.go: -------------------------------------------------------------------------------- 1 | package illegal_enum_names 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "testing" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/algorand/oapi-codegen/pkg/codegen" 13 | ) 14 | 15 | func TestIllegalEnumNames(t *testing.T) { 16 | swagger, err := openapi3.NewLoader().LoadFromFile("spec.yaml") 17 | require.NoError(t, err) 18 | 19 | opts := codegen.Configuration{ 20 | PackageName: "illegal_enum_names", 21 | Generate: codegen.GenerateOptions{ 22 | EchoServer: true, 23 | Client: true, 24 | Models: true, 25 | EmbeddedSpec: true, 26 | }, 27 | } 28 | 29 | code, err := codegen.Generate(swagger, opts) 30 | require.NoError(t, err) 31 | 32 | f, err := parser.ParseFile(token.NewFileSet(), "", code, parser.AllErrors) 33 | require.NoError(t, err) 34 | 35 | constDefs := make(map[string]string) 36 | for _, d := range f.Decls { 37 | switch decl := d.(type) { 38 | case *ast.GenDecl: 39 | if token.CONST == decl.Tok { 40 | for _, s := range decl.Specs { 41 | switch spec := s.(type) { 42 | case *ast.ValueSpec: 43 | constDefs[spec.Names[0].Name] = spec.Names[0].Obj.Decl.(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | require.Equal(t, `"Bar"`, constDefs["BarBar"]) 51 | require.Equal(t, `"Foo"`, constDefs["BarFoo"]) 52 | require.Equal(t, `"Foo Bar"`, constDefs["BarFooBar"]) 53 | require.Equal(t, `"Foo-Bar"`, constDefs["BarFooBar1"]) 54 | require.Equal(t, `"1Foo"`, constDefs["BarN1Foo"]) 55 | require.Equal(t, `" Foo"`, constDefs["BarFoo1"]) 56 | require.Equal(t, `" Foo "`, constDefs["BarFoo2"]) 57 | require.Equal(t, `"_Foo_"`, constDefs["BarFoo3"]) 58 | require.Equal(t, `"1"`, constDefs["BarN1"]) 59 | } 60 | -------------------------------------------------------------------------------- /internal/test/issues/issue-illegal_enum_names/spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | 3 | info: 4 | title: ... 5 | version: 0.0.0 6 | 7 | paths: 8 | /foo: 9 | get: 10 | responses: 11 | 200: 12 | description: ... 13 | content: 14 | application/json: 15 | schema: 16 | type: array 17 | items: 18 | $ref: '#/components/schemas/Bar' 19 | 20 | components: 21 | schemas: 22 | Bar: 23 | type: string 24 | enum: 25 | - Foo 26 | - Bar 27 | - Foo Bar 28 | - Foo-Bar 29 | - 1Foo 30 | - Bar # A swagger validator would catch this duplicate value 31 | - ' Foo' 32 | - ' Foo ' 33 | - _Foo_ 34 | - "1" 35 | -------------------------------------------------------------------------------- /internal/test/parameters/doc.go: -------------------------------------------------------------------------------- 1 | package parameters 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=parameters -o parameters.gen.go parameters.yaml 4 | -------------------------------------------------------------------------------- /internal/test/schemas/doc.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --package=schemas -o schemas.gen.go schemas.yaml 4 | -------------------------------------------------------------------------------- /internal/test/schemas/schemas.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | version: 1.0.0 4 | title: Test Server 5 | description: | 6 | Test cases for various issues found over time. Please add a test case for 7 | any bug fixed. 8 | servers: 9 | - url: http://openapitest.deepmap.ai 10 | paths: 11 | /ensure-everything-is-referenced: 12 | get: 13 | operationId: ensureEverythingIsReferenced 14 | description: | 15 | This endpoint exists so that components can be created in this 16 | spec and not be pruned 17 | responses: 18 | 200: 19 | content: 20 | application/json: 21 | schema: 22 | type: object 23 | properties: 24 | anyType1: 25 | $ref: "#/components/schemas/AnyType1" 26 | anyType2: 27 | $ref: "#/components/schemas/AnyType2" 28 | customStringType: 29 | $ref: "#/components/schemas/CustomStringType" 30 | /issues/9: 31 | get: 32 | operationId: Issue9 33 | description: | 34 | Client params type incorrectly included for request with body and 35 | parameters. 36 | parameters: 37 | - name: foo 38 | in: query 39 | required: true 40 | schema: 41 | type: string 42 | requestBody: 43 | description: Optional body 44 | required: false 45 | content: 46 | application/json: 47 | schema: {} 48 | /issues/30/{fallthrough}: 49 | get: 50 | operationId: Issue30 51 | description: | 52 | Reserved keywords should be prefixed in variable names. 53 | parameters: 54 | - name: fallthrough 55 | in: path 56 | required: true 57 | schema: 58 | type: string 59 | /issues/41/{1param}: 60 | get: 61 | operationId: Issue41 62 | description: Parameter name starting with number 63 | parameters: 64 | - name: 1param 65 | in: path 66 | required: true 67 | schema: 68 | $ref: "#/components/schemas/5StartsWithNumber" 69 | /issues/127: 70 | get: 71 | operationId: Issue127 72 | description: | 73 | Make sure unsupported context types don't preempt supported types. 74 | responses: 75 | 200: 76 | content: 77 | application/json: 78 | schema: 79 | $ref: "#/components/schemas/GenericObject" 80 | text/markdown: 81 | schema: 82 | $ref: "#/components/schemas/GenericObject" 83 | text/yaml: 84 | schema: 85 | $ref: "#/components/schemas/GenericObject" 86 | application/xml: 87 | schema: 88 | $ref: "#/components/schemas/GenericObject" 89 | default: 90 | content: 91 | application/json: 92 | schema: 93 | $ref: "#/components/schemas/GenericObject" 94 | text/markdown: 95 | schema: 96 | $ref: "#/components/schemas/GenericObject" 97 | /issues/185: 98 | get: 99 | operationId: Issue185 100 | description: | 101 | Type generation when optional/required properties are nullable. 102 | requestBody: 103 | content: 104 | application/json: 105 | schema: 106 | $ref: "#/components/schemas/NullableProperties" 107 | /issues/209/${str}: 108 | parameters: 109 | - $ref: '#/components/parameters/StringInPath' 110 | get: 111 | operationId: Issue209 112 | description: Checks if parameters are declared properly 113 | /issues/375: 114 | get: 115 | description: | 116 | Enum declaration was generated twice if the enum was in an object 117 | which was inside of an array. 118 | responses: 119 | 200: 120 | content: 121 | application/json: 122 | schema: 123 | $ref: "#/components/schemas/EnumInObjInArray" 124 | components: 125 | schemas: 126 | GenericObject: 127 | type: object 128 | AnyType1: {} 129 | AnyType2: 130 | description: | 131 | AnyType2 represents any type. 132 | 133 | This should be an interface{} 134 | CustomStringType: 135 | type: string 136 | format: custom 137 | x-oapi-codegen-extra-tags: 138 | foo: bar 139 | NullableProperties: 140 | type: object 141 | properties: 142 | optional: 143 | type: string 144 | nullable: false 145 | optionalAndNullable: 146 | type: string 147 | nullable: true 148 | required: 149 | type: string 150 | nullable: false 151 | requiredAndNullable: 152 | type: string 153 | nullable: true 154 | required: [required, requiredAndNullable] 155 | 5StartsWithNumber: 156 | type: object 157 | description: This schema name starts with a number 158 | EnumInObjInArray: 159 | type: array 160 | items: 161 | type: object 162 | properties: 163 | val: 164 | type: string 165 | enum: 166 | - first 167 | - second 168 | parameters: 169 | StringInPath: 170 | name: str 171 | description: A string path parameter 172 | in: path 173 | required: true 174 | schema: 175 | type: string 176 | securitySchemes: 177 | # This security scheme has a - in it, we need to make sure the name gets 178 | # remapped to a valid Go id. See bug 179 | access-token: 180 | type: http 181 | scheme: bearer 182 | bearerFormat: | 183 | JWT-format access token. 184 | security: 185 | - access-token: [] 186 | 187 | -------------------------------------------------------------------------------- /internal/test/server/doc.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | //go:generate go run github.com/algorand/oapi-codegen/cmd/oapi-codegen --old-config-style --generate=types,chi-server --package=server -o server.gen.go ../test-schema.yaml 4 | //go:generate go run github.com/matryer/moq -out server_moq.gen.go . ServerInterface 5 | -------------------------------------------------------------------------------- /internal/test/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestParameters(t *testing.T) { 14 | m := ServerInterfaceMock{} 15 | 16 | m.CreateResource2Func = func(w http.ResponseWriter, r *http.Request, inlineArgument int, params CreateResource2Params) { 17 | assert.Equal(t, 99, *params.InlineQueryArgument) 18 | assert.Equal(t, 1, inlineArgument) 19 | } 20 | 21 | h := Handler(&m) 22 | 23 | req := httptest.NewRequest("POST", "http://openapitest.deepmap.ai/resource2/1?inline_query_argument=99", nil) 24 | rr := httptest.NewRecorder() 25 | h.ServeHTTP(rr, req) 26 | 27 | assert.Equal(t, 1, len(m.CreateResource2Calls())) 28 | } 29 | 30 | func TestErrorHandlerFunc(t *testing.T) { 31 | m := ServerInterfaceMock{} 32 | 33 | h := HandlerWithOptions(&m, ChiServerOptions{ 34 | ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { 35 | w.Header().Set("Content-Type", "application/json") 36 | var requiredParamError *RequiredParamError 37 | assert.True(t, errors.As(err, &requiredParamError)) 38 | }, 39 | }) 40 | 41 | s := httptest.NewServer(h) 42 | defer s.Close() 43 | 44 | req, err := http.DefaultClient.Get(s.URL + "/get-with-args") 45 | assert.Nil(t, err) 46 | assert.Equal(t, "application/json", req.Header.Get("Content-Type")) 47 | } 48 | 49 | func TestErrorHandlerFuncBackwardsCompatible(t *testing.T) { 50 | m := ServerInterfaceMock{} 51 | 52 | h := HandlerWithOptions(&m, ChiServerOptions{}) 53 | 54 | s := httptest.NewServer(h) 55 | defer s.Close() 56 | 57 | req, err := http.DefaultClient.Get(s.URL + "/get-with-args") 58 | b, _ := ioutil.ReadAll(req.Body) 59 | assert.Nil(t, err) 60 | assert.Equal(t, "text/plain; charset=utf-8", req.Header.Get("Content-Type")) 61 | assert.Equal(t, "Query argument required_argument is required, but not found\n", string(b)) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/chi-middleware/oapi_validate.go: -------------------------------------------------------------------------------- 1 | // Package middleware implements middleware function for go-chi or net/http, 2 | // which validates incoming HTTP requests to make sure that they conform to the given OAPI 3.0 specification. 3 | // When OAPI validation failes on the request, we return an HTTP/400. 4 | package middleware 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/getkin/kin-openapi/openapi3" 13 | "github.com/getkin/kin-openapi/openapi3filter" 14 | "github.com/getkin/kin-openapi/routers" 15 | "github.com/getkin/kin-openapi/routers/gorillamux" 16 | ) 17 | 18 | // ErrorHandler is called when there is an error in validation 19 | type ErrorHandler func(w http.ResponseWriter, message string, statusCode int) 20 | 21 | // Options to customize request validation, openapi3filter specified options will be passed through. 22 | type Options struct { 23 | Options openapi3filter.Options 24 | ErrorHandler ErrorHandler 25 | } 26 | 27 | // OapiRequestValidator Creates middleware to validate request by swagger spec. 28 | // This middleware is good for net/http either since go-chi is 100% compatible with net/http. 29 | func OapiRequestValidator(swagger *openapi3.T) func(next http.Handler) http.Handler { 30 | return OapiRequestValidatorWithOptions(swagger, nil) 31 | } 32 | 33 | // OapiRequestValidatorWithOptions Creates middleware to validate request by swagger spec. 34 | // This middleware is good for net/http either since go-chi is 100% compatible with net/http. 35 | func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) func(next http.Handler) http.Handler { 36 | router, err := gorillamux.NewRouter(swagger) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | return func(next http.Handler) http.Handler { 42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | 44 | // validate request 45 | if statusCode, err := validateRequest(r, router, options); err != nil { 46 | if options != nil && options.ErrorHandler != nil { 47 | options.ErrorHandler(w, err.Error(), statusCode) 48 | } else { 49 | http.Error(w, err.Error(), statusCode) 50 | } 51 | return 52 | } 53 | 54 | // serve 55 | next.ServeHTTP(w, r) 56 | }) 57 | } 58 | 59 | } 60 | 61 | // This function is called from the middleware above and actually does the work 62 | // of validating a request. 63 | func validateRequest(r *http.Request, router routers.Router, options *Options) (int, error) { 64 | 65 | // Find route 66 | route, pathParams, err := router.FindRoute(r) 67 | if err != nil { 68 | return http.StatusBadRequest, err // We failed to find a matching route for the request. 69 | } 70 | 71 | // Validate request 72 | requestValidationInput := &openapi3filter.RequestValidationInput{ 73 | Request: r, 74 | PathParams: pathParams, 75 | Route: route, 76 | } 77 | 78 | if options != nil { 79 | requestValidationInput.Options = &options.Options 80 | } 81 | 82 | if err := openapi3filter.ValidateRequest(context.Background(), requestValidationInput); err != nil { 83 | switch e := err.(type) { 84 | case *openapi3filter.RequestError: 85 | // We've got a bad request 86 | // Split up the verbose error by lines and return the first one 87 | // openapi errors seem to be multi-line with a decent message on the first 88 | errorLines := strings.Split(e.Error(), "\n") 89 | return http.StatusBadRequest, fmt.Errorf(errorLines[0]) 90 | case *openapi3filter.SecurityRequirementsError: 91 | return http.StatusUnauthorized, err 92 | default: 93 | // This should never happen today, but if our upstream code changes, 94 | // we don't want to crash the server, so handle the unexpected error. 95 | return http.StatusInternalServerError, fmt.Errorf("error validating route: %s", err.Error()) 96 | } 97 | } 98 | 99 | return http.StatusOK, nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/chi-middleware/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai/ 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | components: 70 | securitySchemes: 71 | BearerAuth: 72 | type: http 73 | scheme: bearer 74 | bearerFormat: JWT 75 | -------------------------------------------------------------------------------- /pkg/codegen/configuration.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | // Configuration defines code generation customizations 9 | type Configuration struct { 10 | PackageName string `yaml:"package"` // PackageName to generate 11 | Generate GenerateOptions `yaml:"generate,omitempty"` 12 | Compatibility CompatibilityOptions `yaml:"compatibility,omitempty"` 13 | OutputOptions OutputOptions `yaml:"output-options,omitempty"` 14 | ImportMapping map[string]string `yaml:"import-mapping,omitempty"` // ImportMapping specifies the golang package path for each external reference 15 | } 16 | 17 | // GenerateOptions specifies which supported output formats to generate. 18 | type GenerateOptions struct { 19 | ChiServer bool `yaml:"chi-server,omitempty"` // ChiServer specifies whether to generate chi server boilerplate 20 | EchoServer bool `yaml:"echo-server,omitempty"` // EchoServer specifies whether to generate echo server boilerplate 21 | GinServer bool `yaml:"gin-server,omitempty"` // GinServer specifies whether to generate echo server boilerplate 22 | Client bool `yaml:"client,omitempty"` // Client specifies whether to generate client boilerplate 23 | Models bool `yaml:"models,omitempty"` // Models specifies whether to generate type definitions 24 | EmbeddedSpec bool `yaml:"embedded-spec,omitempty"` // Whether to embed the swagger spec in the generated code 25 | } 26 | 27 | // CompatibilityOptions specifies backward compatibility settings for the 28 | // code generator. 29 | type CompatibilityOptions struct { 30 | // In the past, we merged schemas for `allOf` by inlining each schema 31 | // within the schema list. This approach, though, is incorrect because 32 | // `allOf` merges at the schema definition level, not at the resulting model 33 | // level. So, new behavior merges OpenAPI specs but generates different code 34 | // than we have in the past. Set OldMergeSchemas to true for the old behavior. 35 | // Please see https://github.com/deepmap/oapi-codegen/issues/531 36 | OldMergeSchemas bool `yaml:"old-merge-schemas"` 37 | // Enum values can generate conflicting typenames, so we've updated the 38 | // code for enum generation to avoid these conflicts, but it will result 39 | // in some enum types being renamed in existing code. Set OldEnumConflicts to true 40 | // to revert to old behavior. Please see: 41 | // Please see https://github.com/deepmap/oapi-codegen/issues/549 42 | OldEnumConflicts bool `yaml:"old-enum-conflicts"` 43 | // It was a mistake to generate a go type definition for every $ref in 44 | // the OpenAPI schema. New behavior uses type aliases where possible, but 45 | // this can generate code which breaks existing builds. Set OldAliasing to true 46 | // for old behavior. 47 | // Please see https://github.com/deepmap/oapi-codegen/issues/549 48 | OldAliasing bool `yaml:"old-aliasing"` 49 | } 50 | 51 | // OutputOptions are used to modify the output code in some way. 52 | type OutputOptions struct { 53 | SkipFmt bool `yaml:"skip-fmt,omitempty"` // Whether to skip go imports on the generated code 54 | SkipPrune bool `yaml:"skip-prune,omitempty"` // Whether to skip pruning unused components on the generated code 55 | IncludeTags []string `yaml:"include-tags,omitempty"` // Only include operations that have one of these tags. Ignored when empty. 56 | ExcludeTags []string `yaml:"exclude-tags,omitempty"` // Exclude operations that have one of these tags. Ignored when empty. 57 | UserTemplates map[string]string `yaml:"user-templates,omitempty"` // Override built-in templates from user-provided files 58 | 59 | ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"` // Exclude from generation schemas with given names. Ignored when empty. 60 | ResponseTypeSuffix string `yaml:"response-type-suffix,omitempty"` // The suffix used for responses types 61 | 62 | TypeMappings map[string]string `yaml:"type-mappings,omitempty"` 63 | } 64 | 65 | // UpdateDefaults sets reasonable default values for unset fields in Configuration 66 | func (o Configuration) UpdateDefaults() Configuration { 67 | if reflect.ValueOf(o.Generate).IsZero() { 68 | o.Generate = GenerateOptions{ 69 | EchoServer: true, 70 | Models: true, 71 | EmbeddedSpec: true, 72 | } 73 | } 74 | return o 75 | } 76 | 77 | // Validate checks whether Configuration represent a valid configuration 78 | func (o Configuration) Validate() error { 79 | if o.PackageName == "" { 80 | return errors.New("package name must be specified") 81 | } 82 | 83 | // Only one server type should be specified at a time. 84 | nServers := 0 85 | if o.Generate.ChiServer { 86 | nServers++ 87 | } 88 | if o.Generate.EchoServer { 89 | nServers++ 90 | } 91 | if o.Generate.GinServer { 92 | nServers++ 93 | } 94 | if nServers > 1 { 95 | return errors.New("only one server type is supported at a time") 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/codegen/extension.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | extPropGoType = "x-go-type" 10 | extGoFieldName = "x-go-name" 11 | extPropOmitEmpty = "x-omitempty" 12 | extPropExtraTags = "x-oapi-codegen-extra-tags" 13 | ) 14 | 15 | func extString(extPropValue interface{}) (string, error) { 16 | raw, ok := extPropValue.(json.RawMessage) 17 | if !ok { 18 | return "", fmt.Errorf("failed to convert type: %T", extPropValue) 19 | } 20 | var str string 21 | if err := json.Unmarshal(raw, &str); err != nil { 22 | return "", fmt.Errorf("failed to unmarshal json: %w", err) 23 | } 24 | 25 | return str, nil 26 | } 27 | func extTypeName(extPropValue interface{}) (string, error) { 28 | return extString(extPropValue) 29 | } 30 | 31 | func extParseGoFieldName(extPropValue interface{}) (string, error) { 32 | return extString(extPropValue) 33 | } 34 | 35 | func extParseOmitEmpty(extPropValue interface{}) (bool, error) { 36 | raw, ok := extPropValue.(json.RawMessage) 37 | if !ok { 38 | return false, fmt.Errorf("failed to convert type: %T", extPropValue) 39 | } 40 | 41 | var omitEmpty bool 42 | if err := json.Unmarshal(raw, &omitEmpty); err != nil { 43 | return false, fmt.Errorf("failed to unmarshal json: %w", err) 44 | } 45 | 46 | return omitEmpty, nil 47 | } 48 | 49 | func extExtraTags(extPropValue interface{}) (map[string]string, error) { 50 | raw, ok := extPropValue.(json.RawMessage) 51 | if !ok { 52 | return nil, fmt.Errorf("failed to convert type: %T", extPropValue) 53 | } 54 | var tags map[string]string 55 | if err := json.Unmarshal(raw, &tags); err != nil { 56 | return nil, fmt.Errorf("failed to unmarshal json: %w", err) 57 | } 58 | return tags, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/codegen/extension_test.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_extTypeName(t *testing.T) { 11 | type args struct { 12 | extPropValue interface{} 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want string 18 | wantErr bool 19 | }{ 20 | { 21 | name: "success", 22 | args: args{json.RawMessage(`"uint64"`)}, 23 | want: "uint64", 24 | wantErr: false, 25 | }, 26 | { 27 | name: "type conversion error", 28 | args: args{nil}, 29 | want: "", 30 | wantErr: true, 31 | }, 32 | { 33 | name: "json unmarshal error", 34 | args: args{json.RawMessage("invalid json format")}, 35 | want: "", 36 | wantErr: true, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | got, err := extTypeName(tt.args.extPropValue) 42 | if tt.wantErr { 43 | assert.Error(t, err) 44 | return 45 | } 46 | assert.NoError(t, err) 47 | assert.Equal(t, tt.want, got) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/codegen/filter.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import "github.com/getkin/kin-openapi/openapi3" 4 | 5 | func filterOperationsByTag(swagger *openapi3.T, opts Configuration) { 6 | if len(opts.OutputOptions.ExcludeTags) > 0 { 7 | excludeOperationsWithTags(swagger.Paths, opts.OutputOptions.ExcludeTags) 8 | } 9 | if len(opts.OutputOptions.IncludeTags) > 0 { 10 | includeOperationsWithTags(swagger.Paths, opts.OutputOptions.IncludeTags, false) 11 | } 12 | } 13 | 14 | func excludeOperationsWithTags(paths openapi3.Paths, tags []string) { 15 | includeOperationsWithTags(paths, tags, true) 16 | } 17 | 18 | func includeOperationsWithTags(paths openapi3.Paths, tags []string, exclude bool) { 19 | for _, pathItem := range paths { 20 | ops := pathItem.Operations() 21 | names := make([]string, 0, len(ops)) 22 | for name, op := range ops { 23 | if operationHasTag(op, tags) == exclude { 24 | names = append(names, name) 25 | } 26 | } 27 | for _, name := range names { 28 | pathItem.SetOperation(name, nil) 29 | } 30 | } 31 | } 32 | 33 | //operationHasTag returns true if the operation is tagged with any of tags 34 | func operationHasTag(op *openapi3.Operation, tags []string) bool { 35 | if op == nil { 36 | return false 37 | } 38 | for _, hasTag := range op.Tags { 39 | for _, wantTag := range tags { 40 | if hasTag == wantTag { 41 | return true 42 | } 43 | } 44 | } 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /pkg/codegen/filter_test.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFilterOperationsByTag(t *testing.T) { 11 | packageName := "testswagger" 12 | t.Run("include tags", func(t *testing.T) { 13 | opts := Configuration{ 14 | PackageName: packageName, 15 | Generate: GenerateOptions{ 16 | EchoServer: true, 17 | Client: true, 18 | Models: true, 19 | EmbeddedSpec: true, 20 | }, 21 | OutputOptions: OutputOptions{ 22 | IncludeTags: []string{"hippo", "giraffe", "cat"}, 23 | }, 24 | } 25 | 26 | // Get a spec from the test definition in this file: 27 | swagger, err := openapi3.NewLoader().LoadFromData([]byte(testOpenAPIDefinition)) 28 | assert.NoError(t, err) 29 | 30 | // Run our code generation: 31 | code, err := Generate(swagger, opts) 32 | assert.NoError(t, err) 33 | assert.NotEmpty(t, code) 34 | assert.NotContains(t, code, `"/test/:name"`) 35 | assert.Contains(t, code, `"/cat"`) 36 | }) 37 | 38 | t.Run("exclude tags", func(t *testing.T) { 39 | opts := Configuration{ 40 | PackageName: packageName, 41 | Generate: GenerateOptions{ 42 | EchoServer: true, 43 | Client: true, 44 | Models: true, 45 | EmbeddedSpec: true, 46 | }, 47 | OutputOptions: OutputOptions{ 48 | ExcludeTags: []string{"hippo", "giraffe", "cat"}, 49 | }, 50 | } 51 | 52 | // Get a spec from the test definition in this file: 53 | swagger, err := openapi3.NewLoader().LoadFromData([]byte(testOpenAPIDefinition)) 54 | assert.NoError(t, err) 55 | 56 | // Run our code generation: 57 | code, err := Generate(swagger, opts) 58 | assert.NoError(t, err) 59 | assert.NotEmpty(t, code) 60 | assert.Contains(t, code, `"/test/:name"`) 61 | assert.NotContains(t, code, `"/cat"`) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/codegen/inline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package codegen 15 | 16 | import ( 17 | "bytes" 18 | "compress/gzip" 19 | "encoding/base64" 20 | "fmt" 21 | "text/template" 22 | 23 | "github.com/getkin/kin-openapi/openapi3" 24 | ) 25 | 26 | // This generates a gzipped, base64 encoded JSON representation of the 27 | // swagger definition, which we embed inside the generated code. 28 | func GenerateInlinedSpec(t *template.Template, importMapping importMap, swagger *openapi3.T) (string, error) { 29 | // Marshal to json 30 | encoded, err := swagger.MarshalJSON() 31 | if err != nil { 32 | return "", fmt.Errorf("error marshaling swagger: %s", err) 33 | } 34 | 35 | // gzip 36 | var buf bytes.Buffer 37 | zw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) 38 | if err != nil { 39 | return "", fmt.Errorf("error creating gzip compressor: %s", err) 40 | } 41 | _, err = zw.Write(encoded) 42 | if err != nil { 43 | return "", fmt.Errorf("error gzipping swagger file: %s", err) 44 | } 45 | err = zw.Close() 46 | if err != nil { 47 | return "", fmt.Errorf("error gzipping swagger file: %s", err) 48 | } 49 | str := base64.StdEncoding.EncodeToString(buf.Bytes()) 50 | 51 | var parts []string 52 | const width = 80 53 | 54 | // Chop up the string into an array of strings. 55 | for len(str) > width { 56 | part := str[0:width] 57 | parts = append(parts, part) 58 | str = str[width:] 59 | } 60 | if len(str) > 0 { 61 | parts = append(parts, str) 62 | } 63 | 64 | return GenerateTemplates( 65 | []string{"inline.tmpl"}, 66 | t, 67 | struct { 68 | SpecParts []string 69 | ImportMapping importMap 70 | }{ 71 | SpecParts: parts, 72 | ImportMapping: importMapping, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/codegen/merge_schemas.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/getkin/kin-openapi/openapi3" 8 | ) 9 | 10 | // MergeSchemas merges all the fields in the schemas supplied into one giant schema. 11 | // The idea is that we merge all fields together into one schema. 12 | func MergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { 13 | // If someone asked for the old way, for backward compatibility, return the 14 | // old style result. 15 | if options.Compatibility.OldMergeSchemas { 16 | return mergeSchemas_V1(allOf, path) 17 | } 18 | return mergeSchemas(allOf, path) 19 | } 20 | 21 | func mergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { 22 | var schema openapi3.Schema 23 | for _, schemaRef := range allOf { 24 | var err error 25 | schema, err = mergeOpenapiSchemas(schema, *schemaRef.Value) 26 | if err != nil { 27 | return Schema{}, fmt.Errorf("error merging schemas for AllOf: %w", err) 28 | } 29 | } 30 | 31 | return GenerateGoSchema(openapi3.NewSchemaRef("", &schema), path) 32 | } 33 | 34 | func mergeAllOf(allOf []*openapi3.SchemaRef) (openapi3.Schema, error) { 35 | var schema openapi3.Schema 36 | for _, schemaRef := range allOf { 37 | var err error 38 | schema, err = mergeOpenapiSchemas(schema, *schemaRef.Value) 39 | if err != nil { 40 | return openapi3.Schema{}, fmt.Errorf("error merging schemas for AllOf: %w", err) 41 | } 42 | } 43 | return schema, nil 44 | } 45 | 46 | // mergeOpenapiSchemas merges two openAPI schemas and returns the schema 47 | // all of whose fields are composed. 48 | func mergeOpenapiSchemas(s1, s2 openapi3.Schema) (openapi3.Schema, error) { 49 | var result openapi3.Schema 50 | if s1.Extensions != nil || s2.Extensions != nil { 51 | result.Extensions = make(map[string]interface{}) 52 | if s1.Extensions != nil { 53 | for k, v := range s1.Extensions { 54 | result.Extensions[k] = v 55 | } 56 | } 57 | if s2.Extensions != nil { 58 | for k, v := range s2.Extensions { 59 | // TODO: Check for collisions 60 | result.Extensions[k] = v 61 | } 62 | } 63 | } 64 | 65 | result.OneOf = append(s1.OneOf, s2.OneOf...) 66 | 67 | // We are going to make AllOf transitive, so that merging an AllOf that 68 | // contains AllOf's will result in a flat object. 69 | var err error 70 | if s1.AllOf != nil { 71 | var merged openapi3.Schema 72 | merged, err = mergeAllOf(s1.AllOf) 73 | if err != nil { 74 | return openapi3.Schema{}, fmt.Errorf("error transitive merging AllOf on schema 1") 75 | } 76 | s1 = merged 77 | } 78 | if s2.AllOf != nil { 79 | var merged openapi3.Schema 80 | merged, err = mergeAllOf(s2.AllOf) 81 | if err != nil { 82 | return openapi3.Schema{}, fmt.Errorf("error transitive merging AllOf on schema 2") 83 | } 84 | s2 = merged 85 | } 86 | 87 | result.AllOf = append(s1.AllOf, s2.AllOf...) 88 | 89 | if s1.Type != "" && s2.Type != "" && s1.Type != s2.Type { 90 | return openapi3.Schema{}, errors.New("can not merge incompatible types") 91 | } 92 | result.Type = s1.Type 93 | 94 | if s1.Format != s2.Format { 95 | return openapi3.Schema{}, errors.New("can not merge incompatible formats") 96 | } 97 | result.Format = s1.Format 98 | 99 | // For Enums, do we union, or intersect? This is a bit vague. I choose 100 | // to be more permissive and union. 101 | result.Enum = append(s1.Enum, s2.Enum...) 102 | 103 | // I don't know how to handle two different defaults. 104 | if s1.Default != nil || s2.Default != nil { 105 | return openapi3.Schema{}, errors.New("merging two sets of defaults is undefined") 106 | } 107 | if s1.Default != nil { 108 | result.Default = s1.Default 109 | } 110 | if s2.Default != nil { 111 | result.Default = s2.Default 112 | } 113 | 114 | // We skip Example 115 | // We skip ExternalDocs 116 | 117 | // If two schemas disagree on any of these flags, we error out. 118 | if s1.UniqueItems != s2.UniqueItems { 119 | return openapi3.Schema{}, errors.New("merging two schemas with different UniqueItems") 120 | 121 | } 122 | result.UniqueItems = s1.UniqueItems 123 | 124 | if s1.ExclusiveMin != s2.ExclusiveMin { 125 | return openapi3.Schema{}, errors.New("merging two schemas with different ExclusiveMin") 126 | 127 | } 128 | result.ExclusiveMin = s1.ExclusiveMin 129 | 130 | if s1.ExclusiveMax != s2.ExclusiveMax { 131 | return openapi3.Schema{}, errors.New("merging two schemas with different ExclusiveMax") 132 | 133 | } 134 | result.ExclusiveMax = s1.ExclusiveMax 135 | 136 | if s1.Nullable != s2.Nullable { 137 | return openapi3.Schema{}, errors.New("merging two schemas with different Nullable") 138 | 139 | } 140 | result.Nullable = s1.Nullable 141 | 142 | if s1.ReadOnly != s2.ReadOnly { 143 | return openapi3.Schema{}, errors.New("merging two schemas with different ReadOnly") 144 | 145 | } 146 | result.ReadOnly = s1.ReadOnly 147 | 148 | if s1.WriteOnly != s2.WriteOnly { 149 | return openapi3.Schema{}, errors.New("merging two schemas with different WriteOnly") 150 | 151 | } 152 | result.WriteOnly = s1.WriteOnly 153 | 154 | if s1.AllowEmptyValue != s2.AllowEmptyValue { 155 | return openapi3.Schema{}, errors.New("merging two schemas with different AllowEmptyValue") 156 | 157 | } 158 | result.AllowEmptyValue = s1.AllowEmptyValue 159 | 160 | // Required. We merge these. 161 | result.Required = append(s1.Required, s2.Required...) 162 | 163 | // We merge all properties 164 | result.Properties = make(map[string]*openapi3.SchemaRef) 165 | for k, v := range s1.Properties { 166 | result.Properties[k] = v 167 | } 168 | for k, v := range s2.Properties { 169 | // TODO: detect conflicts 170 | result.Properties[k] = v 171 | } 172 | 173 | if SchemaHasAdditionalProperties(&s1) && SchemaHasAdditionalProperties(&s2) { 174 | return openapi3.Schema{}, errors.New("merging two schemas with additional properties, this is unhandled") 175 | } 176 | if s1.AdditionalProperties != nil { 177 | result.AdditionalProperties = s1.AdditionalProperties 178 | } 179 | if s2.AdditionalProperties != nil { 180 | result.AdditionalProperties = s2.AdditionalProperties 181 | } 182 | 183 | // Unhandled for now 184 | if s1.Discriminator != nil || s2.Discriminator != nil { 185 | return openapi3.Schema{}, errors.New("merging two schemas with discriminators is not supported") 186 | } 187 | 188 | return result, nil 189 | } 190 | -------------------------------------------------------------------------------- /pkg/codegen/merge_schemas_v1.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | ) 10 | 11 | func mergeSchemas_V1(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { 12 | var outSchema Schema 13 | for _, schemaOrRef := range allOf { 14 | ref := schemaOrRef.Ref 15 | 16 | var refType string 17 | var err error 18 | if IsGoTypeReference(ref) { 19 | refType, err = RefPathToGoType(ref) 20 | if err != nil { 21 | return Schema{}, fmt.Errorf("error converting reference path to a go type: %w", err) 22 | } 23 | } 24 | 25 | schema, err := GenerateGoSchema(schemaOrRef, path) 26 | if err != nil { 27 | return Schema{}, fmt.Errorf("error generating Go schema in allOf: %w", err) 28 | } 29 | schema.RefType = refType 30 | 31 | for _, p := range schema.Properties { 32 | err = outSchema.AddProperty(p) 33 | if err != nil { 34 | return Schema{}, fmt.Errorf("error merging properties: %w", err) 35 | } 36 | } 37 | 38 | if schema.HasAdditionalProperties { 39 | if outSchema.HasAdditionalProperties { 40 | // Both this schema, and the aggregate schema have additional 41 | // properties, they must match. 42 | if schema.AdditionalPropertiesType.TypeDecl() != outSchema.AdditionalPropertiesType.TypeDecl() { 43 | return Schema{}, errors.New("additional properties in allOf have incompatible types") 44 | } 45 | } else { 46 | // We're switching from having no additional properties to having 47 | // them 48 | outSchema.HasAdditionalProperties = true 49 | outSchema.AdditionalPropertiesType = schema.AdditionalPropertiesType 50 | } 51 | } 52 | } 53 | 54 | // Now, we generate the struct which merges together all the fields. 55 | var err error 56 | outSchema.GoType, err = GenStructFromAllOf(allOf, path) 57 | if err != nil { 58 | return Schema{}, fmt.Errorf("unable to generate aggregate type for AllOf: %w", err) 59 | } 60 | return outSchema, nil 61 | } 62 | 63 | // This function generates an object that is the union of the objects in the 64 | // input array. In the case of Ref objects, we use an embedded struct, otherwise, 65 | // we inline the fields. 66 | func GenStructFromAllOf(allOf []*openapi3.SchemaRef, path []string) (string, error) { 67 | // Start out with struct { 68 | objectParts := []string{"struct {"} 69 | for _, schemaOrRef := range allOf { 70 | ref := schemaOrRef.Ref 71 | if IsGoTypeReference(ref) { 72 | // We have a referenced type, we will generate an inlined struct 73 | // member. 74 | // struct { 75 | // InlinedMember 76 | // ... 77 | // } 78 | goType, err := RefPathToGoType(ref) 79 | if err != nil { 80 | return "", err 81 | } 82 | objectParts = append(objectParts, 83 | fmt.Sprintf(" // Embedded struct due to allOf(%s)", ref)) 84 | objectParts = append(objectParts, 85 | fmt.Sprintf(" %s `yaml:\",inline\"`", goType)) 86 | } else { 87 | // Inline all the fields from the schema into the output struct, 88 | // just like in the simple case of generating an object. 89 | goSchema, err := GenerateGoSchema(schemaOrRef, path) 90 | if err != nil { 91 | return "", err 92 | } 93 | objectParts = append(objectParts, " // Embedded fields due to inline allOf schema") 94 | objectParts = append(objectParts, GenFieldsFromProperties(goSchema.Properties)...) 95 | 96 | if goSchema.HasAdditionalProperties { 97 | addPropsType := goSchema.AdditionalPropertiesType.GoType 98 | if goSchema.AdditionalPropertiesType.RefType != "" { 99 | addPropsType = goSchema.AdditionalPropertiesType.RefType 100 | } 101 | 102 | additionalPropertiesPart := fmt.Sprintf("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType) 103 | if !StringInArray(additionalPropertiesPart, objectParts) { 104 | objectParts = append(objectParts, additionalPropertiesPart) 105 | } 106 | } 107 | } 108 | } 109 | objectParts = append(objectParts, "}") 110 | return strings.Join(objectParts, "\n"), nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/codegen/operations_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package codegen 15 | 16 | import ( 17 | "net/http" 18 | "testing" 19 | ) 20 | 21 | func TestGenerateDefaultOperationID(t *testing.T) { 22 | type test struct { 23 | op string 24 | path string 25 | want string 26 | wantErr bool 27 | } 28 | 29 | suite := []test{ 30 | { 31 | op: http.MethodGet, 32 | path: "/v1/foo/bar", 33 | want: "GetV1FooBar", 34 | wantErr: false, 35 | }, 36 | { 37 | op: http.MethodGet, 38 | path: "/v1/foo/bar/", 39 | want: "GetV1FooBar", 40 | wantErr: false, 41 | }, 42 | { 43 | op: http.MethodPost, 44 | path: "/v1", 45 | want: "PostV1", 46 | wantErr: false, 47 | }, 48 | { 49 | op: http.MethodPost, 50 | path: "v1", 51 | want: "PostV1", 52 | wantErr: false, 53 | }, 54 | { 55 | path: "v1", 56 | want: "", 57 | wantErr: true, 58 | }, 59 | { 60 | path: "", 61 | want: "PostV1", 62 | wantErr: true, 63 | }, 64 | } 65 | 66 | for _, test := range suite { 67 | got, err := generateDefaultOperationID(test.op, test.path) 68 | if err != nil { 69 | if !test.wantErr { 70 | t.Fatalf("did not expected error but got %v", err) 71 | } 72 | } 73 | 74 | if test.wantErr { 75 | return 76 | } 77 | if got != test.want { 78 | t.Fatalf("Operation ID generation error. Want [%v] Got [%v]", test.want, got) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/codegen/templates/additional-properties.tmpl: -------------------------------------------------------------------------------- 1 | {{range .Types}}{{$addType := .Schema.AdditionalPropertiesType.TypeDecl}} 2 | 3 | // Getter for additional properties for {{.TypeName}}. Returns the specified 4 | // element and whether it was found 5 | func (a {{.TypeName}}) Get(fieldName string) (value {{$addType}}, found bool) { 6 | if a.AdditionalProperties != nil { 7 | value, found = a.AdditionalProperties[fieldName] 8 | } 9 | return 10 | } 11 | 12 | // Setter for additional properties for {{.TypeName}} 13 | func (a *{{.TypeName}}) Set(fieldName string, value {{$addType}}) { 14 | if a.AdditionalProperties == nil { 15 | a.AdditionalProperties = make(map[string]{{$addType}}) 16 | } 17 | a.AdditionalProperties[fieldName] = value 18 | } 19 | 20 | // Override default JSON handling for {{.TypeName}} to handle AdditionalProperties 21 | func (a *{{.TypeName}}) UnmarshalJSON(b []byte) error { 22 | object := make(map[string]json.RawMessage) 23 | err := json.Unmarshal(b, &object) 24 | if err != nil { 25 | return err 26 | } 27 | {{range .Schema.Properties}} 28 | if raw, found := object["{{.JsonFieldName}}"]; found { 29 | err = json.Unmarshal(raw, &a.{{.GoFieldName}}) 30 | if err != nil { 31 | return fmt.Errorf("error reading '{{.JsonFieldName}}': %w", err) 32 | } 33 | delete(object, "{{.JsonFieldName}}") 34 | } 35 | {{end}} 36 | if len(object) != 0 { 37 | a.AdditionalProperties = make(map[string]{{$addType}}) 38 | for fieldName, fieldBuf := range object { 39 | var fieldVal {{$addType}} 40 | err := json.Unmarshal(fieldBuf, &fieldVal) 41 | if err != nil { 42 | return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) 43 | } 44 | a.AdditionalProperties[fieldName] = fieldVal 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | // Override default JSON handling for {{.TypeName}} to handle AdditionalProperties 51 | func (a {{.TypeName}}) MarshalJSON() ([]byte, error) { 52 | var err error 53 | object := make(map[string]json.RawMessage) 54 | {{range .Schema.Properties}} 55 | {{if not .Required}}if a.{{.GoFieldName}} != nil { {{end}} 56 | object["{{.JsonFieldName}}"], err = json.Marshal(a.{{.GoFieldName}}) 57 | if err != nil { 58 | return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err) 59 | } 60 | {{if not .Required}} }{{end}} 61 | {{end}} 62 | for fieldName, field := range a.AdditionalProperties { 63 | object[fieldName], err = json.Marshal(field) 64 | if err != nil { 65 | return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) 66 | } 67 | } 68 | return json.Marshal(object) 69 | } 70 | {{end}} 71 | -------------------------------------------------------------------------------- /pkg/codegen/templates/chi/chi-handler.tmpl: -------------------------------------------------------------------------------- 1 | // Handler creates http.Handler with routing matching OpenAPI spec. 2 | func Handler(si ServerInterface) http.Handler { 3 | return HandlerWithOptions(si, ChiServerOptions{}) 4 | } 5 | 6 | type ChiServerOptions struct { 7 | BaseURL string 8 | BaseRouter chi.Router 9 | Middlewares []MiddlewareFunc 10 | ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) 11 | } 12 | 13 | // HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. 14 | func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { 15 | return HandlerWithOptions(si, ChiServerOptions { 16 | BaseRouter: r, 17 | }) 18 | } 19 | 20 | func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { 21 | return HandlerWithOptions(si, ChiServerOptions { 22 | BaseURL: baseURL, 23 | BaseRouter: r, 24 | }) 25 | } 26 | 27 | // HandlerWithOptions creates http.Handler with additional options 28 | func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { 29 | r := options.BaseRouter 30 | 31 | if r == nil { 32 | r = chi.NewRouter() 33 | } 34 | if options.ErrorHandlerFunc == nil { 35 | options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { 36 | http.Error(w, err.Error(), http.StatusBadRequest) 37 | } 38 | } 39 | {{if .}}wrapper := ServerInterfaceWrapper{ 40 | Handler: si, 41 | HandlerMiddlewares: options.Middlewares, 42 | ErrorHandlerFunc: options.ErrorHandlerFunc, 43 | } 44 | {{end}} 45 | {{range .}}r.Group(func(r chi.Router) { 46 | r.{{.Method | lower | title }}(options.BaseURL+"{{.Path | swaggerUriToChiUri}}", wrapper.{{.OperationId}}) 47 | }) 48 | {{end}} 49 | return r 50 | } 51 | -------------------------------------------------------------------------------- /pkg/codegen/templates/chi/chi-interface.tmpl: -------------------------------------------------------------------------------- 1 | // ServerInterface represents all server handlers. 2 | type ServerInterface interface { 3 | {{range .}}{{.SummaryAsComment }} 4 | // ({{.Method}} {{.Path}}) 5 | {{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) 6 | {{end}} 7 | } 8 | -------------------------------------------------------------------------------- /pkg/codegen/templates/client-with-responses.tmpl: -------------------------------------------------------------------------------- 1 | // ClientWithResponses builds on ClientInterface to offer response payloads 2 | type ClientWithResponses struct { 3 | ClientInterface 4 | } 5 | 6 | // NewClientWithResponses creates a new ClientWithResponses, which wraps 7 | // Client with return type handling 8 | func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { 9 | client, err := NewClient(server, opts...) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return &ClientWithResponses{client}, nil 14 | } 15 | 16 | // WithBaseURL overrides the baseURL. 17 | func WithBaseURL(baseURL string) ClientOption { 18 | return func(c *Client) error { 19 | newBaseURL, err := url.Parse(baseURL) 20 | if err != nil { 21 | return err 22 | } 23 | c.Server = newBaseURL.String() 24 | return nil 25 | } 26 | } 27 | 28 | // ClientWithResponsesInterface is the interface specification for the client with responses above. 29 | type ClientWithResponsesInterface interface { 30 | {{range . -}} 31 | {{$hasParams := .RequiresParamObject -}} 32 | {{$pathParams := .PathParams -}} 33 | {{$opid := .OperationId -}} 34 | // {{$opid}} request{{if .HasBody}} with any body{{end}} 35 | {{$opid}}{{if .HasBody}}WithBody{{end}}WithResponse(ctx context.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) 36 | {{range .Bodies}} 37 | {{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) 38 | {{end}}{{/* range .Bodies */}} 39 | {{end}}{{/* range . $opid := .OperationId */}} 40 | } 41 | 42 | {{range .}}{{$opid := .OperationId}}{{$op := .}} 43 | type {{genResponseTypeName $opid | ucFirst}} struct { 44 | Body []byte 45 | HTTPResponse *http.Response 46 | {{- range getResponseTypeDefinitions .}} 47 | {{.TypeName}} *{{.Schema.TypeDecl}} 48 | {{- end}} 49 | } 50 | 51 | // Status returns HTTPResponse.Status 52 | func (r {{genResponseTypeName $opid | ucFirst}}) Status() string { 53 | if r.HTTPResponse != nil { 54 | return r.HTTPResponse.Status 55 | } 56 | return http.StatusText(0) 57 | } 58 | 59 | // StatusCode returns HTTPResponse.StatusCode 60 | func (r {{genResponseTypeName $opid | ucFirst}}) StatusCode() int { 61 | if r.HTTPResponse != nil { 62 | return r.HTTPResponse.StatusCode 63 | } 64 | return 0 65 | } 66 | {{end}} 67 | 68 | 69 | {{range .}} 70 | {{$opid := .OperationId -}} 71 | {{/* Generate client methods (with responses)*/}} 72 | 73 | // {{$opid}}{{if .HasBody}}WithBody{{end}}WithResponse request{{if .HasBody}} with arbitrary body{{end}} returning *{{genResponseTypeName $opid}} 74 | func (c *ClientWithResponses) {{$opid}}{{if .HasBody}}WithBody{{end}}WithResponse(ctx context.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error){ 75 | rsp, err := c.{{$opid}}{{if .HasBody}}WithBody{{end}}(ctx{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}{{if .HasBody}}, contentType, body{{end}}, reqEditors...) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return Parse{{genResponseTypeName $opid | ucFirst}}(rsp) 80 | } 81 | 82 | {{$hasParams := .RequiresParamObject -}} 83 | {{$pathParams := .PathParams -}} 84 | {{$bodyRequired := .BodyRequired -}} 85 | {{range .Bodies}} 86 | func (c *ClientWithResponses) {{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) { 87 | rsp, err := c.{{$opid}}{{.Suffix}}(ctx{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, body, reqEditors...) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return Parse{{genResponseTypeName $opid | ucFirst}}(rsp) 92 | } 93 | {{end}} 94 | 95 | {{end}}{{/* operations */}} 96 | 97 | {{/* Generate parse functions for responses*/}} 98 | {{range .}}{{$opid := .OperationId}} 99 | 100 | // Parse{{genResponseTypeName $opid | ucFirst}} parses an HTTP response from a {{$opid}}WithResponse call 101 | func Parse{{genResponseTypeName $opid | ucFirst}}(rsp *http.Response) (*{{genResponseTypeName $opid}}, error) { 102 | bodyBytes, err := ioutil.ReadAll(rsp.Body) 103 | defer func() { _ = rsp.Body.Close() }() 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | response := {{genResponsePayload $opid}} 109 | 110 | {{genResponseUnmarshal .}} 111 | 112 | return response, nil 113 | } 114 | {{end}}{{/* range . $opid := .OperationId */}} 115 | 116 | -------------------------------------------------------------------------------- /pkg/codegen/templates/constants.tmpl: -------------------------------------------------------------------------------- 1 | {{- if gt (len .SecuritySchemeProviderNames) 0 }} 2 | const ( 3 | {{range $ProviderName := .SecuritySchemeProviderNames}} 4 | {{- $ProviderName | ucFirst}}Scopes = "{{$ProviderName}}.Scopes" 5 | {{end}} 6 | ) 7 | {{end}} 8 | {{range $Enum := .EnumDefinitions}} 9 | // Defines values for {{$Enum.TypeName}}. 10 | const ( 11 | {{range $name, $value := $Enum.GetValues}} 12 | {{$name}} {{$Enum.TypeName}} = {{$Enum.ValueWrapper}}{{$value}}{{$Enum.ValueWrapper -}} 13 | {{end}} 14 | ) 15 | {{end}} 16 | -------------------------------------------------------------------------------- /pkg/codegen/templates/echo/echo-interface.tmpl: -------------------------------------------------------------------------------- 1 | // ServerInterface represents all server handlers. 2 | type ServerInterface interface { 3 | {{range .}}{{.SummaryAsComment }} 4 | // ({{.Method}} {{.Path}}) 5 | {{.OperationId}}(ctx echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error 6 | {{end}} 7 | } 8 | -------------------------------------------------------------------------------- /pkg/codegen/templates/echo/echo-register.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | // This is a simple interface which specifies echo.Route addition functions which 4 | // are present on both echo.Echo and echo.Group, since we want to allow using 5 | // either of them for path registration 6 | type EchoRouter interface { 7 | CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 8 | DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 9 | GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 10 | HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 11 | OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 12 | PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 13 | POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 14 | PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 15 | TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 16 | } 17 | 18 | // RegisterHandlers adds each server route to the EchoRouter. 19 | func RegisterHandlers(router EchoRouter, si ServerInterface, m ...echo.MiddlewareFunc) { 20 | RegisterHandlersWithBaseURL(router, si, "", m...) 21 | } 22 | 23 | // Registers handlers, and prepends BaseURL to the paths, so that the paths 24 | // can be served under a prefix. 25 | func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string, m ...echo.MiddlewareFunc) { 26 | {{if .}} 27 | wrapper := ServerInterfaceWrapper{ 28 | Handler: si, 29 | } 30 | {{end}} 31 | {{range .}}router.{{.Method}}(baseURL + "{{.Path | swaggerUriToEchoUri}}", wrapper.{{.OperationId}}, m...) 32 | {{end}} 33 | } 34 | -------------------------------------------------------------------------------- /pkg/codegen/templates/echo/echo-wrappers.tmpl: -------------------------------------------------------------------------------- 1 | // ServerInterfaceWrapper converts echo contexts to parameters. 2 | type ServerInterfaceWrapper struct { 3 | Handler ServerInterface 4 | } 5 | 6 | {{range .}}{{$opid := .OperationId}}// {{$opid}} converts echo context to params. 7 | func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { 8 | var err error 9 | {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- 10 | var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} 11 | {{if .IsPassThrough}} 12 | {{$varName}} = ctx.Param("{{.ParamName}}") 13 | {{end}} 14 | {{if .IsJson}} 15 | err = json.Unmarshal([]byte(ctx.Param("{{.ParamName}}")), &{{$varName}}) 16 | if err != nil { 17 | return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") 18 | } 19 | {{end}} 20 | {{if .IsStyled}} 21 | err = runtime.BindStyledParameterWithLocation("{{.Style}}",{{.Explode}}, "{{.ParamName}}", runtime.ParamLocationPath, ctx.Param("{{.ParamName}}"), &{{$varName}}) 22 | if err != nil { 23 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) 24 | } 25 | {{end}} 26 | {{end}} 27 | 28 | {{range .SecurityDefinitions}} 29 | ctx.Set({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes, {{toStringArray .Scopes}}) 30 | {{end}} 31 | 32 | {{if .RequiresParamObject}} 33 | // Parameter object where we will unmarshal all parameters from the context 34 | var params {{.OperationId}}Params 35 | {{range $paramIdx, $param := .QueryParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- 36 | {{if .IsStyled}} 37 | err = runtime.BindQueryParameter("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", ctx.QueryParams(), ¶ms.{{.GoName}}) 38 | if err != nil { 39 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) 40 | } 41 | {{else}} 42 | if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { 43 | {{if .IsPassThrough}} 44 | params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue 45 | {{end}} 46 | {{if .IsJson}} 47 | var value {{.TypeDef}} 48 | err = json.Unmarshal([]byte(paramValue), &value) 49 | if err != nil { 50 | return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") 51 | } 52 | params.{{.GoName}} = {{if not .Required}}&{{end}}value 53 | {{end}} 54 | }{{if .Required}} else { 55 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) 56 | }{{end}} 57 | {{end}} 58 | {{end}} 59 | 60 | {{if .HeaderParams}} 61 | headers := ctx.Request().Header 62 | {{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- 63 | if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { 64 | var {{.GoName}} {{.TypeDef}} 65 | n := len(valueList) 66 | if n != 1 { 67 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{.ParamName}}, got %d", n)) 68 | } 69 | {{if .IsPassThrough}} 70 | params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] 71 | {{end}} 72 | {{if .IsJson}} 73 | err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) 74 | if err != nil { 75 | return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") 76 | } 77 | {{end}} 78 | {{if .IsStyled}} 79 | err = runtime.BindStyledParameterWithLocation("{{.Style}}",{{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, valueList[0], &{{.GoName}}) 80 | if err != nil { 81 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) 82 | } 83 | {{end}} 84 | params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} 85 | } {{if .Required}}else { 86 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{.ParamName}} is required, but not found")) 87 | }{{end}} 88 | {{end}} 89 | {{end}} 90 | 91 | {{range .CookieParams}} 92 | if cookie, err := ctx.Cookie("{{.ParamName}}"); err == nil { 93 | {{if .IsPassThrough}} 94 | params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value 95 | {{end}} 96 | {{if .IsJson}} 97 | var value {{.TypeDef}} 98 | var decoded string 99 | decoded, err := url.QueryUnescape(cookie.Value) 100 | if err != nil { 101 | return echo.NewHTTPError(http.StatusBadRequest, "Error unescaping cookie parameter '{{.ParamName}}'") 102 | } 103 | err = json.Unmarshal([]byte(decoded), &value) 104 | if err != nil { 105 | return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") 106 | } 107 | params.{{.GoName}} = {{if not .Required}}&{{end}}value 108 | {{end}} 109 | {{if .IsStyled}} 110 | var value {{.TypeDef}} 111 | err = runtime.BindStyledParameterWithLocation("simple",{{.Explode}}, "{{.ParamName}}", runtime.ParamLocationCookie, cookie.Value, &value) 112 | if err != nil { 113 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) 114 | } 115 | params.{{.GoName}} = {{if not .Required}}&{{end}}value 116 | {{end}} 117 | }{{if .Required}} else { 118 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) 119 | }{{end}} 120 | 121 | {{end}}{{/* .CookieParams */}} 122 | 123 | {{end}}{{/* .RequiresParamObject */}} 124 | // Invoke the callback with all the unmarshalled arguments 125 | err = w.Handler.{{.OperationId}}(ctx{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) 126 | return err 127 | } 128 | {{end}} 129 | -------------------------------------------------------------------------------- /pkg/codegen/templates/gin/gin-interface.tmpl: -------------------------------------------------------------------------------- 1 | // ServerInterface represents all server handlers. 2 | type ServerInterface interface { 3 | {{range .}}{{.SummaryAsComment }} 4 | // ({{.Method}} {{.Path}}) 5 | {{.OperationId}}(c *gin.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) 6 | {{end}} 7 | } 8 | -------------------------------------------------------------------------------- /pkg/codegen/templates/gin/gin-register.tmpl: -------------------------------------------------------------------------------- 1 | // GinServerOptions provides options for the Gin server. 2 | type GinServerOptions struct { 3 | BaseURL string 4 | Middlewares []MiddlewareFunc 5 | } 6 | 7 | // RegisterHandlers creates http.Handler with routing matching OpenAPI spec. 8 | func RegisterHandlers(router *gin.Engine, si ServerInterface) *gin.Engine { 9 | return RegisterHandlersWithOptions(router, si, GinServerOptions{}) 10 | } 11 | 12 | // RegisterHandlersWithOptions creates http.Handler with additional options 13 | func RegisterHandlersWithOptions(router *gin.Engine, si ServerInterface, options GinServerOptions) *gin.Engine { 14 | {{if .}}wrapper := ServerInterfaceWrapper{ 15 | Handler: si, 16 | HandlerMiddlewares: options.Middlewares, 17 | } 18 | {{end}} 19 | {{range .}} 20 | router.{{.Method }}(options.BaseURL+"{{.Path | swaggerUriToGinUri }}", wrapper.{{.OperationId}}) 21 | {{end}} 22 | return router 23 | } 24 | -------------------------------------------------------------------------------- /pkg/codegen/templates/imports.tmpl: -------------------------------------------------------------------------------- 1 | // Package {{.PackageName}} provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/algorand/oapi-codegen DO NOT EDIT. 4 | package {{.PackageName}} 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "context" 10 | "encoding/base64" 11 | "encoding/json" 12 | "encoding/xml" 13 | "errors" 14 | "fmt" 15 | "gopkg.in/yaml.v2" 16 | "io" 17 | "io/ioutil" 18 | "net/http" 19 | "net/url" 20 | "path" 21 | "strings" 22 | "time" 23 | 24 | "github.com/algorand/oapi-codegen/pkg/runtime" 25 | openapi_types "github.com/algorand/oapi-codegen/pkg/types" 26 | "github.com/getkin/kin-openapi/openapi3" 27 | "github.com/go-chi/chi/v5" 28 | "github.com/labstack/echo/v4" 29 | "github.com/gin-gonic/gin" 30 | {{- range .ExternalImports}} 31 | {{ . }} 32 | {{- end}} 33 | ) 34 | -------------------------------------------------------------------------------- /pkg/codegen/templates/inline.tmpl: -------------------------------------------------------------------------------- 1 | // Base64 encoded, gzipped, json marshaled Swagger object 2 | var swaggerSpec = []string{ 3 | {{range .SpecParts}} 4 | "{{.}}",{{end}} 5 | } 6 | 7 | // GetSwagger returns the content of the embedded swagger specification file 8 | // or error if failed to decode 9 | func decodeSpec() ([]byte, error) { 10 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 11 | if err != nil { 12 | return nil, fmt.Errorf("error base64 decoding spec: %s", err) 13 | } 14 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 15 | if err != nil { 16 | return nil, fmt.Errorf("error decompressing spec: %s", err) 17 | } 18 | var buf bytes.Buffer 19 | _, err = buf.ReadFrom(zr) 20 | if err != nil { 21 | return nil, fmt.Errorf("error decompressing spec: %s", err) 22 | } 23 | 24 | return buf.Bytes(), nil 25 | } 26 | 27 | var rawSpec = decodeSpecCached() 28 | 29 | // a naive cached of a decoded swagger spec 30 | func decodeSpecCached() func() ([]byte, error) { 31 | data, err := decodeSpec() 32 | return func() ([]byte, error) { 33 | return data, err 34 | } 35 | } 36 | 37 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 38 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 39 | var res = make(map[string]func() ([]byte, error)) 40 | if len(pathToFile) > 0 { 41 | res[pathToFile] = rawSpec 42 | } 43 | {{ if .ImportMapping }} 44 | pathPrefix := path.Dir(pathToFile) 45 | {{ end }} 46 | {{ range $key, $value := .ImportMapping }} 47 | for rawPath, rawFunc := range {{ $value.Name }}.PathToRawSpec(path.Join(pathPrefix, "{{ $key }}")) { 48 | if _, ok := res[rawPath]; ok { 49 | // it is not possible to compare functions in golang, so always overwrite the old value 50 | } 51 | res[rawPath] = rawFunc 52 | } 53 | {{- end }} 54 | return res 55 | } 56 | 57 | // GetSwagger returns the Swagger specification corresponding to the generated code 58 | // in this file. The external references of Swagger specification are resolved. 59 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 60 | // Externally referenced files must be embedded in the corresponding golang packages. 61 | // Urls can be supported but this task was out of the scope. 62 | func GetSwagger() (swagger *openapi3.T, err error) { 63 | var resolvePath = PathToRawSpec("") 64 | 65 | loader := openapi3.NewLoader() 66 | loader.IsExternalRefsAllowed = true 67 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 68 | var pathToFile = url.String() 69 | pathToFile = path.Clean(pathToFile) 70 | getSpec, ok := resolvePath[pathToFile] 71 | if !ok { 72 | err1 := fmt.Errorf("path not found: %s", pathToFile) 73 | return nil, err1 74 | } 75 | return getSpec() 76 | } 77 | var specData []byte 78 | specData, err = rawSpec() 79 | if err != nil { 80 | return 81 | } 82 | swagger, err = loader.LoadFromData(specData) 83 | if err != nil { 84 | return 85 | } 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /pkg/codegen/templates/param-types.tmpl: -------------------------------------------------------------------------------- 1 | {{range .}}{{$opid := .OperationId}} 2 | {{range .TypeDefinitions}} 3 | // {{.TypeName}} defines parameters for {{$opid}}. 4 | type {{.TypeName}} {{if .IsAlias}}={{end}} {{.Schema.TypeDecl}} 5 | {{end}} 6 | {{end}} 7 | -------------------------------------------------------------------------------- /pkg/codegen/templates/request-bodies.tmpl: -------------------------------------------------------------------------------- 1 | {{range .}}{{$opid := .OperationId}} 2 | {{range .Bodies}} 3 | {{with .TypeDef $opid}} 4 | // {{.TypeName}} defines body for {{$opid}} for application/json ContentType. 5 | type {{.TypeName}} {{if .IsAlias}}={{end}} {{.Schema.TypeDecl}} 6 | {{end}} 7 | {{end}} 8 | {{end}} 9 | -------------------------------------------------------------------------------- /pkg/codegen/templates/typedef.tmpl: -------------------------------------------------------------------------------- 1 | {{range .Types}} 2 | {{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} defines model for {{.JsonName}}.{{ end }} 3 | type {{.TypeName}} {{if .IsAlias }}={{end}} {{.Schema.TypeDecl}} 4 | {{end}} 5 | -------------------------------------------------------------------------------- /pkg/codegen/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | 3 | info: 4 | title: OpenAPI-CodeGen Test 5 | description: 'This is a test OpenAPI Spec' 6 | version: 1.0.0 7 | 8 | servers: 9 | - url: https://test.oapi-codegen.com/v2 10 | - url: http://test.oapi-codegen.com/v2 11 | 12 | paths: 13 | /test/{name}: 14 | get: 15 | tags: 16 | - test 17 | summary: Get test 18 | operationId: getTestByName 19 | parameters: 20 | - name: name 21 | in: path 22 | required: true 23 | schema: 24 | type: string 25 | - name: $top 26 | in: query 27 | required: false 28 | schema: 29 | type: integer 30 | responses: 31 | 200: 32 | description: Success 33 | content: 34 | application/xml: 35 | schema: 36 | type: array 37 | items: 38 | $ref: '#/components/schemas/Test' 39 | application/json: 40 | schema: 41 | type: array 42 | items: 43 | $ref: '#/components/schemas/Test' 44 | 422: 45 | description: InvalidArray 46 | content: 47 | application/xml: 48 | schema: 49 | type: array 50 | application/json: 51 | schema: 52 | type: array 53 | default: 54 | description: Error 55 | content: 56 | application/json: 57 | schema: 58 | $ref: '#/components/schemas/Error' 59 | /cat: 60 | get: 61 | tags: 62 | - cat 63 | summary: Get cat status 64 | operationId: getCatStatus 65 | responses: 66 | 200: 67 | description: Success 68 | content: 69 | application/json: 70 | schema: 71 | oneOf: 72 | - $ref: '#/components/schemas/CatAlive' 73 | - $ref: '#/components/schemas/CatDead' 74 | application/xml: 75 | schema: 76 | anyOf: 77 | - $ref: '#/components/schemas/CatAlive' 78 | - $ref: '#/components/schemas/CatDead' 79 | application/yaml: 80 | schema: 81 | allOf: 82 | - $ref: '#/components/schemas/CatAlive' 83 | - $ref: '#/components/schemas/CatDead' 84 | default: 85 | description: Error 86 | content: 87 | application/json: 88 | schema: 89 | $ref: '#/components/schemas/Error' 90 | 91 | components: 92 | schemas: 93 | 94 | Test: 95 | properties: 96 | name: 97 | type: string 98 | cases: 99 | type: array 100 | items: 101 | $ref: '#/components/schemas/TestCase' 102 | 103 | TestCase: 104 | properties: 105 | name: 106 | type: string 107 | command: 108 | type: string 109 | 110 | Error: 111 | properties: 112 | code: 113 | type: integer 114 | format: int32 115 | message: 116 | type: string 117 | 118 | CatAlive: 119 | properties: 120 | name: 121 | type: string 122 | alive_since: 123 | type: string 124 | format: date-time 125 | 126 | CatDead: 127 | properties: 128 | name: 129 | type: string 130 | dead_since: 131 | type: string 132 | format: date-time 133 | x-oapi-codegen-extra-tags: 134 | tag1: value1 135 | tag2: value2 136 | cause: 137 | type: string 138 | enum: [car, dog, oldage] 139 | -------------------------------------------------------------------------------- /pkg/ecdsafile/ecdsafile.go: -------------------------------------------------------------------------------- 1 | package ecdsafile 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // These are utilities for working with files containing ECDSA public and 12 | // private keys. See this helpful doc for how to generate them: 13 | // https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations 14 | // 15 | // The quick cheat sheet below. 16 | // 1) Generate an ECDSA-P256 private key 17 | // openssl ecparam -name prime256v1 -genkey -noout -out ecprivatekey.pem 18 | // 2) Generate public key from private key 19 | // openssl ec -in ecprivatekey.pem -pubout -out ecpubkey.pem 20 | 21 | // LoadEcdsaPublicKey reads an ECDSA public key from an X509 encoding stored in a PEM encoding. 22 | func LoadEcdsaPublicKey(buf []byte) (*ecdsa.PublicKey, error) { 23 | block, _ := pem.Decode(buf) 24 | 25 | if block == nil { 26 | return nil, errors.New("no PEM data block found") 27 | } 28 | // The public key is loaded via a generic loader. We use X509 key format, 29 | // which supports multiple types of keys. 30 | keyIface, err := x509.ParsePKIXPublicKey(block.Bytes) 31 | if err != nil { 32 | return nil, fmt.Errorf("Error loading public key: %w", err) 33 | } 34 | 35 | // Now, we're assuming the key content is ECDSA, and converting. 36 | publicKey, ok := keyIface.(*ecdsa.PublicKey) 37 | if !ok { 38 | // The cast failed, we might have loaded an RSA file or something. 39 | return nil, errors.New("file contents were not an ECDSA public key") 40 | } 41 | return publicKey, nil 42 | } 43 | 44 | // LoadEcdsaPrivateKey reads an ECDSA private key from an X509 encoding stored in a PEM encoding 45 | func LoadEcdsaPrivateKey(buf []byte) (*ecdsa.PrivateKey, error) { 46 | block, _ := pem.Decode(buf) 47 | 48 | if block == nil { 49 | return nil, errors.New("no PEM data block found") 50 | } 51 | 52 | // At this point, we've got a valid PEM data block. PEM is just an encoding, 53 | // and we're assuming this encoding contains X509 key material. 54 | privateKey, err := x509.ParseECPrivateKey(block.Bytes) 55 | if err != nil { 56 | return nil, fmt.Errorf("Error loading private ECDSA key: %w", err) 57 | } 58 | return privateKey, nil 59 | } 60 | 61 | // StoreEcdsaPublicKey writes an ECDSA public key to a PEM encoding 62 | func StoreEcdsaPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { 63 | encodedKey, err := x509.MarshalPKIXPublicKey(publicKey) 64 | if err != nil { 65 | return nil, fmt.Errorf("error x509 encoding public key: %w", err) 66 | } 67 | pemEncodedKey := pem.EncodeToMemory(&pem.Block{ 68 | Type: "PUBLIC KEY", 69 | Bytes: encodedKey, 70 | }) 71 | return pemEncodedKey, nil 72 | } 73 | 74 | // StoreEcdsaPrivateKey writes an ECDSA private key to a PEM encoding 75 | func StoreEcdsaPrivateKey(privateKey *ecdsa.PrivateKey) ([]byte, error) { 76 | encodedKey, err := x509.MarshalECPrivateKey(privateKey) 77 | if err != nil { 78 | return nil, fmt.Errorf("error x509 encoding private key: %w", err) 79 | } 80 | pemEncodedKey := pem.EncodeToMemory(&pem.Block{ 81 | Type: "PRIVATE KEY", 82 | Bytes: encodedKey, 83 | }) 84 | return pemEncodedKey, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/gin-middleware/oapi_validate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package middleware 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "strings" 24 | 25 | "github.com/getkin/kin-openapi/openapi3" 26 | "github.com/getkin/kin-openapi/openapi3filter" 27 | "github.com/getkin/kin-openapi/routers" 28 | "github.com/getkin/kin-openapi/routers/gorillamux" 29 | "github.com/gin-gonic/gin" 30 | ) 31 | 32 | const ( 33 | GinContextKey = "oapi-codegen/gin-context" 34 | UserDataKey = "oapi-codegen/user-data" 35 | ) 36 | 37 | // Create validator middleware from a YAML file path 38 | func OapiValidatorFromYamlFile(path string) (gin.HandlerFunc, error) { 39 | data, err := ioutil.ReadFile(path) 40 | if err != nil { 41 | return nil, fmt.Errorf("error reading %s: %s", path, err) 42 | } 43 | 44 | swagger, err := openapi3.NewLoader().LoadFromData(data) 45 | if err != nil { 46 | return nil, fmt.Errorf("error parsing %s as Swagger YAML: %s", 47 | path, err) 48 | } 49 | return OapiRequestValidator(swagger), nil 50 | } 51 | 52 | // This is an gin middleware function which validates incoming HTTP requests 53 | // to make sure that they conform to the given OAPI 3.0 specification. When 54 | // OAPI validation fails on the request, we return an HTTP/400 with error message 55 | func OapiRequestValidator(swagger *openapi3.T) gin.HandlerFunc { 56 | return OapiRequestValidatorWithOptions(swagger, nil) 57 | } 58 | 59 | // ErrorHandler is called when there is an error in validation 60 | type ErrorHandler func(c *gin.Context, message string, statusCode int) 61 | 62 | // Options to customize request validation. These are passed through to 63 | // openapi3filter. 64 | type Options struct { 65 | ErrorHandler ErrorHandler 66 | Options openapi3filter.Options 67 | ParamDecoder openapi3filter.ContentParameterDecoder 68 | UserData interface{} 69 | } 70 | 71 | // Create a validator from a swagger object, with validation options 72 | func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) gin.HandlerFunc { 73 | router, err := gorillamux.NewRouter(swagger) 74 | if err != nil { 75 | panic(err) 76 | } 77 | return func(c *gin.Context) { 78 | err := ValidateRequestFromContext(c, router, options) 79 | if err != nil { 80 | if options != nil && options.ErrorHandler != nil { 81 | options.ErrorHandler(c, err.Error(), http.StatusBadRequest) 82 | // in case the handler didn't internally call Abort, stop the chain 83 | c.Abort() 84 | } else { 85 | // note: i am not sure if this is the best way to handle this 86 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 87 | } 88 | } 89 | c.Next() 90 | } 91 | } 92 | 93 | // ValidateRequestFromContext is called from the middleware above and actually does the work 94 | // of validating a request. 95 | func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *Options) error { 96 | req := c.Request 97 | route, pathParams, err := router.FindRoute(req) 98 | 99 | // We failed to find a matching route for the request. 100 | if err != nil { 101 | switch e := err.(type) { 102 | case *routers.RouteError: 103 | // We've got a bad request, the path requested doesn't match 104 | // either server, or path, or something. 105 | return errors.New(e.Reason) 106 | default: 107 | // This should never happen today, but if our upstream code changes, 108 | // we don't want to crash the server, so handle the unexpected error. 109 | return fmt.Errorf("error validating route: %s", err.Error()) 110 | } 111 | } 112 | 113 | validationInput := &openapi3filter.RequestValidationInput{ 114 | Request: req, 115 | PathParams: pathParams, 116 | Route: route, 117 | } 118 | 119 | // Pass the gin context into the request validator, so that any callbacks 120 | // which it invokes make it available. 121 | requestContext := context.WithValue(context.Background(), GinContextKey, c) 122 | 123 | if options != nil { 124 | validationInput.Options = &options.Options 125 | validationInput.ParamDecoder = options.ParamDecoder 126 | requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) 127 | } 128 | 129 | err = openapi3filter.ValidateRequest(requestContext, validationInput) 130 | if err != nil { 131 | switch e := err.(type) { 132 | case *openapi3filter.RequestError: 133 | // We've got a bad request 134 | // Split up the verbose error by lines and return the first one 135 | // openapi errors seem to be multi-line with a decent message on the first 136 | errorLines := strings.Split(e.Error(), "\n") 137 | return fmt.Errorf("error in openapi3filter.RequestError: %s", errorLines[0]) 138 | case *openapi3filter.SecurityRequirementsError: 139 | return fmt.Errorf("error in openapi3filter.SecurityRequirementsError: %s", e.Error()) 140 | default: 141 | // This should never happen today, but if our upstream code changes, 142 | // we don't want to crash the server, so handle the unexpected error. 143 | return fmt.Errorf("error validating request: %s", err) 144 | } 145 | } 146 | return nil 147 | } 148 | 149 | // Helper function to get the echo context from within requests. It returns 150 | // nil if not found or wrong type. 151 | func GetGinContext(c context.Context) *gin.Context { 152 | iface := c.Value(GinContextKey) 153 | if iface == nil { 154 | return nil 155 | } 156 | ginCtx, ok := iface.(*gin.Context) 157 | if !ok { 158 | return nil 159 | } 160 | return ginCtx 161 | } 162 | 163 | func GetUserData(c context.Context) interface{} { 164 | return c.Value(UserDataKey) 165 | } 166 | -------------------------------------------------------------------------------- /pkg/gin-middleware/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai/ 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | components: 70 | securitySchemes: 71 | BearerAuth: 72 | type: http 73 | scheme: bearer 74 | bearerFormat: JWT 75 | -------------------------------------------------------------------------------- /pkg/middleware/test_spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: TestServer 5 | servers: 6 | - url: http://deepmap.ai 7 | paths: 8 | /resource: 9 | get: 10 | operationId: getResource 11 | parameters: 12 | - name: id 13 | in: query 14 | schema: 15 | type: integer 16 | minimum: 10 17 | maximum: 100 18 | responses: 19 | '200': 20 | description: success 21 | content: 22 | application/json: 23 | schema: 24 | properties: 25 | name: 26 | type: string 27 | id: 28 | type: integer 29 | post: 30 | operationId: createResource 31 | responses: 32 | '204': 33 | description: No content 34 | requestBody: 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | properties: 40 | name: 41 | type: string 42 | /protected_resource: 43 | get: 44 | operationId: getProtectedResource 45 | security: 46 | - BearerAuth: 47 | - someScope 48 | responses: 49 | '204': 50 | description: no content 51 | /protected_resource2: 52 | get: 53 | operationId: getProtectedResource 54 | security: 55 | - BearerAuth: 56 | - otherScope 57 | responses: 58 | '204': 59 | description: no content 60 | /protected_resource_401: 61 | get: 62 | operationId: getProtectedResource 63 | security: 64 | - BearerAuth: 65 | - unauthorized 66 | responses: 67 | '401': 68 | description: no content 69 | components: 70 | securitySchemes: 71 | BearerAuth: 72 | type: http 73 | scheme: bearer 74 | bearerFormat: JWT 75 | -------------------------------------------------------------------------------- /pkg/runtime/bind.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package runtime 15 | 16 | // Binder is the interface implemented by types that can be bound to a query string or a parameter string 17 | // The input can be assumed to be a valid string. If you define a Bind method you are responsible for all 18 | // data being completely bound to the type. 19 | // 20 | // By convention, to approximate the behavior of Bind functions themselves, 21 | // Binder implements Bind("") as a no-op. 22 | type Binder interface { 23 | Bind(src string) error 24 | } -------------------------------------------------------------------------------- /pkg/runtime/bindstring.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 DeepMap, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package runtime 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "reflect" 20 | "strconv" 21 | "time" 22 | 23 | "github.com/algorand/oapi-codegen/pkg/types" 24 | ) 25 | 26 | // This function takes a string, and attempts to assign it to the destination 27 | // interface via whatever type conversion is necessary. We have to do this 28 | // via reflection instead of a much simpler type switch so that we can handle 29 | // type aliases. This function was the easy way out, the better way, since we 30 | // know the destination type each place that we use this, is to generate code 31 | // to read each specific type. 32 | func BindStringToObject(src string, dst interface{}) error { 33 | var err error 34 | 35 | v := reflect.ValueOf(dst) 36 | t := reflect.TypeOf(dst) 37 | 38 | // We need to dereference pointers 39 | if t.Kind() == reflect.Ptr { 40 | v = reflect.Indirect(v) 41 | t = v.Type() 42 | } 43 | 44 | // For some optioinal args 45 | if t.Kind() == reflect.Ptr { 46 | if v.IsNil() { 47 | v.Set(reflect.New(t.Elem())) 48 | } 49 | 50 | v = reflect.Indirect(v) 51 | t = v.Type() 52 | } 53 | 54 | // The resulting type must be settable. reflect will catch issues like 55 | // passing the destination by value. 56 | if !v.CanSet() { 57 | return errors.New("destination is not settable") 58 | } 59 | 60 | switch t.Kind() { 61 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 62 | var val int64 63 | val, err = strconv.ParseInt(src, 10, 64) 64 | if err == nil { 65 | v.SetInt(val) 66 | } 67 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 68 | var val uint64 69 | val, err = strconv.ParseUint(src, 10, 64) 70 | if err == nil { 71 | v.SetUint(val) 72 | } 73 | case reflect.String: 74 | v.SetString(src) 75 | err = nil 76 | case reflect.Float64, reflect.Float32: 77 | var val float64 78 | val, err = strconv.ParseFloat(src, 64) 79 | if err == nil { 80 | v.SetFloat(val) 81 | } 82 | case reflect.Bool: 83 | var val bool 84 | val, err = strconv.ParseBool(src) 85 | if err == nil { 86 | v.SetBool(val) 87 | } 88 | case reflect.Struct: 89 | // if this is not of type Time or of type Date look to see if this is of type Binder. 90 | if dstType, ok := dst.(Binder); ok { 91 | return dstType.Bind(src) 92 | } 93 | 94 | if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { 95 | // Don't fail on empty string. 96 | if src == "" { 97 | return nil 98 | } 99 | // Time is a special case of a struct that we handle 100 | parsedTime, err := time.Parse(time.RFC3339Nano, src) 101 | if err != nil { 102 | parsedTime, err = time.Parse(types.DateFormat, src) 103 | if err != nil { 104 | return fmt.Errorf("error parsing '%s' as RFC3339 or 2006-01-02 time: %s", src, err) 105 | } 106 | } 107 | // So, assigning this gets a little fun. We have a value to the 108 | // dereference destination. We can't do a conversion to 109 | // time.Time because the result isn't assignable, so we need to 110 | // convert pointers. 111 | if t != reflect.TypeOf(time.Time{}) { 112 | vPtr := v.Addr() 113 | vtPtr := vPtr.Convert(reflect.TypeOf(&time.Time{})) 114 | v = reflect.Indirect(vtPtr) 115 | } 116 | v.Set(reflect.ValueOf(parsedTime)) 117 | return nil 118 | } 119 | 120 | if t.ConvertibleTo(reflect.TypeOf(types.Date{})) { 121 | // Don't fail on empty string. 122 | if src == "" { 123 | return nil 124 | } 125 | parsedTime, err := time.Parse(types.DateFormat, src) 126 | if err != nil { 127 | return fmt.Errorf("error parsing '%s' as date: %s", src, err) 128 | } 129 | parsedDate := types.Date{Time: parsedTime} 130 | 131 | // We have to do the same dance here to assign, just like with times 132 | // above. 133 | if t != reflect.TypeOf(types.Date{}) { 134 | vPtr := v.Addr() 135 | vtPtr := vPtr.Convert(reflect.TypeOf(&types.Date{})) 136 | v = reflect.Indirect(vtPtr) 137 | } 138 | v.Set(reflect.ValueOf(parsedDate)) 139 | return nil 140 | } 141 | 142 | // We fall through to the error case below if we haven't handled the 143 | // destination type above. 144 | fallthrough 145 | default: 146 | // We've got a bunch of types unimplemented, don't fail silently. 147 | err = fmt.Errorf("can not bind to destination of type: %s", t.Kind()) 148 | } 149 | if err != nil { 150 | return fmt.Errorf("error binding string parameter: %s", err) 151 | } 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /pkg/runtime/deepobject_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type InnerObject struct { 14 | Name string 15 | ID int 16 | } 17 | 18 | // These are all possible field types, mandatory and optional. 19 | type AllFields struct { 20 | I int `json:"i"` 21 | Oi *int `json:"oi,omitempty"` 22 | F float32 `json:"f"` 23 | Of *float32 `json:"of,omitempty"` 24 | B bool `json:"b"` 25 | Ob *bool `json:"ob,omitempty"` 26 | As []string `json:"as"` 27 | Oas *[]string `json:"oas,omitempty"` 28 | O InnerObject `json:"o"` 29 | Oo *InnerObject `json:"oo,omitempty"` 30 | D MockBinder `json:"d"` 31 | Od *MockBinder `json:"od,omitempty"` 32 | } 33 | 34 | func TestDeepObject(t *testing.T) { 35 | oi := int(5) 36 | of := float32(3.7) 37 | ob := true 38 | oas := []string{"foo", "bar"} 39 | oo := InnerObject{ 40 | Name: "Marcin Romaszewicz", 41 | ID: 123, 42 | } 43 | d := MockBinder{Time: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC)} 44 | 45 | srcObj := AllFields{ 46 | I: 12, 47 | Oi: &oi, 48 | F: 4.2, 49 | Of: &of, 50 | B: true, 51 | Ob: &ob, 52 | As: []string{"hello", "world"}, 53 | Oas: &oas, 54 | O: InnerObject{ 55 | Name: "Joe Schmoe", 56 | ID: 456, 57 | }, 58 | Oo: &oo, 59 | D: d, 60 | Od: &d, 61 | } 62 | 63 | marshaled, err := MarshalDeepObject(srcObj, "p") 64 | require.NoError(t, err) 65 | t.Log(marshaled) 66 | 67 | params := make(url.Values) 68 | marshaledParts := strings.Split(marshaled, "&") 69 | for _, p := range marshaledParts { 70 | parts := strings.Split(p, "=") 71 | require.Equal(t, 2, len(parts)) 72 | params.Set(parts[0], parts[1]) 73 | } 74 | 75 | var dstObj AllFields 76 | err = UnmarshalDeepObject(&dstObj, "p", params) 77 | require.NoError(t, err) 78 | assert.EqualValues(t, srcObj, dstObj) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/securityprovider/securityprovider.go: -------------------------------------------------------------------------------- 1 | // Package securityprovider contains some default securityprovider 2 | // implementations, which can be used as a RequestEditorFn of a 3 | // client. 4 | package securityprovider 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | // ErrSecurityProviderApiKeyInvalidIn indicates a usage of an invalid In. 14 | // Should be cookie, header or query 15 | ErrSecurityProviderApiKeyInvalidIn = SecurityProviderError("invalid 'in' specified for apiKey") 16 | ) 17 | 18 | // SecurityProviderError defines error values of a security provider. 19 | type SecurityProviderError string 20 | 21 | // Error implements the error interface. 22 | func (e SecurityProviderError) Error() string { 23 | return string(e) 24 | } 25 | 26 | // NewSecurityProviderBasicAuth provides a SecurityProvider, which can solve 27 | // the BasicAuth challenge for api-calls. 28 | func NewSecurityProviderBasicAuth(username, password string) (*SecurityProviderBasicAuth, error) { 29 | return &SecurityProviderBasicAuth{ 30 | username: username, 31 | password: password, 32 | }, nil 33 | } 34 | 35 | // SecurityProviderBasicAuth sends a base64-encoded combination of 36 | // username, password along with a request. 37 | type SecurityProviderBasicAuth struct { 38 | username string 39 | password string 40 | } 41 | 42 | // Intercept will attach an Authorization header to the request and ensures that 43 | // the username, password are base64 encoded and attached to the header. 44 | func (s *SecurityProviderBasicAuth) Intercept(ctx context.Context, req *http.Request) error { 45 | req.SetBasicAuth(s.username, s.password) 46 | return nil 47 | } 48 | 49 | // NewSecurityProviderBearerToken provides a SecurityProvider, which can solve 50 | // the Bearer Auth challende for api-calls. 51 | func NewSecurityProviderBearerToken(token string) (*SecurityProviderBearerToken, error) { 52 | return &SecurityProviderBearerToken{ 53 | token: token, 54 | }, nil 55 | } 56 | 57 | // SecurityProviderBearerToken sends a token as part of an 58 | // Authorization: Bearer header along with a request. 59 | type SecurityProviderBearerToken struct { 60 | token string 61 | } 62 | 63 | // Intercept will attach an Authorization header to the request 64 | // and ensures that the bearer token is attached to the header. 65 | func (s *SecurityProviderBearerToken) Intercept(ctx context.Context, req *http.Request) error { 66 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.token)) 67 | return nil 68 | } 69 | 70 | // NewSecurityProviderApiKey will attach a generic apiKey for a given name 71 | // either to a cookie, header or as a query parameter. 72 | func NewSecurityProviderApiKey(in, name, apiKey string) (*SecurityProviderApiKey, error) { 73 | interceptors := map[string]func(ctx context.Context, req *http.Request) error{ 74 | "cookie": func(ctx context.Context, req *http.Request) error { 75 | req.AddCookie(&http.Cookie{Name: name, Value: apiKey}) 76 | return nil 77 | }, 78 | "header": func(ctx context.Context, req *http.Request) error { 79 | req.Header.Add(name, apiKey) 80 | return nil 81 | }, 82 | "query": func(ctx context.Context, req *http.Request) error { 83 | query := req.URL.Query() 84 | query.Add(name, apiKey) 85 | req.URL.RawQuery = query.Encode() 86 | return nil 87 | }, 88 | } 89 | 90 | interceptor, ok := interceptors[in] 91 | if !ok { 92 | return nil, ErrSecurityProviderApiKeyInvalidIn 93 | } 94 | 95 | return &SecurityProviderApiKey{ 96 | interceptor: interceptor, 97 | }, nil 98 | } 99 | 100 | // SecurityProviderApiKey will attach an apiKey either to a 101 | // cookie, header or query. 102 | type SecurityProviderApiKey struct { 103 | interceptor func(ctx context.Context, req *http.Request) error 104 | } 105 | 106 | // Intercept will attach a cookie, header or query param for the configured 107 | // name and apiKey. 108 | func (s *SecurityProviderApiKey) Intercept(ctx context.Context, req *http.Request) error { 109 | return s.interceptor(ctx, req) 110 | } 111 | -------------------------------------------------------------------------------- /pkg/securityprovider/securityprovider_test.go: -------------------------------------------------------------------------------- 1 | package securityprovider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/algorand/oapi-codegen/internal/test/client" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | withTrailingSlash string = "https://my-api.com/some-base-url/v1/" 12 | ) 13 | 14 | func TestSecurityProviders(t *testing.T) { 15 | bearer, err := NewSecurityProviderBearerToken("mytoken") 16 | assert.NoError(t, err) 17 | client1, err := client.NewClient( 18 | withTrailingSlash, 19 | client.WithRequestEditorFn(bearer.Intercept), 20 | ) 21 | assert.NoError(t, err) 22 | 23 | apiKey, err := NewSecurityProviderApiKey("cookie", "apikey", "mykey") 24 | assert.NoError(t, err) 25 | client2, err := client.NewClient( 26 | withTrailingSlash, 27 | client.WithRequestEditorFn(apiKey.Intercept), 28 | ) 29 | assert.NoError(t, err) 30 | 31 | basicAuth, err := NewSecurityProviderBasicAuth("username", "password") 32 | assert.NoError(t, err) 33 | client3, err := client.NewClient( 34 | withTrailingSlash, 35 | client.WithRequestEditorFn(basicAuth.Intercept), 36 | ) 37 | assert.NoError(t, err) 38 | 39 | assert.Equal(t, withTrailingSlash, client1.Server) 40 | assert.Equal(t, withTrailingSlash, client2.Server) 41 | assert.Equal(t, withTrailingSlash, client3.Server) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/testutil/response_handlers.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | func init() { 10 | knownHandlers = make(map[string]ResponseHandler) 11 | 12 | RegisterResponseHandler("application/json", jsonHandler) 13 | } 14 | 15 | var ( 16 | knownHandlersMu sync.Mutex 17 | knownHandlers map[string]ResponseHandler 18 | ) 19 | 20 | type ResponseHandler func(contentType string, raw io.Reader, obj interface{}, strict bool) error 21 | 22 | func RegisterResponseHandler(mime string, handler ResponseHandler) { 23 | knownHandlersMu.Lock() 24 | defer knownHandlersMu.Unlock() 25 | 26 | knownHandlers[mime] = handler 27 | } 28 | 29 | func getHandler(mime string) ResponseHandler { 30 | knownHandlersMu.Lock() 31 | defer knownHandlersMu.Unlock() 32 | 33 | return knownHandlers[mime] 34 | } 35 | 36 | // This function assumes that the response contains JSON and unmarshals it 37 | // into the specified object. 38 | func jsonHandler(_ string, r io.Reader, obj interface{}, strict bool) error { 39 | d := json.NewDecoder(r) 40 | if strict { 41 | d.DisallowUnknownFields() 42 | } 43 | return json.NewDecoder(r).Decode(obj) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/types/date.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | const DateFormat = "2006-01-02" 9 | 10 | type Date struct { 11 | time.Time 12 | } 13 | 14 | func (d Date) MarshalJSON() ([]byte, error) { 15 | return json.Marshal(d.Time.Format(DateFormat)) 16 | } 17 | 18 | func (d *Date) UnmarshalJSON(data []byte) error { 19 | var dateStr string 20 | err := json.Unmarshal(data, &dateStr) 21 | if err != nil { 22 | return err 23 | } 24 | parsed, err := time.Parse(DateFormat, dateStr) 25 | if err != nil { 26 | return err 27 | } 28 | d.Time = parsed 29 | return nil 30 | } 31 | 32 | func (d Date) String() string { 33 | return d.Time.Format(DateFormat) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/types/date_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDate_MarshalJSON(t *testing.T) { 13 | testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) 14 | b := struct { 15 | DateField Date `json:"date"` 16 | }{ 17 | DateField: Date{testDate}, 18 | } 19 | jsonBytes, err := json.Marshal(b) 20 | assert.NoError(t, err) 21 | assert.JSONEq(t, `{"date":"2019-04-01"}`, string(jsonBytes)) 22 | } 23 | 24 | func TestDate_UnmarshalJSON(t *testing.T) { 25 | testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) 26 | jsonStr := `{"date":"2019-04-01"}` 27 | b := struct { 28 | DateField Date `json:"date"` 29 | }{} 30 | err := json.Unmarshal([]byte(jsonStr), &b) 31 | assert.NoError(t, err) 32 | assert.Equal(t, testDate, b.DateField.Time) 33 | } 34 | 35 | func TestDate_Stringer(t *testing.T) { 36 | t.Run("nil date", func(t *testing.T) { 37 | var d *Date 38 | assert.Equal(t, "", fmt.Sprintf("%v", d)) 39 | }) 40 | 41 | t.Run("ptr date", func(t *testing.T) { 42 | d := &Date{ 43 | Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), 44 | } 45 | assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) 46 | }) 47 | 48 | t.Run("value date", func(t *testing.T) { 49 | d := Date{ 50 | Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), 51 | } 52 | assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/types/email.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type Email string 9 | 10 | func (e Email) MarshalJSON() ([]byte, error) { 11 | if !emailRegex.MatchString(string(e)) { 12 | return nil, errors.New("email: failed to pass regex validation") 13 | } 14 | return json.Marshal(string(e)) 15 | } 16 | 17 | func (e *Email) UnmarshalJSON(data []byte) error { 18 | var s string 19 | if err := json.Unmarshal(data, &s); err != nil { 20 | return err 21 | } 22 | if !emailRegex.MatchString(s) { 23 | return errors.New("email: failed to pass regex validation") 24 | } 25 | *e = Email(s) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/types/email_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEmail_MarshalJSON(t *testing.T) { 11 | testEmail := "gaben@valvesoftware.com" 12 | b := struct { 13 | EmailField Email `json:"email"` 14 | }{ 15 | EmailField: Email(testEmail), 16 | } 17 | jsonBytes, err := json.Marshal(b) 18 | assert.NoError(t, err) 19 | assert.JSONEq(t, `{"email":"gaben@valvesoftware.com"}`, string(jsonBytes)) 20 | } 21 | 22 | func TestEmail_UnmarshalJSON(t *testing.T) { 23 | testEmail := Email("gaben@valvesoftware.com") 24 | jsonStr := `{"email":"gaben@valvesoftware.com"}` 25 | b := struct { 26 | EmailField Email `json:"email"` 27 | }{} 28 | err := json.Unmarshal([]byte(jsonStr), &b) 29 | assert.NoError(t, err) 30 | assert.Equal(t, testEmail, b.EmailField) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/types/regexes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "regexp" 4 | 5 | const ( 6 | emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" 7 | ) 8 | 9 | var ( 10 | emailRegex = regexp.MustCompile(emailRegexString) 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/types/uuid.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type UUID = uuid.UUID 8 | -------------------------------------------------------------------------------- /pkg/types/uuid_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestUUID_MarshalJSON_Zero(t *testing.T) { 12 | var testUUID UUID 13 | b := struct { 14 | UUIDField UUID `json:"uuid"` 15 | }{ 16 | UUIDField: testUUID, 17 | } 18 | marshaled, err := json.Marshal(b) 19 | assert.NoError(t, err) 20 | assert.JSONEq(t, `{"uuid":"00000000-0000-0000-0000-000000000000"}`, string(marshaled)) 21 | } 22 | 23 | func TestUUID_MarshalJSON_Pass(t *testing.T) { 24 | testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") 25 | b := struct { 26 | UUIDField UUID `json:"uuid"` 27 | }{ 28 | UUIDField: testUUID, 29 | } 30 | jsonBytes, err := json.Marshal(b) 31 | assert.NoError(t, err) 32 | assert.JSONEq(t, `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}`, string(jsonBytes)) 33 | } 34 | 35 | func TestUUID_UnmarshalJSON_Fail(t *testing.T) { 36 | jsonStr := `{"uuid":"this-is-not-a-uuid"}` 37 | b := struct { 38 | UUIDField UUID `json:"uuid"` 39 | }{} 40 | err := json.Unmarshal([]byte(jsonStr), &b) 41 | assert.Error(t, err) 42 | } 43 | 44 | func TestUUID_UnmarshalJSON_Pass(t *testing.T) { 45 | testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") 46 | jsonStr := `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}` 47 | b := struct { 48 | UUIDField UUID `json:"uuid"` 49 | }{} 50 | err := json.Unmarshal([]byte(jsonStr), &b) 51 | assert.NoError(t, err) 52 | assert.Equal(t, testUUID, b.UUIDField) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/util/inputmapping.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // The input mapping is experessed on the command line as `key1:value1,key2:value2,...` 9 | // We parse it here, but need to keep in mind that keys or values may contain 10 | // commas and colons. We will allow escaping those using double quotes, so 11 | // when passing in "key1":"value1", we will not look inside the quoted sections. 12 | func ParseCommandlineMap(src string) (map[string]string, error) { 13 | result := make(map[string]string) 14 | tuples := splitString(src, ',') 15 | for _, t := range tuples { 16 | kv := splitString(t, ':') 17 | if len(kv) != 2 { 18 | return nil, fmt.Errorf("expected key:value, got :%s", t) 19 | } 20 | key := strings.TrimLeft(kv[0], `"`) 21 | key = strings.TrimRight(key, `"`) 22 | 23 | value := strings.TrimLeft(kv[1], `"`) 24 | value = strings.TrimRight(value, `"`) 25 | 26 | result[key] = value 27 | } 28 | return result, nil 29 | } 30 | 31 | // ParseCommandLineList parses comma separated string lists which are passed 32 | // in on the command line. Spaces are trimmed off both sides of result 33 | // strings. 34 | func ParseCommandLineList(input string) []string { 35 | input = strings.TrimSpace(input) 36 | if len(input) == 0 { 37 | return nil 38 | } 39 | splitInput := strings.Split(input, ",") 40 | args := make([]string, 0, len(splitInput)) 41 | for _, s := range splitInput { 42 | s = strings.TrimSpace(s) 43 | if len(s) > 0 { 44 | args = append(args, s) 45 | } 46 | } 47 | return args 48 | } 49 | 50 | // This function splits a string along the specifed separator, but it 51 | // ignores anything between double quotes for splitting. We do simple 52 | // inside/outside quote counting. Quotes are not stripped from output. 53 | func splitString(s string, sep rune) []string { 54 | const escapeChar rune = '"' 55 | 56 | var parts []string 57 | var part string 58 | inQuotes := false 59 | 60 | for _, c := range s { 61 | if c == escapeChar { 62 | if inQuotes { 63 | inQuotes = false 64 | } else { 65 | inQuotes = true 66 | } 67 | } 68 | 69 | // If we've gotten the separator rune, consider the previous part 70 | // complete, but only if we're outside of quoted sections 71 | if c == sep && !inQuotes { 72 | parts = append(parts, part) 73 | part = "" 74 | continue 75 | } 76 | part = part + string(c) 77 | } 78 | return append(parts, part) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/util/inputmapping_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseInputMapping(t *testing.T) { 11 | var src string 12 | var expected, parsed map[string]string 13 | var err error 14 | 15 | src = "key1:value1,key2:value2" 16 | expected = map[string]string{"key1": "value1", "key2": "value2"} 17 | parsed, err = ParseCommandlineMap(src) 18 | require.NoError(t, err) 19 | assert.Equal(t, expected, parsed) 20 | 21 | src = `key1:"value1,value2",key2:value3` 22 | expected = map[string]string{"key1": "value1,value2", "key2": "value3"} 23 | parsed, err = ParseCommandlineMap(src) 24 | require.NoError(t, err) 25 | assert.Equal(t, expected, parsed) 26 | 27 | src = `key1:"value1,value2,key2:value3"` 28 | expected = map[string]string{"key1": "value1,value2,key2:value3"} 29 | parsed, err = ParseCommandlineMap(src) 30 | require.NoError(t, err) 31 | assert.Equal(t, expected, parsed) 32 | 33 | src = `"key1,key2":value1` 34 | expected = map[string]string{"key1,key2": "value1"} 35 | parsed, err = ParseCommandlineMap(src) 36 | require.NoError(t, err) 37 | assert.Equal(t, expected, parsed) 38 | } 39 | 40 | func TestSplitString(t *testing.T) { 41 | var src string 42 | var expected, result []string 43 | var err error 44 | 45 | src = "1,2,3" 46 | expected = []string{"1", "2", "3"} 47 | result = splitString(src, ',') 48 | require.NoError(t, err) 49 | assert.Equal(t, expected, result) 50 | 51 | src = `"1,2",3` 52 | expected = []string{`"1,2"`, "3"} 53 | result = splitString(src, ',') 54 | require.NoError(t, err) 55 | assert.Equal(t, expected, result) 56 | 57 | src = `1,"2,3",` 58 | expected = []string{"1", `"2,3"`, ""} 59 | result = splitString(src, ',') 60 | require.NoError(t, err) 61 | assert.Equal(t, expected, result) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/util/loader.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | ) 8 | 9 | func LoadSwagger(filePath string) (swagger *openapi3.T, err error) { 10 | 11 | loader := openapi3.NewLoader() 12 | loader.IsExternalRefsAllowed = true 13 | 14 | u, err := url.Parse(filePath) 15 | if err == nil && u.Scheme != "" && u.Host != "" { 16 | return loader.LoadFromURI(u) 17 | } else { 18 | return loader.LoadFromFile(filePath) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/cyberdelia/templates" 7 | _ "github.com/matryer/moq" 8 | ) 9 | --------------------------------------------------------------------------------