monolens 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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