From 14b905cf2501d0a210e397a5120227433ac129f4 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:06:05 -0500 Subject: [PATCH 1/9] create versionci.yml for ruby version matrix testing via github actions --- .github/workflows/versionci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/versionci.yml diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml new file mode 100644 index 0000000..29e2f90 --- /dev/null +++ b/.github/workflows/versionci.yml @@ -0,0 +1,27 @@ +name: Version CI +on: + pull_request: + push: + branches: + - main +jobs: + rubocop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2.2" + bundler-cache: true + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby: ["2.6.5", "2.7.7", "3.0.5", "3.1.3", "3.2.0", "3.2.2"] + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - run: bundle exec rspec From 282731cacb9e6faabd5c96975b738442e3b9d34c Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:10:14 -0500 Subject: [PATCH 2/9] remove rubocop from jobs --- .github/workflows/versionci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml index 29e2f90..25f313e 100644 --- a/.github/workflows/versionci.yml +++ b/.github/workflows/versionci.yml @@ -5,14 +5,6 @@ on: branches: - main jobs: - rubocop: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.2.2" - bundler-cache: true test: runs-on: ubuntu-latest strategy: From 2903e7f39a8a9dde45c888828070a548f28420d3 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:27:37 -0500 Subject: [PATCH 3/9] update bundler and actionpack --- .ruby-version | 2 +- Gemfile.lock | 133 +++++++++++++++++++++++++++++------------------ decanter.gemspec | 4 +- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/.ruby-version b/.ruby-version index 57cf282..1f7da99 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.5 +2.7.7 diff --git a/Gemfile.lock b/Gemfile.lock index 301191c..6448c3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,67 +2,98 @@ PATH remote: . specs: decanter (4.0.4) - actionpack (>= 4.2.10) + 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 (2.8.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.15.6-arm64-darwin) + 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 +113,18 @@ 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 DEPENDENCIES - bundler (~> 1.9) + bundler (~> 2.4.22) decanter! dotenv rake (~> 12.0) @@ -99,4 +132,4 @@ DEPENDENCIES simplecov (~> 0.15.1) BUNDLED WITH - 1.17.3 + 2.4.22 diff --git a/decanter.gemspec b/decanter.gemspec index b642ad0..8d2a8e8 100644 --- a/decanter.gemspec +++ b/decanter.gemspec @@ -27,11 +27,11 @@ Gem::Specification.new do |spec| 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' From 2b0bac3eb73cd73be345b22281ea15acee6884ef Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:28:31 -0500 Subject: [PATCH 4/9] bundle lock --add-platform x86_64-linux --- Gemfile.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 6448c3f..59a7b1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,8 @@ GEM mutex_m (0.2.0) nokogiri (1.15.6-arm64-darwin) racc (~> 1.4) + nokogiri (1.15.6-x86_64-linux) + racc (~> 1.4) psych (5.1.2) stringio racc (1.7.3) @@ -122,6 +124,7 @@ GEM PLATFORMS arm64-darwin-22 + x86_64-linux DEPENDENCIES bundler (~> 2.4.22) From ef40312d908085376305316cdf533ee695a81717 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:31:24 -0500 Subject: [PATCH 5/9] add fail-fast: false --- .github/workflows/versionci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml index 25f313e..60fb9e1 100644 --- a/.github/workflows/versionci.yml +++ b/.github/workflows/versionci.yml @@ -8,6 +8,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: ruby: ["2.6.5", "2.7.7", "3.0.5", "3.1.3", "3.2.0", "3.2.2"] steps: From 4de65809cd66180ce97d290b4400374d4b5537e7 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 16:34:06 -0500 Subject: [PATCH 6/9] update ruby version matrix --- .github/workflows/versionci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml index 60fb9e1..1cd4738 100644 --- a/.github/workflows/versionci.yml +++ b/.github/workflows/versionci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["2.6.5", "2.7.7", "3.0.5", "3.1.3", "3.2.0", "3.2.2"] + ruby: ["2.7.7", "3.0.5", "3.1.3", "3.2.2", "3.3.1"] steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 From c37fa0516b283ebd1b8f81b81f77872e5ca0bd93 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 17:00:03 -0500 Subject: [PATCH 7/9] upgrade to ruby 3.2.2, fix 3.x ruby issues --- .ruby-version | 2 +- lib/decanter/core.rb | 115 +++++++++------- spec/decanter/decanter_core_spec.rb | 202 ++++++++++++---------------- 3 files changed, 150 insertions(+), 169 deletions(-) diff --git a/.ruby-version b/.ruby-version index 1f7da99..be94e6f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.7 +3.2.2 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/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', From c73adeadade6c7c214fe7b3847f879a5e4e66618 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 17:05:52 -0500 Subject: [PATCH 8/9] upgrade to ruby 3.3.0 --- .github/workflows/versionci.yml | 2 +- .ruby-version | 2 +- Gemfile.lock | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml index 1cd4738..377f3bc 100644 --- a/.github/workflows/versionci.yml +++ b/.github/workflows/versionci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["2.7.7", "3.0.5", "3.1.3", "3.2.2", "3.3.1"] + ruby: ["3.2.2", "3.3.0"] steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 diff --git a/.ruby-version b/.ruby-version index be94e6f..15a2799 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.3.0 diff --git a/Gemfile.lock b/Gemfile.lock index 59a7b1c..b3c0748 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,11 +56,11 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) + mini_portile2 (2.8.6) minitest (5.22.3) mutex_m (0.2.0) - nokogiri (1.15.6-arm64-darwin) - racc (~> 1.4) - nokogiri (1.15.6-x86_64-linux) + nokogiri (1.15.6) + mini_portile2 (~> 2.8.2) racc (~> 1.4) psych (5.1.2) stringio @@ -124,6 +124,7 @@ GEM PLATFORMS arm64-darwin-22 + arm64-darwin-23 x86_64-linux DEPENDENCIES From 7091f883ecc3b8c5a8d8ca0bbf7266b9f50285c3 Mon Sep 17 00:00:00 2001 From: Owen Roth <69156111+oroth8@users.noreply.github.com> Date: Thu, 2 May 2024 17:16:05 -0500 Subject: [PATCH 9/9] update gem version and documentation to 5.0.0 --- .github/workflows/versionci.yml | 2 +- Gemfile.lock | 10 +++++----- README.md | 13 +++++++------ decanter.gemspec | 16 +++++++--------- lib/decanter/version.rb | 2 +- migration-guides/v5.0.0.md | 21 +++++++++++++++++++++ 6 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 migration-guides/v5.0.0.md diff --git a/.github/workflows/versionci.yml b/.github/workflows/versionci.yml index 377f3bc..d2731ba 100644 --- a/.github/workflows/versionci.yml +++ b/.github/workflows/versionci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["3.2.2", "3.3.0"] + ruby: ["3.2.0", "3.2.2", "3.3.0"] steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 diff --git a/Gemfile.lock b/Gemfile.lock index b3c0748..a3684f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - decanter (4.0.4) + decanter (5.0.0) actionpack (>= 7.1.3.2) activesupport rails-html-sanitizer (>= 1.0.4) @@ -43,7 +43,7 @@ GEM crass (1.0.6) diff-lcs (1.5.1) docile (1.1.5) - dotenv (2.8.1) + dotenv (3.1.1) drb (2.2.1) erubi (1.12.0) i18n (1.14.4) @@ -56,11 +56,11 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mini_portile2 (2.8.6) minitest (5.22.3) mutex_m (0.2.0) - nokogiri (1.15.6) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) psych (5.1.2) stringio 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 8d2a8e8..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,18 +8,17 @@ 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' 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.