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
@@ -5,13 +5,43 @@ describe Monolens, 'coerce.date' do
5
5
  Monolens.lens('coerce.date' => { formats: ['%d/%m/%Y'] })
6
6
  end
7
7
 
8
+ it 'returns Date objects unchanged (idempotency)' do
9
+ input = Date.today
10
+ expect(subject.call(input)).to be(input)
11
+ end
12
+
8
13
  it 'coerces valid dates' do
9
14
  expect(subject.call('11/12/2022')).to eql(Date.parse('2022-12-11'))
10
15
  end
11
16
 
12
- it 'fails on invalid dates' do
13
- expect {
14
- subject.call('invalid')
15
- }.to raise_error(Monolens::LensError)
17
+ describe 'error handling' do
18
+ let(:lens) do
19
+ Monolens.lens({
20
+ 'array.map' => {
21
+ :lenses => 'coerce.date'
22
+ }
23
+ })
24
+ end
25
+
26
+ subject do
27
+ begin
28
+ lens.call(input)
29
+ nil
30
+ rescue Monolens::LensError => ex
31
+ ex
32
+ end
33
+ end
34
+
35
+ let(:input) do
36
+ ['invalid']
37
+ end
38
+
39
+ it 'fails on invalid dates' do
40
+ expect(subject).to be_a(Monolens::LensError)
41
+ end
42
+
43
+ it 'properly sets the location' do
44
+ expect(subject.location).to eql([0])
45
+ end
16
46
  end
17
47
  end
@@ -2,16 +2,79 @@ require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'coerce.datetime' do
4
4
  subject do
5
- Monolens.lens('coerce.datetime' => { formats: ['%d/%m/%Y %H:%M'] })
5
+ Monolens.lens('coerce.datetime' => {
6
+ }.merge(options))
6
7
  end
7
8
 
8
- it 'coerces valid date times' do
9
- expect(subject.call('11/12/2022 17:38')).to eql(DateTime.parse('2022-12-11 17:38'))
9
+ let(:options) do
10
+ {}
10
11
  end
11
12
 
12
- it 'fails on invalid dates' do
13
- expect {
14
- subject.call('invalid')
15
- }.to raise_error(Monolens::LensError)
13
+ it 'returns DateTime objects unchanged (idempotency)' do
14
+ input = DateTime.now
15
+ expect(subject.call(input)).to be(input)
16
+ end
17
+
18
+ describe 'support for formats' do
19
+ let(:options) do
20
+ { formats: ['%d/%m/%Y %H:%M'] }
21
+ end
22
+
23
+ it 'coerces valid date times' do
24
+ expect(subject.call('11/12/2022 17:38')).to eql(DateTime.parse('2022-12-11 17:38'))
25
+ end
26
+ end
27
+
28
+ describe 'support for a timezone' do
29
+ let(:options) do
30
+ { parser: timezone }
31
+ end
32
+
33
+ let(:now) do
34
+ ::DateTime.now
35
+ end
36
+
37
+ let(:timezone) do
38
+ Object.new
39
+ end
40
+
41
+ before do
42
+ expect(timezone).to receive(:parse).and_return(now)
43
+ end
44
+
45
+ it 'uses it to parse' do
46
+ expect(subject.call('2022-01-01')).to be(now)
47
+ end
48
+ end
49
+
50
+ describe 'error handling' do
51
+ let(:lens) do
52
+ Monolens.lens({
53
+ 'array.map' => {
54
+ :lenses => 'coerce.datetime'
55
+ }
56
+ })
57
+ end
58
+
59
+ subject do
60
+ begin
61
+ lens.call(input)
62
+ nil
63
+ rescue Monolens::LensError => ex
64
+ ex
65
+ end
66
+ end
67
+
68
+ let(:input) do
69
+ ['invalid']
70
+ end
71
+
72
+ it 'fails on invalid dates' do
73
+ expect(subject).to be_a(Monolens::LensError)
74
+ end
75
+
76
+ it 'properly sets the location' do
77
+ expect(subject.location).to eql([0])
78
+ end
16
79
  end
17
80
  end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'coerce.integer' do
