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