monolens 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/bin/monolens +11 -0
  4. data/lib/monolens/array/compact.rb +2 -2
  5. data/lib/monolens/array/join.rb +2 -2
  6. data/lib/monolens/array/map.rb +45 -6
  7. data/lib/monolens/array.rb +2 -2
  8. data/lib/monolens/coerce/date.rb +22 -6
  9. data/lib/monolens/coerce/date_time.rb +30 -6
  10. data/lib/monolens/coerce/integer.rb +15 -0
  11. data/lib/monolens/coerce/string.rb +13 -0
  12. data/lib/monolens/coerce.rb +12 -3
  13. data/lib/monolens/command.rb +96 -0
  14. data/lib/monolens/core/chain.rb +2 -2
  15. data/lib/monolens/core/dig.rb +52 -0
  16. data/lib/monolens/core/mapping.rb +23 -3
  17. data/lib/monolens/core.rb +6 -0
  18. data/lib/monolens/error.rb +9 -2
  19. data/lib/monolens/error_handler.rb +21 -0
  20. data/lib/monolens/file.rb +2 -7
  21. data/lib/monolens/lens/fetch_support.rb +19 -0
  22. data/lib/monolens/lens/location.rb +17 -0
  23. data/lib/monolens/lens/options.rb +41 -0
  24. data/lib/monolens/lens.rb +39 -23
  25. data/lib/monolens/object/extend.rb +53 -0
  26. data/lib/monolens/object/keys.rb +8 -10
  27. data/lib/monolens/object/rename.rb +3 -3
  28. data/lib/monolens/object/select.rb +71 -15
  29. data/lib/monolens/object/transform.rb +34 -12
  30. data/lib/monolens/object/values.rb +34 -10
  31. data/lib/monolens/object.rb +6 -0
  32. data/lib/monolens/skip/null.rb +1 -1
  33. data/lib/monolens/str/downcase.rb +2 -2
  34. data/lib/monolens/str/split.rb +2 -2
  35. data/lib/monolens/str/strip.rb +3 -1
  36. data/lib/monolens/str/upcase.rb +2 -2
  37. data/lib/monolens/version.rb +1 -1
  38. data/lib/monolens.rb +6 -0
  39. data/spec/fixtures/coerce.yml +3 -2
  40. data/spec/fixtures/transform.yml +5 -4
  41. data/spec/monolens/array/test_map.rb +89 -6
  42. data/spec/monolens/coerce/test_date.rb +34 -4
  43. data/spec/monolens/coerce/test_datetime.rb +70 -7
  44. data/spec/monolens/coerce/test_integer.rb +46 -0
  45. data/spec/monolens/coerce/test_string.rb +15 -0
  46. data/spec/monolens/command/map-upcase.lens.yml +5 -0
  47. data/spec/monolens/command/names-with-null.json +5 -0
  48. data/spec/monolens/command/names.json +4 -0
  49. data/spec/monolens/command/robust-map-upcase.lens.yml +7 -0
  50. data/spec/monolens/core/test_dig.rb +78 -0
  51. data/spec/monolens/core/test_mapping.rb +53 -11
  52. data/spec/monolens/lens/test_options.rb +73 -0
  53. data/spec/monolens/object/test_extend.rb +94 -0
  54. data/spec/monolens/object/test_keys.rb +54 -22
  55. data/spec/monolens/object/test_rename.rb +1 -1
  56. data/spec/monolens/object/test_select.rb +217 -4
  57. data/spec/monolens/object/test_transform.rb +93 -6
  58. data/spec/monolens/object/test_values.rb +110 -12
  59. data/spec/monolens/test_command.rb +128 -0
  60. data/spec/monolens/test_error_traceability.rb +60 -0
  61. data/spec/monolens/test_lens.rb +1 -1
  62. data/spec/test_readme.rb +7 -5
  63. metadata +37 -2
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 ]
19
- end
14
+ def fail!(msg, world)
15
+ location = world[:location]&.to_a || []
16
+ raise Monolens::LensError.new(msg, location)
20
17
  end
21
18
 
19
+ protected
20
+
22
21
  def option(name, default = nil)
23
- _, option = fetch_on(name, options)
24
- option.nil? ? default : option
22
+ @options.fetch(name, default)
23
+ end
24
+
25
+ def deeper(world, part)
26
+ world[:location] ||= Location.new
27
+ world[:location].deeper(part) do |loc|
28
+ yield(world.merge(location: loc))
29
+ end
30
+ end
31
+
32
+ MISSING_ERROR_HANDLER = <<~TXT
33
+ An :error_handler world entry is required to use error handling
34
+ TXT
35
+
36
+ def error_handler!(world)
37
+ _, handler = fetch_on(:error_handler, world)
38
+ return handler if handler
39
+
40
+ raise Monolens::Error, MISSING_ERROR_HANDLER
25
41
  end
