monolens 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/monolens/array/compact.rb +2 -2
  3. data/lib/monolens/array/join.rb +2 -2
  4. data/lib/monolens/array/map.rb +45 -6
  5. data/lib/monolens/array.rb +2 -2
  6. data/lib/monolens/coerce/date.rb +20 -6
  7. data/lib/monolens/coerce/date_time.rb +20 -6
  8. data/lib/monolens/coerce/string.rb +13 -0
  9. data/lib/monolens/coerce.rb +6 -3
  10. data/lib/monolens/core/chain.rb +2 -2
  11. data/lib/monolens/core/mapping.rb +2 -2
  12. data/lib/monolens/error.rb +9 -2
  13. data/lib/monolens/file.rb +2 -7
  14. data/lib/monolens/lens/fetch_support.rb +21 -0
  15. data/lib/monolens/lens/location.rb +17 -0
  16. data/lib/monolens/lens/options.rb +41 -0
  17. data/lib/monolens/lens.rb +39 -23
  18. data/lib/monolens/object/keys.rb +8 -10
  19. data/lib/monolens/object/rename.rb +3 -3
  20. data/lib/monolens/object/select.rb +34 -16
  21. data/lib/monolens/object/transform.rb +34 -12
  22. data/lib/monolens/object/values.rb +34 -10
  23. data/lib/monolens/skip/null.rb +1 -1
  24. data/lib/monolens/str/downcase.rb +2 -2
  25. data/lib/monolens/str/split.rb +2 -2
  26. data/lib/monolens/str/strip.rb +3 -1
  27. data/lib/monolens/str/upcase.rb +2 -2
  28. data/lib/monolens/version.rb +1 -1
  29. data/spec/fixtures/coerce.yml +3 -2
  30. data/spec/fixtures/transform.yml +5 -4
  31. data/spec/monolens/array/test_map.rb +89 -6
  32. data/spec/monolens/coerce/test_date.rb +29 -4
  33. data/spec/monolens/coerce/test_datetime.rb +29 -4
  34. data/spec/monolens/coerce/test_string.rb +15 -0
  35. data/spec/monolens/core/test_mapping.rb +25 -0
  36. data/spec/monolens/lens/test_options.rb +73 -0
  37. data/spec/monolens/object/test_keys.rb +54 -22
  38. data/spec/monolens/object/test_rename.rb +1 -1
  39. data/spec/monolens/object/test_select.rb +109 -4
  40. data/spec/monolens/object/test_transform.rb +93 -6
  41. data/spec/monolens/object/test_values.rb +110 -12
  42. data/spec/monolens/test_error_traceability.rb +60 -0
  43. data/spec/monolens/test_lens.rb +1 -1
  44. data/spec/test_readme.rb +7 -5
  45. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 704980b0a35ef47c4ea022a0b2bf39922966a11d
4
- data.tar.gz: 0a032437e56dbd4d2214ba7b435120ceceb62709
3
+ metadata.gz: 4a6edbb2568985503bf9228ded96c6d179acc196
4
+ data.tar.gz: f37432cfc01970a18c57884547f5520ac31fb374
5
5
  SHA512:
6
- metadata.gz: 2a580e3564f5bd208273e16c0c1da4c261917d371e0e6236bab873920b6a2372b7f36e5a085654f8f2ff79490f762cfeb68a8d38a24265d8cd4da5e983389f6e
7
- data.tar.gz: '0229785824434dd14c87581f0060a784b623e3154b514bea8548d7cce94bb35563ceb6fdbc45146a883c4ca476c1c329b9adbd6a329ab1382f197377709170a6'
6
+ metadata.gz: e83f9436b81460041e6ed9edd73c1fd95ca8b095f2cd0d27d063649b49c8b125363735258c5d03293c1fb74afaa1d042e4a4663b47c2a45276c0b8a730d8ee77
7
+ data.tar.gz: 5c704058d773ab5bddc4d58541d043a63ef176ad68f9a5f7ba670335b167da7df50c7935691ecb8951e98b354bd39d79ad5d36fb8cafa210372d30d44376051f
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Compact
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_array!(arg)
6
+ def call(arg, world = {})
7
+ is_array!(arg, world)
8
8
 
9
9
  arg.compact
10
10
  end
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Join
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_array!(arg)
6
+ def call(arg, world = {})
7
+ is_array!(arg, world)
8
8
 
9
9
  arg.join(option(:separator, ' '))
