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,46 @@
1
+ module Babl
2
+ module Operators
3
+ module Partial
4
+ module DSL
5
+ # Load a partial template given its name
6
+ # A 'lookup_context' must be defined
7
+ def partial(partial_name)
8
+ raise InvalidTemplateError, "Cannot use partial without lookup context" unless lookup_context
9
+
10
+ path, source, partial_lookup_context = lookup_context.find(partial_name)
11
+ raise InvalidTemplateError, "Cannot find partial '#{partial_name}'" unless path
12
+
13
+ with_lookup_context(partial_lookup_context)
14
+ .source(source, path, 0)
15
+ .with_lookup_context(lookup_context)
16
+ end
17
+
18
+ def with_lookup_context(lookup_context)
19
+ self.class.new(builder.dup.tap { |inst| inst.instance_variable_set(:@lookup_context, lookup_context) })
20
+ end
21
+
22
+ def lookup_context
23
+ builder.instance_variable_get(:@lookup_context)
24
+ end
25
+ end
26
+
27
+ class AbsoluteLookupContext
28
+ attr_reader :search_path
29
+
30
+ def initialize(search_path)
31
+ @search_path = search_path
32
+ raise 'Invalid search path' unless search_path
33
+ end
34
+
35
+ def find(partial_name)
36
+ query = File.join(search_path, "{#{partial_name}}{.babl,}")
37
+ path = Dir[query].first
38
+ return unless path
39
+
40
+ source = File.read(path)
41
+ [path, source, self]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ module Babl
2
+ module Operators
3
+ module Pin
4
+ module DSL
5
+ # Create a pin
6
+ def pin(navigation = nil, &block)
7
+ return pin { |p| block[p.call(navigation)] } if navigation
8
+ ref = ::Object.new
9
+ referenced_scope = unscoped.construct_node(key: nil, continue: nil) { |node| GotoPinNode.new(node, ref) }
10
+ construct_node(continue: nil) { |node| CreatePinNode.new(node, ref) }.call(block[referenced_scope])
11
+ end
12
+
13
+ protected
14
+
15
+ # Override TemplateBase#precompile to ensure that all pin dependencies are satisfied.
16
+ def precompile
17
+ super.tap do |node|
18
+ raise Babl::InvalidTemplateError, 'Unresolved pin' unless node.pinned_dependencies.empty?
19
+ end
20
+ end
21
+ end
22
+
23
+ class CreatePinNode
24
+ def initialize(node, ref)
25
+ @node = node
26
+ @ref = ref
27
+ end
28
+
29
+ def render(ctx)
30
+ node.render(ctx.create_pin(ref))
31
+ end
32
+
33
+ def documentation
34
+ node.documentation
35
+ end
36
+
37
+ def dependencies
38
+ Babl::Utils::Hash.deep_merge(node.dependencies, node.pinned_dependencies[ref] || {})
39
+ end
40
+
41
+ def pinned_dependencies
42
+ node.pinned_dependencies.reject { |k, _v| k == ref }
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :node, :ref
48
+ end
49
+
50
+ class GotoPinNode
51
+ def initialize(node, ref)
52
+ @node = node
53
+ @ref = ref
54
+ end
55
+
56
+ def dependencies
57
+ {}
58
+ end
59
+
60
+ def pinned_dependencies
61
+ Babl::Utils::Hash.deep_merge(node.pinned_dependencies, ref => node.dependencies)
62
+ end
63
+
64
+ def documentation
65
+ node.documentation
66
+ end
67
+
68
+ def render(ctx)
69
+ node.render(ctx.goto_pin(ref))
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :node, :ref
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,12 @@
1
+ module Babl
2
+ module Operators
3
+ module Source
4
+ module DSL
5
+ # Parse BABL source into a Template
6
+ def source(*args, &block)
7
+ call(unscoped.instance_eval(*args, &block))
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ module Babl
2
+ module Operators
3
+ module Static
4
+ module DSL
5
+ # Create a static JSON value
6
+ def static(value)
7
+ construct_terminal { StaticNode.new(value) }
8
+ end
9
+ end
10
+
11
+ class StaticNode
12
+ def initialize(value)
13
+ @serialized_value = Rendering::TerminalValueNode.instance.render_object(value)
14
+ rescue Babl::RenderingError => exception
15
+ raise Babl::InvalidTemplateError, exception.message
16
+ end
17
+
18
+ def documentation
19
+ serialized_value
20
+ end
21
+
22
+ def render(_ctx)
23
+ serialized_value
24
+ end
25
+
26
+ def dependencies
27
+ {}
28
+ end
29
+
30
+ def pinned_dependencies
31
+ {}
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :serialized_value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,71 @@
1
+ module Babl
2
+ module Operators
3
+ module Switch
4
+ 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
+ unscoped.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
22
+ def switch(conds = {})
23
+ construct_node(continue: nil) { |node, context|
24
+ nodes = conds.map { |cond, value|
25
+ cond_node = unscoped.call(cond).builder
26
+ .precompile(Rendering::InternalValueNode.instance, context.merge(continue: nil))
27
+
28
+ value_node = unscoped.call(value).builder
29
+ .precompile(Rendering::TerminalValueNode.instance, context.merge(continue: node))
30
+
31
+ [cond_node, value_node]
32
+ }.to_h
33
+
34
+ SwitchNode.new(nodes)
35
+ }
36
+ end
37
+ 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
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
1
+ module Babl
2
+ module Operators
3
+ module With
4
+ module DSL
5
+ # Produce a value by calling the block, passing it the output value of the templates passed as argument.
6
+ def with(*templates, &block)
7
+ construct_node(key: nil, continue: nil) do |node, context|
8
+ WithNode.new(node, templates.map do |t|
9
+ unscoped.call(t).builder.precompile(
10
+ Rendering::InternalValueNode.instance,
11
+ context.merge(continue: nil)
12
+ )
13
+ end, block)
14
+ end
15
+ end
16
+ 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
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ module Babl
2
+ module ActionView
3
+ module Template
4
+ class Handler
5
+ class_attribute :default_format
6
+ self.default_format = Mime[:json]
7
+
8
+ def self.call(template)
9
+ # This implementation is not efficient: it will recompile the BABL template
10
+ # for each request. I still don't get why Rails template handlers MUST
11
+ # return Ruby code ?! Sucks too much. Ideally, we would like to keep the compiled
12
+ # template somewhere. However, I've not yet measured how much like is wasted.
13
+ # Maybe it is negligible ?
14
+ <<~RUBY
15
+ ::Babl.compile { #{template.source} }.json(local_assigns)
16
+ RUBY
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ class Railtie < Rails::Railtie
23
+ initializer "babl.initialize" do
24
+ ActiveSupport.on_load(:action_view) do
25
+ ::ActionView::Template.register_template_handler(:babl, ::Babl::ActionView::Template::Handler)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require 'oj'
2
+
3
+ module Babl
4
+ 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
+
16
+ def json(root)
17
+ data = render(root)
18
+ Oj.dump(data, indent: pretty ? 4 : 0, mode: :strict)
19
+ end
20
+
21
+ def render(root)
22
+ preloaded_data = preloader.preload([root], dependencies).first
23
+ ctx = Babl::Rendering::Context.new(preloaded_data)
24
+ node.render(ctx)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ module Babl
2
+ module Rendering
3
+ # The rendering context stores the 'current' object.
4
+ # 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
+ #
8
+ # It is important to keep this object as small as possible, since an instance is created each time
9
+ # we navigate into a property.
10
+ class Context
11
+ attr_reader :key, :object, :parent, :pins
12
+
13
+ def initialize(object, key = nil, parent = nil, pins = nil)
14
+ @key = key
15
+ @object = object
16
+ @parent = parent
17
+ @pins = pins
18
+ end
19
+
20
+ # Standard navigation (enter into property)
21
+ def move_forward(new_object, key)
22
+ Context.new(new_object, key, self, pins)
23
+ end
24
+
25
+ # Go back to parent
26
+ def move_backward
27
+ raise Babl::RenderingError, 'There is no parent element' unless parent
28
+ Context.new(parent.object, parent.key, parent.parent, pins)
29
+ end
30
+
31
+ # Go to a pinned context
32
+ def goto_pin(ref)
33
+ pin = pins&.[](ref)
34
+ raise Babl::RenderingError, 'Pin reference cannot be used here' unless pin
35
+ Context.new(pin.object, pin.key, pin.parent, (pin.pins || {}).merge(pins))
36
+ end
37
+
38
+ # Associate a pin to current context
39
+ def create_pin(ref)
40
+ Context.new(object, key, parent, (pins || {}).merge(ref => self))
41
+ end
42
+
43
+ # Wrapper around #move_forward navigating into the return value of
44
+ # the block. However, if an error occurs, it is wrapped in a
45
+ # Babl::RenderingError and the navigation stack trace is added
46
+ # to the error message.
47
+ def move_forward_block(key)
48
+ move_forward(yield, key)
49
+ rescue StandardError => e
50
+ stack_trace = ([:__root__] + stack + [key]).join('.')
51
+ raise Babl::RenderingError, "#{e.message}\nBABL @ #{stack_trace}", e.backtrace
52
+ end
53
+
54
+ # Return an array containing the navigation history
55
+ def stack
56
+ (parent ? parent.stack : []) + [key].compact
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ require 'singleton'
2
+
3
+ module Babl
4
+ module Rendering
5
+ # This Node plays a role similar to TerminalValueNode, but it does not perform any
6
+ # type checking on the produced object, which is allowed to be any Ruby object,
7
+ # including non-serializable objects.
8
+ #
9
+ # It is used when the output is not rendered (conditions in #switch, values passed to block in #with, ...)
10
+ class InternalValueNode
11
+ include Singleton
12
+
13
+ def documentation
14
+ :__value__
15
+ end
16
+
17
+ def dependencies
18
+ {}
19
+ end
20
+
21
+ def pinned_dependencies
22
+ {}
23
+ end
24
+
25
+ def render(ctx)
26
+ ctx.object
27
+ end
28
+ end
29
+ end
30
+ end