26
42
 
27
- def is_string!(arg)
43
+ def is_string!(arg, world)
28
44
  return if arg.is_a?(::String)
29
45
 
30
- raise Monolens::Error, "String expected, got #{arg.class}"
46
+ fail!("String expected, got #{arg.class}", world)
31
47
  end
32
48
 
33
- def is_hash!(arg)
49
+ def is_hash!(arg, world)
34
50
  return if arg.is_a?(::Hash)
35
51
 
36
- raise Monolens::Error, "Hash expected, got #{arg.class}"
52
+ fail!("Hash expected, got #{arg.class}", world)
37
53
  end
38
54
 
39
- def is_enumerable!(arg)
55
+ def is_enumerable!(arg, world)
40
56
  return if arg.is_a?(::Enumerable)
41
57
 
42
- raise Monolens::Error, "Enumerable expected, got #{arg.class}"
58
+ fail!("Enumerable expected, got #{arg.class}", world)
43
59
  end
44
60
 
45
- def is_array!(arg)
61
+ def is_array!(arg, world)
46
62
  return if arg.is_a?(::Array)
47
63
 
48
- raise Monolens::Error, "Array expected, got #{arg.class}"
64
+ fail!("Array expected, got #{arg.class}", world)
49
65
  end
50
66
  end
51
67
  end
@@ -0,0 +1,53 @@
1
+ module Monolens
2
+ module Object
3
+ class Extend
4
+ include Lens
5
+
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
12
+ end
13
+
14
+ def call(arg, world = {})
15
+ is_hash!(arg, world)
16
+
17
+ result = arg.dup
18
+ is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
19
+ option(:defn, {}).each_pair do |attr, lens|
20
+ deeper(world, attr) do |w|
21
+ actual = is_symbol ? attr.to_sym : attr.to_s
22
+ begin
23
+ result[actual] = lens.call(arg, w)
24
+ rescue Monolens::LensError => ex
25
+ strategy = option(:on_error, :fail)
26
+ handle_error(strategy, ex, result, actual, world)
27
+ end
28
+ end
29
+ end
30
+ result
31
+ end
32
+
33
+ def handle_error(strategy, ex, result, attr, world)
34
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
35
+ case strategy
36
+ when ::Array
37
+ strategy.each{|s| handle_error(s, ex, result, attr, world) }
38
+ when :handler
39
+ error_handler!(world).call(ex)
40
+ when :fail
41
+ raise
42
+ when :null
43
+ result[attr] = nil
44
+ when :skip
45
+ nil
46
+ else
47
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
48
+ end
49
+ end
50
+ private :handle_error
51
+ end
52
+ end
53
+ 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,84 @@ module Monolens
3
3
  class Select
4
4
  include Lens
5
5
 
6
- def initialize(selection)
7
- super({})
8
- @selection = selection
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
8
+
9
+ result = {}
10
+ is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
11
+ defn.each_pair do |new_attr, selector|
12
+ new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
13
+
14
+ deeper(world, new_attr) do |w|
15
+ catch (:skip) do
16
+ value = do_select(arg, selector, w)
17
+ result[new_attr] = value
18
+ end
19
+ end
20
+ end
21
+ result
9
22
  end
10
23
 
11
- def call(arg, *rest)
12
- is_hash!(arg)
24
+ private
13
25
 
14
- 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|
26
+ def do_select(arg, selector, world)
27
+ if selector.is_a?(::Array)
28
+ do_array_select(arg, selector, world)
29
+ else
30
+ do_single_select(arg, selector, world)
31
+ end
32
+ end
33
+
34
+ def do_array_select(arg, selector, world)
35
+ case option(:strategy, :all).to_sym
36
+ when :all
37
+ selector.each_with_object([]) do |old_attr, values|
38
+ catch (:skip) do
39
+ values << do_single_select(arg, old_attr, world)
40
+ end
41
+ end
42
+ when :first
43
+ selector.each do |old_attr|
19
44
  actual, fetched = fetch_on(old_attr, arg)
20
- is_symbol ||= actual.is_a?(Symbol)
21
- fetched
45
+ return fetched if actual
22
46
  end
