{"id":13558116,"url":"https://github.com/ahx/openapi_first","last_synced_at":"2025-05-15T13:08:11.779Z","repository":{"id":35002916,"uuid":"171639126","full_name":"ahx/openapi_first","owner":"ahx","description":"openapi_first is a Ruby gem for request / response validation and contract-testing against an OpenAPI API description. It makes APIFirst easy and reliable.","archived":false,"fork":false,"pushed_at":"2025-05-08T08:03:04.000Z","size":1826,"stargazers_count":165,"open_issues_count":4,"forks_count":17,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-05-08T09:20:35.053Z","etag":null,"topics":["api-server","apifirst","design-first","jsonapi","openapi","openapi3","rack","rest-api","ruby","web-framework"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ahx.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-02-20T09:11:47.000Z","updated_at":"2025-05-08T08:03:07.000Z","dependencies_parsed_at":"2023-10-25T10:03:16.882Z","dependency_job_id":"21576f38-3572-4dd5-907d-b7370b40477a","html_url":"https://github.com/ahx/openapi_first","commit_stats":{"total_commits":509,"total_committers":14,"mean_commits":"36.357142857142854","dds":0.306483300589391,"last_synced_commit":"e56cd234a1c6f639dbbc492fd2b55272456cae48"},"previous_names":[],"tags_count":103,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahx%2Fopenapi_first","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahx%2Fopenapi_first/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahx%2Fopenapi_first/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahx%2Fopenapi_first/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ahx","download_url":"https://codeload.github.com/ahx/openapi_first/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254346624,"owners_count":22055808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-server","apifirst","design-first","jsonapi","openapi","openapi3","rack","rest-api","ruby","web-framework"],"created_at":"2024-08-01T12:04:45.367Z","updated_at":"2025-05-15T13:08:06.772Z","avatar_url":"https://github.com/ahx.png","language":"Ruby","readme":"# openapi_first\n\nopenapi_first is a Ruby gem for request / response validation and contract-testing against an [OpenAPI](https://www.openapis.org/) 3.0 or 3.1 API description. It makes an APIFirst workflow easy and reliable.\n\nYou can use openapi_first on production for [request validation](#request-validation) and in your tests to avoid API drift with it's request/response validation and coverage features.\n\n## Contents\n\n\u003c!-- TOC --\u003e\n\n- [Rack Middlewares](#rack-middlewares)\n  - [Request validation](#request-validation)\n  - [Response validation](#response-validation)\n- [Contract testing](#contract-testing)\n  - [Coverage](#coverage)\n  - [Test assertions](#test-assertions)\n- [Manual use](#manual-use)\n- [Framework integration](#framework-integration)\n- [Configuration](#configuration)\n- [Hooks](#hooks)\n- [Alternatives](#alternatives)\n- [Development](#development)\n  - [Benchmarks](#benchmarks)\n  - [Contributing](#contributing)\n\n\u003c!-- /TOC --\u003e\n\n## Rack Middlewares\n\n### Request validation\n\nThe request validation middleware returns a 4xx if the request is invalid or not defined in the API description. It adds a request object to the current Rack environment at `env[OpenapiFirst::REQUEST]` with the request parameters parsed exactly as described in your API description plus access to meta information from your API description. See _[Manual use](#manual-use)_ for more details about that object.\n\n```ruby\nuse OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml'\n\n# Pass `raise_error: true` to raise an error if request is invalid:\nuse OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml', raise_error: true\n```\n\n#### Error responses\n\nopenapi_first produces a useful machine readable error response that can be customized.\nThe default response looks like this. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).\n\n```json\nhttp-status: 400\ncontent-type: \"application/problem+json\"\n\n{\n  \"title\": \"Bad Request Body\",\n  \"status\": 400,\n  \"errors\": [\n    {\n      \"message\": \"value at `/data/name` is not a string\",\n      \"pointer\": \"/data/name\",\n      \"code\": \"string\"\n    },\n    {\n      \"message\": \"number at `/data/numberOfLegs` is less than: 2\",\n      \"pointer\": \"/data/numberOfLegs\",\n      \"code\": \"minimum\"\n    },\n    {\n      \"message\": \"object at `/data` is missing required properties: mandatory\",\n      \"pointer\": \"/data\",\n      \"code\": \"required\"\n    }\n  ]\n}\n```\n\nopenapi_first offers a [JSON:API](https://jsonapi.org/) error response by passing `error_response: :jsonapi`:\n\n```ruby\nuse OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml, error_response: :jsonapi'\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eSee details of JSON:API error response\u003c/summary\u003e\n\n```json\n// http-status: 400\n// content-type: \"application/vnd.api+json\"\n\n{\n  \"errors\": [\n    {\n      \"status\": \"400\",\n      \"source\": {\n        \"pointer\": \"/data/name\"\n      },\n      \"title\": \"value at `/data/name` is not a string\",\n      \"code\": \"string\"\n    },\n    {\n      \"status\": \"400\",\n      \"source\": {\n        \"pointer\": \"/data/numberOfLegs\"\n      },\n      \"title\": \"number at `/data/numberOfLegs` is less than: 2\",\n      \"code\": \"minimum\"\n    },\n    {\n      \"status\": \"400\",\n      \"source\": {\n        \"pointer\": \"/data\"\n      },\n      \"title\": \"object at `/data` is missing required properties: mandatory\",\n      \"code\": \"required\"\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\n#### Custom error responses\n\nYou can build your own custom error response with `error_response: MyCustomClass` that implements `OpenapiFirst::ErrorResponse`.\nYou can define custom error responses globally by including / implementing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(my_name, MyCustomErrorResponse)` and set `error_response: my_name`.\n\n### Response validation\n\nThis middleware raises an error by default if the response is not valid.\nThis can be useful in a test or staging environment, especially if you are adopting OpenAPI for an existing implementation.\n\n```ruby\nuse OpenapiFirst::Middlewares::ResponseValidation, 'openapi.yaml' if ENV['RACK_ENV'] == 'test'\n\n# Pass `raise_error: false` to not raise an error:\nuse OpenapiFirst::Middlewares::ResponseValidation, 'openapi.yaml', raise_error: false\n```\n\nIf you are adopting OpenAPI you can use these options together with [hooks](#hooks) to get notified about requests/responses that do match your API description.\n\n## Contract Testing\n\nHere are two aspects of contract testing: Validation and Coverage\n\n### Validation\n\nBy validating requests and responses, you can avoid that your API implementation processes requests or returns responses that don't match your API description. You can use [test assertions](#test-assertions) or [rack middlewares](#rack-middlewares) or manual validation to validate requests and responses with openapi_first.\n\n### Coverage\n\nTo make sure your _whole_ API description is implemented, openapi_first ships with a coverage feature.\n\n\u003e [!NOTE]\n\u003e This is a brand new feature. ✨ Your feedback is very welcome.\n\nThis feature tracks all requests/responses that are validated via openapi_first and tells you about which request/responses are missing.\nHere is how to set it up with [rack-test](https://github.com/rack/rack-test):\n\n1. Register all OpenAPI documents to track coverage for. This should go at the top of your test helper file before loading your application code.\n  ```ruby\n  require 'openapi_first'\n  OpenapiFirst::Test.setup do |test|\n    test.register('openapi/openapi.yaml')\n    test.minimum_coverage = 100 # (Optional) Setting this will lead to an `exit 2` if coverage is below minimum\n    test.skip_response_coverage { it.status == '500' } # (Optional) Skip certain responses\n  end\n  ```\n2. Add an `app` method to your tests, which wraps your application with silent request / response validation. This validates all requests/responses in your test run. (✷1)\n\n  ```ruby\n  def app\n    OpenapiFirst::Test.app(MyApp)\n  end\n  ```\n3. Run your tests.  The Coverage feature will tell you about missing request/responses.\n\n  Or you can generate a Module and include it in your rspec spec_helper.rb:\n\n  ```ruby\n  config.include OpenapiFirst::Test::Methods[MyApp], type: :request\n  ```\n\n(✷1): It does not matter what method of openapi_first you use to validate requests/responses. Instead of using `OpenapiFirstTest.app` to wrap your application, you could also use the middlewares or [test assertion method](#test-assertions), but you would have to do that for all requests/responses defined in your API description to make coverage work.\n\n### Test assertions\n\nopenapi_first ships with a simple but powerful Test method to run request and response validation in your tests without using the middlewares. This is designed to be used with rack-test or Ruby on Rails integration tests or request specs.\n\nHere is how to set it up for Rails integration tests:\n\nInside your test:\n```ruby\n# test/integration/trips_api_test.rb\nrequire 'test_helper'\n\nclass TripsApiTest \u003c ActionDispatch::IntegrationTest\n  include OpenapiFirst::Test::Methods\n\n  test 'GET /trips' do\n    get '/trips',\n        params: { origin: 'efdbb9d1-02c2-4bc3-afb7-6788d8782b1e', destination: 'b2e783e1-c824-4d63-b37a-d8d698862f1d',\n                  date: '2024-07-02T09:00:00Z' }\n\n    assert_api_conform(status: 200)\n    # assert_api_conform(status: 200, api: :v1) # Or this if you have multiple API descriptions\n  end\nend\n```\n\n## Manual use\n\nLoad the API description:\n\n```ruby\nrequire 'openapi_first'\n\ndefinition = OpenapiFirst.load('openapi.yaml')\n```\n\n### Validate request\n\n```ruby\nvalidated_request = definition.validate_request(rack_request)\n\n# Inspect the request and access parsed parameters\nvalidated_request.valid?\nvalidated_request.invalid?\nvalidated_request.error # =\u003e Failure object or nil\nvalidated_request.parsed_body # =\u003e The parsed request body (Hash)\nvalidated_request.parsed_query # A Hash of query parameters that are defined in the API description, parsed exactly as described.\nvalidated_request.parsed_path_parameters\nvalidated_request.parsed_headers\nvalidated_request.parsed_cookies\nvalidated_request.parsed_params # Merged parsed path, query parameters and request body\n# Access the Openapi 3 Operation Object Hash\nvalidated_request.operation['x-foo']\nvalidated_request.operation['operationId'] =\u003e \"getStuff\"\n# or the whole request definition\nvalidated_request.request_definition.path # =\u003e \"/pets/{petId}\"\nvalidated_request.request_definition.operation_id # =\u003e \"showPetById\"\n\n# Or you can raise an exception if validation fails:\ndefinition.validate_request(rack_request, raise_error: true) # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if request is invalid\n```\n\n### Validate response\n\n```ruby\nvalidated_response = definition.validate_response(rack_request, rack_response)\n\n# Inspect the response and access parsed parameters and\nvalidated_response.valid?\nvalidated_response.invalid?\nvalidated_response.error # =\u003e Failure object or nil\nvalidated_response.status # =\u003e 200\nvalidated_response.parsed_body\nvalidated_response.parsed_headers\n\n# Or you can raise an exception if validation fails:\ndefinition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError\n```\n\n## Configuration\n\nYou can configure default options globally:\n\n```ruby\nOpenapiFirst.configure do |config|\n  # Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)\n  config.request_validation_error_response = :jsonapi\n  # Configure if the request validation middleware should raise an exception (defaults to false)\n  config.request_validation_raise_error = true\nend\n```\n\nor configure per instance:\n\n```ruby\nOpenapiFirst.load('openapi.yaml') do |config|\n  config.request_validation_error_response = :jsonapi\nend\n```\n\n## Hooks\n\nYou can integrate your code at certain points during request/response validation via hooks.\n\nAvailable hooks:\n\n- `after_request_validation`\n- `after_response_validation`\n- `after_request_parameter_property_validation`\n- `after_request_body_property_validation`\n\nSetup per per instance:\n\n```ruby\nOpenapiFirst.load('openapi.yaml') do |config|\n  config.after_request_validation do |validated_request|\n    validated_request.valid? # =\u003e true / false\n  end\n  config.after_response_validation do |validated_response, request|\n    if validated_response.invalid?\n      warn \"#{request.request_method} #{request.path}: #{validated_response.error.message}\"\n    end\n  end\nend\n```\n\nSetup globally:\n\n```ruby\nOpenapiFirst.configure do |config|\n  config.after_request_parameter_property_validation do |data, property, property_schema|\n    data[property] = Date.iso8601(data[property]) if property_schema['format'] == 'date'\n  end\nend\n```\n\n## Framework integration\n\nUsing rack middlewares is supported in probably all Ruby web frameworks.\nIf you are using Ruby on Rails for example, you can add the request validation middleware globally in `config/application.rb` or inside specific controllers.\n\nThe contract testing feature is designed to be used via rack-test, which should be compatible all Ruby web frameworks as well.\n\nThat aside, closer integration with specific frameworks like Sinatra, Hanami, Roda or Rails would be great. If you have ideas, pain points or PRs, please don't hesitate to [share](https://github.com/ahx/openapi_first/discussions).\n\n## Alternatives\n\nThis gem was inspired by [committee](https://github.com/interagent/committee) (Ruby) and [Connexion](https://github.com/spec-first/connexion) (Python).\nHere is a [feature comparison between openapi_first and committee](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).\n\n## Development\n\nRun `bin/setup` to install dependencies.\n\nSee `bundle exec rake` to run the linter and the tests.\n\nRun `bundle exec rspec` to run the tests only.\n\n### Benchmarks\n\n[Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)\n\nRun benchmarks:\n\n```sh\ncd benchmarks\nbundle\nbundle exec ruby benchmarks.rb\n```\n\n### Contributing\n\nIf you have a question or an idea or found a bug, don't hesitate to create an issue [on Github](https://github.com/ahx/openapi_first) or [Codeberg](https://codeberg.org/ahx/openapi_first) or say hi on [Mastodon (ruby.social)](https://ruby.social/@ahx).\n\nPull requests are very welcome as well, of course. Feel free to create a \"draft\" pull request early on, even if your change is still work in progress. 🤗\n","funding_links":[],"categories":["Ruby","ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahx%2Fopenapi_first","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fahx%2Fopenapi_first","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahx%2Fopenapi_first/lists"}