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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +6 -0
  4. data/README.md +1 -3
  5. data/babl.gemspec +3 -1
  6. data/lib/babl.rb +4 -6
  7. data/lib/babl/builder/chain_builder.rb +6 -2
  8. data/lib/babl/builder/template_base.rb +16 -3
  9. data/lib/babl/errors.rb +7 -0
  10. data/lib/babl/nodes/create_pin.rb +24 -0
  11. data/lib/babl/nodes/dep.rb +38 -0
  12. data/lib/babl/nodes/each.rb +29 -0
  13. data/lib/babl/nodes/fixed_array.rb +25 -0
  14. data/lib/babl/nodes/goto_pin.rb +24 -0
  15. data/lib/babl/{rendering/internal_value_node.rb → nodes/internal_value.rb} +6 -5
  16. data/lib/babl/nodes/merge.rb +102 -0
  17. data/lib/babl/nodes/nav.rb +33 -0
  18. data/lib/babl/nodes/object.rb +26 -0
  19. data/lib/babl/nodes/parent.rb +64 -0
  20. data/lib/babl/nodes/static.rb +34 -0
  21. data/lib/babl/nodes/switch.rb +29 -0
  22. data/lib/babl/nodes/terminal_value.rb +76 -0
  23. data/lib/babl/nodes/with.rb +28 -0
  24. data/lib/babl/operators/array.rb +5 -28
  25. data/lib/babl/operators/call.rb +4 -2
  26. data/lib/babl/operators/continue.rb +19 -0
  27. data/lib/babl/operators/default.rb +13 -0
  28. data/lib/babl/operators/dep.rb +3 -36
  29. data/lib/babl/operators/each.rb +3 -33
  30. data/lib/babl/operators/enter.rb +4 -2
  31. data/lib/babl/operators/extends.rb +4 -1
  32. data/lib/babl/operators/merge.rb +7 -30
  33. data/lib/babl/operators/nav.rb +4 -36
  34. data/lib/babl/operators/object.rb +7 -29
  35. data/lib/babl/operators/parent.rb +4 -73
  36. data/lib/babl/operators/partial.rb +4 -2
  37. data/lib/babl/operators/pin.rb +14 -58
  38. data/lib/babl/operators/static.rb +11 -30
  39. data/lib/babl/operators/switch.rb +8 -51
  40. data/lib/babl/operators/with.rb +5 -34
  41. data/lib/babl/railtie.rb +2 -2
  42. data/lib/babl/rendering/compiled_template.rb +5 -13
  43. data/lib/babl/rendering/context.rb +13 -7
  44. data/lib/babl/schema/any_of.rb +137 -0
  45. data/lib/babl/schema/anything.rb +13 -0
  46. data/lib/babl/schema/dyn_array.rb +11 -0
  47. data/lib/babl/schema/fixed_array.rb +13 -0
  48. data/lib/babl/schema/object.rb +35 -0
  49. data/lib/babl/schema/static.rb +14 -0
  50. data/lib/babl/schema/typed.rb +0 -0
  51. data/lib/babl/template.rb +4 -9
  52. data/lib/babl/utils/ref.rb +6 -0
  53. data/lib/babl/version.rb +1 -1
  54. data/spec/operators/array_spec.rb +31 -7
  55. data/spec/operators/call_spec.rb +16 -14
  56. data/spec/operators/continue_spec.rb +25 -0
  57. data/spec/operators/default_spec.rb +15 -0
  58. data/spec/operators/dep_spec.rb +4 -8
  59. data/spec/operators/each_spec.rb +24 -5
  60. data/spec/operators/enter_spec.rb +9 -7
  61. data/spec/operators/extends_spec.rb +19 -5
  62. data/spec/operators/merge_spec.rb +105 -12
  63. data/spec/operators/nav_spec.rb +22 -10
  64. data/spec/operators/null_spec.rb +5 -4
  65. data/spec/operators/nullable_spec.rb +13 -13
  66. data/spec/operators/object_spec.rb +17 -6
  67. data/spec/operators/parent_spec.rb +18 -22
  68. data/spec/operators/partial_spec.rb +8 -6
  69. data/spec/operators/pin_spec.rb +100 -61
  70. data/spec/operators/source_spec.rb +10 -6
  71. data/spec/operators/static_spec.rb +17 -9
  72. data/spec/operators/switch_spec.rb +85 -45
  73. data/spec/operators/with_spec.rb +13 -15
  74. data/spec/spec_helper.rb +2 -31
  75. data/spec/spec_helper/operator_testing.rb +46 -0
  76. data/spec/spec_helper/schema_utils.rb +33 -0
  77. metadata +63 -4
  78. 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
- # To be used as a switch(...) condition. It is strictly equivalent to write 'true' instead,
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(Rendering::InternalValueNode.instance, context.merge(continue: nil))
14
+ .precompile(Nodes::InternalValue.instance, context.merge(continue: nil))
27
15
 
28
16
  value_node = unscoped.call(value).builder
29
- .precompile(Rendering::TerminalValueNode.instance, context.merge(continue: node))
17
+ .precompile(Nodes::TerminalValue.instance, context.merge(continue: node))
30
18
 
31
19
  [cond_node, value_node]
32
20
  }.to_h
33
21
 
34
- SwitchNode.new(nodes)
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
@@ -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
- WithNode.new(node, templates.map do |t|
11
+ Nodes::With.new(node, templates.map do |t|
9
12
  unscoped.call(t).builder.precompile(
10
- Rendering::InternalValueNode.instance,
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
- ::Babl.compile { #{template.source} }.json(local_assigns)
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, ::Babl::ActionView::Template::Handler)
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 = Babl::Rendering::Context.new(preloaded_data)
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 (ParentNode)
6
- # - Keep a reference to all pinned contexts, in order to goto a pinned context at any time (GotoPinNode)
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 Babl::RenderingError, 'There is no parent element' unless parent
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 Babl::RenderingError, 'Pin reference cannot be used here' unless pin
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
- # Babl::RenderingError and the navigation stack trace is added
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
- stack_trace = ([:__root__] + stack + [key]).join('.')
51
- raise Babl::RenderingError, "#{e.message}\nBABL @ #{stack_trace}", e.backtrace
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,13 @@
1
+ require 'singleton'
2
+
3
+ module Babl
4
+ module Schema
5
+ class Anything
6
+ include Singleton
7
+
8
+ def json
9
+ {}
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ require 'values'
2
+
3
+ module Babl
4
+ module Schema
5
+ class DynArray < Value.new(:item, :nullable)
6
+ def json
7
+ { type: nullable ? %w[array null] : 'array', items: item.json }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require 'values'
2
+
3
+ module Babl
4
+ module Schema
5
+ class FixedArray < Value.new(:items, :nullable)
6
+ EMPTY = new([], false)
7
+
8
+ def json
9
+ { type: nullable ? %w[array null] : 'array', items: items.map(&:json) }
10
+ end
11
+ end
12
+ end
13
+ 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