monolens 0.2.0 → 0.5.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 (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