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
@@ -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
- name: [:firstname, :lastname],
8
- status: :priority
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
- 'name' => ['firstname', 'lastname'],
43
- 'status' => 'priority'
44
+ 'defn' => {
45
+ 'name' => ['firstname', 'lastname'],
46
+ 'status' => 'priority'
47
+ }
44
48
  })
45
49
  end
46
50
 
@@ -70,4 +74,213 @@ describe Monolens, 'object.select' do
70
74
  expect(subject.call(input)).to eql(expected)
71
75
  end
72
76
  end
77
+
78
+ context 'when using strategy: first' do
79
+ subject do
80
+ Monolens.lens('object.select' => {
81
+ defn: {
82
+ name: [:firstname, :lastname],
83
+ status: :priority
84
+ },
85
+ strategy: 'first',
86
+ on_missing: on_missing
87
+ }.compact)
88
+ end
89
+
90
+ context 'without on_missing' do
91
+ let(:on_missing) do
92
+ nil
93
+ end
94
+
95
+ it 'works as expected when first option is present' do
96
+ input = {
97
+ firstname: 'Bernard',
98
+ priority: 12
99
+ }
100
+ expected = {
101
+ name: 'Bernard',
102
+ status: 12
103
+ }
104
+ expect(subject.call(input)).to eql(expected)
105
+ end
106
+
107
+ it 'works as expected when second is present' do
108
+ input = {
109
+ lastname: 'Lambeau',
110
+ priority: 12
111
+ }
112
+ expected = {
113
+ name: 'Lambeau',
114
+ status: 12
115
+ }
116
+ expect(subject.call(input)).to eql(expected)
117
+ end
118
+
119
+ it 'fails when none is present' do
120
+ input = {
121
+ priority: 12
122
+ }
123
+ expect {
124
+ subject.call(input)
125
+ }.to raise_error(Monolens::Error)
126
+ end
127
+ end
128
+
129
+ context 'with on_missing: skip' do
130
+ let(:on_missing) do
131
+ :skip
132
+ end
133
+
134
+ it 'works as expected when missing' do
135
+ input = {
136
+ priority: 12
137
+ }
138
+ expected = {
139
+ status: 12
140
+ }
141
+ expect(subject.call(input)).to eql(expected)
142
+ end
143
+ end
144
+
145
+ context 'with on_missing: null' do
146
+ let(:on_missing) do
147
+ :null
148
+ end
149
+
150
+ it 'works as expected when missing' do
151
+ input = {
152
+ priority: 12
153
+ }
154
+ expected = {
155
+ name: nil,
156
+ status: 12
157
+ }
158
+ expect(subject.call(input)).to eql(expected)
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'when using an array as selection' do
164
+ subject do
165
+ Monolens.lens('object.select' => {
166
+ defn: [
167
+ :firstname,
168
+ :priority
169
+ ]
170
+ })
171
+ end
172
+
173
+ it 'works as expected' do
174
+ input = {
175
+ firstname: 'Bernard',
176
+ lastname: 'Lambeau',
177
+ priority: 12
178
+ }
179
+ expected = {
180
+ firstname: 'Bernard',
181
+ priority: 12
182
+ }
183
+ expect(subject.call(input)).to eql(expected)
184
+ end
185
+ end
186
+
187
+ context 'when a key is missing and no option' do
188
+ subject do
189
+ Monolens.lens('object.select' => {
190
+ defn: {
191
+ name: [:firstname, :lastname],
192
+ status: :priority
193
+ }
194
+ })
195
+ end
196
+
197
+ it 'raises an error' do
198
+ input = {
199
+ firstname: 'Bernard'
200
+ }
201
+ expect{
202
+ subject.call(input)
203
+ }.to raise_error(Monolens::LensError, /lastname/)
204
+ end
205
+ end
206
+
207
+ context 'when using on_missing: skip' do
208
+ subject do
209
+ Monolens.lens('object.select' => {
210
+ on_missing: :skip,
211
+ defn: {
212
+ name: [:firstname, :lastname],
213
+ status: :priority
214
+ }
215
+ })
216
+ end
217
+
218
+ it 'works as expected' do
219
+ input = {
220
+ firstname: 'Bernard'
221
+ }
222
+ expected = {
223
+ name: ['Bernard']
224
+ }
225
+ expect(subject.call(input)).to eql(expected)
226
+ end
227
+ end
228
+
229
+ context 'when using on_missing: null' do
230
+ subject do
231
+ Monolens.lens('object.select' => {
232
+ on_missing: :null,
233
+ defn: {
234
+ name: [:firstname, :lastname],
235
+ status: :priority
236
+ }
237
+ })
238
+ end
239
+
240
+ it 'works as expected' do
241
+ input = {
242
+ firstname: 'Bernard'
243
+ }
244
+ expected = {
245
+ name: ['Bernard', nil],
246
+ status: nil
247
+ }
248
+ expect(subject.call(input)).to eql(expected)
249
+ end
250
+
251
+ it 'works as expected' do
252
+ input = {
253
+ priority: 12
254
+ }
255
+ expected = {
256
+ name: [nil, nil],
257
+ status: 12
258
+ }
259
+ expect(subject.call(input)).to eql(expected)
260
+ end
261
+ end
262
+
263
+ describe 'error traceability' do
264
+ let(:lens) do
265
+ Monolens.lens('object.select' => {
266
+ defn: {
267
+ status: :priority
268
+ }
269
+ })
270
+ end
271
+
272
+ subject do
273
+ lens.call(input)
274
+ rescue Monolens::LensError => ex
275
+ ex
276
+ end
277
+
278
+ let(:input) do
279
+ {}
280
+ end
281
+
282
+ it 'correctly updates the location' do
283
+ expect(subject.location).to eql(['status'])
284
+ end
285
+ end
73
286
  end
