babl-json 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +228 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +4 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +7 -0
  9. data/README.md +87 -0
  10. data/babl.gemspec +23 -0
  11. data/lib/babl.rb +41 -0
  12. data/lib/babl/builder/chain_builder.rb +85 -0
  13. data/lib/babl/builder/template_base.rb +37 -0
  14. data/lib/babl/operators/array.rb +42 -0
  15. data/lib/babl/operators/call.rb +25 -0
  16. data/lib/babl/operators/dep.rb +48 -0
  17. data/lib/babl/operators/each.rb +45 -0
  18. data/lib/babl/operators/enter.rb +22 -0
  19. data/lib/babl/operators/merge.rb +49 -0
  20. data/lib/babl/operators/nav.rb +55 -0
  21. data/lib/babl/operators/nullable.rb +16 -0
  22. data/lib/babl/operators/object.rb +55 -0
  23. data/lib/babl/operators/parent.rb +90 -0
  24. data/lib/babl/operators/partial.rb +46 -0
  25. data/lib/babl/operators/pin.rb +78 -0
  26. data/lib/babl/operators/source.rb +12 -0
  27. data/lib/babl/operators/static.rb +40 -0
  28. data/lib/babl/operators/switch.rb +71 -0
  29. data/lib/babl/operators/with.rb +51 -0
  30. data/lib/babl/railtie.rb +29 -0
  31. data/lib/babl/rendering/compiled_template.rb +28 -0
  32. data/lib/babl/rendering/context.rb +60 -0
  33. data/lib/babl/rendering/internal_value_node.rb +30 -0
  34. data/lib/babl/rendering/noop_preloader.rb +10 -0
  35. data/lib/babl/rendering/terminal_value_node.rb +54 -0
  36. data/lib/babl/template.rb +48 -0
  37. data/lib/babl/utils/hash.rb +11 -0
  38. data/lib/babl/version.rb +3 -0
  39. data/spec/construction_spec.rb +246 -0
  40. data/spec/navigation_spec.rb +133 -0
  41. data/spec/partial_spec.rb +53 -0
  42. data/spec/pinning_spec.rb +137 -0
  43. metadata +145 -0
