diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml new file mode 100644 index 0000000..d2731ba --- /dev/null +++ b/.github/workflows/versionci.yml @@ -0,0 +1,20 @@ +name: Version CI +on: + pull_request: + push: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ["3.2.0", "3.2.2", "3.3.0"] + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - run: bundle exec rspec diff --git a/.ruby-version b/.ruby-version index 57cf282..15a2799 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.5 +3.3.0 diff --git a/Gemfile.lock b/Gemfile.lock index 301191c..a3684f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,68 +1,101 @@ PATH remote: . specs: - decanter (4.0.4) - actionpack (>= 4.2.10) + decanter (5.0.0) + actionpack (>= 7.1.3.2) activesupport rails-html-sanitizer (>= 1.0.4) GEM remote: https://rubygems.org/ specs: - actionpack (5.2.4.4) - actionview (= 5.2.4.4) - activesupport (= 5.2.4.4) - rack (~> 2.0, >= 2.0.8) + actionpack (7.1.3.2) + actionview (= 7.1.3.2) + activesupport (= 7.1.3.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.4) - activesupport (= 5.2.4.4) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.3.2) + activesupport (= 7.1.3.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activesupport (5.2.4.4) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (7.1.3.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + base64 (0.2.0) + bigdecimal (3.1.7) builder (3.2.4) - concurrent-ruby (1.1.7) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) crass (1.0.6) - diff-lcs (1.4.4) + diff-lcs (1.5.1) docile (1.1.5) - dotenv (2.2.1) - erubi (1.9.0) - i18n (1.8.5) + dotenv (3.1.1) + drb (2.2.1) + erubi (1.12.0) + i18n (1.14.4) concurrent-ruby (~> 1.0) - json (2.3.0) - loofah (2.7.0) + io-console (0.7.2) + irb (1.13.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.7.2) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - method_source (1.0.0) - mini_portile2 (2.4.0) - minitest (5.14.2) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + nokogiri (>= 1.12.0) + minitest (5.22.3) + mutex_m (0.2.0) + nokogiri (1.16.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.4-x86_64-linux) + racc (~> 1.4) + psych (5.1.2) + stringio + racc (1.7.3) + rack (3.0.10) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (5.2.4.4) - actionpack (= 5.2.4.4) - activesupport (= 5.2.4.4) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.2) + actionpack (= 7.1.3.2) + activesupport (= 7.1.3.2) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rake (12.3.3) + rdoc (6.6.3.1) + psych (>= 4.0.0) + reline (0.5.5) + io-console (~> 0.5) rspec-core (3.9.3) rspec-support (~> 3.9.3) - rspec-expectations (3.9.3) + rspec-expectations (3.9.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-mocks (3.9.1) @@ -82,16 +115,20 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) + stringio (3.1.0) + thor (1.3.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + webrick (1.8.1) + zeitwerk (2.6.13) PLATFORMS - ruby + arm64-darwin-22 + arm64-darwin-23 + x86_64-linux DEPENDENCIES - bundler (~> 1.9) + bundler (~> 2.4.22) decanter! dotenv rake (~> 12.0) @@ -99,4 +136,4 @@ DEPENDENCIES simplecov (~> 0.15.1) BUNDLED WITH - 1.17.3 + 2.4.22 diff --git a/README.md b/README.md index 8ebd332..318178a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Decanter 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. ```ruby -gem 'decanter', '~> 4.0' +gem 'decanter', '~> 5.0' ``` ## Migration Guides @@ -77,6 +77,7 @@ end ``` #### Parsers + ``` rails g parser TruncatedString @@ -133,8 +134,8 @@ You can use the `is_collection` option for explicit control over decanting colle If this option is not provided, autodetect logic is used to determine if the providing incoming params holds a single object or collection of objects. - `nil` or not provided: will try to autodetect single vs collection -- `true` will always treat the incoming params args as *collection* -- `false` will always treat incoming params args as *single object* +- `true` will always treat the incoming params args as _collection_ +- `false` will always treat incoming params args as _single object_ - `truthy` will raise an error ### Nested resources @@ -178,10 +179,11 @@ Some parsers can receive options that modify their behavior. These options are p ```ruby input :start_date, :date, parse_format: '%Y-%m-%d' ``` + **Available Options:** -| Parser | Option | Default | Notes +| Parser | Option | Default | Notes | ----------- | ----------- | -----------| ----------- -| `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. +| `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. | `DateParser`| `parse_format` | `'%m/%d/%Y'`| Accepts any format string accepted by Ruby's `strftime` method | `DateTimeParser` | `parse_format` | `'%m/%d/%Y %I:%M:%S %p'` | Accepts any format string accepted by Ruby's `strftime` method @@ -299,7 +301,6 @@ end _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._ - ### Default values If 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. diff --git a/decanter.gemspec b/decanter.gemspec index b642ad0..0e7585f 100644 --- a/decanter.gemspec +++ b/decanter.gemspec @@ -1,5 +1,4 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'decanter/version' @@ -9,29 +8,28 @@ Gem::Specification.new do |spec| spec.authors = ['Ryan Francis', 'David Corwin'] spec.email = ['ryan@launchpadlab.com'] - spec.summary = %q{Form Parser for Rails} - spec.description = %q{Decanter aims to reduce complexity in Rails controllers by creating a place for transforming data before it hits the model and database.} + spec.summary = 'Form Parser for Rails' + spec.description = 'Decanter aims to reduce complexity in Rails controllers by creating a place for transforming data before it hits the model and database.' spec.homepage = 'https://github.com/launchpadlab/decanter' spec.license = 'MIT' + spec.required_ruby_version = '>= 3.2.0' # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or # delete this section to allow pushing this gem to any host. - if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = 'https://rubygems.org' - else - raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' - end + raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata) + + spec.metadata['allowed_push_host'] = 'https://rubygems.org' spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'actionpack', '>= 4.2.10' + spec.add_dependency 'actionpack', '>= 7.1.3.2' spec.add_dependency 'activesupport' spec.add_dependency 'rails-html-sanitizer', '>= 1.0.4' - spec.add_development_dependency 'bundler', '~> 1.9' + spec.add_development_dependency 'bundler', '~> 2.4.22' spec.add_development_dependency 'dotenv' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'rspec-rails', '~> 3.9' diff --git a/lib/decanter/core.rb b/lib/decanter/core.rb index a510e71..5e44f67 100644 --- a/lib/decanter/core.rb +++ b/lib/decanter/core.rb @@ -1,3 +1,5 @@ +require 'action_controller' + module Decanter module Core DEFAULT_VALUE_KEY = :default_value @@ -8,40 +10,42 @@ def self.included(base) end module ClassMethods - def input(name, parsers=nil, **options) + def input(name, parsers = nil, **options) # Convert all input names to symbols to correctly calculate handled vs. unhandled keys input_names = [name].flatten.map(&:to_sym) if input_names.length > 1 && parsers.blank? - raise ArgumentError.new("#{self.name} no parser specified for input with multiple values.") + raise ArgumentError, "#{self.name} no parser specified for input with multiple values." end handlers[input_names] = { - key: options.fetch(:key, input_names.first), - name: input_names, - options: options, - parsers: parsers, - type: :input + key: options.fetch(:key, input_names.first), + name: input_names, + options:, + parsers:, + type: :input } end + # Adjusting has_many to explicitly define keyword arguments def has_many(assoc, **options) handlers[assoc] = { - assoc: assoc, - key: options.fetch(:key, assoc), - name: assoc, - options: options, - type: :has_many + assoc:, + key: options.fetch(:key, assoc), + name: assoc, + options:, + type: :has_many } end + # Adjusting has_one similarly def has_one(assoc, **options) handlers[assoc] = { - assoc: assoc, - key: options.fetch(:key, assoc), - name: assoc, - options: options, - type: :has_one + assoc:, + key: options.fetch(:key, assoc), + name: assoc, + options:, + type: :has_one } end @@ -50,12 +54,15 @@ def ignore(*args) end def strict(mode) - raise(ArgumentError, "#{self.name}: Unknown strict value #{mode}") unless [:ignore, true, false].include? mode + raise(ArgumentError, "#{name}: Unknown strict value #{mode}") unless [:ignore, true, false].include? mode + @strict_mode = mode end def log_unhandled_keys(mode) - raise(ArgumentError, "#{self.name}: Unknown log_unhandled_keys value #{mode}") unless [true, false].include? mode + raise(ArgumentError, "#{name}: Unknown log_unhandled_keys value #{mode}") unless [true, + false].include? mode + @log_unhandled_keys_mode = mode end @@ -65,16 +72,16 @@ def decant(args) # Convert all params passed to a decanter to a hash with indifferent access to mitigate accessor ambiguity accessible_args = to_indifferent_hash(args) - {}.merge( default_keys ) - .merge( unhandled_keys(accessible_args) ) - .merge( handled_keys(accessible_args) ) + {}.merge(default_keys) + .merge(unhandled_keys(accessible_args)) + .merge(handled_keys(accessible_args)) end def default_keys # return keys with provided default value when key is not defined within incoming args default_result = default_value_inputs - .map { |input| [input[:key], input[:options][DEFAULT_VALUE_KEY]] } - .to_h + .map { |input| [input[:key], input[:options][DEFAULT_VALUE_KEY]] } + .to_h # parse handled default values, including keys # with defaults not already managed by handled_keys @@ -100,8 +107,9 @@ def required_inputs end end - def required_input_keys_present?(args={}) + def required_input_keys_present?(args = {}) return true unless any_inputs_required? + compact_inputs = required_inputs.compact compact_inputs.all? do |input| args.keys.map(&:to_sym).include?(input) && !args[input].nil? @@ -109,7 +117,8 @@ def required_input_keys_present?(args={}) end def empty_required_input_error - raise(MissingRequiredInputValue, 'Required inputs have been declared, but no values for those inputs were passed.') + raise(MissingRequiredInputValue, + 'Required inputs have been declared, but no values for those inputs were passed.') end def empty_args_error @@ -120,20 +129,20 @@ def empty_args_error def unhandled_keys(args) unhandled_keys = args.keys.map(&:to_sym) - - handlers.keys.flatten.uniq - - keys_to_ignore - - handlers.values - .select { |handler| handler[:type] != :input } - .map { |handler| "#{handler[:name]}_attributes".to_sym } + handlers.keys.flatten.uniq - + keys_to_ignore - + handlers.values + .select { |handler| handler[:type] != :input } + .map { |handler| "#{handler[:name]}_attributes".to_sym } return {} unless unhandled_keys.any? case strict_mode when :ignore - p "#{self.name} ignoring unhandled keys: #{unhandled_keys.join(', ')}." if log_unhandled_keys_mode + p "#{name} ignoring unhandled keys: #{unhandled_keys.join(', ')}." if log_unhandled_keys_mode {} when true - raise(UnhandledKeysError, "#{self.name} received unhandled keys: #{unhandled_keys.join(', ')}.") + raise(UnhandledKeysError, "#{name} received unhandled keys: #{unhandled_keys.join(', ')}.") else args.select { |key| unhandled_keys.include? key.to_sym } end @@ -155,22 +164,22 @@ def handled_keys(args) def handle(handler, args) values = args.values_at(*handler[:name]) values = values.length == 1 ? values.first : values - self.send("handle_#{handler[:type]}", handler, values) + send("handle_#{handler[:type]}", handler, values) end def handle_input(handler, args) - values = args.values_at(*handler[:name]) - values = values.length == 1 ? values.first : values - parse(handler[:key], handler[:parsers], values, handler[:options]) + values = args.values_at(*handler[:name]) + values = values.length == 1 ? values.first : values + parse(handler[:key], handler[:parsers], values, handler[:options]) end def handle_association(handler, args) assoc_handlers = [ handler, handler.merge({ - key: handler[:options].fetch(:key, "#{handler[:name]}_attributes").to_sym, - name: "#{handler[:name]}_attributes".to_sym - }) + key: handler[:options].fetch(:key, "#{handler[:name]}_attributes").to_sym, + name: "#{handler[:name]}_attributes".to_sym + }) ] assoc_handler_names = assoc_handlers.map { |_handler| _handler[:name] } @@ -180,20 +189,21 @@ def handle_association(handler, args) {} when 1 _handler = assoc_handlers.detect { |_handler| args.has_key?(_handler[:name]) } - self.send("handle_#{_handler[:type]}", _handler, args[_handler[:name]]) + send("handle_#{_handler[:type]}", _handler, args[_handler[:name]]) else - raise ArgumentError.new("Handler #{handler[:name]} matches multiple keys: #{assoc_handler_names}.") + raise ArgumentError, "Handler #{handler[:name]} matches multiple keys: #{assoc_handler_names}." end end def handle_has_many(handler, values) decanter = decanter_for_handler(handler) if values.is_a?(Hash) - parsed_values = values.map do |index, input_values| + parsed_values = values.map do |_index, input_values| next if input_values.nil? + decanter.decant(input_values) end - return { handler[:key] => parsed_values } + { handler[:key] => parsed_values } else { handler[:key] => values.compact.map { |value| decanter.decant(value) } @@ -207,17 +217,16 @@ def handle_has_one(handler, values) def decanter_for_handler(handler) if specified_decanter = handler[:options][:decanter] - Decanter::decanter_from(specified_decanter) + Decanter.decanter_from(specified_decanter) else - Decanter::decanter_for(handler[:assoc]) + Decanter.decanter_for(handler[:assoc]) end end def parse(key, parsers, value, options) return { key => value } unless parsers - if options[:required] && value_missing?(value) - raise ArgumentError.new("No value for required argument: #{key}") - end + raise ArgumentError, "No value for required argument: #{key}" if options[:required] && value_missing?(value) + parser_classes = Parser.parsers_for(parsers) Parser.compose_parsers(parser_classes).parse(key, value, options) end @@ -235,7 +244,8 @@ def strict_mode end def log_unhandled_keys_mode - return !!(Decanter.configuration.log_unhandled_keys) if @log_unhandled_keys_mode.nil? + return !!Decanter.configuration.log_unhandled_keys if @log_unhandled_keys_mode.nil? + !!@log_unhandled_keys_mode end @@ -244,11 +254,12 @@ def log_unhandled_keys_mode private def value_missing?(value) - value.nil? || value == "" + value.nil? || value == '' end def to_indifferent_hash(args) - return args.to_unsafe_h if args.class.name == ACTION_CONTROLLER_PARAMETERS_CLASS_NAME + return args.to_unsafe_h if args.instance_of?(ActionController::Parameters) + args.to_h.with_indifferent_access end end diff --git a/lib/decanter/version.rb b/lib/decanter/version.rb index 09aa6a1..7a560e6 100644 --- a/lib/decanter/version.rb +++ b/lib/decanter/version.rb @@ -1,3 +1,3 @@ module Decanter - VERSION = '4.0.4'.freeze + VERSION = '5.0.0'.freeze end diff --git a/migration-guides/v5.0.0.md b/migration-guides/v5.0.0.md new file mode 100644 index 0000000..413edd5 --- /dev/null +++ b/migration-guides/v5.0.0.md @@ -0,0 +1,21 @@ +# v5.0.0 Migration Guide + +_Note: this guide assumes you are upgrading from decanter v4 to v5._ + +This version contains major updates including the upgrade to Ruby version 3.3.0. Ensure your environment is compatible with this Ruby version before proceeding. + +## Major Changes + +1. **Ruby Version Update**: Decanter now requires Ruby 3.3.0. This update brings several language improvements and performance enhancements. You must ensure that your environment is running Ruby 3.3.0 or higher. Update your Ruby version using your Ruby version manager (e.g., RVM, rbenv): + +2. **Deprecated Features**: Review any deprecated Ruby methods or features that may affect your project and adjust accordingly. Refer to the [official Ruby 3.3.0 release notes](https://www.ruby-lang.org/en/news/2023/02/24/ruby-3-3-0-released/) for detailed information on deprecations and changes. + +3. **Testing and Compatibility**: After updating Ruby, run your test suite and check for any deprecations or failures. Update your gemfile to specify the Ruby version: + +## Migration Tips + +- Backup your current project before making significant version changes. +- Test your application thoroughly after the migration. +- Consider using a continuous integration (CI) environment to run your tests against multiple Ruby versions if you support them. + +By following this guide, you should be able to successfully migrate your project to use Decanter v5.0.0 with Ruby 3.3.0. diff --git a/spec/decanter/decanter_core_spec.rb b/spec/decanter/decanter_core_spec.rb index 2fa8573..42f89ce 100644 --- a/spec/decanter/decanter_core_spec.rb +++ b/spec/decanter/decanter_core_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe Decanter::Core do - let(:dummy) { Class.new { include Decanter::Core } } before(:each) do @@ -15,23 +14,21 @@ end describe '#input' do - let(:name) { [:profile] } let(:parser) { :string } let(:options) { {} } - before(:each) { dummy.input name, parser, options } + before(:each) { dummy.input(name, parser, **options) } it 'adds a handler for the provided name' do - expect(dummy.handlers.has_key? name ).to be true + expect(dummy.handlers.key?(name)).to be true end context 'for multiple values' do - - let(:name) { [:first_name, :last_name] } + let(:name) { %i[first_name last_name] } it 'adds a handler for the provided name' do - expect(dummy.handlers.has_key? name ).to be true + expect(dummy.handlers.has_key?(name)).to be true end it 'raises an error if multiple values are passed without a parser' do @@ -69,15 +66,14 @@ end describe '#has_one' do - let(:assoc) { :profile } let(:name) { ["#{assoc}_attributes".to_sym] } let(:options) { {} } - before(:each) { dummy.has_one assoc, options } + before(:each) { dummy.has_one(assoc, **options) } it 'adds a handler for the association' do - expect(dummy.handlers.has_key? assoc ).to be true + expect(dummy.handlers.has_key?(assoc)).to be true end it 'the handler has type :has_one' do @@ -110,15 +106,14 @@ end describe '#has_many' do - let(:assoc) { :profile } let(:name) { ["#{assoc}_attributes".to_sym] } let(:options) { {} } - before(:each) { dummy.has_many assoc, options } + before(:each) { dummy.has_many(assoc, **options) } it 'adds a handler for the assoc' do - expect(dummy.handlers.has_key? assoc ).to be true + expect(dummy.handlers.has_key?(assoc)).to be true end it 'the handler has type :has_many' do @@ -151,7 +146,6 @@ end describe '#strict' do - let(:mode) { true } it 'sets the strict mode' do @@ -160,7 +154,6 @@ end context 'for an unknown mode' do - let(:mode) { :foo } it 'raises an error' do @@ -170,7 +163,6 @@ end describe '#log_unhandled_keys' do - let(:mode) { false } it 'sets the @log_unhandled_keys_mode' do @@ -179,7 +171,6 @@ end context 'for an unknown mode' do - let(:mode) { :foo } it 'raises an error' do @@ -189,10 +180,8 @@ end describe '#parse' do - context 'when a parser is not specified' do - - let(:parser) { double("parser", parse: nil) } + let(:parser) { double('parser', parse: nil) } before(:each) do allow(Decanter::Parser) @@ -201,7 +190,7 @@ end it 'returns the provided key and value' do - expect(dummy.parse(:first_name, nil, 'bar', {})).to eq({:first_name => 'bar'}) + expect(dummy.parse(:first_name, nil, 'bar', {})).to eq({ first_name: 'bar' }) end it 'does not call Parser.parsers_for' do @@ -212,8 +201,8 @@ context 'when a parser is specified but a required value is not present' do it 'raises an argument error specifying the key' do - expect { dummy.parse(:first_name, :foo, nil, {required: true}) } - .to raise_error(ArgumentError, "No value for required argument: first_name") + expect { dummy.parse(:first_name, :foo, nil, { required: true }) } + .to raise_error(ArgumentError, 'No value for required argument: first_name') end end @@ -222,7 +211,7 @@ let(:val) { 8.0 } it 'returns the a key-value pair with the parsed value' do - expect(dummy.parse(key, :float, val.to_s, {})).to eq({key => val}) + expect(dummy.parse(key, :float, val.to_s, {})).to eq({ key => val }) end end @@ -231,63 +220,59 @@ let(:val) { 8.0 } it 'returns the a key-value pair with the parsed value' do - expect(dummy.parse(key, [:string, :float], val, {})).to eq({key => val}) + expect(dummy.parse(key, %i[string float], val, {})).to eq({ key => val }) end end context 'when a parser with a preparser is specified' do - Object.const_set('PctParser', - Class.new(Decanter::Parser::ValueParser) do - def self.name - 'PctParser' - end - end.tap do |parser| - parser.pre :float - parser.parser do |val, options| - val / 100 - end - end - ) + Class.new(Decanter::Parser::ValueParser) do + def self.name + 'PctParser' + end + end.tap do |parser| + parser.pre :float + parser.parser do |val, _options| + val / 100 + end + end) Object.const_set('KeyValueSplitterParser', - Class.new(Decanter::Parser::HashParser) do - def self.name - 'KeyValueSplitterParser' - end - end.tap do |parser| - parser.parser do |_name, val, _options| - item_delimiter = ',' - pair_delimiter = ':' - val.split(item_delimiter).reduce({}) { |memo, pair| memo.merge( Hash[ *pair.split(pair_delimiter) ] ) } - end - end - ) + Class.new(Decanter::Parser::HashParser) do + def self.name + 'KeyValueSplitterParser' + end + end.tap do |parser| + parser.parser do |_name, val, _options| + item_delimiter = ',' + pair_delimiter = ':' + val.split(item_delimiter).reduce({}) do |memo, pair| + memo.merge(Hash[*pair.split(pair_delimiter)]) + end + end + end) let(:key) { :afloat } let(:val) { 8.0 } it 'returns the a key-value pair with the parsed value' do - expect(dummy.parse(key, [:string, :pct], val, {})).to eq({key => val/100}) + expect(dummy.parse(key, %i[string pct], val, {})).to eq({ key => val / 100 }) end end context 'when a hash parser and other parsers are specified' do - let(:key) { :split_it! } - let(:val) { "foo:3.45,baz:91" } + let(:val) { 'foo:3.45,baz:91' } it 'returns the a key-value pairs with the parsed values' do - expect(dummy.parse(key, [:key_value_splitter, :pct], val, {})) + expect(dummy.parse(key, %i[key_value_splitter pct], val, {})) .to eq({ 'foo' => 0.0345, 'baz' => 0.91 }) end end end describe '#decanter_for_handler' do - context 'when decanter option is specified' do - let(:handler) { { options: { decanter: 'FooDecanter' } } } before(:each) { allow(Decanter).to receive(:decanter_from) } @@ -299,7 +284,6 @@ def self.name end context 'when decanter option is not specified' do - let(:handler) { { assoc: :foo, options: {} } } before(:each) { allow(Decanter).to receive(:decanter_for) } @@ -312,12 +296,10 @@ def self.name end describe '#unhandled_keys' do - let(:args) { { foo: :bar, 'baz' => 'foo' } } context 'when there are no unhandled keys' do - - before(:each) { allow(dummy).to receive(:handlers).and_return({foo: { type: :input }, baz: { type: :input }}) } + before(:each) { allow(dummy).to receive(:handlers).and_return({ foo: { type: :input }, baz: { type: :input } }) } it 'returns an empty hash' do expect(dummy.unhandled_keys(args)).to match({}) @@ -325,9 +307,7 @@ def self.name end context 'when there are unhandled keys' do - context 'and strict mode is true' do - before(:each) { allow(dummy).to receive(:handlers).and_return({}) } before(:each) { dummy.strict true } @@ -346,7 +326,6 @@ def self.name end context 'and strict mode is :ignore' do - it 'returns a hash without the unhandled keys and values' do dummy.strict :ignore expect(dummy.unhandled_keys(args)).to match({}) @@ -358,7 +337,6 @@ def self.name end context 'and log_unhandled_keys mode is false' do - it 'does not log the unhandled keys' do dummy.strict :ignore dummy.log_unhandled_keys false @@ -378,11 +356,10 @@ def self.name end describe '#handle' do - let(:args) { { foo: 'hi', bar: 'bye' } } - let(:name) { [:foo, :bar] } + let(:name) { %i[foo bar] } let(:values) { args.values_at(*name) } - let(:handler) { { type: :input, name: name } } + let(:handler) { { type: :input, name: } } before(:each) { allow(dummy).to receive(:handle_input).and_return(:foobar) } @@ -401,13 +378,12 @@ def self.name end describe '#handle_input' do - let(:name) { :name } let(:parser) { double('parser') } let(:options) { double('options') } let(:args) { { name => 'Hi', foo: 'bar' } } let(:values) { args[name] } - let(:handler) { { key: name, name: name, parsers: parser, options: options } } + let(:handler) { { key: name, name:, parsers: parser, options: } } before(:each) do allow(dummy).to receive(:parse) @@ -422,14 +398,12 @@ def self.name end describe '#handle_has_one' do - let(:output) { { foo: 'bar' } } - let(:handler) { { key: 'key', options: {}} } + let(:handler) { { key: 'key', options: {} } } let(:values) { { baz: 'foo' } } let(:decanter) { double('decanter') } before(:each) do - allow(decanter) .to receive(:decant) .and_return(output) @@ -455,19 +429,17 @@ def self.name it 'returns an array containing the key, and the decanted value' do expect(dummy.handle_has_one(handler, values)) - .to match ({handler[:key] => output}) + .to match({ handler[:key] => output }) end end describe '#handle_has_many' do - - let(:output) { [{ foo: 'bar' },{ bar: 'foo' }] } + let(:output) { [{ foo: 'bar' }, { bar: 'foo' }] } let(:handler) { { key: 'key', options: {} } } let(:values) { [{ baz: 'foo' }, { faz: 'boo' }] } let(:decanter) { double('decanter') } before(:each) do - allow(decanter) .to receive(:decant) .and_return(*output) @@ -496,28 +468,28 @@ def self.name it 'returns an array containing the key, and an array of decanted values' do expect(dummy.handle_has_many(handler, values)) - .to match ({handler[:key] => output}) + .to match({ handler[:key] => output }) end end describe '#handle_association' do - let(:assoc) { :profile } - let(:handler) { { - assoc: assoc, - key: assoc, - name: assoc, - type: :has_one, - options: {} - } } + let(:handler) do + { + assoc:, + key: assoc, + name: assoc, + type: :has_one, + options: {} + } + end before(:each) do allow(dummy).to receive(:handle_has_one) end context 'when there is a verbatim matching key' do - - let(:args) { { assoc => 'bar', :baz => 'foo'} } + let(:args) { { assoc => 'bar', :baz => 'foo' } } it 'calls handler_has_one with the handler and args' do dummy.handle_association(handler, args) @@ -528,8 +500,7 @@ def self.name end context 'when there is a matching key for _attributes' do - - let(:args) { { "#{assoc}_attributes".to_sym => 'bar', :baz => 'foo'} } + let(:args) { { "#{assoc}_attributes".to_sym => 'bar', :baz => 'foo' } } it 'calls handler_has_one with the _attributes handler and args' do dummy.handle_association(handler, args) @@ -540,8 +511,7 @@ def self.name end context 'when there is no matching key' do - - let(:args) { { :foo => 'bar', :baz => 'foo'} } + let(:args) { { foo: 'bar', baz: 'foo' } } it 'does not call handler_has_one' do dummy.handle_association(handler, args) @@ -554,18 +524,18 @@ def self.name end context 'when there are multiple matching keys' do - let(:args) { { "#{assoc}_attributes".to_sym => 'bar', assoc => 'foo' } } it 'raises an argument error' do expect { dummy.handle_association(handler, args) } - .to raise_error(ArgumentError, "Handler #{handler[:name]} matches multiple keys: [:profile, :profile_attributes].") + .to raise_error(ArgumentError, + "Handler #{handler[:name]} matches multiple keys: [:profile, :profile_attributes].") end end end describe '#decant' do - let(:args) { { foo: 'bar', baz: 'foo'} } + let(:args) { { foo: 'bar', baz: 'foo' } } let(:subject) { dummy.decant(args) } let(:is_required) { true } @@ -588,12 +558,12 @@ def self.name context 'with args' do context 'when strict mode is set to :ignore' do context 'and params include unhandled keys' do - let(:decanter) { + let(:decanter) do Class.new(Decanter::Base) do input :name, :string input :description, :string end - } + end let(:args) { { name: 'My Trip', description: 'My Trip Description', foo: 'bar' } } @@ -608,42 +578,42 @@ def self.name end context 'when inputs are required' do - let(:decanter) { + let(:decanter) do Class.new do include Decanter::Core input :name, :pass, required: true end - } + end it 'should raise an exception if required values are missing' do - expect{ decanter.decant({ name: nil }) } + expect { decanter.decant({ name: nil }) } .to raise_error(Decanter::MissingRequiredInputValue) end it 'should not raise an exception if required values are present' do - expect{ decanter.decant({ name: 'foo' }) } + expect { decanter.decant({ name: 'foo' }) } .not_to raise_error end it 'should treat empty arrays as present' do - expect{ decanter.decant({ name: [] }) } + expect { decanter.decant({ name: [] }) } .not_to raise_error end it 'should treat empty strings as missing' do - expect{ decanter.decant({ name: '' }) } + expect { decanter.decant({ name: '' }) } .to raise_error(ArgumentError) end it 'should treat blank strings as present' do - expect{ decanter.decant({ name: ' ' })} + expect { decanter.decant({ name: ' ' }) } .not_to raise_error end end context 'when params keys are strings' do - let(:decanter) { + let(:decanter) do Class.new do include Decanter::Core input :name, :string input :description, :string end - } + end let(:args) { { 'name' => 'My Trip', 'description' => 'My Trip Description' } } it 'returns a hash with the declared key-value pairs' do decanted_params = decanter.decant(args) @@ -655,13 +625,13 @@ def self.name end context 'and when inputs are strings' do - let(:decanter) { + let(:decanter) do Class.new do include Decanter::Core input 'name', :string input 'description', :string end - } + end it 'returns a hash with the declared key-value pairs' do decanted_params = decanter.decant(args) expect(decanted_params.with_indifferent_access).to match(args) @@ -685,13 +655,13 @@ def self.name end context 'with missing non-required args' do - let(:decanter) { + let(:decanter) do Class.new do include Decanter::Core input :name, :string input :description, :string end - } + end let(:params) { { description: 'My Trip Description' } } it 'should omit missing values' do decanted_params = decanter.decant(params) @@ -701,14 +671,14 @@ def self.name end context 'with key having a :default_value in the decanter' do - let(:decanter) { + let(:decanter) do Class.new do include Decanter::Core input :name, :string, default_value: 'foo' input :cost, :float, default_value: '99.99' input :description, :string end - } + end it 'should include missing keys and their parsed default values' do params = { description: 'My Trip Description' } @@ -748,12 +718,12 @@ def self.name end context 'with present non-required args containing an empty value' do - let(:decanter) { + let(:decanter) do Class.new(Decanter::Base) do input :name, :string input :description, :string end - } + end let(:params) { { name: '', description: 'My Trip Description' } } let(:desired_result) { { name: nil, description: 'My Trip Description' } } it 'should pass through the values' do @@ -772,7 +742,7 @@ def self.name context 'when at least one input is required' do it 'should raise an exception' do - expect{ subject }.to raise_error(ArgumentError) + expect { subject }.to raise_error(ArgumentError) end end @@ -798,9 +768,9 @@ def self.name end let(:handler) { [[:title], input_hash] } let(:handlers) { [handler] } - before(:each) { + before(:each) do allow(dummy).to receive(:handlers).and_return(handlers) - } + end context 'when required' do it 'should return true' do @@ -818,7 +788,7 @@ def self.name describe 'required_input_keys_present?' do let(:is_required) { true } - let(:args) { { "title": "RubyConf" } } + let(:args) { { "title": 'RubyConf' } } let(:input_hash) do { key: 'foo',