babl-json 0.1.4 → 0.2.0
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 +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +6 -0
- data/README.md +1 -3
- data/babl.gemspec +3 -1
- data/lib/babl.rb +4 -6
- data/lib/babl/builder/chain_builder.rb +6 -2
- data/lib/babl/builder/template_base.rb +16 -3
- data/lib/babl/errors.rb +7 -0
- data/lib/babl/nodes/create_pin.rb +24 -0
- data/lib/babl/nodes/dep.rb +38 -0
- data/lib/babl/nodes/each.rb +29 -0
- data/lib/babl/nodes/fixed_array.rb +25 -0
- data/lib/babl/nodes/goto_pin.rb +24 -0
- data/lib/babl/{rendering/internal_value_node.rb → nodes/internal_value.rb} +6 -5
- data/lib/babl/nodes/merge.rb +102 -0
- data/lib/babl/nodes/nav.rb +33 -0
- data/lib/babl/nodes/object.rb +26 -0
- data/lib/babl/nodes/parent.rb +64 -0
- data/lib/babl/nodes/static.rb +34 -0
- data/lib/babl/nodes/switch.rb +29 -0
- data/lib/babl/nodes/terminal_value.rb +76 -0
- data/lib/babl/nodes/with.rb +28 -0
- data/lib/babl/operators/array.rb +5 -28
- data/lib/babl/operators/call.rb +4 -2
- data/lib/babl/operators/continue.rb +19 -0
- data/lib/babl/operators/default.rb +13 -0
- data/lib/babl/operators/dep.rb +3 -36
- data/lib/babl/operators/each.rb +3 -33
- data/lib/babl/operators/enter.rb +4 -2
- data/lib/babl/operators/extends.rb +4 -1
- data/lib/babl/operators/merge.rb +7 -30
- data/lib/babl/operators/nav.rb +4 -36
- data/lib/babl/operators/object.rb +7 -29
- data/lib/babl/operators/parent.rb +4 -73
- data/lib/babl/operators/partial.rb +4 -2
- data/lib/babl/operators/pin.rb +14 -58
- data/lib/babl/operators/static.rb +11 -30
- data/lib/babl/operators/switch.rb +8 -51
- data/lib/babl/operators/with.rb +5 -34
- data/lib/babl/railtie.rb +2 -2
- data/lib/babl/rendering/compiled_template.rb +5 -13
- data/lib/babl/rendering/context.rb +13 -7
- data/lib/babl/schema/any_of.rb +137 -0
- data/lib/babl/schema/anything.rb +13 -0
- data/lib/babl/schema/dyn_array.rb +11 -0
- data/lib/babl/schema/fixed_array.rb +13 -0
- data/lib/babl/schema/object.rb +35 -0
- data/lib/babl/schema/static.rb +14 -0
- data/lib/babl/schema/typed.rb +0 -0
- data/lib/babl/template.rb +4 -9
- data/lib/babl/utils/ref.rb +6 -0
- data/lib/babl/version.rb +1 -1
- data/spec/operators/array_spec.rb +31 -7
- data/spec/operators/call_spec.rb +16 -14
- data/spec/operators/continue_spec.rb +25 -0
- data/spec/operators/default_spec.rb +15 -0
- data/spec/operators/dep_spec.rb +4 -8
- data/spec/operators/each_spec.rb +24 -5
- data/spec/operators/enter_spec.rb +9 -7
- data/spec/operators/extends_spec.rb +19 -5
- data/spec/operators/merge_spec.rb +105 -12
- data/spec/operators/nav_spec.rb +22 -10
- data/spec/operators/null_spec.rb +5 -4
- data/spec/operators/nullable_spec.rb +13 -13
- data/spec/operators/object_spec.rb +17 -6
- data/spec/operators/parent_spec.rb +18 -22
- data/spec/operators/partial_spec.rb +8 -6
- data/spec/operators/pin_spec.rb +100 -61
- data/spec/operators/source_spec.rb +10 -6
- data/spec/operators/static_spec.rb +17 -9
- data/spec/operators/switch_spec.rb +85 -45
- data/spec/operators/with_spec.rb +13 -15
- data/spec/spec_helper.rb +2 -31
- data/spec/spec_helper/operator_testing.rb +46 -0
- data/spec/spec_helper/schema_utils.rb +33 -0
- metadata +63 -4
- data/lib/babl/rendering/terminal_value_node.rb +0 -54
@@ -1,71 +1,28 @@
|
|
1
|
+
require 'babl/nodes/switch'
|
2
|
+
require 'babl/nodes/internal_value'
|
3
|
+
require 'babl/nodes/terminal_value'
|
4
|
+
|
1
5
|
module Babl
|
2
6
|
module Operators
|
3
7
|
module Switch
|
4
8
|
module DSL
|
5
|
-
#
|
6
|
-
# but convey more meaning.
|
7
|
-
def default
|
8
|
-
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
|
9
|
+
# Conditional switching between different templates
|
22
10
|
def switch(conds = {})
|
23
11
|
construct_node(continue: nil) { |node, context|
|
24
12
|
nodes = conds.map { |cond, value|
|
25
13
|
cond_node = unscoped.call(cond).builder
|
26
|
-
.precompile(
|
14
|
+
.precompile(Nodes::InternalValue.instance, context.merge(continue: nil))
|
27
15
|
|
28
16
|
value_node = unscoped.call(value).builder
|
29
|
-
.precompile(
|
17
|
+
.precompile(Nodes::TerminalValue.instance, context.merge(continue: node))
|
30
18
|
|
31
19
|
[cond_node, value_node]
|
32
20
|
}.to_h
|
33
21
|
|
34
|
-
|
22
|
+
Nodes::Switch.new(nodes)
|
35
23
|
}
|
36
24
|
end
|
37
25
|
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
26
|
end
|
70
27
|
end
|
71
28
|
end
|
data/lib/babl/operators/with.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'babl/nodes/with'
|
2
|
+
require 'babl/nodes/internal_value'
|
3
|
+
|
1
4
|
module Babl
|
2
5
|
module Operators
|
3
6
|
module With
|
@@ -5,47 +8,15 @@ module Babl
|
|
5
8
|
# Produce a value by calling the block, passing it the output value of the templates passed as argument.
|
6
9
|
def with(*templates, &block)
|
7
10
|
construct_node(key: nil, continue: nil) do |node, context|
|
8
|
-
|
11
|
+
Nodes::With.new(node, templates.map do |t|
|
9
12
|
unscoped.call(t).builder.precompile(
|
10
|
-
|
13
|
+
Nodes::InternalValue.instance,
|
11
14
|
context.merge(continue: nil)
|
12
15
|
)
|
13
16
|
end, block)
|
14
17
|
end
|
15
18
|
end
|
16
19
|
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
20
|
end
|
50
21
|
end
|
51
22
|
end
|
data/lib/babl/railtie.rb
CHANGED
@@ -12,7 +12,7 @@ module Babl
|
|
12
12
|
# template somewhere. However, I've not yet measured how much like is wasted.
|
13
13
|
# Maybe it is negligible ?
|
14
14
|
<<~RUBY
|
15
|
-
|
15
|
+
Babl.compile { #{template.source} }.json(local_assigns)
|
16
16
|
RUBY
|
17
17
|
end
|
18
18
|
end
|
@@ -22,7 +22,7 @@ module Babl
|
|
22
22
|
class Railtie < Rails::Railtie
|
23
23
|
initializer "babl.initialize" do
|
24
24
|
ActiveSupport.on_load(:action_view) do
|
25
|
-
::ActionView::Template.register_template_handler(:babl,
|
25
|
+
::ActionView::Template.register_template_handler(:babl, Babl::ActionView::Template::Handler)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -1,26 +1,18 @@
|
|
1
1
|
require 'oj'
|
2
|
+
require 'babl/rendering/context'
|
3
|
+
require 'values'
|
2
4
|
|
3
5
|
module Babl
|
4
6
|
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
|
-
|
7
|
+
class CompiledTemplate < Value.new(:node, :dependencies, :preloader, :pretty, :json_schema)
|
16
8
|
def json(root)
|
17
9
|
data = render(root)
|
18
|
-
Oj.dump(data, indent: pretty ? 4 : 0, mode: :strict)
|
10
|
+
::Oj.dump(data, indent: pretty ? 4 : 0, mode: :strict)
|
19
11
|
end
|
20
12
|
|
21
13
|
def render(root)
|
22
14
|
preloaded_data = preloader.preload([root], dependencies).first
|
23
|
-
ctx =
|
15
|
+
ctx = Context.new(preloaded_data)
|
24
16
|
node.render(ctx)
|
25
17
|
end
|
26
18
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'babl/errors'
|
2
|
+
|
1
3
|
module Babl
|
2
4
|
module Rendering
|
3
5
|
# The rendering context stores the 'current' object.
|
4
6
|
# Additionally, the context also:
|
5
|
-
# - Keep a reference to the parent context, in order to implement the parent operation (
|
6
|
-
# - Keep a reference to all pinned contexts, in order to goto a pinned context at any time (
|
7
|
+
# - Keep a reference to the parent context, in order to implement the parent operation (Parent)
|
8
|
+
# - Keep a reference to all pinned contexts, in order to goto a pinned context at any time (GotoPin)
|
7
9
|
#
|
8
10
|
# It is important to keep this object as small as possible, since an instance is created each time
|
9
11
|
# we navigate into a property.
|
@@ -24,14 +26,14 @@ module Babl
|
|
24
26
|
|
25
27
|
# Go back to parent
|
26
28
|
def move_backward
|
27
|
-
raise
|
29
|
+
raise Errors::RenderingError, 'There is no parent element' unless parent
|
28
30
|
Context.new(parent.object, parent.key, parent.parent, pins)
|
29
31
|
end
|
30
32
|
|
31
33
|
# Go to a pinned context
|
32
34
|
def goto_pin(ref)
|
33
35
|
pin = pins&.[](ref)
|
34
|
-
raise
|
36
|
+
raise Errors::RenderingError, 'Pin reference cannot be used here' unless pin
|
35
37
|
Context.new(pin.object, pin.key, pin.parent, (pin.pins || {}).merge(pins))
|
36
38
|
end
|
37
39
|
|
@@ -42,13 +44,17 @@ module Babl
|
|
42
44
|
|
43
45
|
# Wrapper around #move_forward navigating into the return value of
|
44
46
|
# the block. However, if an error occurs, it is wrapped in a
|
45
|
-
#
|
47
|
+
# RenderingError and the navigation stack trace is added
|
46
48
|
# to the error message.
|
47
49
|
def move_forward_block(key)
|
48
50
|
move_forward(yield, key)
|
49
51
|
rescue StandardError => e
|
50
|
-
|
51
|
-
|
52
|
+
raise Errors::RenderingError, "#{e.message}\n" + formatted_stack(key), e.backtrace
|
53
|
+
end
|
54
|
+
|
55
|
+
def formatted_stack(*additional_stack_items)
|
56
|
+
stack_trace = ([:__root__] + stack + additional_stack_items).join('.')
|
57
|
+
"BABL @ #{stack_trace}"
|
52
58
|
end
|
53
59
|
|
54
60
|
# Return an array containing the navigation history
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'values'
|
2
|
+
require 'set'
|
3
|
+
require 'babl/schema/static'
|
4
|
+
require 'babl/schema/object'
|
5
|
+
|
6
|
+
module Babl
|
7
|
+
module Schema
|
8
|
+
class AnyOf < Value.new(:choice_set)
|
9
|
+
attr_reader :choices
|
10
|
+
|
11
|
+
def initialize(choices)
|
12
|
+
flattened_choices = choices.flat_map { |doc| AnyOf === doc ? doc.choices : [doc] }.uniq
|
13
|
+
@choices = flattened_choices
|
14
|
+
super(flattened_choices.to_set)
|
15
|
+
end
|
16
|
+
|
17
|
+
def json
|
18
|
+
{ anyOf: choices.map(&:json) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Perform simple transformations in order to reduce the size of the generated
|
22
|
+
# schema.
|
23
|
+
def simplify
|
24
|
+
simplify_single ||
|
25
|
+
simplify_nullability ||
|
26
|
+
simplify_empty_array ||
|
27
|
+
simplify_push_down_dyn_array ||
|
28
|
+
simplify_dyn_and_fixed_array ||
|
29
|
+
simplify_many_objects_only_one_difference ||
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# We can completely get rid of the AnyOf element of there is only one possible schema.
|
36
|
+
def simplify_single
|
37
|
+
choices.size == 1 ? choices.first : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Try to merge nullability into one of the object elements if they support that
|
41
|
+
# (Object, DynArray and FixedArray).
|
42
|
+
def simplify_nullability
|
43
|
+
if choices.include?(Static::NULL)
|
44
|
+
others = choices - [Static::NULL]
|
45
|
+
others.each do |other|
|
46
|
+
new_other =
|
47
|
+
case other
|
48
|
+
when Object then Object.new(other.properties, other.additional, true)
|
49
|
+
when DynArray then DynArray.new(other.item, true)
|
50
|
+
when FixedArray then FixedArray.new(other.items, true)
|
51
|
+
when Anything then other
|
52
|
+
end
|
53
|
+
return AnyOf.new(others - [other] + [new_other]).simplify if new_other
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# An always empty FixedArray is just a special case of a DynArray
|
61
|
+
# We can get rid of the former and only keep the DynArray
|
62
|
+
def simplify_empty_array
|
63
|
+
fixed_array = choices.find { |s| FixedArray === s && s.items.empty? }
|
64
|
+
if fixed_array
|
65
|
+
others = choices - [fixed_array]
|
66
|
+
others.each do |other|
|
67
|
+
next unless DynArray === other
|
68
|
+
new_other = DynArray.new(other.item, other.nullable || fixed_array.nullable)
|
69
|
+
return AnyOf.new(others - [other] + [new_other]).simplify
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# If the static array is an instance of another dyn array, then the fixed array can be
|
77
|
+
# removed.
|
78
|
+
def simplify_dyn_and_fixed_array
|
79
|
+
dyns = choices.select { |s| DynArray === s }
|
80
|
+
fixeds = choices.select { |s| FixedArray === s && s.items.uniq.size == 1 }
|
81
|
+
|
82
|
+
dyns.each do |dyn|
|
83
|
+
fixeds.each do |fixed|
|
84
|
+
new_dyn = DynArray.new(dyn.item, dyn.nullable || fixed.nullable)
|
85
|
+
return AnyOf.new(choices - [fixed, dyn] + [new_dyn]).simplify if dyn.item == fixed.items.first
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# If two objects have exactly the same structure, with the exception of only one property
|
93
|
+
# having a different type, then AnyOf can be pushed down to this property.
|
94
|
+
def simplify_many_objects_only_one_difference
|
95
|
+
return unless choices.all? { |s| Object === s }
|
96
|
+
|
97
|
+
choices.each_with_index { |obj1, index1|
|
98
|
+
choices.each_with_index { |obj2, index2|
|
99
|
+
next if index2 <= index1
|
100
|
+
next unless Object === obj1 && Object === obj2
|
101
|
+
next unless obj1.nullable == obj2.nullable && obj1.additional == obj2.additional
|
102
|
+
next unless obj1.properties.map { |p| [p.name, p.required] }.to_set ==
|
103
|
+
obj2.properties.map { |p| [p.name, p.required] }.to_set
|
104
|
+
|
105
|
+
diff1 = obj1.properties - obj2.properties
|
106
|
+
diff2 = obj2.properties - obj1.properties
|
107
|
+
|
108
|
+
next unless diff1.size == 1 && diff2.size == 1 && diff1.first.name == diff2.first.name
|
109
|
+
|
110
|
+
merged = Object.new(
|
111
|
+
obj1.properties.map { |p|
|
112
|
+
next p unless p == diff1.first
|
113
|
+
Object::Property.new(
|
114
|
+
p.name,
|
115
|
+
AnyOf.new([diff1.first.value, diff2.first.value]).simplify,
|
116
|
+
p.required
|
117
|
+
)
|
118
|
+
},
|
119
|
+
obj1.additional,
|
120
|
+
obj1.nullable
|
121
|
+
)
|
122
|
+
|
123
|
+
return AnyOf.new(choices - [obj1, obj2] + [merged]).simplify
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Push down the AnyOf to the item if all outputs are of type DynArray
|
131
|
+
def simplify_push_down_dyn_array
|
132
|
+
return unless choices.all? { |s| DynArray === s }
|
133
|
+
DynArray.new(AnyOf.new(choices.map(&:item)).simplify, choices.any?(&:nullable))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'values'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Babl
|
5
|
+
module Schema
|
6
|
+
class Object < Value.new(:property_set, :additional, :nullable)
|
7
|
+
attr_reader :properties
|
8
|
+
|
9
|
+
def initialize(properties, additional, nullable)
|
10
|
+
@properties = properties
|
11
|
+
super(properties.to_set, additional, nullable)
|
12
|
+
end
|
13
|
+
|
14
|
+
class Property < Value.new(:name, :value, :required)
|
15
|
+
def initialize(name, value, required)
|
16
|
+
super(name, value, required)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
EMPTY = new([], false, false)
|
21
|
+
EMPTY_WITH_ADDITIONAL = new([], true, false)
|
22
|
+
|
23
|
+
def json
|
24
|
+
{ type: nullable ? %w[object null] : 'object' }.tap { |out|
|
25
|
+
next if properties.empty?
|
26
|
+
out[:properties] = properties.map { |property| [property.name, property.value.json] }.to_h
|
27
|
+
out[:additionalProperties] = additional
|
28
|
+
required_properties = properties.select(&:required)
|
29
|
+
next if required_properties.empty?
|
30
|
+
out[:required] = properties.select(&:required).map(&:name).map(&:to_s)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|