babl-json 0.1.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +228 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +4 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +7 -0
  9. data/README.md +87 -0
  10. data/babl.gemspec +23 -0
  11. data/lib/babl.rb +41 -0
  12. data/lib/babl/builder/chain_builder.rb +85 -0
  13. data/lib/babl/builder/template_base.rb +37 -0
  14. data/lib/babl/operators/array.rb +42 -0
  15. data/lib/babl/operators/call.rb +25 -0
  16. data/lib/babl/operators/dep.rb +48 -0
  17. data/lib/babl/operators/each.rb +45 -0
  18. data/lib/babl/operators/enter.rb +22 -0
  19. data/lib/babl/operators/merge.rb +49 -0
  20. data/lib/babl/operators/nav.rb +55 -0
  21. data/lib/babl/operators/nullable.rb +16 -0
  22. data/lib/babl/operators/object.rb +55 -0
  23. data/lib/babl/operators/parent.rb +90 -0
  24. data/lib/babl/operators/partial.rb +46 -0
  25. data/lib/babl/operators/pin.rb +78 -0
  26. data/lib/babl/operators/source.rb +12 -0
  27. data/lib/babl/operators/static.rb +40 -0
  28. data/lib/babl/operators/switch.rb +71 -0
  29. data/lib/babl/operators/with.rb +51 -0
  30. data/lib/babl/railtie.rb +29 -0
  31. data/lib/babl/rendering/compiled_template.rb +28 -0
  32. data/lib/babl/rendering/context.rb +60 -0
  33. data/lib/babl/rendering/internal_value_node.rb +30 -0
  34. data/lib/babl/rendering/noop_preloader.rb +10 -0
  35. data/lib/babl/rendering/terminal_value_node.rb +54 -0
  36. data/lib/babl/template.rb +48 -0
  37. data/lib/babl/utils/hash.rb +11 -0
  38. data/lib/babl/version.rb +3 -0
  39. data/spec/construction_spec.rb +246 -0
  40. data/spec/navigation_spec.rb +133 -0
  41. data/spec/partial_spec.rb +53 -0
  42. data/spec/pinning_spec.rb +137 -0
  43. metadata +145 -0