4
+ subject do
5
+ Monolens.lens('coerce.integer')
6
+ end
7
+
8
+ it 'is idempotent' do
9
+ expect(subject.call(12)).to eql(12)
10
+ end
11
+
12
+ it 'coerces valid integers' do
13
+ expect(subject.call('12')).to eql(12)
14
+ end
15
+
16
+ describe 'error handling' do
17
+ let(:lens) do
18
+ Monolens.lens({
19
+ 'array.map' => {
20
+ :lenses => 'coerce.integer'
21
+ }
22
+ })
23
+ end
24
+
25
+ subject do
26
+ begin
27
+ lens.call(input)
28
+ nil
29
+ rescue Monolens::LensError => ex
30
+ ex
31
+ end
32
+ end
33
+
34
+ let(:input) do
35
+ ['12sh']
36
+ end
37
+
38
+ it 'fails on invalid integers' do
39
+ expect(subject).to be_a(Monolens::LensError)
40
+ end
41
+
42
+ it 'properly sets the location' do
43
+ expect(subject.location).to eql([0])
44
+ end
45
+ end
46
+ 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
@@ -0,0 +1,5 @@
1
+ ---
2
+ version: '1.0'
3
+ lenses:
4
+ - array.map:
5
+ - str.upcase
@@ -0,0 +1,5 @@
1
+ [
2
+ "Bernard",
3
+ null,
4
+ "David"
5
+ ]
@@ -0,0 +1,4 @@
1
+ [
2
+ "Bernard",
3
+ "David"
4
+ ]
@@ -0,0 +1,7 @@
1
+ ---
2
+ version: '1.0'
3
+ lenses:
4
+ - array.map:
5
+ on_error: handler
6
+ lenses:
7
+ - str.upcase
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, "core.dig" do
4
+ let(:lens) do
5
+ Monolens.lens('core.dig' => { defn: ['hobbies', 1, 'name'] })
6
+ end
7
+
8
+ it 'works' do
9
+ input = {
10
+ hobbies: [
11
+ { name: 'programming' },
12
+ { name: 'music' }
13
+ ]
14
+ }
15
+ expected = 'music'
16
+ expect(lens.call(input)).to eql(expected)
17
+ end
18
+
19
+ describe 'error handling' do
20
+ let(:lens) do
21
+ Monolens.lens({
22
+ 'array.map' => {
23
+ lenses: {
24
+ 'core.dig' => {
25
+ on_missing: on_missing,
26
+ defn: ['hobbies', 1, 'name']
27
+ }.compact
28
+ }
29
+ }
30
+ })
31
+ end
32
+
33
+ subject do
34
+ begin
35
+ lens.call(input)
36
+ rescue Monolens::LensError => ex
37
+ ex
38
+ end
39
+ end
40
+
41
+ context 'default behavior' do
42
+ let(:on_missing) do
43
+ nil
44
+ end
45
+
46
+ let(:input) do
47
+ [{
48
+ hobbies: [
49
+ { name: 'programming' }
50
+ ]
51
+ }]
52
+ end
53
+
54
+ it 'fails as expected' do
55
+ expect(subject).to be_a(Monolens::LensError)
56
+ expect(subject.location).to eql([0])
57
+ end
58
+ end
59
+
60
+ context 'on_missing: null' do
61
+ let(:on_missing) do
62
+ :null
63
+ end
64
+
65
+ let(:input) do
66
+ [{
67
+ hobbies: [
68
+ { name: 'programming' }
69
+ ]
70
+ }]
71
+ end
72
+
73
+ it 'works' do
74
+ expect(subject).to eql([nil])
75
+ end
76
+ end
77
+ end
78
+ end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'core.mapping' do
4
4
  let(:mapping) do
5
- { 'values' => { 'todo' => 'open' }}
5
+ { 'values' => { 'todo' => 'open' } }
6
6
  end
7
7
 
8
8
  context 'with default options' do
@@ -14,14 +14,16 @@ describe Monolens, 'core.mapping' do
14
14
  expect(subject.call('todo')).to eql('open')
15
15
  end
16
16
 
17
- it 'returns nil if not found' do
18
- expect(subject.call('nosuchone')).to eql(nil)
17
+ it 'raises if not found' do
18
+ expect {
19
+ subject.call('nosuchone')
20
+ }.to raise_error(Monolens::LensError)
19
21
  end
20
22
  end
21
23
 
22
- context 'specifying a default value' do
24
+ context 'on_missing: default' do
23
25
  subject do
24
- Monolens.lens('core.mapping' => mapping.merge('default' => 'foo'))
26
+ Monolens.lens('core.mapping' => mapping.merge('on_missing' => 'default', 'default' => 'foo'))
25
27
  end
26
28
 
27
29
  it 'replaces the value by its mapped' do
