babl-json 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +228 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +5 -0
- data/LICENSE +7 -0
- data/README.md +87 -0
- data/babl.gemspec +23 -0
- data/lib/babl.rb +41 -0
- data/lib/babl/builder/chain_builder.rb +85 -0
- data/lib/babl/builder/template_base.rb +37 -0
- data/lib/babl/operators/array.rb +42 -0
- data/lib/babl/operators/call.rb +25 -0
- data/lib/babl/operators/dep.rb +48 -0
- data/lib/babl/operators/each.rb +45 -0
- data/lib/babl/operators/enter.rb +22 -0
- data/lib/babl/operators/merge.rb +49 -0
- data/lib/babl/operators/nav.rb +55 -0
- data/lib/babl/operators/nullable.rb +16 -0
- data/lib/babl/operators/object.rb +55 -0
- data/lib/babl/operators/parent.rb +90 -0
- data/lib/babl/operators/partial.rb +46 -0
- data/lib/babl/operators/pin.rb +78 -0
- data/lib/babl/operators/source.rb +12 -0
- data/lib/babl/operators/static.rb +40 -0
- data/lib/babl/operators/switch.rb +71 -0
- data/lib/babl/operators/with.rb +51 -0
- data/lib/babl/railtie.rb +29 -0
- data/lib/babl/rendering/compiled_template.rb +28 -0
- data/lib/babl/rendering/context.rb +60 -0
- data/lib/babl/rendering/internal_value_node.rb +30 -0
- data/lib/babl/rendering/noop_preloader.rb +10 -0
- data/lib/babl/rendering/terminal_value_node.rb +54 -0
- data/lib/babl/template.rb +48 -0
- data/lib/babl/utils/hash.rb +11 -0
- data/lib/babl/version.rb +3 -0
- data/spec/construction_spec.rb +246 -0
- data/spec/navigation_spec.rb +133 -0
- data/spec/partial_spec.rb +53 -0
- data/spec/pinning_spec.rb +137 -0
- metadata +145 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Partial
|
4
|
+
module DSL
|
5
|
+
# Load a partial template given its name
|
6
|
+
# A 'lookup_context' must be defined
|
7
|
+
def partial(partial_name)
|
8
|
+
raise InvalidTemplateError, "Cannot use partial without lookup context" unless lookup_context
|
9
|
+
|
10
|
+
path, source, partial_lookup_context = lookup_context.find(partial_name)
|
11
|
+
raise InvalidTemplateError, "Cannot find partial '#{partial_name}'" unless path
|
12
|
+
|
13
|
+
with_lookup_context(partial_lookup_context)
|
14
|
+
.source(source, path, 0)
|
15
|
+
.with_lookup_context(lookup_context)
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_lookup_context(lookup_context)
|
19
|
+
self.class.new(builder.dup.tap { |inst| inst.instance_variable_set(:@lookup_context, lookup_context) })
|
20
|
+
end
|
21
|
+
|
22
|
+
def lookup_context
|
23
|
+
builder.instance_variable_get(:@lookup_context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class AbsoluteLookupContext
|
28
|
+
attr_reader :search_path
|
29
|
+
|
30
|
+
def initialize(search_path)
|
31
|
+
@search_path = search_path
|
32
|
+
raise 'Invalid search path' unless search_path
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(partial_name)
|
36
|
+
query = File.join(search_path, "{#{partial_name}}{.babl,}")
|
37
|
+
path = Dir[query].first
|
38
|
+
return unless path
|
39
|
+
|
40
|
+
source = File.read(path)
|
41
|
+
[path, source, self]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Pin
|
4
|
+
module DSL
|
5
|
+
# Create a pin
|
6
|
+
def pin(navigation = nil, &block)
|
7
|
+
return pin { |p| block[p.call(navigation)] } if navigation
|
8
|
+
ref = ::Object.new
|
9
|
+
referenced_scope = unscoped.construct_node(key: nil, continue: nil) { |node| GotoPinNode.new(node, ref) }
|
10
|
+
construct_node(continue: nil) { |node| CreatePinNode.new(node, ref) }.call(block[referenced_scope])
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Override TemplateBase#precompile to ensure that all pin dependencies are satisfied.
|
16
|
+
def precompile
|
17
|
+
super.tap do |node|
|
18
|
+
raise Babl::InvalidTemplateError, 'Unresolved pin' unless node.pinned_dependencies.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class CreatePinNode
|
24
|
+
def initialize(node, ref)
|
25
|
+
@node = node
|
26
|
+
@ref = ref
|
27
|
+
end
|
28
|
+
|
29
|
+
def render(ctx)
|
30
|
+
node.render(ctx.create_pin(ref))
|
31
|
+
end
|
32
|
+
|
33
|
+
def documentation
|
34
|
+
node.documentation
|
35
|
+
end
|
36
|
+
|
37
|
+
def dependencies
|
38
|
+
Babl::Utils::Hash.deep_merge(node.dependencies, node.pinned_dependencies[ref] || {})
|
39
|
+
end
|
40
|
+
|
41
|
+
def pinned_dependencies
|
42
|
+
node.pinned_dependencies.reject { |k, _v| k == ref }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :node, :ref
|
48
|
+
end
|
49
|
+
|
50
|
+
class GotoPinNode
|
51
|
+
def initialize(node, ref)
|
52
|
+
@node = node
|
53
|
+
@ref = ref
|
54
|
+
end
|
55
|
+
|
56
|
+
def dependencies
|
57
|
+
{}
|
58
|
+
end
|
59
|
+
|
60
|
+
def pinned_dependencies
|
61
|
+
Babl::Utils::Hash.deep_merge(node.pinned_dependencies, ref => node.dependencies)
|
62
|
+
end
|
63
|
+
|
64
|
+
def documentation
|
65
|
+
node.documentation
|
66
|
+
end
|
67
|
+
|
68
|
+
def render(ctx)
|
69
|
+
node.render(ctx.goto_pin(ref))
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :node, :ref
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Static
|
4
|
+
module DSL
|
5
|
+
# Create a static JSON value
|
6
|
+
def static(value)
|
7
|
+
construct_terminal { StaticNode.new(value) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class StaticNode
|
12
|
+
def initialize(value)
|
13
|
+
@serialized_value = Rendering::TerminalValueNode.instance.render_object(value)
|
14
|
+
rescue Babl::RenderingError => exception
|
15
|
+
raise Babl::InvalidTemplateError, exception.message
|
16
|
+
end
|
17
|
+
|
18
|
+
def documentation
|
19
|
+
serialized_value
|
20
|
+
end
|
21
|
+
|
22
|
+
def render(_ctx)
|
23
|
+
serialized_value
|
24
|
+
end
|
25
|
+
|
26
|
+
def dependencies
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
def pinned_dependencies
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :serialized_value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Switch
|
4
|
+
module DSL
|
5
|
+
# To be used as a switch(...) condition. It is strictly equivalent to write 'true' instead,
|
6
|
+
# but convey more meaning.
|
7
|
+
def default
|
8
|
+
unscoped.static(true)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return a special placeholder that can be used as a switch(...) value. It tells BABL to continue
|
12
|
+
# the evaluation of the original chain after switch().
|
13
|
+
def continue
|
14
|
+
construct_terminal { |context|
|
15
|
+
node = context[:continue]
|
16
|
+
raise Babl::InvalidTemplateError, 'continue() cannot be used outside switch()' unless node
|
17
|
+
node
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Conditional switching
|
22
|
+
def switch(conds = {})
|
23
|
+
construct_node(continue: nil) { |node, context|
|
24
|
+
nodes = conds.map { |cond, value|
|
25
|
+
cond_node = unscoped.call(cond).builder
|
26
|
+
.precompile(Rendering::InternalValueNode.instance, context.merge(continue: nil))
|
27
|
+
|
28
|
+
value_node = unscoped.call(value).builder
|
29
|
+
.precompile(Rendering::TerminalValueNode.instance, context.merge(continue: node))
|
30
|
+
|
31
|
+
[cond_node, value_node]
|
32
|
+
}.to_h
|
33
|
+
|
34
|
+
SwitchNode.new(nodes)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class SwitchNode
|
40
|
+
def initialize(nodes)
|
41
|
+
@nodes = nodes
|
42
|
+
end
|
43
|
+
|
44
|
+
def dependencies
|
45
|
+
(nodes.values + nodes.keys).map(&:dependencies)
|
46
|
+
.reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def pinned_dependencies
|
50
|
+
(nodes.values + nodes.keys).map(&:pinned_dependencies)
|
51
|
+
.reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def documentation
|
55
|
+
(nodes.values).map(&:documentation).each_with_index.map { |doc, idx|
|
56
|
+
[:"Case #{idx + 1}", doc]
|
57
|
+
}.to_h
|
58
|
+
end
|
59
|
+
|
60
|
+
def render(ctx)
|
61
|
+
nodes.each { |cond, value| return value.render(ctx) if cond.render(ctx) }
|
62
|
+
raise Babl::RenderingError, 'A least one switch() condition must be taken'
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
attr_reader :nodes, :default_node
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module With
|
4
|
+
module DSL
|
5
|
+
# Produce a value by calling the block, passing it the output value of the templates passed as argument.
|
6
|
+
def with(*templates, &block)
|
7
|
+
construct_node(key: nil, continue: nil) do |node, context|
|
8
|
+
WithNode.new(node, templates.map do |t|
|
9
|
+
unscoped.call(t).builder.precompile(
|
10
|
+
Rendering::InternalValueNode.instance,
|
11
|
+
context.merge(continue: nil)
|
12
|
+
)
|
13
|
+
end, block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class WithNode
|
19
|
+
def initialize(node, nodes, block)
|
20
|
+
@node = node
|
21
|
+
@nodes = nodes
|
22
|
+
@block = block
|
23
|
+
end
|
24
|
+
|
25
|
+
def documentation
|
26
|
+
node.documentation
|
27
|
+
end
|
28
|
+
|
29
|
+
def dependencies
|
30
|
+
# Dependencies of 'node' are explicitely ignored
|
31
|
+
nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def pinned_dependencies
|
35
|
+
(nodes + [node]).map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def render(ctx)
|
39
|
+
values = nodes.map { |n| n.render(ctx) }
|
40
|
+
node.render(ctx.move_forward_block(:__block__) do
|
41
|
+
block.arity.zero? ? ctx.object.instance_exec(&block) : block.call(*values)
|
42
|
+
end)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :node, :nodes, :block
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/babl/railtie.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Babl
|
2
|
+
module ActionView
|
3
|
+
module Template
|
4
|
+
class Handler
|
5
|
+
class_attribute :default_format
|
6
|
+
self.default_format = Mime[:json]
|
7
|
+
|
8
|
+
def self.call(template)
|
9
|
+
# This implementation is not efficient: it will recompile the BABL template
|
10
|
+
# for each request. I still don't get why Rails template handlers MUST
|
11
|
+
# return Ruby code ?! Sucks too much. Ideally, we would like to keep the compiled
|
12
|
+
# template somewhere. However, I've not yet measured how much like is wasted.
|
13
|
+
# Maybe it is negligible ?
|
14
|
+
<<~RUBY
|
15
|
+
::Babl.compile { #{template.source} }.json(local_assigns)
|
16
|
+
RUBY
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Railtie < Rails::Railtie
|
23
|
+
initializer "babl.initialize" do
|
24
|
+
ActiveSupport.on_load(:action_view) do
|
25
|
+
::ActionView::Template.register_template_handler(:babl, ::Babl::ActionView::Template::Handler)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'oj'
|
2
|
+
|
3
|
+
module Babl
|
4
|
+
module Rendering
|
5
|
+
class CompiledTemplate
|
6
|
+
attr_reader :node, :dependencies, :documentation, :preloader, :pretty
|
7
|
+
|
8
|
+
def initialize(node, preloader: NoopPreloader, pretty: true)
|
9
|
+
@node = node
|
10
|
+
@dependencies = node.dependencies
|
11
|
+
@documentation = node.documentation
|
12
|
+
@preloader = preloader
|
13
|
+
@pretty = pretty
|
14
|
+
end
|
15
|
+
|
16
|
+
def json(root)
|
17
|
+
data = render(root)
|
18
|
+
Oj.dump(data, indent: pretty ? 4 : 0, mode: :strict)
|
19
|
+
end
|
20
|
+
|
21
|
+
def render(root)
|
22
|
+
preloaded_data = preloader.preload([root], dependencies).first
|
23
|
+
ctx = Babl::Rendering::Context.new(preloaded_data)
|
24
|
+
node.render(ctx)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Babl
|
2
|
+
module Rendering
|
3
|
+
# The rendering context stores the 'current' object.
|
4
|
+
# Additionally, the context also:
|
5
|
+
# - Keep a reference to the parent context, in order to implement the parent operation (ParentNode)
|
6
|
+
# - Keep a reference to all pinned contexts, in order to goto a pinned context at any time (GotoPinNode)
|
7
|
+
#
|
8
|
+
# It is important to keep this object as small as possible, since an instance is created each time
|
9
|
+
# we navigate into a property.
|
10
|
+
class Context
|
11
|
+
attr_reader :key, :object, :parent, :pins
|
12
|
+
|
13
|
+
def initialize(object, key = nil, parent = nil, pins = nil)
|
14
|
+
@key = key
|
15
|
+
@object = object
|
16
|
+
@parent = parent
|
17
|
+
@pins = pins
|
18
|
+
end
|
19
|
+
|
20
|
+
# Standard navigation (enter into property)
|
21
|
+
def move_forward(new_object, key)
|
22
|
+
Context.new(new_object, key, self, pins)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Go back to parent
|
26
|
+
def move_backward
|
27
|
+
raise Babl::RenderingError, 'There is no parent element' unless parent
|
28
|
+
Context.new(parent.object, parent.key, parent.parent, pins)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Go to a pinned context
|
32
|
+
def goto_pin(ref)
|
33
|
+
pin = pins&.[](ref)
|
34
|
+
raise Babl::RenderingError, 'Pin reference cannot be used here' unless pin
|
35
|
+
Context.new(pin.object, pin.key, pin.parent, (pin.pins || {}).merge(pins))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Associate a pin to current context
|
39
|
+
def create_pin(ref)
|
40
|
+
Context.new(object, key, parent, (pins || {}).merge(ref => self))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Wrapper around #move_forward navigating into the return value of
|
44
|
+
# the block. However, if an error occurs, it is wrapped in a
|
45
|
+
# Babl::RenderingError and the navigation stack trace is added
|
46
|
+
# to the error message.
|
47
|
+
def move_forward_block(key)
|
48
|
+
move_forward(yield, key)
|
49
|
+
rescue StandardError => e
|
50
|
+
stack_trace = ([:__root__] + stack + [key]).join('.')
|
51
|
+
raise Babl::RenderingError, "#{e.message}\nBABL @ #{stack_trace}", e.backtrace
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return an array containing the navigation history
|
55
|
+
def stack
|
56
|
+
(parent ? parent.stack : []) + [key].compact
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Babl
|
4
|
+
module Rendering
|
5
|
+
# This Node plays a role similar to TerminalValueNode, but it does not perform any
|
6
|
+
# type checking on the produced object, which is allowed to be any Ruby object,
|
7
|
+
# including non-serializable objects.
|
8
|
+
#
|
9
|
+
# It is used when the output is not rendered (conditions in #switch, values passed to block in #with, ...)
|
10
|
+
class InternalValueNode
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
def documentation
|
14
|
+
:__value__
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependencies
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
|
21
|
+
def pinned_dependencies
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(ctx)
|
26
|
+
ctx.object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|