23
- new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
24
- result[new_attr] = is_array ? values : values.first
47
+ on_missing(selector.first, [], world).first
48
+ else
49
+ raise Monolens::Error, "Unexpected strategy `#{strategy}`"
50
+ end
51
+ end
52
+
53
+ def do_single_select(arg, selector, world)
54
+ actual, fetched = fetch_on(selector, arg)
55
+ if actual.nil?
56
+ on_missing(selector, [], world).first
57
+ else
58
+ fetched
59
+ end
60
+ end
61
+
62
+ def defn
63
+ defn = option(:defn, {})
64
+ defn = defn.each_with_object({}) do |attr, memo|
65
+ memo[attr] = attr
66
+ end if defn.is_a?(::Array)
67
+ defn
68
+ end
69
+
70
+ def on_missing(attr, values, world)
71
+ strategy = option(:on_missing, :fail)
72
+ case strategy.to_sym
73
+ when :fail
74
+ fail!("Expected `#{attr}` to be defined", world)
75
+ when :null
76
+ values << nil
77
+ when :skip
78
+ throw :skip
79
+ else
80
+ raise Monolens::Error, "Unexpected on_missing strategy `#{strategy}`"
25
81
  end
26
- result
27
82
  end
83
+ private :on_missing
28
84
  end
29
85
  end
30
86
  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
@@ -3,20 +3,44 @@ module Monolens
3
3
  class Values
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
 
14
- dup = arg.dup
9
+ lenses = option(:lenses)
10
+ result = arg.dup
15
11
  arg.each_pair do |attr, value|
16
- dup[attr] = @lens.call(value)
12
+ deeper(world, attr) do |w|
13
+ begin
14
+ result[attr] = lenses.call(value, w)
15
+ rescue Monolens::LensError => ex
16
+ strategy = option(:on_error, :fail)
17
+ handle_error(strategy, ex, result, attr, value, world)
18
+ end
19
+ end
20
+ end
21
+ result
22
+ end
23
+
24
+ def handle_error(strategy, ex, result, attr, value, world)
25
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
26
+ case strategy
27
+ when ::Array
28
+ strategy.each{|s| handle_error(s, ex, result, attr, value, world) }
29
+ when :handler
30
+ error_handler!(world).call(ex)
31
+ when :fail
32
+ raise
33
+ when :null
34
+ result[attr] = nil
35
+ when :skip
36
+ result.delete(attr)
37
+ when :keep
38
+ result[attr] = value
39
+ else
40
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
17
41
  end
18
- dup
19
42
  end
43
+ private :handle_error
20
44
  end
21
45
  end
22
46
  end
@@ -1,5 +1,10 @@
1
1
  module Monolens
2
2
  module Object
3
+ def extend(options)
4
+ Extend.new(options)
5
+ end
6
+ module_function :extend
7
+
3
8
  def rename(parts)
4
9
  Rename.new(parts)
5
10
  end
@@ -33,3 +38,4 @@ require_relative 'object/transform'
33
38
  require_relative 'object/keys'
34
39
  require_relative 'object/values'
35
40
  require_relative 'object/select'
41
+ require_relative 'object/extend'
@@ -3,7 +3,7 @@ module Monolens
3
3
  class Null
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
7
  throw :skip if arg.nil?
8
8
 
9
9
  arg
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Downcase
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_string!(arg)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
8
 
9
9
  arg.downcase
10
10
  end
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Split
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_string!(arg)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
8
 
9
9
  sep = option(:separator)
10
10
  sep ? arg.split(sep) : arg.split
@@ -3,7 +3,9 @@ module Monolens
3
3
  class Strip
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
+
7
9
  arg.to_s.strip
8
10
  end
9
11
  end
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Upcase
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_string!(arg)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
8
 
9
9
  arg.upcase
10
10
  end
@@ -1,7 +1,7 @@
1
1
  module Monolens
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 2
4
+ MINOR = 5
5
5
  TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
data/lib/monolens.rb CHANGED
@@ -5,6 +5,7 @@ module Monolens
5
5
  class << self
6
6
  require_relative 'monolens/version'
7
7
  require_relative 'monolens/error'
8
+ require_relative 'monolens/error_handler'
8
9
  require_relative 'monolens/lens'
9
10
 
10
11
  def define_namespace(name, impl_module)
@@ -43,6 +44,10 @@ module Monolens
43
44
  end
44
45
  private :chain
45
46
 
47
+ def file_lens(arg)
48
+ File.new(arg)
49
+ end
50
+
46
51
  def leaf_lens(arg)
