{"id":18336317,"url":"https://github.com/launchpadlab/decanter","last_synced_at":"2025-04-05T05:02:30.533Z","repository":{"id":45968961,"uuid":"51669646","full_name":"LaunchPadLab/decanter","owner":"LaunchPadLab","description":"Decanter aims to reduce complexity in Rails controllers by creating a place for transforming data before it hits the model and database.","archived":false,"fork":false,"pushed_at":"2025-03-13T21:44:12.000Z","size":454,"stargazers_count":43,"open_issues_count":16,"forks_count":5,"subscribers_count":21,"default_branch":"main","last_synced_at":"2025-03-29T04:03:31.420Z","etag":null,"topics":[],"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/LaunchPadLab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","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}},"created_at":"2016-02-13T22:53:09.000Z","updated_at":"2025-02-12T15:18:05.000Z","dependencies_parsed_at":"2023-01-28T16:01:22.705Z","dependency_job_id":"f78518a8-fd0e-4641-bd46-20ecf6c98e03","html_url":"https://github.com/LaunchPadLab/decanter","commit_stats":{"total_commits":214,"total_committers":11,"mean_commits":"19.454545454545453","dds":0.6214953271028038,"last_synced_commit":"45a54d5ced0e8a95fb38d4c10b1d2073d04f4f57"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fdecanter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fdecanter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fdecanter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fdecanter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LaunchPadLab","download_url":"https://codeload.github.com/LaunchPadLab/decanter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247289408,"owners_count":20914464,"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":[],"created_at":"2024-11-05T20:07:28.651Z","updated_at":"2025-04-05T05:02:30.528Z","avatar_url":"https://github.com/LaunchPadLab.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Decanter\n\nDecanter is a Ruby gem that makes it easy to transform incoming data before it hits the model. You can think of Decanter as the opposite of Active Model Serializers (AMS). While AMS transforms your outbound data into a format that your frontend consumes, Decanter transforms your incoming data into a format that your backend consumes.\n\n```ruby\ngem 'decanter', '~\u003e 5.0'\n```\n\n## Migration Guides\n\n- [v3.0.0](migration-guides/v3.0.0.md)\n\n## Contents\n\n- [Basic Usage](#basic-usage)\n  - [Decanters](#decanters)\n  - [Generators](#generators)\n  - [Decanting Collections](#decanting-collections)\n  - [Nested resources](#nested-resources)\n  - [Default parsers](#default-parsers)\n  - [Parser options](#parser-options)\n  - [Exceptions](#exceptions)\n- [Advanced usage](#advanced-usage)\n  - [Custom parsers](#custom-parsers)\n  - [Squashing inputs](#squashing-inputs)\n  - [Chaining parsers](#chaining-parsers)\n  - [Requiring params](#requiring-params)\n  - [Global configuration](#global-configuration)\n- [Contributing](#contributing)\n\n## Basic Usage\n\n### Decanters\n\nDeclare a `Decanter` for a model:\n\n```ruby\n# app/decanters/trip_decanter.rb\n\nclass TripDecanter \u003c Decanter::Base\n  input :name, :string\n  input :start_date, :date\n  input :end_date, :date\nend\n```\n\nThen, transform incoming params in your controller using `Decanter#decant`:\n\n```rb\n# app/controllers/trips_controller.rb\n\n  def create\n    trip_params = params.require(:trip) # or params[:trip] if you are not using Strong Parameters\n    decanted_trip_params = TripDecanter.decant(trip_params)\n    @trip = Trip.new(decanted_trip_params)\n\n    # ...any response logic\n  end\n\n```\n\n### Generators\n\nDecanter comes with custom generators for creating `Decanter` and `Parser` files:\n\n#### Decanters\n\n```\nrails g decanter Trip name:string start_date:date end_date:date\n\n# Creates app/decanters/trip_decanter.rb:\nclass TripDecanter \u003c Decanter::Base\n  input :name, :string\n  input :start_date, :date\n  input :end_date, :date\nend\n```\n\n#### Parsers\n\n```\nrails g parser TruncatedString\n\n# Creates lib/decanter/parsers/truncated_string_parser.rb:\nclass TruncatedStringParser \u003c Decanter::Parser::ValueParser\n  parser do |value, options|\n    value\n  end\nend\n```\n\n[Learn more about using custom parsers](#custom-parsers)\n\n#### Resources\n\nWhen using the Rails resource generator in a project that includes Decanter, a decanter will be automatically created for the new resource:\n\n```\nrails g resource Trip name:string start_date:date end_date:date\n\n# Creates app/decanters/trip_decanter.rb:\nclass TripDecanter \u003c Decanter::Base\n  input :name, :string\n  input :start_date, :date\n  input :end_date, :date\nend\n```\n\n### Decanting Collections\n\nDecanter can decant a collection of a resource, applying the patterns used in the [fast JSON API gem](https://github.com/Netflix/fast_jsonapi#collection-serialization):\n\n```rb\n# app/controllers/trips_controller.rb\n\n  def create\n    trip_params = {\n      trips: [\n        { name: 'Disney World', start_date: '12/24/2018', end_date: '12/28/2018' },\n        { name: 'Yosemite', start_date: '5/1/2017', end_date: '5/4/2017' }\n      ]\n    }\n    decanted_trip_params = TripDecanter.decant(trip_params[:trips])\n    Trip.create(decanted_trip_params) # bulk create trips with decanted params\n  end\n```\n\n#### Control Over Decanting Collections\n\nYou can use the `is_collection` option for explicit control over decanting collections.\n\n`decanted_trip_params = TripDecanter.decant(trip_params[:trips], is_collection: true)`\n\nIf this option is not provided, autodetect logic is used to determine if the providing incoming params holds a single object or collection of objects.\n\n- `nil` or not provided: will try to autodetect single vs collection\n- `true` will always treat the incoming params args as _collection_\n- `false` will always treat incoming params args as _single object_\n- `truthy` will raise an error\n\n### Nested resources\n\nDecanters can declare relationships using `ActiveRecord`-style declarators:\n\n```ruby\nclass TripDecanter \u003c Decanter::Base\n  has_many :destinations\nend\n```\n\nThis decanter will look up and apply the corresponding `DestinationDecanter` whenever necessary to transform nested resources.\n\n### Default parsers\n\nDecanter comes with the following parsers out of the box:\n\n- `:boolean`\n- `:date`\n- `:date_time`\n- `:float`\n- `:integer`\n- `:pass`\n- `:phone`\n- `:string`\n- `:array`\n\nNote: these parsers are designed to operate on a single value, except for `:array`. This parser expects an array, and will use the `parse_each` option to call a given parser on each of its elements:\n\n```ruby\ninput :ids, :array, parse_each: :integer\n```\n\n### Parser options\n\nSome parsers can receive options that modify their behavior. These options are passed in as named arguments to `input`:\n\n**Example:**\n\n```ruby\ninput :start_date, :date, parse_format: '%Y-%m-%d'\n```\n\n**Available Options:**\n| Parser | Option | Default | Notes\n| ----------- | ----------- | -----------| -----------\n| `ArrayParser` | `parse_each`| N/A | Accepts a parser type, then uses that parser to parse each element in the array. If this option is not defined, each element is simply returned.\n| `DateParser`| `parse_format` | `'%m/%d/%Y'`| Accepts any format string accepted by Ruby's `strftime` method\n| `DateTimeParser` | `parse_format` | `'%m/%d/%Y %I:%M:%S %p'` | Accepts any format string accepted by Ruby's `strftime` method\n\n### Exceptions\n\nBy default, `Decanter#decant` will raise an exception when unexpected parameters are passed. To override this behavior, you can change the strict mode option to one of:\n\n- `true` (default): unhandled keys will raise an unexpected parameters exception\n- `false`: all parameter key-value pairs will be included in the result\n- `:ignore`: unhandled keys will be excluded from the decanted result\n\n```ruby\nclass TripDecanter \u003c  Decanter::Base\n  strict false\n  # ...\nend\n```\n\nOr explicitly ignore a key:\n\n```rb\nclass TripDecanter \u003c  Decanter::Base\n  ignore :created_at, :updated_at\n  # ...\nend\n```\n\nYou can also disable strict mode globally using a [global configuration](#global-configuration) setting.\n\n## Advanced Usage\n\n### Custom Parsers\n\nTo add a custom parser, first create a parser class:\n\n```rb\n# app/parsers/truncated_string_parser.rb\nclass TruncatedStringParser \u003c Decanter::Parser::ValueParser\n\n  parser do |value, options|\n    length = options.fetch(:length, 100)\n    value.truncate(length)\n  end\nend\n```\n\nThen, use the appropriate key to look up the parser:\n\n```ruby\n  input :name, :truncated_string #=\u003e TruncatedStringParser\n```\n\n#### Custom parser methods\n\n- `#parse \u003cblock\u003e`: (required) recieves a block for parsing a value. Block parameters are `|value, options|` for `ValueParser` and `|name, value, options|` for `HashParser`.\n- `#allow [\u003cclass\u003e]`: skips parse step if the incoming value `is_a?` instance of class(es).\n- `#pre [\u003cparser\u003e]`: applies the given parser(s) before parsing the value.\n\n#### Custom parser base classes\n\n- `Decanter::Parser::ValueParser`: subclasses are expected to return a single value.\n- `Decanter::Parser::HashParser`: subclasses are expected to return a hash of keys and values.\n\n### Squashing inputs\n\nSometimes, you may want to take several inputs and combine them into one finished input prior to sending to your model. You can achieve this with a custom parser:\n\n```ruby\nclass TripDecanter \u003c Decanter::Base\n  input [:day, :month, :year], :squash_date, key: :start_date\nend\n```\n\n```ruby\nclass SquashDateParser \u003c Decanter::Parser::ValueParser\n  parser do |values, options|\n    day, month, year = values.map(\u0026:to_i)\n    Date.new(year, month, day)\n  end\nend\n```\n\n### Chaining parsers\n\nYou can compose multiple parsers by using the `#pre` method:\n\n```ruby\nclass FloatPercentParser \u003c Decanter::Parser::ValueParser\n\n  pre :float\n\n  parser do |val, options|\n    val / 100\n  end\nend\n```\n\nOr by declaring multiple parsers for a single input:\n\n```ruby\nclass SomeDecanter \u003c Decanter::Base\n  input :some_percent, [:float, :percent]\nend\n```\n\n### Requiring params\n\nIf you provide the option `:required` for an input in your decanter, an exception will be thrown if the parameter is `nil` or an empty string.\n\n```ruby\nclass TripDecanter \u003c  Decanter::Base\n  input :name, :string, required: true\nend\n```\n\n_Note: we recommend using [Active Record validations](https://guides.rubyonrails.org/active_record_validations.html) to check for presence of an attribute, rather than using the `required` option. This method is intended for use in non-RESTful routes or cases where Active Record validations are not available._\n\n### Default values\n\nIf you provide the option `:default_value` for an input in your decanter, the input key will be initialized with the given default value. Input keys not found in the incoming data parameters will be set to the provided default rather than ignoring the missing key. Note: `nil` and empty keys will not be overridden.\n\n```ruby\nclass TripDecanter \u003c  Decanter::Base\n  input :name, :string\n  input :destination, :string, default_value: 'Chicago'\nend\n\n```\n\n```\nTripDecanter.decant({ name: 'Vacation 2020' })\n=\u003e { name: 'Vacation 2020', destination: 'Chicago' }\n\n```\n\n### Global configuration\n\nYou can generate a local copy of the default configuration with `rails generate decanter:install`. This will create an initializer where you can do global configuration:\n\nSetting strict mode to :ignore will log out any unhandled keys. To avoid excessive logging, the global configuration can be set to `log_unhandled_keys = false`\n\n```ruby\n# ./config/initializers/decanter.rb\n\nDecanter.config do |config|\n  config.strict = false\n  config.log_unhandled_keys = false\nend\n```\n\n## Contributing\n\nThis project is maintained by developers at [LaunchPad Lab](https://launchpadlab.com/). Contributions of any kind are welcome!\n\nWe aim to provide a response to incoming issues within 48 hours. However, please note that we are an active dev shop and these responses may be as simple as _\"we do not have time to respond to this right now, but can address it at {x} time\"_.\n\nFor detailed information specific to contributing to this project, reference our [Contribution guide](CONTRIBUTING.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchpadlab%2Fdecanter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaunchpadlab%2Fdecanter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchpadlab%2Fdecanter/lists"}