From 0115a1f74461f9e1e497643c769e056aaddd1abf Mon Sep 17 00:00:00 2001 From: Linus Oleander Date: Thu, 16 Dec 2021 08:31:03 +0100 Subject: [PATCH 1/2] memoize mapper close #37 --- .rubocop.yml | 3 +- Gemfile | 2 + Rakefile | 6 + benchmarks/basic.rb | 1 + benchmarks/profile.rb | 118 +++++++++++++ lib/remap.rb | 1 + lib/remap/base.rb | 5 +- lib/remap/compiler.rb | 6 +- lib/remap/extensions/enumerable.rb | 6 +- lib/remap/extensions/object.rb | 4 +- lib/remap/failure.rb | 2 +- lib/remap/iteration.rb | 26 --- lib/remap/iteration/array.rb | 35 ---- lib/remap/iteration/hash.rb | 35 ---- lib/remap/iteration/other.rb | 18 -- lib/remap/mapper/support/api.rb | 2 +- lib/remap/path/input.rb | 2 +- lib/remap/rule/block.rb | 7 +- lib/remap/rule/map/enum.rb | 31 ++-- lib/remap/selector/all.rb | 2 +- lib/remap/selector/index.rb | 2 +- lib/remap/selector/key.rb | 2 +- lib/remap/state.rb | 8 +- lib/remap/state/extension.rb | 156 ++++++++++-------- spec/unit/remap/extensions/enumerable_spec.rb | 16 +- spec/unit/remap/extensions/object_spec.rb | 4 +- spec/unit/remap/iteration/array_spec.rb | 54 ------ spec/unit/remap/iteration/hash_spec.rb | 56 ------- spec/unit/remap/iteration/other_spec.rb | 46 ------ spec/unit/remap/iteration_spec.rb | 61 ------- spec/unit/remap/rule/map/enum_spec.rb | 128 -------------- spec/unit/remap/state/extension_spec.rb | 13 -- 32 files changed, 262 insertions(+), 596 deletions(-) create mode 100644 benchmarks/profile.rb delete mode 100644 lib/remap/iteration.rb delete mode 100644 lib/remap/iteration/array.rb delete mode 100644 lib/remap/iteration/hash.rb delete mode 100644 lib/remap/iteration/other.rb delete mode 100644 spec/unit/remap/iteration/array_spec.rb delete mode 100644 spec/unit/remap/iteration/hash_spec.rb delete mode 100644 spec/unit/remap/iteration/other_spec.rb delete mode 100644 spec/unit/remap/iteration_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index f150b57ee..84a7873bb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -35,6 +35,7 @@ Metrics/MethodLength: Metrics/BlockLength: Enabled: false + Metrics/AbcSize: Max: 40 @@ -266,7 +267,7 @@ RSpec/RepeatedDescription: Enabled: false Performance/BlockGivenWithExplicitBlock: - Enabled: false + Enabled: true Naming/MethodParameterName: Enabled: false diff --git a/Gemfile b/Gemfile index 3935f0498..eb17e51f3 100644 --- a/Gemfile +++ b/Gemfile @@ -41,3 +41,5 @@ group :test do end gem "benchmark-ips" +gem "stackprof" +gem "stackprof-webnav" diff --git a/Rakefile b/Rakefile index 1a271c103..5ee935f90 100644 --- a/Rakefile +++ b/Rakefile @@ -57,3 +57,9 @@ namespace :gem do exec "bundle", "exec", "rake", "release" end end + +desc "Create profile" +task :profile do + system "bundle", "exec", "ruby", "benchmarks/profile.rb" + exec "stackprof-webnav -d tmp" +end diff --git a/benchmarks/basic.rb b/benchmarks/basic.rb index c0f53a572..d559fcc3e 100644 --- a/benchmarks/basic.rb +++ b/benchmarks/basic.rb @@ -226,6 +226,7 @@ class Fixed < Remap::Base } } +# GC.disable Benchmark.ips do |x| x.report("fixed") { Fixed.call(input) } diff --git a/benchmarks/profile.rb b/benchmarks/profile.rb new file mode 100644 index 000000000..ab04c0dd1 --- /dev/null +++ b/benchmarks/profile.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "bundler/setup" +Bundler.require +require "remap" +require "stackprof" +require "benchmark/ips" + +class Mapper < Remap::Base + option :date # <= Custom required value + + define do + # Fixed values + set :description, to: value("This is a description") + + # Semi-dynamic values + set :date, to: option(:date) + + # Required rules + get :friends do + each do + # Post processors + map(:name, to: :id).adjust(&:upcase) + + # Field conditions + get?(:age).if do |age| + (30..50).cover?(age) + end + + # Map to a finite set of values + get(:phones) do + each do + map.enum do + from "iPhone", to: "iOS" + value "iOS", "Android" + + otherwise "Unknown" + end + end + end + end + end + + class Linux < Remap::Base + define do + get :kernel + end + end + + class Windows < Remap::Base + define do + get :price + end + end + + # Composable mappers + to :os do + map :computer, :operating_system do + embed Linux | Windows + end + end + + # Wrapping values in an array + to :houses do + wrap :array do + map :house + end + end + + # Array selector (all) + map :cars, all, :model, to: :cars + end +end + +input = { + house: "100kvm", + friends: [ + { + name: "Lisa", + age: 20, + phones: ["iPhone"] + }, { + name: "Jane", + age: 40, + phones: ["Samsung"] + } + ], + computer: { + operating_system: { + kernel: :latest + } + }, + cars: [ + { + model: "Volvo" + }, { + model: "Tesla" + } + ] +} + +def path(name) + Pathname(__dir__).join("..", "tmp/#{name}.dump").to_s +end + +options = { date: Date.today } +state = Remap::State.call(input, mapper: Mapper, options: options) +mapper = Mapper.new(options) + +runner = -> do + 10.times do + mapper.call(state) + end +end + +StackProf.run(raw: true, out: path("wall"), mode: :wall, &runner) +StackProf.run(raw: true, out: path("object"), mode: :object, &runner) +StackProf.run(raw: true, out: path("cpu"), mode: :cpu, &runner) diff --git a/lib/remap.rb b/lib/remap.rb index 820d83d02..3bcecb834 100644 --- a/lib/remap.rb +++ b/lib/remap.rb @@ -6,6 +6,7 @@ require "active_support/proxy_object" require "dry/validation" +require "dry/core/memoizable" require "dry/interface" require "dry/schema" require "dry/struct" diff --git a/lib/remap/base.rb b/lib/remap/base.rb index 738bdf497..a420aa4a2 100644 --- a/lib/remap/base.rb +++ b/lib/remap/base.rb @@ -106,6 +106,7 @@ module Remap class Base < Mapper include ActiveSupport::Configurable include Dry::Core::Constants + include Dry::Core::Memoizable include Catchable extend Mapper::API using State::Extension @@ -239,7 +240,7 @@ def self.option(field, type: Types::Any) # @return [void] # rubocop:disable Layout/LineLength def self.define(target = Nothing, method: :new, strategy: :argument, backtrace: caller, &context) - unless block_given? + unless context raise ArgumentError, "#{self}.define requires a block" end @@ -318,5 +319,7 @@ def call(state, &error) def validation Contract.call(attributes: attributes, contract: contract, options: options, rules: rules) end + + memoize :validation end end diff --git a/lib/remap/compiler.rb b/lib/remap/compiler.rb index 9222cec38..652c79635 100644 --- a/lib/remap/compiler.rb +++ b/lib/remap/compiler.rb @@ -32,7 +32,7 @@ class Compiler < Proxy # # @return [Rule] def self.call(backtrace: caller, &block) - unless block_given? + unless block return Rule::VOID end @@ -317,7 +317,7 @@ def to?(*path, map: EMPTY_ARRAY, backtrace: caller, &block) # @return [Rule::Each]] # @raise [ArgumentError] if no block given def each(backtrace: caller, &block) - unless block_given? + unless block raise ArgumentError, "#each requires a block" end @@ -350,7 +350,7 @@ def each(backtrace: caller, &block) # @return [Rule::Wrap] # @raise [ArgumentError] if type is not :array def wrap(type, backtrace: caller, &block) - unless block_given? + unless block raise ArgumentError, "#wrap requires a block" end diff --git a/lib/remap/extensions/enumerable.rb b/lib/remap/extensions/enumerable.rb index 5d7921b92..6e2a6e7b7 100644 --- a/lib/remap/extensions/enumerable.rb +++ b/lib/remap/extensions/enumerable.rb @@ -33,15 +33,13 @@ def get(*path, trace: [], &fallback) key = path.first - unless block_given? + unless fallback return get(*path, trace: trace) do - raise PathError, trace + [key] + throw :ignore, trace + [key] end end fetch(key, &fallback).get(*path[1..], trace: trace + [key], &fallback) - rescue TypeError - raise PathError, trace + [key] end end end diff --git a/lib/remap/extensions/object.rb b/lib/remap/extensions/object.rb index 86711c1ff..545de1512 100644 --- a/lib/remap/extensions/object.rb +++ b/lib/remap/extensions/object.rb @@ -38,9 +38,9 @@ def paths def get(*path, trace: [], &fallback) return self if path.empty? - unless block_given? + unless fallback return get(*path, trace: trace) do - raise PathError, trace + throw :ignore, trace + path end end diff --git a/lib/remap/failure.rb b/lib/remap/failure.rb index 73481302b..cdc7397ea 100644 --- a/lib/remap/failure.rb +++ b/lib/remap/failure.rb @@ -17,7 +17,7 @@ def merge(other) ] end - failure = attributes.deep_merge(other.attributes) do |key, value1, value2| + failure = attributes.merge(other.attributes) do |key, value1, value2| case [key, value1, value2] in [:failures | :notices, Array, Array] value1 + value2 diff --git a/lib/remap/iteration.rb b/lib/remap/iteration.rb deleted file mode 100644 index 1e6e7d74d..000000000 --- a/lib/remap/iteration.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Remap - class Iteration < Dry::Interface - # @return [State] - attribute :state, Types::State - - # @return [T] - attribute :value, Types::Any - - # Maps every element in {#value} - # - # @abstract - # - # @yieldparam element [V] - # @yieldparam key [K, Integer] - # @yieldreturn [Array, Hash] - # - # @return [Array, Hash] - def call(state) - raise NotImplementedError, "#{self.class}#call not implemented" - end - - order :Hash, :Array, :Other - end -end diff --git a/lib/remap/iteration/array.rb b/lib/remap/iteration/array.rb deleted file mode 100644 index 7394568f6..000000000 --- a/lib/remap/iteration/array.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Remap - class Iteration - using State::Extension - - # Implements an array iterator which defines index in state - class Array < Concrete - # @return [Array] - attribute :value, Types::Array, alias: :array - - # @return [State>] - attribute :state, Types::State - - # @see Iteration#map - def call(&block) - array.each_with_index.reduce(init) do |state, (value, index)| - reduce(state, value, index, &block) - end - end - - private - - def init - state.set(EMPTY_ARRAY) - end - - def reduce(state, value, index, &block) - s0 = block[value, index: index] - s1 = s0.set(**state.only(:ids, :fatal_id)) - state.combine(s1.fmap { [_1] }) - end - end - end -end diff --git a/lib/remap/iteration/hash.rb b/lib/remap/iteration/hash.rb deleted file mode 100644 index 3677a0c16..000000000 --- a/lib/remap/iteration/hash.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Remap - class Iteration - using State::Extension - - # Implements a hash iterator which defines key in state - class Hash < Concrete - # @return [Hash] - attribute :value, Types::Hash, alias: :hash - - # @return [State] - attribute :state, Types::State - - # @see Iteration#map - def call(&block) - hash.reduce(init) do |state, (key, value)| - reduce(state, key, value, &block) - end - end - - private - - def reduce(state, key, value, &block) - s0 = block[value, key: key] - s1 = s0.set(fatal_id: state.fatal_id, ids: state.ids) - state.combine(s1.fmap { { key => _1 } }) - end - - def init - state.set(EMPTY_HASH) - end - end - end -end diff --git a/lib/remap/iteration/other.rb b/lib/remap/iteration/other.rb deleted file mode 100644 index 7ededba8b..000000000 --- a/lib/remap/iteration/other.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Remap - class Iteration - using State::Extension - - # Default iterator which doesn't do anything - class Other < Concrete - attribute :value, Types::Any, alias: :other - attribute :state, Types::State - - # @see Iteration#map - def call(&block) - state.fatal!("Expected an enumerable") - end - end - end -end diff --git a/lib/remap/mapper/support/api.rb b/lib/remap/mapper/support/api.rb index 21e0eee92..b342cce15 100644 --- a/lib/remap/mapper/support/api.rb +++ b/lib/remap/mapper/support/api.rb @@ -22,7 +22,7 @@ def validate? # # @return [Any, T] def call(input, backtrace: caller, **options, &error) - unless block_given? + unless error return call(input, **options) do |failure| raise failure.exception(backtrace) end diff --git a/lib/remap/path/input.rb b/lib/remap/path/input.rb index 7526b2e52..7ded147b1 100644 --- a/lib/remap/path/input.rb +++ b/lib/remap/path/input.rb @@ -25,7 +25,7 @@ class Input < Unit # # @return [State] def call(state, &iterator) - unless block_given? + unless iterator raise ArgumentError, "Input path requires an iterator block" end diff --git a/lib/remap/rule/block.rb b/lib/remap/rule/block.rb index f8f017a6d..6b4594178 100644 --- a/lib/remap/rule/block.rb +++ b/lib/remap/rule/block.rb @@ -26,8 +26,11 @@ def call(state) s2 = state.set(fatal_id: fatal_id) catch_ignored(s1) do |s3, id:| - rules.reduce(s3) do |s4, rule| - s5 = rule.call(s2) + states = rules.map do |rule| + rule.call(s2) + end + + states.reduce(s3) do |s4, s5| s6 = s5.set(id: id) s4.combine(s6) end diff --git a/lib/remap/rule/map/enum.rb b/lib/remap/rule/map/enum.rb index fd284262d..d65e5ec0f 100644 --- a/lib/remap/rule/map/enum.rb +++ b/lib/remap/rule/map/enum.rb @@ -4,13 +4,9 @@ module Remap class Rule class Map class Enum < Proxy - include Dry::Monads[:maybe] - # @return [Hash] - option :mappings, default: -> { Hash.new { default } } - - # @return [Maybe] - option :default, default: -> { None() } + option :table, default: -> { {} } + option :default, default: -> { Undefined } alias execute instance_eval @@ -37,7 +33,7 @@ def self.call(&block) new.tap { _1.execute(&block) } end - # Translates key into a value using predefined mappings + # Translates key into a value using predefined table # # @param key [#hash] # @@ -50,24 +46,21 @@ def get(key, &error) return get(key) { raise Error, _1 } end - self[key].bind { return _1 }.or do - error["Enum key [#{key}] not found among [#{mappings.keys.inspect}]"] + table.fetch(key) do + unless default == Undefined + return default + end + + error["Enum key [#{key}] not found among [#{table.keys.inspect}]"] end end alias call get - # @return [Maybe] - def [](key) - mappings[key] - end - # @return [void] def from(*keys, to:) - value = Some(to) - keys.each do |key| - mappings[key] = value - mappings[to] = value + table[key] = to + table[to] = to end end @@ -80,7 +73,7 @@ def value(*ids) # @return [void] def otherwise(value) - mappings.default = Some(value) + @default = value end end end diff --git a/lib/remap/selector/all.rb b/lib/remap/selector/all.rb index 5d3bb67af..2740d1b9c 100644 --- a/lib/remap/selector/all.rb +++ b/lib/remap/selector/all.rb @@ -26,7 +26,7 @@ class All < Concrete # # @return [State] def call(outer_state, &block) - unless block_given? + unless block raise ArgumentError, "All selector requires an iteration block" end diff --git a/lib/remap/selector/index.rb b/lib/remap/selector/index.rb index 8874c2871..90a644394 100644 --- a/lib/remap/selector/index.rb +++ b/lib/remap/selector/index.rb @@ -31,7 +31,7 @@ class Index < Unit # # @return [State] def call(state, &block) - unless block_given? + unless block raise ArgumentError, "The index selector requires an iteration block" end diff --git a/lib/remap/selector/key.rb b/lib/remap/selector/key.rb index 5855ee3b0..7019d15dd 100644 --- a/lib/remap/selector/key.rb +++ b/lib/remap/selector/key.rb @@ -28,7 +28,7 @@ class Key < Unit # # @return [State] def call(state, &block) - unless block_given? + unless block raise ArgumentError, "The key selector requires an iteration block" end diff --git a/lib/remap/state.rb b/lib/remap/state.rb index f004a0abd..ccceda18d 100644 --- a/lib/remap/state.rb +++ b/lib/remap/state.rb @@ -31,11 +31,11 @@ class Dummy < Remap::Base # @return [Hash] A valid state def self.call(value, mapper: Dummy, options: EMPTY_HASH) { - fatal_ids: EMPTY_ARRAY, - notices: EMPTY_ARRAY, - path: EMPTY_ARRAY, + fatal_ids: [], + notices: [], + path: [], options: options, - ids: EMPTY_ARRAY, + ids: [], mapper: mapper, values: value, value: value, diff --git a/lib/remap/state/extension.rb b/lib/remap/state/extension.rb index 2e8468fc7..71ce4030c 100644 --- a/lib/remap/state/extension.rb +++ b/lib/remap/state/extension.rb @@ -46,11 +46,7 @@ def paths # @returns [Hash] a hash containing the given path # @raise Europace::Error when path doesn't exist def only(*path) - path.reduce(EMPTY_HASH) do |hash, key| - next hash unless key?(key) - - hash.deep_merge(key => fetch(key)) - end + dup.extract!(*path) end # @see #notice @@ -112,11 +108,30 @@ def _(&block) # # @return [State] def map(&block) - bind do |value, state| - Iteration.call(state: state, value: value).call do |other, **options| - state.set(other, **options).then(&block) - end.except(:index, :element, :key) + result = case self + in { value: Array => array } + array.each_with_index.each_with_object([]) do |(value, index), array| + s1 = block[set(value, index: index)] + + if s1.key?(:value) + array << s1[:value] + end + end + in { value: Hash => hash } + hash.each_with_object({}) do |(key, value), acc| + s1 = block[set(value, key: key)] + + if s1.key?(:value) + acc[key] = s1[:value] + end + end + in { value: } + fatal!("Expected an enumerable got %s", value.class) + else + return self end + + set(result) end # @return [String] @@ -130,10 +145,14 @@ def inspect # # @return [State] def combine(other) - deep_merge(other) do |key, value1, value2| + merge(other) do |key, value1, value2| case [key, value1, value2] - in [:value, Array => list1, Array => list2] - list1 + list2 + in [_, Hash => left, Hash => right] + left.merge(right) + in [:ids | :fatal_ids, _, right] + right + in [_, Array => left, Array => right] + left + right in [:value, left, right] other.fatal!( "Could not merge [%s] (%s) with [%s] (%s)", @@ -142,15 +161,6 @@ def combine(other) right.formatted, right.class ) - in [:notices, Array => n1, Array => n2] - n1 + n2 - in [:ids, i1, i2] if i1.all? { i2.include?(_1) } - i2 - in [:ids, i1, i2] if i2.all? { i1.include?(_1) } - i1 - in [:ids, i1, i2] - other.fatal!("Could not merge #ids [%s] (%s) with [%s] (%s)", i1, i1.class, i2, - i2.class) in [Symbol, _, value] value end @@ -160,31 +170,39 @@ def combine(other) # @todo Merge with {#remove_fatal_id} # @return [State] def remove_id - case self + state = dup + + case state in { ids: [], id: } - except(:id) + state.except!(:id) in { ids:, id: } - merge(ids: ids[1...], id: ids[0]) + state.merge!(ids: ids[1...], id: ids[0]) in { ids: [] } - self + state in { ids: } raise ArgumentError, "[BUG] #ids for state are set, but not #id: %s" % formatted - end._ + end + + state end # @todo Merge with {#remove_id} # @return [State] def remove_fatal_id - case self + state = dup + + case state in { fatal_ids: [], fatal_id: } - except(:fatal_id) - in { fatal_ids: ids, fatal_id: id } - merge(fatal_ids: ids[1...], fatal_id: ids[0]) + state.except!(:fatal_id) + in { fatal_ids: ids, fatal_id: } + state.merge!(fatal_ids: ids[1...], fatal_id: ids[0]) in { fatal_ids: [] } - self + state in { fatal_ids: } raise ArgumentError, "[BUG] #ids for state are set, but not #id: %s" % formatted - end._ + end + + state end # Creates a new state with params @@ -198,24 +216,30 @@ def set(value = Undefined, **options) return set(**options, value: value) end - case [self, options] - in [{notices:}, {notice: notice, **rest}] - merge(notices: notices + [notice]).set(**rest) - in [{value:}, {mapper:, **rest}] - merge(scope: value, mapper: mapper).set(**rest) - in [{path:}, {key:, **rest}] - merge(path: path + [key], key: key).set(**rest) - in [{path:}, {index:, value:, **rest}] - merge(path: path + [index], element: value, index: index, value: value).set(**rest) - in [{path:}, {index:, **rest}] - merge(path: path + [index], index: index).set(**rest) - in [{ids:, id: old_id}, {id: new_id, **rest}] - merge(ids: [old_id] + ids, id: new_id).set(**rest) - in [{fatal_ids:, fatal_id: old_id}, {fatal_id: new_id, **rest}] - merge(fatal_ids: [old_id] + fatal_ids, fatal_id: new_id).set(**rest) + state = dup + + case [state, options] + in [{notices:}, {notice: notice}] + state.merge!(notices: notices + [notice]) + in [{value:}, {mapper:}] + state.merge!(scope: value, mapper: mapper) + in [{path:}, {key:, value:}] + state.merge!(path: path + [key], key: key, value: value) + in [{path:}, {key:}] + state.merge!(path: path + [key], key: key) + in [{path:}, {index:, value:}] + state.merge!(path: path + [index], element: value, index: index, value: value) + in [{path:}, {index:}] + state.merge!(path: path + [index], index: index) + in [{ids:, id: old_id}, {id: new_id}] + state.merge!(ids: [old_id] + ids, id: new_id) + in [{fatal_ids:, fatal_id: old_id}, {fatal_id: new_id}] + state.merge!(fatal_ids: [old_id] + fatal_ids, fatal_id: new_id) else - merge(options) + state.merge!(options) end + + state end # Passes {#value} to block, if defined @@ -235,22 +259,6 @@ def fmap(**options, &block) end end - # Creates a failure to be used in {Remap::Base} & {Remap::Mapper} - # - # @param reason [#to_s] - # - # @see State::Schema - # - # @return [Failure] - - # class Failure < Dry::Interface - # attribute :notices, [Notice], min_size: 1 - # end - - def failure(reason = Undefined) - raise NotImplementedError, "Not implemented" - end - # Passes {#value} to block, if defined # {options} are combine into the final state # @@ -262,7 +270,7 @@ def failure(reason = Undefined) # # @return [Y] def bind(**options, &block) - unless block_given? + unless block raise ArgumentError, "State#bind requires a block" end @@ -282,19 +290,23 @@ def bind(**options, &block) # @return [State] def execute(&block) bind do |value| - result = context(value).instance_exec(value, &block) + result = catch :done do + tail_path = catch :ignore do + throw :done, context(value).instance_exec(value, &block) + rescue KeyError => e + [e.key] + rescue IndexError + [] + end + + set(path: path + tail_path).ignore!("Undefined path") + end if result.equal?(Dry::Core::Constants::Undefined) ignore!("Undefined returned, skipping!") end set(result) - rescue KeyError => e - set(path: path + [e.key]).ignore!(e.message) - rescue IndexError => e - ignore!(e.message) - rescue PathError => e - set(path: path + e.path).ignore!("Undefined path") end end diff --git a/spec/unit/remap/extensions/enumerable_spec.rb b/spec/unit/remap/extensions/enumerable_spec.rb index 730069080..e2d4b94ee 100644 --- a/spec/unit/remap/extensions/enumerable_spec.rb +++ b/spec/unit/remap/extensions/enumerable_spec.rb @@ -18,8 +18,8 @@ let(:receiver) { {} } let(:path) { [0] } - it "raises a path error" do - expect { result }.to raise_error(Remap::PathError) + it "throw a path" do + expect { result }.to throw_symbol(:ignore, path) end end @@ -35,8 +35,8 @@ let(:receiver) { { a: { b: "value" } } } let(:path) { [:a, 0] } - it "raises a path error" do - expect { result }.to raise_error(Remap::PathError) + it "throw a path" do + expect { result }.to throw_symbol(:ignore, [:a]) end end end @@ -56,8 +56,8 @@ let(:receiver) { [] } let(:path) { [0] } - it "raises a path error" do - expect { result }.to raise_error(Remap::PathError) + it "throw a path" do + expect { result }.to throw_symbol(:ignore, path) end end @@ -73,8 +73,8 @@ let(:receiver) { ["value"] } let(:path) { [0, 1] } - it "raises a path error" do - expect { result }.to raise_error(Remap::PathError) + it "throw a path" do + expect { result }.to throw_symbol(:ignore, [0]) end end end diff --git a/spec/unit/remap/extensions/object_spec.rb b/spec/unit/remap/extensions/object_spec.rb index f6ebbcab4..b294beace 100644 --- a/spec/unit/remap/extensions/object_spec.rb +++ b/spec/unit/remap/extensions/object_spec.rb @@ -6,8 +6,8 @@ let(:target) { string! } describe "#get" do - it "raises a path error" do - expect { target.get(:a) }.to raise_error(Remap::PathError) + it "throw a path" do + expect { target.get(:a) }.to throw_symbol(:ignore, [:a]) end end diff --git a/spec/unit/remap/iteration/array_spec.rb b/spec/unit/remap/iteration/array_spec.rb deleted file mode 100644 index afc8b01fe..000000000 --- a/spec/unit/remap/iteration/array_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -describe Remap::Iteration::Array do - using Remap::State::Extension - subject(:iterator) { described_class.call(state: state, value: value) } - - let(:state) { state!(value, :with_fatal_id, id: :ignore) } - - context "given an empty array" do - let(:value) { [] } - - context "when called with a block" do - it "does not yield block" do - expect { |block| iterator.call(&block) }.not_to yield_control - end - - it "contains the input value" do - expect(iterator.call(&:itself)).to contain(value) - end - end - end - - context "given a non-empty array" do - let(:value) { [:one, :two, :tree].map(&:to_s) } - - context "when no values are rejected" do - subject(:result) do - iterator.call do |value| - state.set(value.upcase) - end - end - - let(:output) { value.map(&:upcase) } - - it "contains the input value" do - expect(result).to contain(output) - end - end - - context "when all values are rejected" do - it_behaves_like "an ignored exception" do - subject(:result) do - iterator.call do - state.ignore!(reason) - end - end - - let(:attributes) do - { reason: reason } - end - end - end - end -end diff --git a/spec/unit/remap/iteration/hash_spec.rb b/spec/unit/remap/iteration/hash_spec.rb deleted file mode 100644 index f0a12551c..000000000 --- a/spec/unit/remap/iteration/hash_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -# custom rspec match with custom error message -describe Remap::Iteration::Hash do - using Remap::State::Extension - - subject(:iterator) { described_class.call(state: state, value: value) } - - let(:state) { state!(value, :with_fatal_id) } - - context "given an empty hash" do - let(:value) { {} } - - context "when called with a block" do - subject do - iterator.call do |value, key:| - state.set(value, key: key) - end - end - - it { is_expected.to include(value: value) } - end - end - - context "given a non-empty hash" do - let(:value) { { a: 1, b: 2, c: 3 } } - - context "when no values are rejected" do - subject(:result) do - iterator.call do |value| - state.set(value.next) - end - end - - let(:output) { value.transform_values(&:next) } - - it "includes the input value" do - expect(result).to contain(output) - end - end - - context "when all values are rejected" do - it_behaves_like "an ignored exception" do - subject(:result) do - iterator.call do - state.ignore!("Ignore!") - end - end - - let(:attributes) do - { reason: "Ignore!" } - end - end - end - end -end diff --git a/spec/unit/remap/iteration/other_spec.rb b/spec/unit/remap/iteration/other_spec.rb deleted file mode 100644 index 3f57876d9..000000000 --- a/spec/unit/remap/iteration/other_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -describe Remap::Iteration::Other do - using Remap::State::Extension - - describe "#map" do - let(:state) { state!(value, fatal_id: :fatal) } - let(:other) { described_class.call(state: state, value: value) } - - context "when called with a defined value" do - context "when error block is invoked" do - subject(:result) do - other.call do |_value| - fail "this is not called" - end - end - - let(:value) { [1, 2, 3] } - let(:output) { value.size } - - it_behaves_like "a fatal exception" do - let(:attributes) do - { value: value } - end - end - end - - context "when error block is not invoked" do - subject(:result) do - other.call do |value| - state.set(value.size) - end - end - - let(:value) { [1, 2, 3] } - let(:output) { value.size } - - it_behaves_like "a fatal exception" do - let(:attributes) do - { value: value } - end - end - end - end - end -end diff --git a/spec/unit/remap/iteration_spec.rb b/spec/unit/remap/iteration_spec.rb deleted file mode 100644 index 52e222a69..000000000 --- a/spec/unit/remap/iteration_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -describe Remap::Iteration do - describe "#call" do - using Remap::State::Extension - subject(:iteration) do - described_class.call(state: state, value: state.value) - end - - let(:state) { state!(input, fatal_id: :fatal_id) } - - context "when enumerable" do - context "when hash" do - subject(:result) do - iteration.call do |value| - state.set(value.downcase) - end - end - - let(:input) { { one: "ONE", two: "TWO" } } - let(:output) { input.transform_values(&:downcase) } - - it "invokes block" do - expect(result).to contain(output) - end - end - - context "when not array or hash" do - subject(:result) do - iteration.call do |value| - state.set(value.downcase) - end - end - - let(:input) { "value" } - let(:output) { input.downcase } - - it_behaves_like "a fatal exception" do - let(:attributes) do - { value: input } - end - end - end - - context "when array" do - subject(:result) do - iteration.call do |value| - state.set(value.downcase) - end - end - - let(:input) { ["ONE", "TWO"] } - let(:output) { input.map(&:downcase) } - - it "invokes block" do - expect(result).to contain(output) - end - end - end - end -end diff --git a/spec/unit/remap/rule/map/enum_spec.rb b/spec/unit/remap/rule/map/enum_spec.rb index 298e9866a..685941ca1 100644 --- a/spec/unit/remap/rule/map/enum_spec.rb +++ b/spec/unit/remap/rule/map/enum_spec.rb @@ -38,132 +38,4 @@ end end end - - describe "#[]" do - context "without mappings" do - it_behaves_like described_class do - let(:lookup) { :does_not_exist } - let(:output) { None() } - let(:enum) do - described_class.call do - # NOP - end - end - end - end - - context "with default value" do - context "when key exist" do - context "when not mapped to another value" do - it_behaves_like described_class do - let(:output) { Some(lookup) } - let(:fallback) { :fallback } - let(:lookup) { :ID } - let(:enum) do |context: self| - described_class.call do - otherwise context.fallback - value context.lookup - end - end - end - end - - context "when mapped from multiply values" do - it_behaves_like described_class do - let(:output) { Some(value) } - let(:fallback) { :fallback } - let(:lookup) { :ID } - let(:lookups) { [lookup, :OTHER] } - let(:value) { :value } - let(:enum) do |context: self| - described_class.call do - otherwise context.fallback - from(*context.lookups, to: context.value) - end - end - end - end - - context "when mapped to another value" do - it_behaves_like described_class do - let(:output) { Some(value) } - let(:fallback) { :fallback } - let(:lookup) { :ID } - let(:value) { :value } - let(:enum) do |context: self| - described_class.call do - otherwise context.fallback - from context.lookup, to: context.value - end - end - end - end - end - - context "when key does not exist" do - it_behaves_like described_class do - let(:output) { Some(fallback) } - let(:fallback) { :fallback } - let(:lookup) { :does_not_exist } - let(:enum) do |context: self| - described_class.call do - otherwise context.fallback - end - end - end - end - end - - context "without default value" do - context "when key is not found" do - it_behaves_like described_class do - let(:output) { None() } - let(:lookup) { :ID } - let(:enum) do - described_class.call do - value :does_not_exist - end - end - end - end - end - - context "with mappings" do - context "when not found" do - it_behaves_like described_class do - let(:output) { None() } - let(:lookup) { :does_not_exist } - let(:enum) do - described_class.call do - value :ID - end - end - end - end - - context "when found" do - it_behaves_like described_class do - let(:output) { Some(lookup) } - let(:lookup) { :ID } - let(:enum) do - described_class.call do - value :ID - end - end - end - end - - context "when to: is found, but not from:" do - it_behaves_like described_class do - let(:output) { Some(lookup) } - let(:lookup) { :ID } - let(:enum) do - described_class.call do - from :MISSING, to: :ID - end - end - end - end - end - end end diff --git a/spec/unit/remap/state/extension_spec.rb b/spec/unit/remap/state/extension_spec.rb index b30daef37..1606e19c6 100644 --- a/spec/unit/remap/state/extension_spec.rb +++ b/spec/unit/remap/state/extension_spec.rb @@ -251,19 +251,6 @@ end end - context "when ids differs in length" do - subject(:result) do - left.combine(right) - end - - let(:left) { defined!(ids: [:left_id], fatal_id: :left_fatal_id) } - let(:right) { defined!(ids: [:right_id, :right_id2], fatal_id: :right_fatal_id) } - - it "raises an argument error" do - expect { result }.to raise_error(ArgumentError) - end - end - context "when left has fatal_id" do let(:left) { defined!(fatal_id: :left_id) } From 73a8eddfdc3dee553eee8a69bea1c5baba3c9340 Mon Sep 17 00:00:00 2001 From: Linus Oleander Date: Thu, 16 Dec 2021 08:36:31 +0100 Subject: [PATCH 2/2] remove dry monads --- benchmarks/basic.rb | 276 ++++++++++++-------------------------------- lib/remap.rb | 3 +- lib/remap/base.rb | 2 +- lib/remap/types.rb | 1 - remap.gemspec | 1 - spec/spec_helper.rb | 1 - 6 files changed, 73 insertions(+), 211 deletions(-) diff --git a/benchmarks/basic.rb b/benchmarks/basic.rb index d559fcc3e..11cab0b29 100644 --- a/benchmarks/basic.rb +++ b/benchmarks/basic.rb @@ -5,230 +5,96 @@ require "remap" require "benchmark/ips" -class Fixed < Remap::Base +class Mapper < Remap::Base define do - map :a do - map :b do - map :c do - map :d do - map :e do - map :f do - map :g do - map :h do - map :i do - map :j do - map :k do - map :l do - map :m do - map :n do - map :o do - map :p do - map :q do - map :r do - map :s do - map :t do - map :u do - map :v do - map :w do - map :x do - map :y do - map :z do - to :a - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end + # Fixed values + set :description, to: value("This is a description") + + # Required rules + get :friends do + each do + # Post processors + map(:name, to: :id).adjust(&:upcase) + + # Field conditions + get?(:age).if do |age| + (30..50).cover?(age) + end + + # Map to a finite set of values + get(:phones) do + each do + map.enum do + from "iPhone", to: "iOS" + value "iOS", "Android" + + otherwise "Unknown" end end end end end - map :z do - map :a do - map :b do - map :c do - map :d do - map :e do - map :f do - map :g do - map :h do - map :i do - map :j do - map :k do - map :l do - map :m do - map :n do - map :o do - map :p do - map :q do - map :r do - map :s do - map :t do - map :u do - map :v do - map :w do - map :x do - map :y do - map :z do - to :a - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end - end + class Linux < Remap::Base + define do + get :kernel end end + + class Windows < Remap::Base + define do + get :price + end + end + + # Composable mappers + to :os do + map :computer, :operating_system do + embed Linux | Windows + end + end + + # Wrapping values in an array + to :houses do + wrap :array do + map :house + end + end + + # Array selector (all) + map :cars, all, :model, to: :cars end end input = { - z: { - a: { - b: { - c: { - d: { - e: { - f: { - g: { - h: { - i: { - j: { - k: { - l: { - m: { - n: { - o: { - p: { - q: { - r: { - s: { - t: { - u: { - v: { - w: { - x: { - y: { - z: "U" - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } + house: "100kvm", + friends: [ + { + name: "Lisa", + age: 20, + phones: ["iPhone"] + }, { + name: "Jane", + age: 40, + phones: ["Samsung"] + } + ], + computer: { + operating_system: { + kernel: :latest } }, - a: { - b: { - c: { - d: { - e: { - f: { - g: { - h: { - i: { - j: { - k: { - l: { - m: { - n: { - o: { - p: { - q: { - r: { - s: { - t: { - u: { - v: { - w: { - x: { - y: { - z: "U" - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } + cars: [ + { + model: "Volvo" + }, { + model: "Tesla" } - } + ] } -# GC.disable Benchmark.ips do |x| - x.report("fixed") { Fixed.call(input) } + x.report("fixed") { Mapper.call(input) } x.compare! end diff --git a/lib/remap.rb b/lib/remap.rb index 3bcecb834..ba46ace3d 100644 --- a/lib/remap.rb +++ b/lib/remap.rb @@ -5,12 +5,11 @@ require "active_support/core_ext/array/wrap" require "active_support/proxy_object" -require "dry/validation" require "dry/core/memoizable" +require "dry/validation" require "dry/interface" require "dry/schema" require "dry/struct" -require "dry/monads" require "dry/types" require "neatjson" diff --git a/lib/remap/base.rb b/lib/remap/base.rb index a420aa4a2..b58f9dfb5 100644 --- a/lib/remap/base.rb +++ b/lib/remap/base.rb @@ -105,8 +105,8 @@ module Remap # Mapper.call([1, 2, 3]) # => 2 class Base < Mapper include ActiveSupport::Configurable - include Dry::Core::Constants include Dry::Core::Memoizable + include Dry::Core::Constants include Catchable extend Mapper::API using State::Extension diff --git a/lib/remap/types.rb b/lib/remap/types.rb index 0cf0e7f97..1990bf2ae 100644 --- a/lib/remap/types.rb +++ b/lib/remap/types.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "dry/monads/maybe" require "dry/logic/operations/negation" require "dry/logic" diff --git a/remap.gemspec b/remap.gemspec index e9244b013..cf75f5974 100644 --- a/remap.gemspec +++ b/remap.gemspec @@ -34,7 +34,6 @@ Gem::Specification.new do |spec| spec.add_dependency "dry-core", "~> 0.7.1" spec.add_dependency "dry-initializer", "~> 3.0.4" spec.add_dependency "dry-interface", "~> 1.0.3" - spec.add_dependency "dry-monads", "~> 1.4.0" spec.add_dependency "dry-schema", "~> 1.8.0" spec.add_dependency "dry-struct", "~> 1.4.0" spec.add_dependency "dry-types", "~> 1.5.1" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 34e85843b..b863c97ee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,7 +24,6 @@ class Remap::Base RSpec.configure do |config| config.filter_run_when_matching :focus - config.include Dry::Monads[:maybe, :result, :do] config.include RSpec::Benchmark::Matchers config.include FactoryBot::Syntax::Methods config.include Support