monolens 0.3.0 → 0.5.1

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.
@@ -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,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
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'core.mapping' do
4
4
  let(:mapping) do
5
- { 'values' => { 'todo' => 'open' }}
5
+ { 'values' => { 'todo' => 'open' } }
6
6
  end
7
7
 
8
8
  context 'with default options' do
@@ -14,14 +14,16 @@ describe Monolens, 'core.mapping' do
14
14
  expect(subject.call('todo')).to eql('open')
15
15
  end
16
16
 
17
- it 'returns nil if not found' do
18
- expect(subject.call('nosuchone')).to eql(nil)
17
+ it 'raises if not found' do
18
+ expect {
19
+ subject.call('nosuchone')
20
+ }.to raise_error(Monolens::LensError)
19
21
  end
20
22
  end
21
23
 
22
- context 'specifying a default value' do
24
+ context 'on_missing: default' do
23
25
  subject do
24
- Monolens.lens('core.mapping' => mapping.merge('default' => 'foo'))
26
+ Monolens.lens('core.mapping' => mapping.merge('on_missing' => 'default', 'default' => 'foo'))
25
27
  end
26
28
 
27
29
  it 'replaces the value by its mapped' do
@@ -33,19 +35,34 @@ describe Monolens, 'core.mapping' do
33
35
  end
34
36
  end
35
37
 
36
- context 'lets raise if not found' do
38
+ context 'on_missing: null' do
37
39
  subject do
38
- Monolens.lens('core.mapping' => mapping.merge('fail_if_missing' => true))
40
+ Monolens.lens('core.mapping' => mapping.merge('on_missing' => 'null'))
39
41
  end
40
42
 
41
43
  it 'replaces the value by its mapped' do
42
44
  expect(subject.call('todo')).to eql('open')
43
45
  end
44
46
 
45
- it 'raises if not found' do
46
- expect {
47
- subject.call('nosuchone')
48
- }.to raise_error(Monolens::LensError)
47
+ it 'returns nil if missing' do
48
+ expect(subject.call('nosuchone')).to eql(nil)
49
+ end
50
+ end
51
+
52
+ context 'on_missing: fallback' do
53
+ subject do
54
+ Monolens.lens('core.mapping' => mapping.merge(
55
+ 'on_missing' => 'fallback',
56
+ 'fallback' => ->(lens, arg, world) { 'foo' }
57
+ ))
58
+ end
59
+
60
+ it 'replaces the value by its mapped' do
61
+ expect(subject.call('todo')).to eql('open')
62
+ end
63
+
64
+ it 'returns nil if missing' do
65
+ expect(subject.call('nosuchone')).to eql('foo')
49
66
  end
50
67
  end
51
68
 
@@ -54,7 +71,7 @@ describe Monolens, 'core.mapping' do
54
71
  Monolens.lens({
55
72
  'array.map' => {
56
73
  :lenses => {
57
- 'core.mapping' => mapping.merge('fail_if_missing' => true)
74
+ 'core.mapping' => mapping.merge('on_missing' => 'fail')
58
75
  }
59
76
  }
60
77
  })
@@ -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
@@ -75,6 +75,115 @@ describe Monolens, 'object.select' do
75
75
  end
76
76
  end
77
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
+
78
187
  context 'when a key is missing and no option' do
79
188
  subject do
80
189
  Monolens.lens('object.select' => {
@@ -162,7 +271,6 @@ describe Monolens, 'object.select' do
162
271
 
163
272
  subject do
164
273
  lens.call(input)
165
- nil
166
274
  rescue Monolens::LensError => ex
167
275
  ex
168
276
  end
@@ -172,7 +280,7 @@ describe Monolens, 'object.select' do
172
280
  end
173
281
 
174
282
  it 'correctly updates the location' do
175
- expect(subject.location).to eql([:status])
283
+ expect(subject.location).to eql(['status'])
176
284
  end
177
285
  end
178
286
  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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monolens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-06 00:00:00.000000000 Z
11
+ date: 2022-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bmg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Data transformations inspired by Cambria lenses
56
70
  email: blambeau@gmail.com
57
71
  executables: []
@@ -62,6 +76,7 @@ files:
62
76
  - LICENSE.md
63
77
  - README.md
64
78
  - Rakefile
79
+ - bin/monolens
65
80
  - lib/monolens.rb
66
81
  - lib/monolens/array.rb
67
82
  - lib/monolens/array/compact.rb
@@ -70,17 +85,22 @@ files:
70
85
  - lib/monolens/coerce.rb
71
86
  - lib/monolens/coerce/date.rb
72
87
  - lib/monolens/coerce/date_time.rb
88
+ - lib/monolens/coerce/integer.rb
73
89
  - lib/monolens/coerce/string.rb
90
+ - lib/monolens/command.rb
74
91
  - lib/monolens/core.rb
75
92
  - lib/monolens/core/chain.rb
93
+ - lib/monolens/core/dig.rb
76
94
  - lib/monolens/core/mapping.rb
77
95
  - lib/monolens/error.rb
96
+ - lib/monolens/error_handler.rb
78
97
  - lib/monolens/file.rb
79
98
  - lib/monolens/lens.rb
80
99
  - lib/monolens/lens/fetch_support.rb
81
100
  - lib/monolens/lens/location.rb
82
101
  - lib/monolens/lens/options.rb
83
102
  - lib/monolens/object.rb
103
+ - lib/monolens/object/extend.rb
84
104
  - lib/monolens/object/keys.rb
85
105
  - lib/monolens/object/rename.rb
86
106
  - lib/monolens/object/select.rb
@@ -102,9 +122,16 @@ files:
102
122
  - spec/monolens/array/test_map.rb
103
123
  - spec/monolens/coerce/test_date.rb
104
124
  - spec/monolens/coerce/test_datetime.rb
125
+ - spec/monolens/coerce/test_integer.rb
105
126
  - spec/monolens/coerce/test_string.rb
127
+ - spec/monolens/command/map-upcase.lens.yml
128
+ - spec/monolens/command/names-with-null.json
129
+ - spec/monolens/command/names.json
130
+ - spec/monolens/command/robust-map-upcase.lens.yml
131
+ - spec/monolens/core/test_dig.rb
106
132
  - spec/monolens/core/test_mapping.rb
107
133
  - spec/monolens/lens/test_options.rb
134
+ - spec/monolens/object/test_extend.rb
108
135
  - spec/monolens/object/test_keys.rb
109
136
  - spec/monolens/object/test_rename.rb
110
137
  - spec/monolens/object/test_select.rb
@@ -115,6 +142,7 @@ files:
115
142
  - spec/monolens/str/test_split.rb
116
143
  - spec/monolens/str/test_strip.rb
117
144
  - spec/monolens/str/test_upcase.rb
145
+ - spec/monolens/test_command.rb
118
146
  - spec/monolens/test_error_traceability.rb
119
147
  - spec/monolens/test_lens.rb
120
148
  - spec/spec_helper.rb