10
10
  end
@@ -3,16 +3,55 @@ module Monolens
3
3
  class Map
4
4
  include Lens
5
5
 
6
- def initialize(lens)
7
- super({})
8
- @lens = Monolens.lens(lens)
6
+ def initialize(arg)
7
+ options, lenses = case arg
8
+ when ::Hash
9
+ opts = arg.dup; opts.delete(:lenses)
10
+ _, ls = fetch_on(:lenses, arg)
11
+ raise ArgumentError, 'Lenses are required' if ls.nil?
12
+ [ opts, ls ]
13
+ else
14
+ [{}, arg]
15
+ end
16
+ super(options)
17
+ @lenses = Monolens.lens(lenses)
9
18
  end
10
19
 
11
- def call(arg, *rest)
12
- is_enumerable!(arg)
20
+ def call(arg, world = {})
21
+ is_enumerable!(arg, world)
13
22
 
14
- arg.map { |a| @lens.call(a) }
23
+ result = []
24
+ arg.each_with_index do |a, i|
25
+ deeper(world, i) do |w|
26
+ begin
27
+ result << @lenses.call(a, w)
28
+ rescue Monolens::LensError => ex
29
+ strategy = option(:on_error, :fail)
30
+ handle_error(strategy, ex, result, world)
31
+ end
32
+ end
33
+ end
34
+ result
15
35
  end
36
+
37
+ def handle_error(strategy, ex, result, world)
38
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
39
+ case strategy
40
+ when ::Array
41
+ strategy.each{|s| handle_error(s, ex, result, world) }
42
+ when :handler
43
+ error_handler!(world).call(ex)
44
+ when :fail
45
+ raise
46
+ when :null
47
+ result << nil
48
+ when :skip
49
+ nil
50
+ else
51
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
52
+ end
53
+ end
54
+ private :handle_error
16
55
  end
17
56
  end
18
57
  end
@@ -10,8 +10,8 @@ module Monolens
10
10
  end
11
11
  module_function :join
12
12
 
13
- def map(lens)
14
- Map.new(lens)
13
+ def map(options)
14
+ Map.new(options)
15
15
  end
16
16
  module_function :map
17
17
 
@@ -5,13 +5,19 @@ module Monolens
5
5
  class Date
6
6
  include Lens
7
7
 
8
- def call(arg, *rest)
9
- is_string!(arg)
8
+ DEFAULT_FORMATS = [
9
+ nil
10
+ ]
10
11
 
11
- date, first_error = nil, nil
12
- @options[:formats].each do |format|
12
+ def call(arg, world = {})
13
+ is_string!(arg, world)
14
+
15
+ date = nil
16
+ first_error = nil
17
+ formats = @options.fetch(:formats, DEFAULT_FORMATS)
18
+ formats.each do |format|
13
19
  begin
14
- return date = ::Date.strptime(arg, format)
20
+ return date = strptime(arg, format)
15
21
  rescue ArgumentError => ex
16
22
  first_error ||= ex
17
23
  rescue ::Date::Error => ex
@@ -19,7 +25,15 @@ module Monolens
19
25
  end
20
26
  end
21
27
 
22
- raise Monolens::LensError, first_error.message
28
+ fail!(first_error.message, world)
29
+ end
30
+
31
+ def strptime(arg, format = nil)
32
+ if format.nil?
33
+ ::Date.strptime(arg)
34
+ else
35
+ ::Date.strptime(arg, format)
36
+ end
23
37
  end
24
38
  end
25
39
  end
@@ -5,13 +5,19 @@ module Monolens
5
5
  class DateTime
6
6
  include Lens
7
7
 
8
- def call(arg, *rest)
9
- is_string!(arg)
8
+ DEFAULT_FORMATS = [
9
+ nil
10
+ ]
10
11
 
11
- date, first_error = nil, nil
12
- @options[:formats].each do |format|
12
+ def call(arg, world = {})
13
+ is_string!(arg, world)
14
+
15
+ date = nil
16
+ first_error = nil
17
+ formats = @options.fetch(:formats, DEFAULT_FORMATS)
18
+ formats.each do |format|
13
19
  begin
14
- return date = ::DateTime.strptime(arg, format)
20
+ return date = strptime(arg, format)
15
21
  rescue ArgumentError => ex
16
22
  first_error ||= ex
17
23
  rescue ::Date::Error => ex
@@ -19,7 +25,15 @@ module Monolens
19
25
  end