@@ -1,15 +1,102 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'object.transform' do
4
- subject do
5
- Monolens.lens('object.transform' => { firstname: 'str.upcase' })
4
+ context 'with default options' do
5
+ subject do
6
+ Monolens.lens('object.transform' => {
7
+ defn: { firstname: 'str.upcase' }
8
+ })
9
+ end
10
+
11
+ it 'works as expected' do
12
+ expect(subject.call(firstname: 'Bernard')).to eql(firstname: 'BERNARD')
13
+ end
14
+
15
+ it 'works as expected on an object with String keys' do
16
+ expect(subject.call('firstname' => 'Bernard')).to eql('firstname' => 'BERNARD')
17
+ end
18
+
19
+ it 'raises an error if input object does not have a key' do
20
+ expect {
21
+ subject.call(lastname: 'Lambeau')
22
+ }.to raise_error(Monolens::LensError, /firstname/)
23
+ end
24
+ end
25
+
26
+ context 'with on_missing: skip' do
27
+ subject do
28
+ Monolens.lens('object.transform' => {
29
+ on_missing: :skip,
30
+ defn: { firstname: 'str.upcase' }
31
+ })
32
+ end
33
+
34
+ it 'works as expected' do
35
+ expect(subject.call(firstname: 'Bernard')).to eql(firstname: 'BERNARD')
36
+ end
37
+
38
+ it 'skpis if missing' do
39
+ expect(subject.call(lastname: 'Lambeau')).to eql(lastname: 'Lambeau')
40
+ end
6
41
  end
7
42
 
8
- it 'works as expected' do
9
- expect(subject.call(firstname: 'Bernard')).to eql(firstname: 'BERNARD')
43
+ context 'with on_missing: null' do
44
+ subject do
45
+ Monolens.lens('object.transform' => {
46
+ on_missing: :null,
47
+ defn: { firstname: 'str.upcase' }
48
+ })
49
+ end
50
+
51
+ it 'works as expected' do
52
+ expect(subject.call(firstname: 'Bernard')).to eql(firstname: 'BERNARD')
53
+ end
54
+
55
+ it 'skpis if missing' do
56
+ expect(subject.call(lastname: 'Lambeau')).to eql(firstname: nil, lastname: 'Lambeau')
57
+ end
10
58
  end
11
59
 
12
- it 'works as expected on an object with String keys' do
13
- expect(subject.call('firstname' => 'Bernard')).to eql('firstname' => 'BERNARD')
60
+ describe 'error traceability' do
61
+ let(:lens) do
62
+ Monolens.lens({
63
+ 'array.map' => {
64
+ :lenses => {
65
+ 'object.transform' => {
66
+ defn: { firstname: 'str.upcase' }
67
+ }
68
+ }
69
+ }
70
+ })
71
+ end
72
+
73
+ subject do
74
+ lens.call(input)
75
+ nil
76
+ rescue Monolens::LensError => ex
77
+ ex
78
+ end
79
+
80
+ context 'when missing key' do
81
+ let(:input) do
82
+ [{}]
83
+ end
84
+
85
+ it 'correctly updates the location' do
86
+ expect(subject.location).to eql([0])
87
+ end
88
+ end
89
+
90
+ context 'when an error down the line' do
91
+ let(:input) do
92
+ [{
93
+ firstname: nil
94
+ }]
95
+ end
96
+
97
+ it 'correctly updates the location' do
98
+ expect(subject.location).to eql([0, :firstname])
99
+ end
100
+ end
14
101
  end
