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
@@ -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
@@ -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 = 3
5
5
  TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
@@ -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
@@ -9,9 +9,34 @@ describe Monolens, 'coerce.date' do
9
9
  expect(subject.call('11/12/2022')).to eql(Date.parse('2022-12-11'))
10
10
  end
11
11
 
12
- it 'fails on invalid dates' do
13
- expect {
14
- subject.call('invalid')
15
- }.to raise_error(Monolens::LensError)
12
+ describe 'error handling' do
13
+ let(:lens) do
14
+ Monolens.lens({
15
+ 'array.map' => {
16
+ :lenses => 'coerce.date'
17
+ }
18
+ })
19
+ end
20
+
21
+ subject do
22
+ begin
23
+ lens.call(input)
24
+ nil
25
+ rescue Monolens::LensError => ex
26
+ ex
27
+ end
28
+ end
29
+
30
+ let(:input) do
31
+ ['invalid']
32
+ end
33
+
34
+ it 'fails on invalid dates' do
35
+ expect(subject).to be_a(Monolens::LensError)
36
+ end
37
+
38
+ it 'properly sets the location' do
39
+ expect(subject.location).to eql([0])
40
+ end
16
41
  end
17
42
  end
@@ -9,9 +9,34 @@ describe Monolens, 'coerce.datetime' do
9
9
  expect(subject.call('11/12/2022 17:38')).to eql(DateTime.parse('2022-12-11 17:38'))
10
10
  end
11
11
 
12
- it 'fails on invalid dates' do
13
- expect {
14
- subject.call('invalid')
15
- }.to raise_error(Monolens::LensError)
12
+ describe 'error handling' do
13
+ let(:lens) do
14
+ Monolens.lens({
15
+ 'array.map' => {
16
+ :lenses => 'coerce.datetime'
17
+ }
18
+ })
19
+ end
20
+
21
+ subject do
22
+ begin
23
+ lens.call(input)
24
+ nil
25
+ rescue Monolens::LensError => ex
26
+ ex
27
+ end
28
+ end
29
+
30
+ let(:input) do
31
+ ['invalid']
32
+ end
33
+
34
+ it 'fails on invalid dates' do
35
+ expect(subject).to be_a(Monolens::LensError)
36
+ end
37
+
38
+ it 'properly sets the location' do
39
+ expect(subject.location).to eql([0])
40
+ end
16
41
  end
17
42
  end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'coerce.string' do
4
+ subject do
5
+ Monolens.lens('coerce.string')
6
+ end
7
+
8
+ it 'works' do
9
+ expect(subject.call(12)).to eql('12')
10
+ end
11
+
12
+ it 'accepts null' do
13
+ expect(subject.call(nil)).to eql('')
14
+ end
15
+ end
@@ -48,4 +48,29 @@ describe Monolens, 'core.mapping' do
48
48
  }.to raise_error(Monolens::LensError)
49
49
  end
50
50
  end
51
+
52
+ describe 'error handling' do
53
+ let(:lens) do
54
+ Monolens.lens({
55
+ 'array.map' => {
56
+ :lenses => {
57
+ 'core.mapping' => mapping.merge('fail_if_missing' => true)
58
+ }
59
+ }
60
+ })
61
+ end
62
+
63
+ subject do
64
+ begin
65
+ lens.call(['todo', 'foo'])
66
+ nil
67
+ rescue Monolens::LensError => ex
68
+ ex
69
+ end
70
+ end
71
+
72
+ it 'sets the location correctly' do
73
+ expect(subject.location).to eql([1])
74
+ end
75
+ end
51
76
  end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ module Monolens