47
52
  namespace_name, lens_name = arg.to_s.split('.')
48
53
  factor_lens(namespace_name, lens_name, {})
@@ -50,6 +55,7 @@ module Monolens
50
55
  private :leaf_lens
51
56
 
52
57
  def hash_lens(arg)
58
+ return file_lens(arg) if arg['version'] || arg[:version]
53
59
  raise "Invalid lens #{arg}" unless arg.size == 1
54
60
 
55
61
  name, options = arg.to_a.first
@@ -2,5 +2,6 @@
2
2
  version: "1.0"
3
3
  lenses:
4
4
  - object.transform:
5
- at:
6
- - coerce.date: { formats: [ '%d/%m/%Y', '%Y/%m/%d' ] }
5
+ defn:
6
+ at:
7
+ - coerce.date: { formats: [ '%d/%m/%Y', '%Y/%m/%d' ] }
@@ -2,7 +2,8 @@
2
2
  version: "1.0"
3
3
  lenses:
4
4
  - object.transform:
5
- firstname:
6
- - str.upcase
7
- lastname:
8
- - str.downcase
5
+ defn:
6
+ firstname:
7
+ - str.upcase
8
+ lastname:
9
+ - str.downcase
@@ -1,13 +1,96 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'array.map' do
4
- subject do
5
- Monolens.lens('array.map' => 'str.upcase')
4
+ context 'without options' do
5
+ subject do
6
+ Monolens.lens('array.map' => 'str.upcase')
7
+ end
8
+
9
+ it 'joins values with spaces' do
10
+ input = ['hello', 'world']
11
+ expected = ['HELLO', 'WORLD']
12
+ expect(subject.call(input)).to eql(expected)
13
+ end
14
+ end
15
+
16
+ context 'default on error' do
17
+ subject do
18
+ Monolens.lens('array.map' => {
19
+ lenses: [ 'str.upcase' ]
20
+ })
21
+ end
22
+
23
+ it 'raise errors' do
24
+ input = [nil, 'world']
25
+ expect {
26
+ subject.call(input)
27
+ }.to raise_error(Monolens::LensError)
28
+ end
29
+ end
30
+
31
+ context 'skipping on error' do
32
+ subject do
33
+ Monolens.lens('array.map' => {
34
+ on_error: 'skip',
35
+ lenses: [ 'str.upcase' ]
36
+ })
37
+ end
38
+
39
+ it 'skips errors' do
40
+ input = [nil, 'world']
41
+ expected = ['WORLD']
42
+ expect(subject.call(input)).to eql(expected)
43
+ end
6
44
  end
7
45
 
8
- it 'joins values with spaces' do
9
- input = ['hello', 'world']
10
- expected = ['HELLO', 'WORLD']
11
- expect(subject.call(input)).to eql(expected)
46
+ context 'nulling on error' do
47
+ subject do
48
+ Monolens.lens('array.map' => {
49
+ on_error: 'null',
50
+ lenses: [ 'str.upcase' ]
51
+ })
52
+ end
53
+
54
+ it 'skips errors' do
55
+ input = [nil, 'world']
56
+ expected = [nil, 'WORLD']
57
+ expect(subject.call(input)).to eql(expected)
58
+ end
59
+ end
60
+
61
+ context 'on error with :handler' do
62
+ subject do
63
+ Monolens.lens('array.map' => {
64
+ on_error: 'handler',
65
+ lenses: [ 'str.upcase' ]
66
+ })
67
+ end
68
+
69
+ it 'collects the error then skips' do
70
+ input = [nil, 'world']
71
+ expected = ['WORLD']
72
+ errs = []
73
+ got = subject.call(input, error_handler: ->(err){ errs << err })
74
+ expect(errs.size).to eql(1)
75
+ expect(got).to eql(expected)
76
+ end
77
+ end
78
+
79
+ context 'collecting on error then nulling' do
80
+ subject do
81
+ Monolens.lens('array.map' => {
82
+ on_error: ['handler', 'null'],
83
+ lenses: [ 'str.upcase' ]
84
+ })
85
+ end
86
+
87
+ it 'uses the handler' do
88
+ input = [nil, 'world']
89
+ expected = [nil, 'WORLD']
90
+ errs = []
91
+ got = subject.call(input, error_handler: ->(err){ errs << err })
92
+ expect(errs.size).to eql(1)
93
+ expect(got).to eql(expected)
94
+ end
12
95
  end
13
96
  end