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