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
@@ -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