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.
- checksums.yaml +4 -4
- data/lib/monolens/array/compact.rb +2 -2
- data/lib/monolens/array/join.rb +2 -2
- data/lib/monolens/array/map.rb +45 -6
- data/lib/monolens/array.rb +2 -2
- data/lib/monolens/coerce/date.rb +20 -6
- data/lib/monolens/coerce/date_time.rb +20 -6
- data/lib/monolens/coerce/string.rb +13 -0
- data/lib/monolens/coerce.rb +6 -3
- data/lib/monolens/core/chain.rb +2 -2
- data/lib/monolens/core/mapping.rb +2 -2
- data/lib/monolens/error.rb +9 -2
- data/lib/monolens/file.rb +2 -7
- data/lib/monolens/lens/fetch_support.rb +21 -0
- data/lib/monolens/lens/location.rb +17 -0
- data/lib/monolens/lens/options.rb +41 -0
- data/lib/monolens/lens.rb +39 -23
- data/lib/monolens/object/keys.rb +8 -10
- data/lib/monolens/object/rename.rb +3 -3
- data/lib/monolens/object/select.rb +34 -16
- data/lib/monolens/object/transform.rb +34 -12
- data/lib/monolens/object/values.rb +34 -10
- data/lib/monolens/skip/null.rb +1 -1
- data/lib/monolens/str/downcase.rb +2 -2
- data/lib/monolens/str/split.rb +2 -2
- data/lib/monolens/str/strip.rb +3 -1
- data/lib/monolens/str/upcase.rb +2 -2
- data/lib/monolens/version.rb +1 -1
- data/spec/fixtures/coerce.yml +3 -2
- data/spec/fixtures/transform.yml +5 -4
- data/spec/monolens/array/test_map.rb +89 -6
- data/spec/monolens/coerce/test_date.rb +29 -4
- data/spec/monolens/coerce/test_datetime.rb +29 -4
- data/spec/monolens/coerce/test_string.rb +15 -0
- data/spec/monolens/core/test_mapping.rb +25 -0
- data/spec/monolens/lens/test_options.rb +73 -0
- data/spec/monolens/object/test_keys.rb +54 -22
- data/spec/monolens/object/test_rename.rb +1 -1
- data/spec/monolens/object/test_select.rb +109 -4
- data/spec/monolens/object/test_transform.rb +93 -6
- data/spec/monolens/object/test_values.rb +110 -12
- data/spec/monolens/test_error_traceability.rb +60 -0
- data/spec/monolens/test_lens.rb +1 -1
- data/spec/test_readme.rb +7 -5
- metadata +9 -2
@@ -3,20 +3,44 @@ module Monolens
|
|
3
3
|
class Values
|
4
4
|
include Lens
|
5
5
|
|
6
|
-
def
|
7
|
-
|
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
|
-
|
9
|
+
lenses = option(:lenses)
|
10
|
+
result = arg.dup
|
15
11
|
arg.each_pair do |attr, value|
|
16
|
-
|
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
|
data/lib/monolens/skip/null.rb
CHANGED
data/lib/monolens/str/split.rb
CHANGED
data/lib/monolens/str/strip.rb
CHANGED
data/lib/monolens/str/upcase.rb
CHANGED
data/lib/monolens/version.rb
CHANGED
data/spec/fixtures/coerce.yml
CHANGED
data/spec/fixtures/transform.yml
CHANGED
@@ -1,13 +1,96 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Monolens, 'array.map' do
|
4
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
'
|
11
|
-
|
12
|
-
|
13
|
-
expected
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
@@ -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
|
-
|
8
|
-
|
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
|
-
'
|
43
|
-
|
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
|