@@ -0,0 +1,10 @@
1
+ module Babl
2
+ module Rendering
3
+ # BABL uses this preloader by default. It does nothing.
4
+ class NoopPreloader
5
+ def self.preload(data, *_params)
6
+ data
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,54 @@
1
+ require 'singleton'
2
+
3
+ module Babl
4
+ module Rendering
5
+ # A TerminalValueNode node is always implicitely added to the end of the
6
+ # chain during compilation. It basically ensures that the output contains only
7
+ # primitives, arrays and hashes.
8
+ class TerminalValueNode
9
+ include Singleton
10
+
11
+ def documentation
12
+ :__value__
13
+ end
14
+
15
+ def dependencies
16
+ {}
17
+ end
18
+
19
+ def pinned_dependencies
20
+ {}
21
+ end
22
+
23
+ def render(ctx)
24
+ render_object(ctx.object)
25
+ end
26
+
27
+ def render_object(obj)
28
+ case obj
29
+ when String, Numeric, NilClass, TrueClass, FalseClass then obj
30
+ when Hash then render_hash(obj)
31
+ when Array then render_array(obj)
32
+ else raise Babl::RenderingError, "Only primitives can be serialized: #{obj}"
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def render_array(array)
39
+ array.map { |obj| render_object(obj) }
40
+ end
41
+
42
+ def render_hash(hash)
43
+ hash.map { |k, v| [render_key(k), render_object(v)] }.to_h
44
+ end
45
+
46
+ def render_key(key)
47
+ case key
48
+ when Symbol, String, Numeric, NilClass, TrueClass, FalseClass then :"#{key}"
49
+ else raise Babl::RenderingError, "Invalid key for JSON object: #{obj}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ require 'babl/utils/hash'
2
+
3
+ require 'babl/operators/array'
4
+ require 'babl/operators/call'
5
+ require 'babl/operators/dep'
6
+ require 'babl/operators/each'
7
+ require 'babl/operators/enter'
8
+ require 'babl/operators/merge'
9
+ require 'babl/operators/nav'
10
+ require 'babl/operators/nullable'
11
+ require 'babl/operators/object'
12
+ require 'babl/operators/parent'
13
+ require 'babl/operators/partial'
14
+ require 'babl/operators/pin'
15
+ require 'babl/operators/source'
16
+ require 'babl/operators/static'
17
+ require 'babl/operators/switch'
18
+ require 'babl/operators/with'
19
+
20
+ require 'babl/builder/template_base'
21
+ require 'babl/builder/chain_builder'
22
+
23
+ require 'babl/rendering/compiled_template'
24
+ require 'babl/rendering/context'
25
+ require 'babl/rendering/internal_value_node'
26
+ require 'babl/rendering/terminal_value_node'
27
+ require 'babl/rendering/noop_preloader'
28
+
29
+ module Babl
30
+ class Template < Babl::Builder::TemplateBase
31
+ include Operators::Array::DSL
32
+ include Operators::Call::DSL
33
+ include Operators::Dep::DSL
34
+ include Operators::Each::DSL
35
+ include Operators::Enter::DSL
36
+ include Operators::Merge::DSL
37
+ include Operators::Nav::DSL
38
+ include Operators::Nullable::DSL
39
+ include Operators::Object::DSL
40
+ include Operators::Parent::DSL
41
+ include Operators::Partial::DSL
42
+ include Operators::Pin::DSL
43
+ include Operators::Source::DSL
44
+ include Operators::Static::DSL
45
+ include Operators::Switch::DSL
46
+ include Operators::With::DSL
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module Babl
2
+ module Utils
3
+ class Hash
4
+ # Source: http://stackoverflow.com/a/9381776/1434017 (Jon M)
5
+ def self.deep_merge(first, second)
6
+ merger = proc { |_key, v1, v2| ::Hash === v1 && ::Hash === v2 ? v1.merge(v2, &merger) : v2 }
7
+ first.merge(second, &merger)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Babl
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,246 @@
1
+ require 'babl'
2
+
3
+ describe ::Babl::Template do
4
+ let(:dsl) { ::Babl::Template.new }
5
+ let(:compiled) { template.compile }
6
+ let(:json) { Oj.load(compiled.json(object)) }
7
+ let(:dependencies) { compiled.dependencies }
8
+ let(:documentation) { compiled.documentation }
9
+
10
+ describe '#object' do
11
+ let(:template) { dsl.source { object(:a, :b, c: _, d: nav(:d)) } }
12
+ let(:object) { { a: 1, b: 2, c: 3, d: 4 } }
13
+
14
+ it { expect(json).to eq('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4) }
15
+ it { expect(documentation).to eq(a: :__value__, b: :__value__, c: :__value__, d: :__value__) }
16
+ it { expect(dependencies).to eq(a: {}, b: {}, c: {}, d: {}) }
17
+
18
+ context 'misused (chaining after object)' do
19
+ let(:template) { dsl.source { object(:a).object(:b) } }
20
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
21
+ end
22
+ end
23
+
24
+ describe '#each' do
25
+ context 'when everything is fine' do
26
+ let(:template) { dsl.source { each.nav(:a) } }
27
+ let(:object) { [{ a: 3 }, { a: 2 }, { a: 1 }] }
28
+
29
+ it { expect(json).to eq [3, 2, 1] }
30
+ it { expect(dependencies).to eq(__each__: { a: {} }) }
31
+ it { expect(documentation).to eq [:__value__] }
32
+ end
33
+
34
+ context 'error while navigating' do
35
+ let(:object) { { box: [{ trololol: 2 }, 42] } }
36
+ let(:template) { dsl.source { nav(:box).each.nav(:trololol) } }
37
+
38
+ it { expect { json }.to raise_error(/\__root__\.box\.1\.trololol/) }
39
+ end
40
+ end
41
+
42
+ describe '#static' do
43
+ let(:template) { dsl.source { static('1': 'cava') } }
44
+ let(:object) { nil }
45
+
46
+ it { expect(json).to eq('1' => 'cava') }
47
+ it { expect(dependencies).to eq({}) }
48
+ it { expect(documentation).to eq('1': 'cava') }
49
+
50
+ context 'invalid' do
51
+ let(:template) { dsl.source { static(test: Object.new) } }
52
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
53
+ end
54
+ end
55
+
56
+ describe '#array' do
57
+ let(:template) { dsl.source { array('coucou', 45, a: 1, b: [_]) } }
58
+ let(:object) { { b: 12 } }
59
+ it { expect(json).to eq ['coucou', 45, { 'a' => 1, 'b' => [12] }] }
60
+ it { expect(dependencies).to eq(b: {}) }
61
+ it { expect(documentation).to eq ['coucou', 45, a: 1, b: [:__value__]] }
62
+ end
63
+
64
+ describe '#call' do
65
+ let(:object) { nil }
66
+
67
+ context 'false' do
68
+ let(:template) { dsl.call(false) }
69
+
70
+ it { expect(json).to eq false }
71
+ it { expect(dependencies).to eq({}) }
72
+ it { expect(documentation).to eq false }
73
+ end
74
+
75
+ context 'block' do
76
+ let(:object) { 2 }
77
+ let(:template) { dsl.call { self * 2 } }
78
+
79
+ it { expect(json).to eq 4 }
80
+ end
81
+
82
+ context 'hash' do
83
+ let(:object) { nil }
84
+ let(:template) { dsl.call('a' => 1, b: 2) }
85
+
86
+ it { expect(json).to eq('a' => 1, 'b' => 2) }
87
+ it { expect(dependencies).to eq({}) }
88
+ end
89
+
90
+ context 'array' do
91
+ let(:object) { { b: 42 } }
92
+ let(:template) { dsl.call(['a', 2, :b]) }
93
+
94
+ it { expect(json).to eq(['a', 2, 42]) }
95
+ it { expect(dependencies).to eq(b: {}) }
96
+ end
97
+
98
+ context 'block' do
99
+ let(:object) { OpenStruct.new(lol: 'tam') }
100
+ let(:template) { dsl.call(:lol) }
101
+
102
+ it { expect(json).to eq 'tam' }
103
+ it { expect(dependencies).to eq(lol: {}) }
104
+ end
105
+
106
+ context 'template' do
107
+ let(:object) { OpenStruct.new(a: OpenStruct.new(b: 1)) }
108
+ let(:template) { dsl.source { object(coucou: nav(:a).call(nav(:b))) } }
109
+
110
+ it { expect(dependencies).to eq(a: { b: {} }) }
111
+ it { expect(json).to eq('coucou' => 1) }
112
+ it { expect(documentation).to eq(coucou: :__value__) }
113
+ end
114
+ end
115
+
116
+ describe '#switch' do
117
+ context do
118
+ let(:template) {
119
+ dsl.source {
120
+ even = nullable.nav(:even?)
121
+ odd = nullable.nav(:odd?)
122
+
123
+ each.switch(
124
+ even => nav { |x| "#{x} is even" },
125
+ odd => nav { |x| "#{x} is odd" },
126
+ default => continue
127
+ ).static('WTF')
128
+ }
129
+ }
130
+
131
+ let(:object) { [1, 2, nil, 5] }
132
+
133
+ it { expect(json).to eq ['1 is odd', '2 is even', 'WTF', '5 is odd'] }
134
+ it { expect(dependencies).to eq(__each__: { even?: {}, odd?: {} }) }
135
+ it {
136
+ expect(documentation).to eq [
137
+ 'Case 1': :__value__,
138
+ 'Case 2': :__value__,
139
+ 'Case 3': 'WTF'
140
+ ]
141
+ }
142
+ end
143
+
144
+ context 'static condition' do
145
+ let(:template) { dsl.source { switch(true => 42) } }
146
+ let(:object) { {} }
147
+ it { expect(json).to eq 42 }
148
+ end
149
+
150
+ context 'navigation before continue' do
151
+ let(:template) { dsl.source { nav(:abc).switch(false => 1, default => nav(:lol).continue).object(val: nav(:ok)) } }
152
+
153
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
154
+ end
155
+
156
+ context 'continue in sub-object' do
157
+ let(:template) { dsl.source { object(a: switch(default => object(x: continue))) } }
158
+
159
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
160
+ end
161
+
162
+ context 'unhandled default' do
163
+ let(:object) { { abc: { lol: { ok: 42 } } } }
164
+ let(:template) { dsl.source { switch(false => 1) } }
165
+
166
+ it { expect(dependencies).to eq({}) }
167
+ it { expect { json }.to raise_error Babl::RenderingError }
168
+ end
169
+
170
+ context 'continue without switch' do
171
+ let(:template) { dsl.source { continue } }
172
+
173
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
174
+ end
175
+
176
+ context 'non serializable objects are allowed internally' do
177
+ let(:template) { dsl.source { switch(test: 42) } }
178
+ let(:object) { { test: Object.new } }
179
+
180
+ it { expect(json).to eq 42 }
181
+ end
182
+
183
+ context do
184
+ let(:template) {
185
+ dsl.source {
186
+ nav(:test).switch(nav(:keke) => parent.nav(:lol))
187
+ }
188
+ }
189
+ it { expect(dependencies).to eq(test: { keke: {} }, lol: {}) }
190
+ end
191
+ end
192
+
193
+ describe '#with' do
194
+ context 'everything is fine' do
195
+ let(:template) {
196
+ dsl.source {
197
+ object(
198
+ result: with(
199
+ unscoped,
200
+ :msg,
201
+ _.parent.nav(:msg).dep(:lol)
202
+ ) { |obj, a, b| "#{a} #{b} #{obj[:msg]}" }
203
+ )
204
+ }
205
+ }
206
+
207
+ let(:object) { { result: 42, msg: 'Hello C' } }
208
+
209
+ it { expect(json).to eq('result' => 'Hello C Hello C Hello C') }
210
+ it { expect(dependencies).to eq(result: {}, msg: { lol: {} }) }
211
+ it { expect(documentation).to eq(result: :__value__) }
212
+ end
213
+
214
+ context 'when the block raise an exception' do
215
+ let(:object) { nil }
216
+ let(:template) { dsl.source { with { raise 'lol' } } }
217
+ it { expect { json }.to raise_error(/\__root__\.__block__/) }
218
+ end
219
+ end
220
+
221
+ describe '#merge' do
222
+ context do
223
+ let(:template) {
224
+ dsl.source {
225
+ merge(
226
+ object(a: static('A')),
227
+ b: _
228
+ )
229
+ }
230
+ }
231
+
232
+ let(:object) { { b: 42 } }
233
+
234
+ it { expect(json).to eq('a' => 'A', 'b' => 42) }
235
+ it { expect(dependencies).to eq(b: {}) }
236
+ it { expect(documentation).to eq('Merge 1': { a: 'A' }, 'Merge 2': { b: :__value__ }) }
237
+ end
238
+
239
+ context 'merge inside object' do
240
+ let(:template) { dsl.source { object(toto: merge(_, lol: 42)) } }
241
+ let(:object) { { toto: { cool: 'ouai' } } }
242
+
243
+ it { expect(json).to eq('toto' => { 'lol' => 42, 'cool' => 'ouai' }) }
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,133 @@
1
+ require 'babl'
2
+
3
+ describe ::Babl::Template do
4
+ let(:dsl) { ::Babl::Template.new }
5
+ let(:compiled) { template.compile }
6
+ let(:json) { Oj.load(compiled.json(object)) }
7
+ let(:dependencies) { compiled.dependencies }
8
+ let(:documentation) { compiled.documentation }
9
+
10
+ describe '#parent' do
11
+ context 'valid usage' do
12
+ let(:template) {
13
+ dsl.source {
14
+ nav(:box).dep(:box_dep).parent.dep(:root_dep)
15
+ }
16
+ }
17
+
18
+ let(:object) { { box: 42 } }
19
+
20
+ it { expect(documentation).to eq :__value__ }
21
+ it { expect(dependencies).to eq(box: { box_dep: {} }, root_dep: {}) }
22
+ it { expect(json).to eq('box' => 42) }
23
+ end
24
+
25
+ context 'error while navigating' do
26
+ let(:object) { { a: { b: { c: 56 } } } }
27
+ let(:template) { dsl.source { nav(:a).parent.nav(:a).nav(:b, :x) } }
28
+
29
+ it { expect { json }.to raise_error(/\__root__\.a\.b\.x/) }
30
+ end
31
+
32
+ context 'invalid usage' do
33
+ let(:template) { dsl.source { parent } }
34
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
35
+ end
36
+
37
+ context 'deeply nested parent chain' do
38
+ let(:template) {
39
+ dsl.source {
40
+ nav(:a, :b, :c, :d, :e).parent.parent.parent.nav(:f, :g, :h).parent.parent.parent.parent.nav(:i)
41
+ }
42
+ }
43
+ it { expect(dependencies).to eq(a: { b: { f: { g: { h: {} } }, c: { d: { e: {} } } }, i: {} }) }
44
+ end
45
+
46
+ context 'same-level key + nested parent chain' do
47
+ let(:template) {
48
+ dsl.source {
49
+ object(
50
+ a: _.nav(:b, :c).parent.parent.nav(:h),
51
+ b: _.nav(:a).parent.nav(:a)
52
+ )
53
+ }
54
+ }
55
+ it { expect(dependencies).to eq(a: { b: { c: {} }, h: {} }, b: { a: {} }) }
56
+ end
57
+ end
58
+
59
+ describe '#dep' do
60
+ let(:template) {
61
+ dsl.source {
62
+ dep(a: [:b, :c]).nav(:b).dep(x: :y).nav(:z)
63
+ }
64
+ }
65
+
66
+ let(:object) { { b: { z: 42 } } }
67
+
68
+ it { expect(documentation).to eq :__value__ }
69
+ it { expect(dependencies).to eq(a: { b: {}, c: {} }, b: { x: { y: {} }, z: {} }) }
70
+ it { expect(json).to eq(42) }
71
+ end
72
+
73
+ describe '#enter' do
74
+ context 'invalid usage' do
75
+ let(:template) { dsl.source { enter } }
76
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
77
+ end
78
+
79
+ context 'valid usage' do
80
+ let(:template) { dsl.source { object(a: enter) } }
81
+ let(:object) { { a: 42 } }
82
+
83
+ it { expect(documentation).to eq(a: :__value__) }
84
+ it { expect(dependencies).to eq(a: {}) }
85
+ it { expect(json).to eq('a' => 42) }
86
+ end
87
+ end
88
+
89
+ describe '#nav' do
90
+ let(:template) { dsl.source { nav(:a) } }
91
+
92
+ context 'hash navigation' do
93
+ let(:object) { { a: 42 } }
94
+ it { expect(json).to eq(42) }
95
+ it { expect(dependencies).to eq(a: {}) }
96
+
97
+ context 'block navigation propagate dependency chain' do
98
+ let(:template) { dsl.source { nav(:a).nav(:to_i) } }
99
+ it { expect(dependencies).to eq(a: { to_i: {} }) }
100
+ end
101
+ end
102
+
103
+ context 'navigate to non serializable' do
104
+ let(:template) { dsl.source { nav(:a) } }
105
+ let(:object) { { a: :test } }
106
+ it { expect { json }.to raise_error Babl::RenderingError }
107
+ end
108
+
109
+ context 'object navigation' do
110
+ let(:object) { Struct.new(:a).new(42) }
111
+ it { expect(json).to eq(42) }
112
+ it { expect(dependencies).to eq(a: {}) }
113
+ end
114
+
115
+ context 'block navigation' do
116
+ let(:object) { 42 }
117
+ let(:template) { dsl.source { nav { |x| x * 2 } } }
118
+
119
+ it { expect(json).to eq(84) }
120
+ it { expect(dependencies).to eq({}) }
121
+
122
+ context 'block navigation breaks dependency chain' do
123
+ let(:template) { dsl.source { nav { |x| x * 2 }.nav(:to_i) } }
124
+ it { expect(dependencies).to eq({}) }
125
+ end
126
+ end
127
+
128
+ context '#nav should stop key propagation for #enter' do
129
+ let(:template) { dsl.source { object(a: nav._) } }
130
+ it { expect { compiled }.to raise_error Babl::InvalidTemplateError }
131
+ end
132
+ end
133
+ end