20
26
  end
21
27
 
22
- raise Monolens::LensError, first_error.message
28
+ fail!(first_error.message, world)
29
+ end
30
+
31
+ def strptime(arg, format = nil)
32
+ if format.nil?
33
+ ::DateTime.strptime(arg)
34
+ else
35
+ ::DateTime.strptime(arg, format)
36
+ end
23
37
  end
24
38
  end
25
39
  end
@@ -0,0 +1,13 @@
1
+ require 'date'
2
+
3
+ module Monolens
4
+ module Coerce
5
+ class String
6
+ include Lens
7
+
8
+ def call(arg, world = {})
9
+ arg.to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -10,11 +10,14 @@ module Monolens
10
10
  end
11
11
  module_function :datetime
12
12
 
13
+ def string(options = {})
14
+ String.new(options)
15
+ end
16
+ module_function :string
17
+
13
18
  Monolens.define_namespace 'coerce', self
14
19
  end
15
20
  end
16
- require_relative 'str/strip'
17
- require_relative 'str/upcase'
18
- require_relative 'str/downcase'
19
21
  require_relative 'coerce/date'
20
22
  require_relative 'coerce/date_time'
23
+ require_relative 'coerce/string'
@@ -8,12 +8,12 @@ module Monolens
8
8
  @lenses = lenses
9
9
  end
10
10
 
11
- def call(arg, *rest)
11
+ def call(arg, world = {})
12
12
  result = arg
13
13
  @lenses.each do |lens|
14
14
  done = false
15
15
  catch(:skip) do
16
- result = lens.call(result, *rest)
16
+ result = lens.call(result, world)
17
17
  done = true
18
18
  end
19
19
  break unless done
@@ -3,9 +3,9 @@ module Monolens
3
3
  class Mapping
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
7
  option(:values, {}).fetch(arg) do
8
- raise LensError if option(:fail_if_missing)
8
+ fail!("Unrecognized value `#{arg}`", world) if option(:fail_if_missing)
9
9
 
10
10
  option(:default)
11
11
  end
@@ -1,4 +1,11 @@
1
1
  module Monolens
2
- class Error < StandardError; end
3
- class LensError < Error; end
2
+ class Error < StandardError
3
+ end
4
+ class LensError < Error
5
+ def initialize(message, location = [])
6
+ super(message)
7
+ @location = location
8
+ end
9
+ attr_reader :location
10
+ end
4
11
  end
data/lib/monolens/file.rb CHANGED
@@ -2,13 +2,8 @@ module Monolens
2
2
  class File
3
3
  include Lens
4
4
 
5
- def initialize(info)
6
- super
7
- options[:lenses] = Monolens.lens(options[:lenses])
8
- end
9
-
10
- def call(*args, &bl)
11
- options[:lenses].call(*args, &bl)
5
+ def call(arg, world = {})
6
+ option(:lenses).call(arg, world)
12
7
  end
13
8
  end
14
9
  end
@@ -0,0 +1,21 @@
1
+ module Monolens
2
+ module Lens
3
+ module FetchSupport
4
+
5
+ def fetch_on(attr, arg, default = nil)
6
+ if arg.key?(attr)
7
+ [ attr, arg[attr] ]
8
+ elsif arg.key?(attr_s = attr.to_s)
9
+ [ attr_s, arg[attr_s] ]
10
+ elsif arg.key?(attr_sym = attr.to_sym)
11
+ [ attr_sym, arg[attr_sym] ]
12
+ elsif default
13
+ [ attr, default ]
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Monolens
2
+ module Lens
3
+ class Location
4
+ def initialize(parts = [])
5
+ @parts = parts
6
+ end
7
+
8
+ def deeper(part)
9
+ yield Location.new(@parts + [part])
10
+ end
11
+
12
+ def to_a
13
+ @parts.dup
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Monolens
2
+ module Lens
3
+ class Options
4
+ include FetchSupport
5
+
6
+ def initialize(options)
7
+ @options = case options
8
+ when Hash
9
+ options.dup
10
+ else
11
+ { lenses: options }
12
+ end
13
+ actual, lenses = fetch_on(:lenses, @options)
14
+ @options[actual] = Monolens.lens(lenses) if actual && lenses
15
+ @options.freeze
16
+ end
17
+ attr_reader :options
18
+ private :options
19
+
20
+ NO_DEFAULT = Object.new.freeze
21
+
22
+ def fetch(key, default = NO_DEFAULT, on = @options)
23
+ if on.key?(key)
24
+ on[key]
25
+ elsif on.key?(s = key.to_s)
26
+ on[s]
27
+ elsif on.key?(sym = key.to_sym)
28
+ on[sym]
29
+ elsif default != NO_DEFAULT
30
+ default
31
+ else
32
+ raise Error, "Missing option #{key}"
33
+ end
34
+ end
35
+
36
+ def to_h
37
+ @options.dup
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/monolens/lens.rb CHANGED
@@ -1,51 +1,67 @@
1
+ require_relative 'lens/fetch_support'
2
+ require_relative 'lens/options'
3
+ require_relative 'lens/location'
4
+
1
5
  module Monolens
