monolens 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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