babl-json 0.1.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.
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