4
+ module Lens
5
+ describe Options do
6
+ subject do
7
+ Options.new(input)
8
+ end
9
+
10
+ describe 'initialize' do
11
+ context('when used with a Hash') do
12
+ let(:input) do
13
+ { separator: ',' }
14
+ end
15
+
16
+ it 'uses a copy of the hash' do
17
+ expect(subject.send(:options)).not_to be(input)
18
+ expect(subject.to_h).not_to be(input)
19
+ expect(subject.to_h).to eql(input)
20
+ end
21
+ end
22
+
23
+ context('when used with an Array') do
24
+ let(:input) do
25
+ ['str.strip']
26
+ end
27
+
28
+ it 'converts it to lenses' do
29
+ expect(subject.to_h.keys).to eql([:lenses])
30
+ lenses = subject.to_h[:lenses]
31
+ expect(lenses).to be_a(Core::Chain)
32
+ end
33
+ end
34
+
35
+ context('when used with a String') do
36
+ let(:input) do
37
+ 'str.strip'
38
+ end
39
+
40
+ it 'converts it to lenses' do
41
+ expect(subject.to_h.keys).to eql([:lenses])
42
+ lenses = subject.to_h[:lenses]
43
+ expect(lenses).to be_a(Str::Strip)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe 'fetch' do
49
+ context 'when used with Symbols' do
50
+ let(:input) do
51
+ { separator: ',' }
52
+ end
53
+
54
+ it 'lets fetch as Symbols' do
55
+ expect(subject.fetch(:separator)).to eql(',')
56
+ end
57
+
58
+ it 'lets fetch as Strings' do
59
+ expect(subject.fetch('separator')).to eql(',')
60
+ end
61
+
62
+ it 'raises if not found' do
63
+ expect { subject.fetch('nosuchone') }.to raise_error(Monolens::Error)
64
+ end
65
+
66
+ it 'lets pass a default value' do
67
+ expect(subject.fetch('nosuchone', 'foo')).to eql('foo')
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,31 +1,63 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'object.keys' do
4
- subject do
5
- Monolens.lens('object.keys' => ['str.upcase'])
4
+ context 'with string keys' do
5
+ subject do
6
+ Monolens.lens('object.keys' => ['str.upcase'])
7
+ end
8
+
9
+ it 'works as expected' do
10
+ input = {
11
+ 'firstname' => 'Bernard',
12
+ 'lastname' => 'Lambeau'
13
+ }
14
+ expected = {
15
+ 'FIRSTNAME' => 'Bernard',
16
+ 'LASTNAME' => 'Lambeau'
17
+ }
18
+ expect(subject.call(input)).to eql(expected)
19
+ end
6
20
  end
7
21
 
8
- it 'works as expected' do
9
- input = {
10
- 'firstname' => 'Bernard',
11
- 'lastname' => 'Lambeau'
12
- }
13
- expected = {
14
- 'FIRSTNAME' => 'Bernard',
15
- 'LASTNAME' => 'Lambeau'
16
- }
17
- expect(subject.call(input)).to eql(expected)
22
+ context 'with symbol keys' do
23
+ subject do
24
+ Monolens.lens('object.keys' => ['coerce.string', 'str.upcase'])
25
+ end
26
+
27
+ it 'works as expected with Symbol keys' do
28
+ input = {
29
+ firstname: 'Bernard',
30
+ lastname: 'Lambeau'
31
+ }
32
+ expected = {
33
+ FIRSTNAME: 'Bernard',
34
+ LASTNAME: 'Lambeau'
35
+ }
36
+ expect(subject.call(input)).to eql(expected)
37
+ end
18
38
  end
19
39
 
20
- it 'works as expected with Symbol keys' do
21
- input = {
22
- firstname: 'Bernard',
23
- lastname: 'Lambeau'
24
- }
25
- expected = {
26
- FIRSTNAME: 'Bernard',
27
- LASTNAME: 'Lambeau'
28
- }
29
- expect(subject.call(input)).to eql(expected)
40
+ describe 'error handling' do
41
+ let(:lens) do
42
+ Monolens.lens('object.keys' => ['str.upcase'])
43
+ end
44
+
45
+ subject do
46
+ lens.call(input)
47
+ nil
48
+ rescue Monolens::LensError => ex
49
+ ex
50
+ end
51
+
52
+ let(:input) do
53
+ {
54
+ 'firstname' => 'Bernard',
55
+ nil => 'Lambeau'
56
+ }
57
+ end
58
+
59
+ it 'correctly updates the location' do
60
+ expect(subject.location).to eql([nil])
61
+ end
30
62
  end