2
6
  module Lens
7
+ include FetchSupport
8
+
3
9
  def initialize(options = {})
4
- @options = options.each_with_object({}){|(k,v),memo|
5
- memo[k.to_sym] = v
6
- }
10
+ @options = Options.new(options)
7
11
  end
8
12
  attr_reader :options
9
13
 
10
- def fetch_on(attr, arg)
11
- if arg.key?(attr)
12
- [ attr, arg[attr] ]
13
- elsif arg.key?(attr_s = attr.to_s)
14
- [ attr_s, arg[attr_s] ]
15
- elsif arg.key?(attr_sym = attr.to_sym)
16
- [ attr_sym, arg[attr_sym] ]
17
- else
18
- [ attr, nil ]
14
+ protected
15
+
16
+ def option(name, default = nil)
17
+ @options.fetch(name, default)
18
+ end
19
+
20
+ def deeper(world, part)
21
+ world[:location] ||= Location.new
22
+ world[:location].deeper(part) do |loc|
23
+ yield(world.merge(location: loc))
19
24
  end
20
25
  end
21
26
 
22
- def option(name, default = nil)
23
- _, option = fetch_on(name, options)
24
- option.nil? ? default : option
27
+ MISSING_ERROR_HANDLER = <<~TXT
28
+ An :error_handler world entry is required to use error handling
29
+ TXT
30
+
31
+ def error_handler!(world)
32
+ _, handler = fetch_on(:error_handler, world)
33
+ return handler if handler
34
+
35
+ raise Monolens::Error, MISSING_ERROR_HANDLER
25
36
  end
26
37
 
27
- def is_string!(arg)
38
+ def is_string!(arg, world)
28
39
  return if arg.is_a?(::String)
29
40
 
30
- raise Monolens::Error, "String expected, got #{arg.class}"
41
+ fail!("String expected, got #{arg.class}", world)
31
42
  end
32
43
 
33
- def is_hash!(arg)
44
+ def is_hash!(arg, world)
34
45
  return if arg.is_a?(::Hash)
35
46
 
36
- raise Monolens::Error, "Hash expected, got #{arg.class}"
47
+ fail!("Hash expected, got #{arg.class}", world)
37
48
  end
38
49
 
39
- def is_enumerable!(arg)
50
+ def is_enumerable!(arg, world)
40
51
  return if arg.is_a?(::Enumerable)
41
52
 
42
- raise Monolens::Error, "Enumerable expected, got #{arg.class}"
53
+ fail!("Enumerable expected, got #{arg.class}", world)
43
54
  end
44
55
 
45
- def is_array!(arg)
56
+ def is_array!(arg, world)
46
57
  return if arg.is_a?(::Array)
47
58
 
48
- raise Monolens::Error, "Array expected, got #{arg.class}"
59
+ fail!("Array expected, got #{arg.class}", world)
60
+ end
61
+
62
+ def fail!(msg, world)
63
+ location = world[:location]&.to_a || []
64
+ raise Monolens::LensError.new(msg, location)
49
65
  end
50
66
  end
51
67
  end
@@ -3,19 +3,17 @@ module Monolens
3
3
  class Keys
4
4
  include Lens
5
5
 
6
- def initialize(lens)
7
- super({})
8
- @lens = Monolens.lens(lens)
9
- end
10
-
11
- def call(arg, *rest)
12
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
13
8
 
9
+ lenses = option(:lenses)
14
10
  dup = {}
15
11
  arg.each_pair do |attr, value|
