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,37 @@
|
|
1
|
+
module Babl
|
2
|
+
module Builder
|
3
|
+
# TemplateBase is a thin wrapper around Builder.
|
4
|
+
#
|
5
|
+
# Since the BABL code is run via #instance_eval within an instance of this class, we want to
|
6
|
+
# define as few methods as possible here.
|
7
|
+
class TemplateBase
|
8
|
+
def initialize(builder = ChainBuilder.new(&:itself))
|
9
|
+
@builder = builder
|
10
|
+
end
|
11
|
+
|
12
|
+
def compile(**options)
|
13
|
+
Rendering::CompiledTemplate.new(precompile, **options)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def unscoped
|
19
|
+
self.class.new builder.rescope(&:itself)
|
20
|
+
end
|
21
|
+
|
22
|
+
def precompile
|
23
|
+
builder.precompile(Babl::Rendering::TerminalValueNode.instance)
|
24
|
+
end
|
25
|
+
|
26
|
+
def construct_node(**new_context, &block)
|
27
|
+
self.class.new builder.construct_node(**new_context, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def construct_terminal(&block)
|
31
|
+
self.class.new builder.construct_terminal(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :builder
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Array
|
4
|
+
module DSL
|
5
|
+
# Produce an fixed-size array, using the provided templates to populate its elements.
|
6
|
+
def array(*templates)
|
7
|
+
construct_terminal { |ctx|
|
8
|
+
FixedArrayNode.new(templates.map { |t|
|
9
|
+
unscoped.call(t).builder.precompile(Rendering::TerminalValueNode.instance, ctx.merge(continue: nil))
|
10
|
+
})
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class FixedArrayNode
|
16
|
+
def initialize(nodes)
|
17
|
+
@nodes = nodes
|
18
|
+
end
|
19
|
+
|
20
|
+
def documentation
|
21
|
+
nodes.map(&:documentation)
|
22
|
+
end
|
23
|
+
|
24
|
+
def dependencies
|
25
|
+
nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def pinned_dependencies
|
29
|
+
nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def render(ctx)
|
33
|
+
nodes.map { |node| node.render(ctx) }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :nodes
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Call
|
4
|
+
module DSL
|
5
|
+
# Interpret whatever is passed to this method as BABL template. It is idempotent.
|
6
|
+
def call(*args, &block)
|
7
|
+
return with(*args, &block) unless block.nil?
|
8
|
+
raise ::Babl::InvalidTemplateError, 'call() expects exactly 1 argument (unless block)' unless args.size == 1
|
9
|
+
|
10
|
+
arg = args.first
|
11
|
+
|
12
|
+
case arg
|
13
|
+
when self.class then self.class.new(builder.wrap { |bound| arg.builder.bind(bound) })
|
14
|
+
when ::Symbol then nav(arg)
|
15
|
+
when ::Proc then call(&arg)
|
16
|
+
when ::Hash then object(**arg.map { |k, v| [k.to_s.to_sym, v] }.to_h)
|
17
|
+
when ::Array then array(*arg)
|
18
|
+
when ::String, ::Numeric, ::NilClass, ::TrueClass, ::FalseClass then unscoped.static(arg)
|
19
|
+
else raise ::Babl::InvalidTemplateError, "call() received invalid argument: #{arg}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Dep
|
4
|
+
module DSL
|
5
|
+
# Declare dependencies as if they were generated by nav()
|
6
|
+
# but without navigating.
|
7
|
+
def dep(*path)
|
8
|
+
construct_node(continue: nil) { |node| DepNode.new(node, path) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class DepNode
|
13
|
+
def initialize(node, path)
|
14
|
+
@node = node
|
15
|
+
@path = canonicalize(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(ctx)
|
19
|
+
node.render(ctx)
|
20
|
+
end
|
21
|
+
|
22
|
+
def documentation
|
23
|
+
node.documentation
|
24
|
+
end
|
25
|
+
|
26
|
+
def pinned_dependencies
|
27
|
+
node.pinned_dependencies
|
28
|
+
end
|
29
|
+
|
30
|
+
def dependencies
|
31
|
+
Babl::Utils::Hash.deep_merge(node.dependencies, path)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def canonicalize(path)
|
37
|
+
case path
|
38
|
+
when ::Array then path.reduce({}) { |a, p| a.merge(canonicalize(p)) }
|
39
|
+
when ::Hash then path.map { |k, v| [k.to_sym, canonicalize(v)] }.to_h
|
40
|
+
else { path.to_sym => {} }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :node, :path
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Each
|
4
|
+
module DSL
|
5
|
+
# Construct a JSON array by iterating over the current collection,
|
6
|
+
# using the chained template for rendering each element.
|
7
|
+
def each
|
8
|
+
construct_node(key: nil, continue: nil) { |node| EachNode.new(node) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class EachNode
|
13
|
+
def initialize(node)
|
14
|
+
@node = node
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependencies
|
18
|
+
{ __each__: node.dependencies }
|
19
|
+
end
|
20
|
+
|
21
|
+
def documentation
|
22
|
+
[node.documentation]
|
23
|
+
end
|
24
|
+
|
25
|
+
def pinned_dependencies
|
26
|
+
node.pinned_dependencies
|
27
|
+
end
|
28
|
+
|
29
|
+
def render(ctx)
|
30
|
+
collection = ctx.object
|
31
|
+
|
32
|
+
unless collection.is_a?(Enumerable)
|
33
|
+
raise RenderingError, "Object is not enumerable : #{collection.inspect}"
|
34
|
+
end
|
35
|
+
|
36
|
+
collection.each_with_index.map { |value, idx| node.render(ctx.move_forward(value, idx)) }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Enter
|
4
|
+
module DSL
|
5
|
+
# Navigate to a named property of current element. The name
|
6
|
+
# is inferred based on the object()
|
7
|
+
def enter
|
8
|
+
construct_node(key: nil, continue: nil) { |node, context|
|
9
|
+
key = context[:key]
|
10
|
+
raise InvalidTemplateError, "No key to enter into" unless key
|
11
|
+
Nav::NavNode.new(key, node)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Simple convenience alias
|
16
|
+
def _
|
17
|
+
enter
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Merge
|
4
|
+
module DSL
|
5
|
+
# Merge multiple JSON objects (non-deep)
|
6
|
+
def merge(*templates)
|
7
|
+
construct_terminal { |context|
|
8
|
+
MergeNode.new(
|
9
|
+
templates.map { |t|
|
10
|
+
unscoped.call(t).builder.precompile(
|
11
|
+
Rendering::TerminalValueNode.instance,
|
12
|
+
context.merge(continue: nil)
|
13
|
+
)
|
14
|
+
}
|
15
|
+
)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class MergeNode
|
21
|
+
def initialize(nodes)
|
22
|
+
@nodes = nodes
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies
|
26
|
+
nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def pinned_dependencies
|
30
|
+
nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def documentation
|
34
|
+
nodes.map(&:documentation).each_with_index.map { |doc, idx|
|
35
|
+
[:"Merge #{idx + 1}", doc]
|
36
|
+
}.to_h
|
37
|
+
end
|
38
|
+
|
39
|
+
def render(ctx)
|
40
|
+
nodes.map { |node| node.render(ctx) }.compact.reduce({}, :merge)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :nodes
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Nav
|
4
|
+
module DSL
|
5
|
+
# Navigate to a named property of the current element
|
6
|
+
# Multiple properties can be chained
|
7
|
+
#
|
8
|
+
# A block can also be passed, but in that case, dependency tracking
|
9
|
+
# is disabled for the rest of the chain.
|
10
|
+
def nav(*path, &block)
|
11
|
+
if path.empty?
|
12
|
+
return (block ? with(unscoped, &block) : construct_node(key: nil, continue: nil) { |node| node })
|
13
|
+
end
|
14
|
+
|
15
|
+
construct_node(key: nil, continue: nil) { |node| NavNode.new(path.first, node) }.nav(*path[1..-1], &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NavNode
|
20
|
+
def initialize(through, node)
|
21
|
+
@through = through
|
22
|
+
@node = node
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies
|
26
|
+
{ through => node.dependencies }
|
27
|
+
end
|
28
|
+
|
29
|
+
def documentation
|
30
|
+
node.documentation
|
31
|
+
end
|
32
|
+
|
33
|
+
def pinned_dependencies
|
34
|
+
node.pinned_dependencies
|
35
|
+
end
|
36
|
+
|
37
|
+
def render(ctx)
|
38
|
+
node.render(ctx.move_forward_block(through) { navigate(ctx.object) })
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :through, :node
|
44
|
+
|
45
|
+
def navigate(object)
|
46
|
+
if object.is_a?(Hash)
|
47
|
+
object.fetch(through)
|
48
|
+
else
|
49
|
+
object.send(through)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Nullable
|
4
|
+
module DSL
|
5
|
+
# Nullify the current construction if
|
6
|
+
# the current element is Nil.
|
7
|
+
def nullable
|
8
|
+
switch(
|
9
|
+
unscoped.nav(&:nil?) => nil,
|
10
|
+
unscoped.default => unscoped.continue
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Object
|
4
|
+
module DSL
|
5
|
+
# Create a JSON object node with static structure
|
6
|
+
def object(*attrs, **nested)
|
7
|
+
(attrs.map(&:to_sym) + nested.keys).group_by(&:itself).values.each do |keys|
|
8
|
+
raise ::Babl::InvalidTemplateError, "Duplicate key in object(): #{keys.first}" if keys.size > 1
|
9
|
+
end
|
10
|
+
|
11
|
+
construct_terminal { |ctx|
|
12
|
+
nodes = attrs
|
13
|
+
.map { |name| [name.to_sym, unscoped.enter] }.to_h
|
14
|
+
.merge(nested)
|
15
|
+
.map { |k, v|
|
16
|
+
[k, unscoped.call(v).builder.precompile(
|
17
|
+
Rendering::TerminalValueNode.instance,
|
18
|
+
ctx.merge(key: k, continue: nil)
|
19
|
+
)]
|
20
|
+
}
|
21
|
+
.to_h
|
22
|
+
|
23
|
+
ObjectNode.new(nodes)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class ObjectNode
|
29
|
+
def initialize(nodes)
|
30
|
+
@nodes = nodes
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependencies
|
34
|
+
nodes.values.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def pinned_dependencies
|
38
|
+
nodes.values.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def documentation
|
42
|
+
nodes.map { |k, v| [k, v.documentation] }.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
def render(ctx)
|
46
|
+
nodes.map { |k, v| [k, v.render(ctx)] }.to_h
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_reader :nodes
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Babl
|
2
|
+
module Operators
|
3
|
+
module Parent
|
4
|
+
PARENT = ::Object.new
|
5
|
+
|
6
|
+
module DSL
|
7
|
+
# Navigate to the parent of the current object.
|
8
|
+
def parent
|
9
|
+
construct_node(key: nil, continue: nil) { |node| ParentNode.new(node) }
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
# Override TemplateBase#precompile to add parent dependencies verification
|
15
|
+
def precompile
|
16
|
+
ParentResolverNode.new(super)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ParentResolverNode
|
21
|
+
def initialize(node)
|
22
|
+
@node = node
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies
|
26
|
+
backpropagate_dependencies(node.dependencies)
|
27
|
+
end
|
28
|
+
|
29
|
+
def documentation
|
30
|
+
node.documentation
|
31
|
+
end
|
32
|
+
|
33
|
+
def pinned_dependencies
|
34
|
+
node.pinned_dependencies
|
35
|
+
end
|
36
|
+
|
37
|
+
def render(ctx)
|
38
|
+
node.render(ctx)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :node
|
44
|
+
|
45
|
+
def backpropagate_dependencies(deps)
|
46
|
+
raise InvalidTemplateError, 'Out of context parent dependency' if deps.key? PARENT
|
47
|
+
new_deps = backpropagate_dependencies_one_level(deps)
|
48
|
+
deps == new_deps ? new_deps : backpropagate_dependencies(new_deps)
|
49
|
+
end
|
50
|
+
|
51
|
+
def backpropagate_dependencies_one_level(deps)
|
52
|
+
deps.reduce({}) do |out, (k, v)|
|
53
|
+
next out if k == PARENT
|
54
|
+
|
55
|
+
Babl::Utils::Hash.deep_merge(
|
56
|
+
Babl::Utils::Hash.deep_merge(out, k => backpropagate_dependencies_one_level(v)),
|
57
|
+
v[PARENT] || {}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ParentNode
|
64
|
+
def initialize(node)
|
65
|
+
@node = node
|
66
|
+
end
|
67
|
+
|
68
|
+
def documentation
|
69
|
+
node.documentation
|
70
|
+
end
|
71
|
+
|
72
|
+
def pinned_dependencies
|
73
|
+
node.pinned_dependencies
|
74
|
+
end
|
75
|
+
|
76
|
+
def dependencies
|
77
|
+
{ PARENT => node.dependencies }
|
78
|
+
end
|
79
|
+
|
80
|
+
def render(ctx)
|
81
|
+
node.render(ctx.move_backward)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :node
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|