monolens 0.1.0 → 0.4.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 (73) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +14 -4
  3. data/bin/monolens +11 -0
  4. data/lib/monolens/array/compact.rb +2 -2
  5. data/lib/monolens/array/join.rb +13 -0
  6. data/lib/monolens/array/map.rb +57 -0
  7. data/lib/monolens/array.rb +12 -0
  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 +87 -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 +15 -0
  17. data/lib/monolens/core.rb +10 -4
  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 +41 -18
  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 +58 -0
  29. data/lib/monolens/object/transform.rb +34 -12
  30. data/lib/monolens/object/values.rb +34 -10
  31. data/lib/monolens/object.rb +12 -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 +14 -0
  35. data/lib/monolens/str/strip.rb +3 -1
  36. data/lib/monolens/str/upcase.rb +2 -2
  37. data/lib/monolens/str.rb +12 -6
  38. data/lib/monolens/version.rb +1 -1
  39. data/lib/monolens.rb +7 -1
  40. data/spec/fixtures/coerce.yml +3 -2
  41. data/spec/fixtures/transform.yml +5 -4
  42. data/spec/monolens/array/test_compact.rb +15 -0
  43. data/spec/monolens/array/test_join.rb +27 -0
  44. data/spec/monolens/array/test_map.rb +96 -0
  45. data/spec/monolens/coerce/test_date.rb +34 -4
  46. data/spec/monolens/coerce/test_datetime.rb +70 -7
  47. data/spec/monolens/coerce/test_integer.rb +46 -0
  48. data/spec/monolens/coerce/test_string.rb +15 -0
  49. data/spec/monolens/command/map-upcase.lens.yml +5 -0
  50. data/spec/monolens/command/names-with-null.json +5 -0
  51. data/spec/monolens/command/names.json +4 -0
  52. data/spec/monolens/command/robust-map-upcase.lens.yml +7 -0
  53. data/spec/monolens/core/test_dig.rb +78 -0
  54. data/spec/monolens/core/test_mapping.rb +76 -0
  55. data/spec/monolens/lens/test_options.rb +73 -0
  56. data/spec/monolens/object/test_extend.rb +94 -0
  57. data/spec/monolens/object/test_keys.rb +54 -22
  58. data/spec/monolens/object/test_rename.rb +1 -1
  59. data/spec/monolens/object/test_select.rb +202 -0
  60. data/spec/monolens/object/test_transform.rb +93 -6
  61. data/spec/monolens/object/test_values.rb +110 -12
  62. data/spec/monolens/skip/test_null.rb +2 -2
  63. data/spec/monolens/str/test_downcase.rb +13 -0
  64. data/spec/monolens/str/test_split.rb +39 -0
  65. data/spec/monolens/str/test_strip.rb +13 -0
  66. data/spec/monolens/str/test_upcase.rb +13 -0
  67. data/spec/monolens/test_command.rb +128 -0
  68. data/spec/monolens/test_error_traceability.rb +60 -0
  69. data/spec/monolens/test_lens.rb +1 -1
  70. data/spec/test_readme.rb +8 -6
  71. metadata +39 -5
  72. data/lib/monolens/core/map.rb +0 -18
  73. data/spec/monolens/core/test_map.rb +0 -11
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'array.map' do
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
44
+ end
45
+
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
95
+ end
96
+ end
@@ -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
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'core.mapping' do
4
+ let(:mapping) do
5
+ { 'values' => { 'todo' => 'open' }}
6
+ end
7
+
8
+ context 'with default options' do
9
+ subject do
10
+ Monolens.lens('core.mapping' => mapping)
11
+ end
12
+
13
+ it 'replaces the value by its mapped' do
14
+ expect(subject.call('todo')).to eql('open')
15
+ end
16
+
17
+ it 'returns nil if not found' do
18
+ expect(subject.call('nosuchone')).to eql(nil)
19
+ end
20
+ end
21
+
22
+ context 'specifying a default value' do
23
+ subject do
24
+ Monolens.lens('core.mapping' => mapping.merge('default' => 'foo'))
25
+ end
26
+
27
+ it 'replaces the value by its mapped' do
28
+ expect(subject.call('todo')).to eql('open')
29
+ end
30
+
31
+ it 'returns the default if not found' do
32
+ expect(subject.call('nosuchone')).to eql('foo')
33
+ end
34
+ end
35
+
36
+ context 'lets raise if not found' do
37
+ subject do
38
+ Monolens.lens('core.mapping' => mapping.merge('fail_if_missing' => true))
39
+ end
40
+
41
+ it 'replaces the value by its mapped' do
42
+ expect(subject.call('todo')).to eql('open')
43
+ end
44
+
45
+ it 'raises if not found' do
46
+ expect {
47
+ subject.call('nosuchone')
48
+ }.to raise_error(Monolens::LensError)
49
+ end
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
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
@@ -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