15
102
  end
@@ -1,19 +1,117 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'object.values' do
4
- subject do
5
- Monolens.lens('object.values' => ['str.upcase'])
4
+ context 'with default options' do
5
+ subject do
6
+ Monolens.lens('object.values' => ['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
20
+
21
+ it 'raises an error on any problem' do
22
+ input = {
23
+ firstname: nil,
24
+ lastname: 'Lambeau'
25
+ }
26
+ expect {
27
+ subject.call(input)
28
+ }.to raise_error(Monolens::LensError)
29
+ end
30
+ end
31
+
32
+ context 'with on_error: skip' do
33
+ subject do
34
+ Monolens.lens('object.values' => {
35
+ 'on_error' => 'skip',
36
+ 'lenses' => ['str.upcase']
37
+ })
38
+ end
39
+
40
+ it 'skips key/value when an error occurs' do
41
+ input = {
42
+ firstname: nil,
43
+ lastname: 'Lambeau'
44
+ }
45
+ expected = {
46
+ lastname: 'LAMBEAU'
47
+ }
48
+ expect(subject.call(input)).to eql(expected)
49
+ end
50
+ end
51
+
52
+ context 'with on_error: null' do
53
+ subject do
54
+ Monolens.lens('object.values' => {
55
+ 'on_error' => 'null',
56
+ 'lenses' => ['str.upcase']
57
+ })
58
+ end
59
+
60
+ it 'uses nil as value' do
61
+ input = {
62
+ firstname: 12,
63
+ lastname: 'Lambeau'
64
+ }
65
+ expected = {
66
+ firstname: nil,
67
+ lastname: 'LAMBEAU'
68
+ }
69
+ expect(subject.call(input)).to eql(expected)
70
+ end
71
+ end
72
+
73
+ context 'with on_error: keep' do
74
+ subject do
75
+ Monolens.lens('object.values' => {
76
+ 'on_error' => 'keep',
77
+ 'lenses' => ['str.upcase']
78
+ })
79
+ end
80
+
81
+ it 'uses nil as value' do
82
+ input = {
83
+ firstname: 12,
84
+ lastname: 'Lambeau'
85
+ }
86
+ expected = {
87
+ firstname: 12,
88
+ lastname: 'LAMBEAU'
89
+ }
90
+ expect(subject.call(input)).to eql(expected)
91
+ end
6
92
  end
7
93
 
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)
94
+ describe 'error traceability' do
95
+ let(:lens) do
96
+ Monolens.lens('object.values' => ['str.upcase'])
97
+ end
98
+
99
+ subject do
100
+ lens.call(input)
101
+ nil
102
+ rescue Monolens::LensError => ex
103
+ ex
104
+ end
105
+
106
+ let(:input) do
107
+ {
108
+ 'firstname' => 'Bernard',
109
+ 'lastname' => nil
110
+ }
111
+ end
112
+
113
+ it 'correctly updates the location' do
114
+ expect(subject.location).to eql(['lastname'])
115
+ end
18
116
  end
19
117
  end
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'monolens'
4
+ require 'monolens/command'
5
+
6
+ module Monolens
7
+ class Exited < Monolens::Error
8
+ end
9
+ class Command
10
+ attr_reader :exit_status
11
+
12
+ def do_exit(status)
13
+ @exit_status = status
14
+ raise Exited
15
+ end
16
+ end
17
+ describe Command do
18
+ FIXTURES = (Path.dir/"command").expand_path
19
+
20
+ let(:command) do
21
+ Command.new(argv, stdin, stdout, stderr)
22
+ end
23
+
24
+ let(:stdin) do
25
+ StringIO.new
26
+ end
27
+
28
+ let(:stdout) do
29
+ StringIO.new
30
+ end
31
+
32
+ let(:stderr) do
33
+ StringIO.new
34
+ end
35
+
36
+ let(:file_args) do
37
+ [FIXTURES/'map-upcase.lens.yml', FIXTURES/'names.json']
38
+ end
39
+
40
+ subject do
41
+ begin
42
+ command.call
43
+ rescue Exited
44
+ end
45
+ end
46
+
47
+ before do
48
+ subject
49
+ end
50
+
51
+ def exit_status
52
+ command.exit_status
53
+ end
54
+
55
+ def reloaded_json
56
+ JSON.parse(stdout.string)
57
+ end
58
+
59
+ context 'with no option nor args' do
60
+ let(:argv) do
61
+ []
62
+ end
63
+
64
+ it 'prints the help and exits' do
65
+ expect(exit_status).to eql(0)
66
+ expect(stdout.string).to match(/monolens/)
67
+ end
68
+ end
69
+
70
+ context 'with --version' do
71
+ let(:argv) do
72
+ ['--version']
73
+ end
74
+
75
+ it 'prints the version and exits' do
76
+ expect(exit_status).to eql(0)
77
+ expect(stdout.string).to eql("Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}\n")
78
+ end
79
+ end
80
+
81
+ context 'with a lens and a json input' do
82
+ let(:argv) do
83
+ file_args
84
+ end
85
+
86
+ it 'works as expected' do
87
+ expect(exit_status).to be_nil
88
+ expect(reloaded_json).to eql(['BERNARD', 'DAVID'])
89
+ end
90
+ end
91
+
92
+ context 'with --pretty' do
93
+ let(:argv) do
94
+ ['--pretty'] + file_args
95
+ end
96
+
97
+ it 'works as expected' do
98
+ expect(exit_status).to be_nil
99
+ expect(stdout.string).to match(/^\[\n/)
100
+ expect(reloaded_json).to eql(['BERNARD', 'DAVID'])
101
+ end
102
+ end
103
+
104
+ context 'when yielding an error' do
105
+ let(:argv) do
106
+ [FIXTURES/'map-upcase.lens.yml', FIXTURES/'names-with-null.json']
107
+ end
108
+
109
+ it 'works as expected' do
110
+ expect(exit_status).to eql(-2)
111
+ expect(stdout.string).to eql('')
112
+ expect(stderr.string).to eql("[1] String expected, got NilClass\n")
113
+ end
114
+ end
115
+
116
+ context 'when yielding an error on a robust lens' do
117
+ let(:argv) do
118
+ [FIXTURES/'robust-map-upcase.lens.yml', FIXTURES/'names-with-null.json']
119
+ end
120
+
121
+ it 'works as expected' do
122
+ expect(exit_status).to be_nil
123
+ expect(stdout.string).to eql('["BERNARD","DAVID"]'+"\n")
124
+ expect(stderr.string).to eql("[1] String expected, got NilClass\n")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'error traceability' do
4
+ context 'on a leaf monolens' do
5
+ let(:lens) do
6
+ Monolens.lens('str.upcase')
7
+ end
8
+
9
+ subject do
10
+ begin
11
+ lens.call(nil)
12
+ rescue => ex
13
+ ex
14
+ end
15
+ end
16
+
17
+ it 'works as expected' do
18
+ expect(subject).to be_a(Monolens::LensError)
19
+ expect(subject.location).to eql([])
20
+ end
21
+ end
22
+
23
+ context 'on array.map' do
24
+ let(:lens) do
25
+ Monolens.lens('array.map' => 'str.upcase')
26
+ end
27
+
28
+ subject do
29
+ begin
30
+ lens.call(['foo', nil])
31
+ rescue => ex
32
+ ex
33
+ end
34
+ end
35
+
36
+ it 'works as expected' do
37
+ expect(subject).to be_a(Monolens::LensError)
38
+ expect(subject.location).to eql([1])
39
+ end
40
+ end
41
+
42
+ context 'on array.map => object.values' do
43
+ let(:lens) do
44
+ Monolens.lens('array.map' => { lenses: { 'object.values' => 'str.upcase' } })
45
+ end
46
+
47
+ subject do
48
+ begin
49
+ lens.call([{ hello: 'foo' }, { hello: nil }])
50
+ rescue Monolens::LensError => ex
51
+ ex
52
+ end
53
+ end
54
+
55
+ it 'works as expected' do
56
+ expect(subject).to be_a(Monolens::LensError)
57
+ expect(subject.location).to eql([1, :hello])
58
+ end
59
+ end
60
+ end
@@ -16,7 +16,7 @@ describe Monolens, '.lens' do
16
16
  it 'preserves options' do
17
17
  got = Monolens.lens(:"coerce.date" => { formats: ['%Y'] })
18
18
  expect(got).to be_a(Monolens::Coerce::Date)
19
- expect(got.options).to eql({ formats: ['%Y'] })
19
+ expect(got.options.to_h).to eql({ formats: ['%Y'] })
20
20
  end
21
21
 
22
22
  it 'allows using an Array, factors a Chain with coercion recursion' do
data/spec/test_readme.rb CHANGED
@@ -23,12 +23,14 @@ describe "What's said in README" do
23
23
  lenses:
24
24
  - array.map:
25
25
  - object.transform:
26
- status:
27
- - str.upcase
28
- body:
29
- - str.strip
26
+ defn:
27
+ status:
28
+ - str.upcase
29
+ body:
30
+ - str.strip
30
31
  - object.rename:
31
- body: description
32
+ defn:
33
+ body: description
32
34
  YML
33
35
  }
34
36