babl-json 0.1.1

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