@@ -0,0 +1,37 @@
1
+ module Babl
2
+ module Builder
3
+ # TemplateBase is a thin wrapper around Builder.
4
+ #
5
+ # Since the BABL code is run via #instance_eval within an instance of this class, we want to
6
+ # define as few methods as possible here.
7
+ class TemplateBase
8
+ def initialize(builder = ChainBuilder.new(&:itself))
9
+ @builder = builder
10
+ end
11
+
12
+ def compile(**options)
13
+ Rendering::CompiledTemplate.new(precompile, **options)
14
+ end
15
+
16
+ protected
17
+
18
+ def unscoped
19
+ self.class.new builder.rescope(&:itself)
20
+ end
21
+
22
+ def precompile
23
+ builder.precompile(Babl::Rendering::TerminalValueNode.instance)
24
+ end
25
+
26
+ def construct_node(**new_context, &block)
27
+ self.class.new builder.construct_node(**new_context, &block)
28
+ end
29
+
30
+ def construct_terminal(&block)
31
+ self.class.new builder.construct_terminal(&block)
32
+ end
33
+
34
+ attr_reader :builder
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ module Babl
2
+ module Operators
3
+ module Array
4
+ module DSL
5
+ # Produce an fixed-size array, using the provided templates to populate its elements.
6
+ def array(*templates)
7
+ construct_terminal { |ctx|
8
+ FixedArrayNode.new(templates.map { |t|
9
+ unscoped.call(t).builder.precompile(Rendering::TerminalValueNode.instance, ctx.merge(continue: nil))
10
+ })
11
+ }
12
+ end
13
+ 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
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ module Babl
2
+ module Operators
3
+ module Call
4
+ module DSL
5
+ # Interpret whatever is passed to this method as BABL template. It is idempotent.
6
+ def call(*args, &block)
7
+ return with(*args, &block) unless block.nil?
8
+ raise ::Babl::InvalidTemplateError, 'call() expects exactly 1 argument (unless block)' unless args.size == 1
9
+
10
+ arg = args.first
11
+
12
+ case arg
13
+ when self.class then self.class.new(builder.wrap { |bound| arg.builder.bind(bound) })
14
+ when ::Symbol then nav(arg)
15
+ when ::Proc then call(&arg)
16
+ when ::Hash then object(**arg.map { |k, v| [k.to_s.to_sym, v] }.to_h)
17
+ when ::Array then array(*arg)
18
+ when ::String, ::Numeric, ::NilClass, ::TrueClass, ::FalseClass then unscoped.static(arg)
19
+ else raise ::Babl::InvalidTemplateError, "call() received invalid argument: #{arg}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ module Babl
2
+ module Operators
3
+ module Dep
4
+ module DSL
5
+ # Declare dependencies as if they were generated by nav()
6
+ # but without navigating.
7
+ 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
24
+ 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
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ module Babl
2
+ module Operators
3
+ module Each
4
+ module DSL
5
+ # Construct a JSON array by iterating over the current collection,
6
+ # using the chained template for rendering each element.
7
+ 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]
23
+ 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
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ module Babl
2
+ module Operators
3
+ module Enter
4
+ module DSL
5
+ # Navigate to a named property of current element. The name
6
+ # is inferred based on the object()
7
+ def enter
8
+ construct_node(key: nil, continue: nil) { |node, context|
9
+ key = context[:key]
10
+ raise InvalidTemplateError, "No key to enter into" unless key
11
+ Nav::NavNode.new(key, node)
12
+ }
13
+ end
14
+
15
+ # Simple convenience alias
16
+ def _
17
+ enter
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module Babl
2
+ module Operators
3
+ module Merge
4
+ module DSL
5
+ # Merge multiple JSON objects (non-deep)
6
+ def merge(*templates)
7
+ construct_terminal { |context|
8
+ MergeNode.new(
9
+ templates.map { |t|
10
+ unscoped.call(t).builder.precompile(
11
+ Rendering::TerminalValueNode.instance,
12
+ context.merge(continue: nil)
13
+ )
14
+ }
15
+ )
16
+ }
17
+ end
18
+ end
19
+
20
+ class MergeNode
21
+ def initialize(nodes)
22
+ @nodes = nodes
23
+ end
24
+
25
+ def dependencies
26
+ nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
27
+ end
28
+
29
+ def pinned_dependencies
30
+ nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
31
+ end
32
+
33
+ def documentation
34
+ nodes.map(&:documentation).each_with_index.map { |doc, idx|
35
+ [:"Merge #{idx + 1}", doc]
36
+ }.to_h
37
+ end
38
+
39
+ def render(ctx)
40
+ nodes.map { |node| node.render(ctx) }.compact.reduce({}, :merge)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :nodes
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ module Babl
2
+ module Operators
3
+ module Nav
4
+ module DSL
5
+ # Navigate to a named property of the current element
6
+ # Multiple properties can be chained
7
+ #
8
+ # A block can also be passed, but in that case, dependency tracking
9
+ # is disabled for the rest of the chain.
10
+ def nav(*path, &block)
11
+ if path.empty?
12
+ return (block ? with(unscoped, &block) : construct_node(key: nil, continue: nil) { |node| node })
13
+ end
14
+
15
+ construct_node(key: nil, continue: nil) { |node| NavNode.new(path.first, node) }.nav(*path[1..-1], &block)
16
+ end
17
+ end
18
+
19
+ class NavNode
20
+ def initialize(through, node)
21
+ @through = through
22
+ @node = node
23
+ end
24
+
25
+ def dependencies
26
+ { through => node.dependencies }
27
+ end
28
+
29
+ def documentation
30
+ node.documentation
31
+ end
32
+
33
+ def pinned_dependencies
34
+ node.pinned_dependencies
35
+ end
36
+
37
+ def render(ctx)
38
+ node.render(ctx.move_forward_block(through) { navigate(ctx.object) })
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :through, :node
44
+
45
+ def navigate(object)
46
+ if object.is_a?(Hash)
47
+ object.fetch(through)
48
+ else
49
+ object.send(through)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ module Babl
2
+ module Operators
3
+ module Nullable
4
+ module DSL
5
+ # Nullify the current construction if
6
+ # the current element is Nil.
7
+ def nullable
8
+ switch(
9
+ unscoped.nav(&:nil?) => nil,
10
+ unscoped.default => unscoped.continue
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,55 @@
1
+ module Babl
2
+ module Operators
3
+ module Object
4
+ module DSL
5
+ # Create a JSON object node with static structure
6
+ def object(*attrs, **nested)
7
+ (attrs.map(&:to_sym) + nested.keys).group_by(&:itself).values.each do |keys|
8
+ raise ::Babl::InvalidTemplateError, "Duplicate key in object(): #{keys.first}" if keys.size > 1
9
+ end
10
+
11
+ construct_terminal { |ctx|
12
+ nodes = attrs
13
+ .map { |name| [name.to_sym, unscoped.enter] }.to_h
14
+ .merge(nested)
15
+ .map { |k, v|
16
+ [k, unscoped.call(v).builder.precompile(
17
+ Rendering::TerminalValueNode.instance,
18
+ ctx.merge(key: k, continue: nil)
19
+ )]
20
+ }
21
+ .to_h
22
+
23
+ ObjectNode.new(nodes)
24
+ }
25
+ end
26
+ end
27
+
28
+ class ObjectNode
29
+ def initialize(nodes)
30
+ @nodes = nodes
31
+ end
32
+
33
+ def dependencies
34
+ nodes.values.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
35
+ end
36
+
37
+ def pinned_dependencies
38
+ nodes.values.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
39
+ end
40
+
41
+ def documentation
42
+ nodes.map { |k, v| [k, v.documentation] }.to_h
43
+ end
44
+
45
+ def render(ctx)
46
+ nodes.map { |k, v| [k, v.render(ctx)] }.to_h
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :nodes
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ module Babl
2
+ module Operators
3
+ module Parent
4
+ PARENT = ::Object.new
5
+
6
+ module DSL
7
+ # Navigate to the parent of the current object.
8
+ def parent
9
+ construct_node(key: nil, continue: nil) { |node| ParentNode.new(node) }
10
+ end
11
+
12
+ protected
13
+
14
+ # Override TemplateBase#precompile to add parent dependencies verification
15
+ def precompile
16
+ ParentResolverNode.new(super)
17
+ end
18
+ end
19
+
20
+ class ParentResolverNode
21
+ def initialize(node)
22
+ @node = node
23
+ end
24
+
25
+ def dependencies
26
+ backpropagate_dependencies(node.dependencies)
27
+ end
28
+
29
+ def documentation
30
+ node.documentation
31
+ end
32
+
33
+ def pinned_dependencies
34
+ node.pinned_dependencies
35
+ end
36
+
37
+ def render(ctx)
38
+ node.render(ctx)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :node
44
+
45
+ def backpropagate_dependencies(deps)
46
+ raise InvalidTemplateError, 'Out of context parent dependency' if deps.key? PARENT
47
+ new_deps = backpropagate_dependencies_one_level(deps)
48
+ deps == new_deps ? new_deps : backpropagate_dependencies(new_deps)
49
+ end
50
+
51
+ def backpropagate_dependencies_one_level(deps)
52
+ deps.reduce({}) do |out, (k, v)|
53
+ next out if k == PARENT
54
+
55
+ Babl::Utils::Hash.deep_merge(
56
+ Babl::Utils::Hash.deep_merge(out, k => backpropagate_dependencies_one_level(v)),
57
+ v[PARENT] || {}
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ class ParentNode
64
+ def initialize(node)
65
+ @node = node
66
+ end
67
+
68
+ def documentation
69
+ node.documentation
70
+ end
71
+
72
+ def pinned_dependencies
73
+ node.pinned_dependencies
74
+ end
75
+
76
+ def dependencies
77
+ { PARENT => node.dependencies }
78
+ end
79
+
80
+ def render(ctx)
81
+ node.render(ctx.move_backward)
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :node
87
+ end
88
+ end
89
+ end
90
+ end