16
- lensed = @lens.call(attr.to_s)
17
- lensed = lensed.to_sym if lensed && attr.is_a?(Symbol)
18
- dup[lensed] = value
12
+ deeper(world, attr) do |w|
13
+ lensed = lenses.call(attr, w)
14
+ lensed = lensed.to_sym if lensed && attr.is_a?(Symbol)
15
+ dup[lensed] = value
16
+ end
19
17
  end
20
18
  dup
21
19
  end
@@ -3,11 +3,11 @@ module Monolens
3
3
  class Rename
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
8
8
 
9
9
  dup = arg.dup
10
- options.each_pair do |oldname, newname|
10
+ option(:defn).each_pair do |oldname, newname|
11
11
  actual_name, value = fetch_on(oldname, arg)
12
12
  newname = actual_name.is_a?(Symbol) ? newname.to_sym : newname.to_s
13
13
  dup.delete(actual_name)
@@ -3,28 +3,46 @@ module Monolens
3
3
  class Select
4
4
  include Lens
5
5
 
6
- def initialize(selection)
7
- super({})
8
- @selection = selection
9
- end
10
-
11
- def call(arg, *rest)
12
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
13
8
 
14
9
  result = {}
15
- @selection.each_pair do |new_attr, selector|
16
- is_array = selector.is_a?(::Array)
17
- is_symbol = false
18
- values = Array(selector).map do |old_attr|
19
- actual, fetched = fetch_on(old_attr, arg)
20
- is_symbol ||= actual.is_a?(Symbol)
21
- fetched
10
+ is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
11
+ option(:defn, {}).each_pair do |new_attr, selector|
12
+ deeper(world, new_attr) do |w|
13
+ is_array = selector.is_a?(::Array)
14
+ values = []
15
+ Array(selector).each do |old_attr|
16
+ actual, fetched = fetch_on(old_attr, arg)
17
+ if actual.nil?
18
+ on_missing(old_attr, values, w)
19
+ else
20
+ values << fetched
21
+ end
22
+ end
23
+ new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
24
+ unless values.empty?
25
+ result[new_attr] = is_array ? values : values.first
26
+ end
22
27
  end
23
- new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
24
- result[new_attr] = is_array ? values : values.first
25
28
  end
26
29
  result
27
30
  end
31
+
32
+ def on_missing(attr, values, world)
33
+ strategy = option(:on_missing, :fail)
34
+ case strategy.to_sym
35
+ when :fail
36
+ fail!("Expected `#{attr}` to be defined", world)
37
+ when :null
38
+ values << nil
39
+ when :skip
40
+ nil
41
+ else
42
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
43
+ end
44
+ end
45
+ private :on_missing
28
46
  end
29
47
  end
30
48
  end
@@ -3,23 +3,45 @@ module Monolens
3
3
  class Transform
4
4
  include Lens
5
5
 
6
- def initialize(transformation)
7
- super({})
8
- @transformation = transformation.each_with_object({}){|(attr,lens),memo|
9
- memo[attr.to_sym] = Monolens.lens(lens)
10
- }
6
+ def initialize(options)
7
+ super(options)
8
+ ts = option(:defn, {})
9
+ ts.each_pair do |k,v|
10
+ ts[k] = Monolens.lens(v)
11
+ end
11
12
  end
12
13
 
13
- def call(arg, *rest)
14
- is_hash!(arg)
14
+ def call(arg, world = {})
15
+ is_hash!(arg, world)
16
+
17
+ result = arg.dup
18
+ option(:defn, {}).each_pair do |attr, sub_lens|
19
+ deeper(world, attr) do |w|
20
+ actual_attr, fetched = fetch_on(attr, arg)
21
+ if actual_attr
22
+ result[actual_attr] = sub_lens.call(fetched, w)
23
+ else
24
+ on_missing(result, attr, world)
25
+ end
26
+ end
27
+ end
28
+ result
29
+ end
15
30
 
16
- dup = arg.dup
17
- @transformation.each_pair do |attr, sub_lens|
18
- actual_attr, fetched = fetch_on(attr, dup)
19
- dup[actual_attr] = sub_lens.call(fetched)
31
+ def on_missing(result, attr, world)
32
+ strategy = option(:on_missing, :fail)
33
+ case strategy.to_sym
34
+ when :fail
35
+ fail!("Expected `#{attr}` to be defined", world)
36
+ when :null
37
+ result[attr] = nil
38
+ when :skip
39
+ nil
40
+ else
41
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
20
42
  end
21
- dup
22
43
  end
44
+ private :on_missing
23
45
  end
24
46
  end
25
47
  end