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