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