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