babl-json 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|