{"id":13879335,"url":"https://github.com/runtastic/request_handler","last_synced_at":"2025-07-16T15:32:09.690Z","repository":{"id":15627959,"uuid":"78537205","full_name":"runtastic/request_handler","owner":"runtastic","description":"easy to use shared base for jsonapi request handler using dry-* gems","archived":false,"fork":false,"pushed_at":"2023-02-07T13:15:13.000Z","size":385,"stargazers_count":24,"open_issues_count":7,"forks_count":9,"subscribers_count":18,"default_branch":"master","last_synced_at":"2024-11-21T02:56:20.236Z","etag":null,"topics":["dry-rb","json-api","jsonapi","query-parser","request-handler","runtastic"],"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/runtastic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-01-10T13:44:15.000Z","updated_at":"2022-11-17T01:34:53.000Z","dependencies_parsed_at":"2023-02-19T17:30:47.634Z","dependency_job_id":null,"html_url":"https://github.com/runtastic/request_handler","commit_stats":null,"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runtastic%2Frequest_handler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runtastic%2Frequest_handler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runtastic%2Frequest_handler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/runtastic%2Frequest_handler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/runtastic","download_url":"https://codeload.github.com/runtastic/request_handler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226143895,"owners_count":17580245,"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":["dry-rb","json-api","jsonapi","query-parser","request-handler","runtastic"],"created_at":"2024-08-06T08:02:17.524Z","updated_at":"2024-11-24T08:31:16.329Z","avatar_url":"https://github.com/runtastic.png","language":"Ruby","readme":"# RequestHandler\n\n[![Gem Version](https://badge.fury.io/rb/request_handler.svg)](https://badge.fury.io/rb/request_handler)\n[![CircleCI](https://circleci.com/gh/andreaseger/receptacle.svg?style=svg)](https://circleci.com/gh/runtastic/request_handler)\n[![codecov](https://codecov.io/gh/runtastic/request_handler/branch/master/graph/badge.svg)](https://codecov.io/gh/runtastic/request_handler)\n\nThis gem allows easy and dry handling of requests based on the dry-validation\ngem for validation and data coersion. It allows to handle headers, filters,\ninclude_options, sorting and of course to validate the body.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'request_handler'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install request_handler\n\n## Configuration\n\nYou have to chose a validation engine and configure it globally:\n```ruby\nRequestHandler.configure do |config|\n  config.validation_engine = RequestHandler::Validation::DryEngine\nend\n```\n\nIf you want to use the included dry engine you also have to add the dry gems to\nyour Gemfile:\n```ruby\n  gem 'dry-validation', '~\u003e 1.0'\n  gem 'dry-types', '~\u003e 1.0'\n```\nNote that only dry \u003e= 1.0 is supported.\n\nThe default logger and separator can be changed globally:\n\n```ruby\nRequestHandler.configure do |config|\n  config.logger = Logger.new(STDERR)\n  config.separator = '____'\nend\n```\n\nJSON:API-style error data can be included in validation errors raised by `RequestHandler`.\n\n```ruby\nRequestHandler.configure do |config|\n  config.raise_jsonapi_errors = true # default: false\nend\n```\n\n\n### Validation Engine\nYou have to chose a validation engine and configure it globally (see\nconfiguration section above).\nAll examples in this Readme assume you are using the `DryEngine` which relies on\ndry-validation. However you can also use the builtin `DefinitionEngine`, which\nuses [Definition](https://github.com/Goltergaul/definition) as validation\nlibrary:\n\n```ruby\nRequestHandler.configure do |config|\n  require 'request_handler/validation/definition_engine'\n  config.validation_engine = RequestHandler::Validation::DefinitionEngine\nend\n```\n\nYou can also implement your own engine to use any other library, by implementing\nthe abstract class `RequestHandler::Validation::Engine`\n\n## Usage\n\nTo set up a handler, you need create a class which inherits from\n`RequestHandler::Base`, providing at least the options block and a `to_dto`\nmethod with the parts you want to use. To use it, create a new instance of the\nhandler passing in the request, after that you can use the handler.to_dto method to\nprocess and access the data. Here is a short example, check\n`spec/integration/request_handler_spec.rb` for a detailed one.\n\nPlease note that pagination only considers options that are configured on the\nserver (at least an empty configuration block int the page block), other options\nsent by the client are ignored and will cause a warning.\n\nGeneric query params can be added by using the `query` block. This may be useful\nif parameters should be validated which cannot be assigned to other predefined\noption blocks.\n\nA `type` param can be passed in the `body` block, or the `resource` block in\n[multipart requests](#multipart-requests) (like `question` in the example below).\nYou can pass either a symbol or a string.\nAt the moment there are only \"jsonapi\" and \"json\" available for `type`. This\ndefines if the JsonApiDocumentParser or JsonParser is used.\nIf nothing is defined, JsonApiDocumentParser will be used by default.\n\n```ruby\nrequire \"request_handler\"\nclass DemoHandler \u003c RequestHandler::Base\n  options do\n    page do\n      default_size 10\n      max_size 20\n      resource :comments do\n        default_size 20\n        max_size 100\n      end\n    end\n\n    include_options do\n      allowed Dry::Types[\"strict.string\"].enum(\"comments\", \"author\")\n    end\n\n    sort_options do\n      allowed Dry::Types[\"strict.string\"].enum(\"age\", \"name\")\n    end\n\n    filter do\n      schema(\n        Class.new(Dry::Validation::Contract) do\n          option :foo\n          params do\n            required(:name).filled(:string)\n          end\n        end\n      )\n      additional_url_filter %i(user_id id)\n      options(-\u003e(_handler, _request) { { foo: \"bar\" } })\n      # options({foo: \"bar\"}) # also works for hash options instead of procs\n    end\n\n    query do\n      schema(\n        Dry::Schema.Params do\n          optional(:name).filled(:string)\n        end\n      )\n    end\n\n    body do\n      type :jsonapi\n      schema(\n        Class.new(Dry::Validation::Contract) do\n          option :foo\n          json do\n            required(:id).filled(:string)\n          end\n        end\n      )\n      options(-\u003e(_handler, _request) { { foo: \"bar\" } })\n      # options({foo: \"bar\"}) # also works for hash options instead of procs\n    end\n  end\n\n  def to_dto\n    OpenStruct.new(\n      body:    body_params,\n      page:    page_params,\n      include: include_params,\n      filter:  filter_params,\n      sort:    sort_params,\n      headers: headers\n    )\n  end\nend\n\n# Given a Rack::Request you can create a well defined dto through the request handler:\nDemoHandler.new(request: request).to_dto\n```\n### Nested Attributes\n\nFor nested attributes all options or parameter will be flattened and nesting\nwill be represented by joining the nesting levels with the defined separator\nstring. By default this will be double underscore `__`.\n\nThis means in the request handler options one must use the attributes as flat\nstructure with the configured separator.\n\n#### Example\n\nInput query parameters like the following:\n\n```http\nGET /users?filter[name]=John\u0026filter[posts.tag]=health\n```\n\nwill be parsed as\n\n```ruby\n{\n  name: \"John\",\n  posts__tag: \"health\"\n}\n```\n\nSame is applied for sort and include options.\n\n```http\nGET /users?sort=posts.published_on\u0026include=posts.comments\n```\n\nbecomes\n\n```ruby\ninclude_options = [:posts__comments]\nsort_options = SortOption.new(:posts__published_on, :asc)\n```\n\n### Multipart requests\nIt is also possible to process and validate multipart requests, consisting of an arbitrary number of parts.\nYou can require specific resources, all the other listed resources are optional\n\nThe following request handler requires a question (which will be uploaded as a json-file) and accepts an additional\nfile related to the question\n\n```ruby\nclass CreateQuestionHandler \u003c RequestHandler::Base\n  options do\n    multipart do\n      resource :question do\n        required true\n        type \"json\"\n        schema(\n          Dry::Schema.JSON do\n            required(:id).filled(:string)\n            required(:type).filled(:string)\n            required(:content).filled(:string)\n          end\n        )\n      end\n\n      resource :file do\n        # no validation necessary\n      end\n    end\n  end\n\n  def to_dto\n    # see the resulting multipart_params below\n    { multipart: multipart_params }\n  end\nend\n```\n\nAssuming that the request consists of a json file `question.json` containing\n``` json\n{\n  \"id\": \"1\",\n  \"type\": \"questions\",\n  \"content\": \"How much is the fish?\"\n}\n```\n\nand an additional file `image.png`, the resulting `multipart_params` will be the following:\n\n``` ruby\n{\n  question:\n    {\n      id:      '1',\n      type:    'questions',\n      content: 'How much is the fish?'\n    },\n  file:\n    {\n      filename: 'image.png',\n      type:     'application/octet-stream'\n      name:     'file',\n      tempfile: #\u003cTempfile:/...\u003e,\n      head:     'Content-Disposition: form-data;...'\n    }\n}\n```\n\nPlease note that each part's content has to be uploaded as a separate file currently.\n\n### JSON:API errors\n\nErrors caused by bad requests respond to `:errors`.\n\nWhen the gem is configured to `raise_jsonapi_errors`, this method returns a list of hashes\ncontaining `code`, `status`, `detail`, (`links`) and `source` for each specific issue\nthat contributed to the error. Otherwise it returns an empty array.\n\nThe exception message contains `\u003cerror code\u003e: \u003csource\u003e \u003cdetail\u003e` for every issue,\nwith one issue per line.\n\n| `:code`                   | `:status` | What is it? |\n|:--------------------------|:----------|:------------|\n| INVALID_RESOURCE_SCHEMA   | 422       | Resource did not pass configured validation |\n| INVALID_QUERY_PARAMETER   | 400       | Query parameter violates syntax or did not pass configured validation |\n| MISSING_QUERY_PARAMETER   | 400       | Query parameter required in configuration is missing |\n| INVALID_JSON_API          | 400       | Request body violates JSON:API syntax |\n| INVALID_MULTIPART_REQUEST | 400       | Sidecar resource missing or invalid JSON |\n\n#### Example\n```ruby\nrescue RequestHandler::SchemaValidationError =\u003e e\n  puts e.errors\nend\n```\n\n```ruby\n[\n  {\n    status: '422',\n    code: 'INVALID_RESOURCE_SCHEMA',\n    title: 'Invalid resource',\n    detail: 'is missing',\n    source: { pointer: '/data/attributes/name' }\n  }\n]\n```\n\n### Caveats\n\nIt is currently expected that _url_ parameter are already parsed and included in\nthe request params. With Sinatra requests the following is needed to accomplish\nthis:\n\n```ruby\nget \"/users/:user_id/posts\" do\n  request.params.merge!(params)\n  dto = DemoHandler.new(request: request).to_dto\n  # more code\nend\n```\n\n## v1 to v2 migration guide\nMultiple breaking changes were introduced with request_handler 2.0. This section\ndescribes which steps have to be taken in order to migrate from 1.x to 2.0.\n\n### Configure validation engine\nBy default the DryEngine was used in 1.0. You now have to explicitly configure\na validation engine:\n\n```ruby\nRequestHandler.configure do |config|\n  config.validation_engine = RequestHandler::Validation::DryEngine\nend\n```\n\n### Add dry dependency if you use the DryEngine\nSince the DryEngine is not configured by default anymore, the dependency to the\ndry gems could be removed from request_handler. If you use the DryEngine\nsimply add the dry-gems to your Gemfile:\n\n```ruby\ngem 'dry-validation', '~\u003e 1.0'\ngem 'dry-types', '~\u003e 1.0'\n```\nNote that only dry \u003e= 1.0 is supported.\n\n### Define custom resources via the `resource` key\nIn request_handler 1.x it was possible to define custom resource names like this:\n\n```ruby\noptions  do\n  fieldsets do\n    allowed do\n      posts schema\n    end\n  end\nend\n```\n\nThis was possible in multiple places (`page`, `multipart`, `fieldsets.allowed`).\nStarting with version 2.0 you will have to define those custom resources via the\n`resource` key:\n\n```ruby\noptions do\n  fieldsets do\n    allowed do\n      resource :posts, schema\n    end\n  end\nend\n```\n\n### Use dry-* 1.x instead of dry-* 0.x if you use the DryEngine\nSome of the most common required changes are listed here:\n\n* Use `Dry::Schema.Params` instead of `Dry::Validation.Schema`\n* Use `Dry::Schema.JSON` instead of `Dry::Validation.JSON`\n* If you use some more complex validation rules with options like this:\n\n```\nDry::Validation.Params do\n  configure do\n    option :query_id\n  end\n  required(:id).value(eql?: query_id)\nend\n\noptions(-\u003e(_parser, request) { { query_id: request.params['id'] } })\n```\n\nplease rewrite it using `Dry::Validation::Contract` like this:\n\n```\nClass.new(Dry::Validation::Contract) do\n  option :query_id\n  params do\n    required(:id).value(:string)\n  end\n  rule(:id) do\n    key.failure('invalid id') unless values[:id] == query_id\n  end\nend)\noptions(-\u003e(_parser, request) { { query_id: request.params['id'] } })\n```\n\nA useful guide for upgrading to dry 1 types, validations and schemas can be\nfound [here](https://www.morozov.is/2019/05/31/upgrading-dry-gems.html).\n\nAlso please refer to the official docs of\n[dry-schema](https://dry-rb.org/gems/dry-schema) and\n[dry-validation](https://dry-rb.org/gems/dry-validation).\n\n### Remove config inheritance\nIt was possible to (partially) overwrite configs defined in a request-handler\nsuper-class:\n```\nclass Parent \u003c RequestHandler::Base\n  options  do\n    page do\n      comments do\n        default_size 20\n      end\n    end\n  end\nend\n```\n\n```ruby\nclass Child \u003c Parent\n  options  do\n    page do\n      comments do\n        default_size 10\n      end\n    end\n  end\nend\n```\n\nSupport for this has been fully removed. If you overwrite configs in subclasses\nplease remove the inheritance and define the two request-handlers separately.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. You can\nalso run `bin/console` for an interactive prompt that will allow you to experiment.\n\nRun `bundle exec rspec` to run the tests.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push git\ncommits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\nBug reports and pull requests are welcome on GitHub at https://github.com/runtastic/request_handler.\nThis project is intended to be a safe, welcoming space for collaboration, and\ncontributors are expected to adhere to the [code of conduct][cc].\n\nCheck out our [career page](https://www.runtastic.com/career/) if you'd like to work with us.\n\n## License\nThe gem is available as open source under [the terms of the MIT License][mit].\n\n[mit]: https://choosealicense.com/licenses/mit/\n[cc]: ../CODE_OF_CONDUCT.md\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruntastic%2Frequest_handler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruntastic%2Frequest_handler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruntastic%2Frequest_handler/lists"}