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
@@ -0,0 +1,26 @@
1
+ require 'babl/utils/hash'
2
+ require 'babl/schema/object'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class Object < Value.new(:nodes)
8
+ def dependencies
9
+ nodes.values.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
10
+ end
11
+
12
+ def pinned_dependencies
13
+ nodes.values.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
14
+ end
15
+
16
+ def schema
17
+ properties = nodes.map { |k, v| Schema::Object::Property.new(k, v.schema, true) }
18
+ Schema::Object.new(properties, false, false)
19
+ end
20
+
21
+ def render(ctx)
22
+ nodes.map { |k, v| [k, v.render(ctx)] }.to_h
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ require 'babl/utils/ref'
2
+ require 'babl/errors'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class Parent < Value.new(:node)
8
+ PARENT_MARKER = Utils::Ref.new
9
+
10
+ class Resolver < Value.new(:node)
11
+ def dependencies
12
+ backpropagate_dependencies(node.dependencies)
13
+ end
14
+
15
+ def schema
16
+ node.schema
17
+ end
18
+
19
+ def pinned_dependencies
20
+ node.pinned_dependencies
21
+ end
22
+
23
+ def render(ctx)
24
+ node.render(ctx)
25
+ end
26
+
27
+ private
28
+
29
+ def backpropagate_dependencies(deps)
30
+ raise Errors::InvalidTemplateError, 'Out of context parent dependency' if deps.key? PARENT_MARKER
31
+ new_deps = backpropagate_dependencies_one_level(deps)
32
+ deps == new_deps ? new_deps : backpropagate_dependencies(new_deps)
33
+ end
34
+
35
+ def backpropagate_dependencies_one_level(deps)
36
+ deps.reduce({}) do |out, (k, v)|
37
+ next out if k == PARENT_MARKER
38
+
39
+ Babl::Utils::Hash.deep_merge(
40
+ Babl::Utils::Hash.deep_merge(out, k => backpropagate_dependencies_one_level(v)),
41
+ v[PARENT_MARKER] || {}
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ def schema
48
+ node.schema
49
+ end
50
+
51
+ def pinned_dependencies
52
+ node.pinned_dependencies
53
+ end
54
+
55
+ def dependencies
56
+ { PARENT_MARKER => node.dependencies }
57
+ end
58
+
59
+ def render(ctx)
60
+ node.render(ctx.move_backward)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ require 'babl/nodes/terminal_value'
2
+ require 'babl/schema/static'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class Static < Value.new(:serialized_value)
8
+ def initialize(value)
9
+ super(value)
10
+ end
11
+
12
+ def schema
13
+ Schema::Static.new(serialized_value)
14
+ end
15
+
16
+ def render(_ctx)
17
+ serialized_value
18
+ end
19
+
20
+ def dependencies
21
+ {}
22
+ end
23
+
24
+ def pinned_dependencies
25
+ {}
26
+ end
27
+
28
+ private
29
+
30
+ def generate_doc
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ require 'babl/utils/hash'
2
+ require 'babl/schema/any_of'
3
+ require 'babl/errors'
4
+ require 'values'
5
+
6
+ module Babl
7
+ module Nodes
8
+ class Switch < Value.new(:nodes)
9
+ def dependencies
10
+ (nodes.values + nodes.keys).map(&:dependencies)
11
+ .reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
12
+ end
13
+
14
+ def pinned_dependencies
15
+ (nodes.values + nodes.keys).map(&:pinned_dependencies)
16
+ .reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
17
+ end
18
+
19
+ def schema
20
+ Schema::AnyOf.new(nodes.values.map(&:schema)).simplify
21
+ end
22
+
23
+ def render(ctx)
24
+ nodes.each { |cond, value| return value.render(ctx) if cond.render(ctx) }
25
+ raise Errors::RenderingError, 'A least one switch() condition must be taken'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ require 'singleton'
2
+ require 'babl/schema/anything'
3
+ require 'babl/errors'
4
+
5
+ module Babl
6
+ module Nodes
7
+ # A TerminalValue node is always implicitely added to the end of the
8
+ # chain during compilation. It basically ensures that the output contains only
9
+ # primitives, arrays and hashes.
10
+ class TerminalValue
11
+ include Singleton
12
+
13
+ def schema
14
+ Schema::Anything.instance
15
+ end
16
+
17
+ def dependencies
18
+ {}
19
+ end
20
+
21
+ def pinned_dependencies
22
+ {}
23
+ end
24
+
25
+ def render(ctx)
26
+ render_object(ctx.object)
27
+ rescue TerminalValueError => e
28
+ raise Errors::RenderingError, "#{e.message}\n" + ctx.formatted_stack(e.babl_stack), e.backtrace
29
+ end
30
+
31
+ def render_object(obj, stack = [])
32
+ case obj
33
+ when String, Numeric, NilClass, TrueClass, FalseClass then obj
34
+ when Hash then render_hash(obj, stack)
35
+ when Array then render_array(obj, stack)
36
+ else raise TerminalValueError.new("Only primitives can be serialized: #{obj}", stack)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def render_array(array, stack)
43
+ array.each_with_index.map { |obj, index|
44
+ stack.push index
45
+ render_object(obj, stack)
46
+ stack.pop
47
+ }
48
+ end
49
+
50
+ def render_hash(hash, stack)
51
+ hash.map { |k, v|
52
+ key = render_key(k, stack)
53
+ stack.push key
54
+ out = [key, render_object(v, stack)]
55
+ stack.pop
56
+ out
57
+ }.to_h
58
+ end
59
+
60
+ def render_key(key, stack)
61
+ case key
62
+ when Symbol, String, Numeric, NilClass, TrueClass, FalseClass then :"#{key}"
63
+ else raise TerminalValueError.new("Invalid key for JSON object: #{key}", stack)
64
+ end
65
+ end
66
+
67
+ class TerminalValueError < Errors::RenderingError
68
+ attr_reader :babl_stack
69
+ def initialize(message, babl_stack)
70
+ @babl_stack = babl_stack
71
+ super(message)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,28 @@
1
+ require 'babl/utils/hash'
2
+ require 'values'
3
+
4
+ module Babl
5
+ module Nodes
6
+ class With < Value.new(:node, :nodes, :block)
7
+ def schema
8
+ node.schema
9
+ end
10
+
11
+ def dependencies
12
+ # Dependencies of 'node' are explicitely ignored
13
+ nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
14
+ end
15
+
16
+ def pinned_dependencies
17
+ (nodes + [node]).map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
18
+ end
19
+
20
+ def render(ctx)
21
+ values = nodes.map { |n| n.render(ctx) }
22
+ node.render(ctx.move_forward_block(:__block__) do
23
+ block.arity.zero? ? ctx.object.instance_exec(&block) : block.call(*values)
24
+ end)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,6 @@
1
+ require 'babl/nodes/fixed_array'
2
+ require 'babl/nodes/terminal_value'
3
+
1
4
  module Babl
2
5
  module Operators
3
6
  module Array
@@ -5,38 +8,12 @@ module Babl
5
8
  # Produce an fixed-size array, using the provided templates to populate its elements.
6
9
  def array(*templates)
7
10
  construct_terminal { |ctx|
8
- FixedArrayNode.new(templates.map { |t|
9
- unscoped.call(t).builder.precompile(Rendering::TerminalValueNode.instance, ctx.merge(continue: nil))
11
+ Nodes::FixedArray.new(templates.map { |t|
12
+ unscoped.call(t).builder.precompile(Nodes::TerminalValue.instance, ctx.merge(continue: nil))
10
13
  })
11
14
  }
12
15
  end
13
16
  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
17
  end
41
18
  end
42
19
  end
@@ -1,3 +1,5 @@
1
+ require 'babl/errors'
2
+
1
3
  module Babl
2
4
  module Operators
3
5
  module Call
@@ -5,7 +7,7 @@ module Babl
5
7
  # Interpret whatever is passed to this method as BABL template. It is idempotent.
6
8
  def call(*args, &block)
7
9
  return with(*args, &block) unless block.nil?
8
- raise ::Babl::InvalidTemplateError, 'call() expects exactly 1 argument (unless block)' unless args.size == 1
10
+ raise Errors::InvalidTemplateError, 'call() expects exactly 1 argument (unless block)' unless args.size == 1
9
11
 
10
12
  arg = args.first
11
13
 
@@ -16,7 +18,7 @@ module Babl
16
18
  when ::Hash then object(**arg.map { |k, v| [:"#{k}", v] }.to_h)
17
19
  when ::Array then array(*arg)
18
20
  when ::String, ::Numeric, ::NilClass, ::TrueClass, ::FalseClass then static(arg)
19
- else raise ::Babl::InvalidTemplateError, "call() received invalid argument: #{arg}"
21
+ else raise Errors::InvalidTemplateError, "call() received invalid argument: #{arg}"
20
22
  end
21
23
  end
22
24
  end
@@ -0,0 +1,19 @@
1
+ require 'babl/errors'
2
+
3
+ module Babl
4
+ module Operators
5
+ module Continue
6
+ module DSL
7
+ # Return a special placeholder that can be used as a switch(...) value. It tells BABL to continue
8
+ # the evaluation of the original chain after switch().
9
+ def continue
10
+ construct_terminal { |context|
11
+ node = context[:continue]
12
+ raise Errors::InvalidTemplateError, 'continue() cannot be used outside switch()' unless node
13
+ node
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Babl
2
+ module Operators
3
+ module Default
4
+ module DSL
5
+ # To be used as a switch(...) condition. It is strictly equivalent to write 'true' instead,
6
+ # but it conveys more meaning.
7
+ def default
8
+ static(true)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,5 @@
1
+ require 'babl/nodes/dep'
2
+
1
3
  module Babl
2
4
  module Operators
3
5
  module Dep
@@ -5,43 +7,8 @@ module Babl
5
7
  # Declare dependencies as if they were generated by nav()
6
8
  # but without navigating.
7
9
  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
10
+ construct_node(continue: nil) { |node| Nodes::Dep.new(node, path) }
24
11
  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
12
  end
46
13
  end
47
14
  end
@@ -1,3 +1,5 @@
1
+ require 'babl/nodes/each'
2
+
1
3
  module Babl
2
4
  module Operators
3
5
  module Each
@@ -5,40 +7,8 @@ module Babl
5
7
  # Construct a JSON array by iterating over the current collection,
6
8
  # using the chained template for rendering each element.
7
9
  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]
10
+ construct_node(key: nil, continue: nil) { |node| Nodes::Each.new(node) }
23
11
  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
12
  end
43
13
  end
44
14
  end