@@ -33,19 +35,59 @@ describe Monolens, 'core.mapping' do
33
35
  end
34
36
  end
35
37
 
36
- context 'lets raise if not found' do
38
+ context 'on_missing: null' do
37
39
  subject do
38
- Monolens.lens('core.mapping' => mapping.merge('fail_if_missing' => true))
40
+ Monolens.lens('core.mapping' => mapping.merge('on_missing' => 'null'))
39
41
  end
40
42
 
41
43
  it 'replaces the value by its mapped' do
42
44
  expect(subject.call('todo')).to eql('open')
43
45
  end
44
46
 
45
- it 'raises if not found' do
46
- expect {
47
- subject.call('nosuchone')
48
- }.to raise_error(Monolens::LensError)
47
+ it 'returns nil if missing' do
48
+ expect(subject.call('nosuchone')).to eql(nil)
49
+ end
50
+ end
51
+
52
+ context 'on_missing: fallback' do
53
+ subject do
54
+ Monolens.lens('core.mapping' => mapping.merge(
55
+ 'on_missing' => 'fallback',
56
+ 'fallback' => ->(lens, arg, world) { 'foo' }
57
+ ))
58
+ end
59
+
60
+ it 'replaces the value by its mapped' do
61
+ expect(subject.call('todo')).to eql('open')
62
+ end
63
+
64
+ it 'returns nil if missing' do
65
+ expect(subject.call('nosuchone')).to eql('foo')
66
+ end
67
+ end
68
+
69
+ describe 'error handling' do
70
+ let(:lens) do
71
+ Monolens.lens({
72
+ 'array.map' => {
73
+ :lenses => {
74
+ 'core.mapping' => mapping.merge('on_missing' => 'fail')
75
+ }
76
+ }
77
+ })
78
+ end
79
+
80
+ subject do
81
+ begin
82
+ lens.call(['todo', 'foo'])
83
+ nil
84
+ rescue Monolens::LensError => ex
85
+ ex
86
+ end
87
+ end
88
+
89
+ it 'sets the location correctly' do
90
+ expect(subject.location).to eql([1])
49
91
  end
50
92
  end
51
93
  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
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'object.extend' do
4
+ subject do
5
+ Monolens.lens('object.extend' => {
6
+ defn: {
7
+ name: [
8
+ { 'core.dig' => { defn: ['firstname'] } },
9
+ 'str.upcase'
10
+ ]
11
+ }
12
+ })
13
+ end
14
+
15
+ it 'works as expected' do
16
+ input = {
17
+ 'firstname' => 'Bernard',
18
+ 'lastname' => 'Lambeau'
19
+ }
20
+ expected = input.merge({
21
+ 'name' => 'BERNARD',
22
+ })
23
+ expect(subject.call(input)).to eql(expected)
24
+ end
25
+
26
+ describe 'on_error' do
27
+ let(:lens) do
28
+ Monolens.lens({
29
+ 'array.map' => {
30
+ :lenses => {
31
+ 'object.extend' => {
32
+ on_error: on_error,
33
+ defn: {
34
+ upcased: [
35
+ { 'core.dig' => { defn: ['firstname'] } },
36
+ 'str.upcase'
37
+ ]
38
+ }
39
+ }.compact
40
+ }
41
+ }
42
+ })
43
+ end
44
+
45
+ subject do
46
+ lens.call(input)
47
+ rescue Monolens::LensError => ex
48
+ ex
49
+ end
50
+
51
+ context 'default' do
52
+ let(:on_error) do
53
+ nil
54
+ end
55
+
56
+ let(:input) do
57
+ [{}]
58
+ end
59
+
60
+ it 'works as expected' do
61
+ expect(subject).to be_a(Monolens::LensError)
62
+ expect(subject.location).to eql([0, :upcased])
63
+ end
64
+ end
65
+
66
+ context 'with :null' do
67
+ let(:on_error) do
68
+ :null
69
+ end
70
+
71
+ let(:input) do
72
+ [{}]
73
+ end
74
+
75
+ it 'works as expected' do
76
+ expect(subject).to eql([{'upcased' => nil}])
77
+ end
78
+ end
79
+
80
+ context 'with :skip' do
81
+ let(:on_error) do
82
+ :skip
83
+ end
84
+
85
+ let(:input) do
86
+ [{}]
87
+ end
88
+
89
+ it 'works as expected' do
90
+ expect(subject).to eql([{}])
91
+ end
92
+ end
93
+ end
94
+ 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