31
63
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'object.rename' do
4
4
  subject do
5
- Monolens.lens('object.rename' => { lastname: :name })
5
+ Monolens.lens('object.rename' => { defn: { lastname: :name } })
6
6
  end
7
7
 
8
8
  it 'works as expected' do
@@ -4,8 +4,10 @@ describe Monolens, 'object.select' do
4
4
  context 'when using symbols in the definition' do
5
5
  subject do
6
6
  Monolens.lens('object.select' => {
7
- name: [:firstname, :lastname],
8
- status: :priority
7
+ defn: {
8
+ name: [:firstname, :lastname],
9
+ status: :priority
10
+ }
9
11
  })
10
12
  end
11
13
 
@@ -39,8 +41,10 @@ describe Monolens, 'object.select' do
39
41
  context 'when using strings in the definition' do
40
42
  subject do
41
43
  Monolens.lens('object.select' => {
42
- 'name' => ['firstname', 'lastname'],
43
- 'status' => 'priority'
44
+ 'defn' => {
45
+ 'name' => ['firstname', 'lastname'],
46
+ 'status' => 'priority'
47
+ }
44
48
  })
45
49
  end
46
50
 
@@ -70,4 +74,105 @@ describe Monolens, 'object.select' do
70
74
  expect(subject.call(input)).to eql(expected)
71
75
  end
72
76
  end
77
+
78
+ context 'when a key is missing and no option' do
79
+ subject do
80
+ Monolens.lens('object.select' => {
81
+ defn: {
82
+ name: [:firstname, :lastname],
83
+ status: :priority
84
+ }
85
+ })
86
+ end
87
+
88
+ it 'raises an error' do
89
+ input = {
90
+ firstname: 'Bernard'
91
+ }
92
+ expect{
93
+ subject.call(input)
94
+ }.to raise_error(Monolens::LensError, /lastname/)
95
+ end
96
+ end
97
+
98
+ context 'when using on_missing: skip' do
99
+ subject do
100
+ Monolens.lens('object.select' => {
101
+ on_missing: :skip,
102
+ defn: {
103
+ name: [:firstname, :lastname],
104
+ status: :priority
105
+ }
106
+ })
107
+ end
108
+
109
+ it 'works as expected' do
110
+ input = {
111
+ firstname: 'Bernard'
112
+ }
113
+ expected = {
114
+ name: ['Bernard']
115
+ }
116
+ expect(subject.call(input)).to eql(expected)
117
+ end
118
+ end
119
+
120
+ context 'when using on_missing: null' do
121
+ subject do
122
+ Monolens.lens('object.select' => {
123
+ on_missing: :null,
124
+ defn: {
125
+ name: [:firstname, :lastname],
126
+ status: :priority
127
+ }
128
+ })
129
+ end
130
+
131
+ it 'works as expected' do
132
+ input = {
133
+ firstname: 'Bernard'
134
+ }
135
+ expected = {
136
+ name: ['Bernard', nil],
137
+ status: nil
138
+ }
139
+ expect(subject.call(input)).to eql(expected)
140
+ end
141
+
142
+ it 'works as expected' do
143
+ input = {
144
+ priority: 12
145
+ }
146
+ expected = {
147
+ name: [nil, nil],
148
+ status: 12
149
+ }
150
+ expect(subject.call(input)).to eql(expected)
151
+ end
152
+ end
153
+
154
+ describe 'error traceability' do
155
+ let(:lens) do
156
+ Monolens.lens('object.select' => {
157
+ defn: {
158
+ status: :priority
159
+ }
160
+ })
161
+ end
162
+
163
+ subject do
164
+ lens.call(input)
165
+ nil
166
+ rescue Monolens::LensError => ex
167
+ ex
168
+ end
169
+
170
+ let(:input) do
171
+ {}
172
+ end
173
+
174
+ it 'correctly updates the location' do
175
+ expect(subject.location).to eql([:status])
176
+ end
177
+ end
73
178
  end