babl-json 0.1.4 → 0.2.0

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 (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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e6fcf82a20874d904622914b9253af7377adcbe
4
- data.tar.gz: e74d60dee7726f6efd41d02bd1b6777673087c0e
3
+ metadata.gz: 20dbd1b42e36b6fa9d7f7744b2cae7caba856a05
4
+ data.tar.gz: 25e21d06230f8694532b962ab9062a648682cde9
5
5
  SHA512:
6
- metadata.gz: c73c9e24b87d003d01f29bc1670d189fd449e3f74012141c835b833d9e657da96bbd6a1285e419ada89dd69bfc73207c0451f6ba97320d2793c082f553b43c25
7
- data.tar.gz: 2863794485cca3b329105e717c76b1f69124221176f732bd9a17065cf81f2d57c17b9c8d5bd056174b9a5b89f84c088f3f7856fcb06eb08c88538d2b8fe4631f
6
+ metadata.gz: 3b133517888b63bef55810cd3815fc4fc5378a0b5e200e519aefebed0f3bf50d9d64141c72f9f508b181b153c3c2f6348f4126b5eba8a00a73a6908341bd8ac4
7
+ data.tar.gz: e762bbdc629534d829cc56d13fe40617111754b15ca64a13e0865b3f55d60ca13dd226ad8584c06be492069285b295e09b9baaa38c2bafc61dfce1cdc95e670a
data/.travis.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
  install:
3
3
  - bundle install
4
- script: bundle exec rspec spec
4
+ script: bundle exec rspec spec && bundle exec rubocop
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # BABL Changelog
2
2
 
3
+ ## 0.2.0 (July 11, 2017)
4
+
5
+ - Added support for producing documentation using [JSON-Schema](http://json-schema.org/).
6
+ - Improved some error messages occurring during rendering phase.
7
+ - Stricter validation of templates passed to the `merge` operator. Compilation will fail if at least one of them is not producing object.
8
+
3
9
  ## 0.1.4 (July 6, 2017)
4
10
 
5
11
  - Added operator `extends`, to simplify the common usage pattern `merge(partial(...), ...)`.
data/README.md CHANGED
@@ -66,9 +66,7 @@ As of today, the only compatible preloader implementation *has not been released
66
66
 
67
67
  ### Automatic documentation
68
68
 
69
- Support for automatic documentation is very limited, and the output is indecently ugly. It is more a proof-of-concept than a useful feature.
70
-
71
- Mid-term goal is to generate a [JSON schema](http://json-schema.org/).
69
+ The structure of the JSON produced by a BABL template can be documentated using [JSON-Schema](http://json-schema.org/).
72
70
 
73
71
  ### Rails integration
74
72
 
data/babl.gemspec CHANGED
@@ -2,7 +2,7 @@ require File.join(File.dirname(__FILE__), 'lib/babl/version')
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "babl-json"
5
- gem.version = ::Babl::VERSION
5
+ gem.version = Babl::VERSION
6
6
  gem.licenses = ['MIT']
7
7
  gem.authors = ['Frederic Terrazzoni']
8
8
  gem.email = ['frederic.terrazzoni@gmail.com']
@@ -18,6 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.add_development_dependency 'pry', '~> 0'
19
19
  gem.add_development_dependency 'rspec', '~> 3'
20
20
  gem.add_development_dependency 'rubocop', '~> 0.48'
21
+ gem.add_development_dependency 'json-schema', '~> 2.8'
21
22
 
22
23
  gem.add_dependency 'oj', '~> 3.0'
24
+ gem.add_dependency 'values', '~> 1.8'
23
25
  end
data/lib/babl.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  require 'babl/railtie' if defined?(Rails)
2
2
  require 'babl/template'
3
3
  require 'babl/version'
4
+ require 'babl/rendering/noop_preloader'
5
+ require 'babl/operators/partial'
4
6
 
5
7
  module Babl
6
- class BablError < StandardError; end
7
- class InvalidTemplateError < BablError; end
8
- class RenderingError < BablError; end
9
-
10
8
  class Config
11
9
  attr_accessor :search_path, :preloader, :pretty
12
10
 
@@ -18,9 +16,9 @@ module Babl
18
16
  end
19
17
 
20
18
  class << self
21
- def compile(template: ::Babl::Template.new, &source)
19
+ def compile(template: Babl::Template.new, &source)
22
20
  if config.search_path
23
- ctx = ::Babl::Operators::Partial::AbsoluteLookupContext.new(config.search_path)
21
+ ctx = Babl::Operators::Partial::AbsoluteLookupContext.new(config.search_path)
24
22
  template = template.with_lookup_context(ctx)
25
23
  end
26
24
 
@@ -1,3 +1,7 @@
1
+ require 'babl/nodes/internal_value'
2
+ require 'babl/nodes/terminal_value'
3
+ require 'babl/errors'
4
+
1
5
  module Babl
2
6
  module Builder
3
7
  # Builder provides a simple framework for defining & chaining BABL's operators easily.
@@ -36,8 +40,8 @@ module Babl
36
40
  # Append a terminal operator, and return a new Builder object
37
41
  def construct_terminal
38
42
  construct_node do |node, context|
39
- unless [Rendering::InternalValueNode.instance, Rendering::TerminalValueNode.instance].include?(node)
40
- raise ::Babl::InvalidTemplateError, 'Chaining is not allowed after a terminal operator'
43
+ unless [Nodes::InternalValue.instance, Nodes::TerminalValue.instance].include?(node)
44
+ raise Errors::InvalidTemplateError, 'Chaining is not allowed after a terminal operator'
41
45
  end
42
46
  yield context
43
47
  end
@@ -1,3 +1,8 @@
1
+ require 'babl/nodes/terminal_value'
2
+ require 'babl/builder/chain_builder'
3
+ require 'babl/rendering/compiled_template'
4
+ require 'babl/rendering/noop_preloader'
5
+
1
6
  module Babl
2
7
  module Builder
3
8
  # TemplateBase is a thin wrapper around Builder.
@@ -9,8 +14,16 @@ module Babl
9
14
  @builder = builder
10
15
  end
11
16
 
12
- def compile(**options)
13
- Rendering::CompiledTemplate.new(precompile, **options)
17
+ def compile(preloader: Rendering::NoopPreloader, pretty: true)
18
+ node = precompile
19
+
20
+ Rendering::CompiledTemplate.with(
21
+ preloader: preloader,
22
+ pretty: pretty,
23
+ node: node,
24
+ dependencies: node.dependencies,
25
+ json_schema: node.schema.json
26
+ )
14
27
  end
15
28
 
16
29
  protected
@@ -20,7 +33,7 @@ module Babl
20
33
  end
21
34
 
22
35
  def precompile
23
- builder.precompile(Babl::Rendering::TerminalValueNode.instance)
36
+ builder.precompile(Nodes::TerminalValue.instance)
24
37
  end
25
38
 
26
39
  def construct_node(**new_context, &block)
@@ -0,0 +1,7 @@
1
+ module Babl
2
+ module Errors
3
+ class BablError < StandardError; end
4
+ class InvalidTemplateError < BablError; end
5
+ class RenderingError < BablError; end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ require 'babl/utils/hash'
2
+ require 'values'
3
+
4
+ module Babl
5
+ module Nodes
6
+ class CreatePin < Value.new(:node, :ref)
7
+ def render(ctx)
8
+ node.render(ctx.create_pin(ref))
9
+ end
10
+
11
+ def schema
12
+ node.schema
13
+ end
14
+
15
+ def dependencies
16
+ Babl::Utils::Hash.deep_merge(node.dependencies, node.pinned_dependencies[ref] || {})
17
+ end
18
+
19
+ def pinned_dependencies
20
+ node.pinned_dependencies.reject { |k, _v| k == ref }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'babl/utils/hash'
2
+ require 'values'
3
+
4
+ module Babl
5
+ module Nodes
6
+ class Dep < Value.new(:node, :path)
7
+ def initialize(node, path)
8
+ super(node, canonicalize(path))
9
+ end
10
+
11
+ def render(ctx)
12
+ node.render(ctx)
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 dependencies
24
+ Babl::Utils::Hash.deep_merge(node.dependencies, path)
25
+ end
26
+
27
+ private
28
+
29
+ def canonicalize(path)
30
+ case path
31
+ when ::Array then path.reduce({}) { |a, p| a.merge(canonicalize(p)) }
32
+ when ::Hash then path.map { |k, v| [k.to_sym, canonicalize(v)] }.to_h
33
+ else { path.to_sym => {} }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ require 'babl/schema/dyn_array'
2
+ require 'babl/errors'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class Each < Value.new(:node)
8
+ def dependencies
9
+ { __each__: node.dependencies }
10
+ end
11
+
12
+ def schema
13
+ Schema::DynArray.new(node.schema, false)
14
+ end
15
+
16
+ def pinned_dependencies
17
+ node.pinned_dependencies
18
+ end
19
+
20
+ def render(ctx)
21
+ collection = ctx.object
22
+ unless Enumerable === collection
23
+ raise Errors::RenderingError, "Not enumerable : #{collection}\n#{ctx.formatted_stack}"
24
+ end
25
+ collection.each_with_index.map { |value, idx| node.render(ctx.move_forward(value, idx)) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ require 'babl/utils/hash'
2
+ require 'babl/schema/fixed_array'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class FixedArray < Value.new(:nodes)
8
+ def schema
9
+ Schema::FixedArray.new(nodes.map(&:schema), false)
10
+ end
11
+
12
+ def dependencies
13
+ nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
14
+ end
15
+
16
+ def pinned_dependencies
17
+ nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
18
+ end
19
+
20
+ def render(ctx)
21
+ nodes.map { |node| node.render(ctx) }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ require 'babl/utils/hash'
2
+ require 'values'
3
+
4
+ module Babl
5
+ module Nodes
6
+ class GotoPin < Value.new(:node, :ref)
7
+ def dependencies
8
+ {}
9
+ end
10
+
11
+ def pinned_dependencies
12
+ Babl::Utils::Hash.deep_merge(node.pinned_dependencies, ref => node.dependencies)
13
+ end
14
+
15
+ def schema
16
+ node.schema
17
+ end
18
+
19
+ def render(ctx)
20
+ node.render(ctx.goto_pin(ref))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,17 +1,18 @@
1
1
  require 'singleton'
2
+ require 'babl/errors'
2
3
 
3
4
  module Babl
4
- module Rendering
5
- # This Node plays a role similar to TerminalValueNode, but it does not perform any
5
+ module Nodes
6
+ # This Node plays a role similar to TerminalValue, but it does not perform any
6
7
  # type checking on the produced object, which is allowed to be any Ruby object,
7
8
  # including non-serializable objects.
8
9
  #
9
10
  # It is used when the output is not rendered (conditions in #switch, values passed to block in #with, ...)
10
- class InternalValueNode
11
+ class InternalValue
11
12
  include Singleton
12
13
 
13
- def documentation
14
- :__value__
14
+ def schema
15
+ raise Errors::InvalidTemplateError, 'Internal nodes cannot be documented'
15
16
  end
16
17
 
17
18
  def dependencies
@@ -0,0 +1,102 @@
1
+ require 'babl/utils/hash'
2
+ require 'babl/errors'
3
+ require 'values'
4
+
5
+ module Babl
6
+ module Nodes
7
+ class Merge < Value.new(:nodes)
8
+ def initialize(nodes)
9
+ super
10
+ end
11
+
12
+ def dependencies
13
+ nodes.map(&:dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
14
+ end
15
+
16
+ def pinned_dependencies
17
+ nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
18
+ end
19
+
20
+ def schema
21
+ nodes.map(&:schema).reduce(Schema::Object::EMPTY) { |a, b| merge_doc(a, b) }
22
+ end
23
+
24
+ def render(ctx)
25
+ nodes.map { |node| node.render(ctx) }.compact.reduce({}) { |acc, val|
26
+ raise Errors::RenderingError, "Only objects can be merged\n" + ctx.formatted_stack unless ::Hash === val
27
+ acc.merge!(val)
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ # Merge two documentations together
34
+ def merge_doc(doc1, doc2)
35
+ doc1 = Schema::Object::EMPTY if Schema::Static::NULL == doc1
36
+ doc2 = Schema::Object::EMPTY if Schema::Static::NULL == doc2
37
+
38
+ case
39
+ when Schema::AnyOf === doc1 || Schema::AnyOf === doc2
40
+ merge_extended(doc1, doc2)
41
+ when Schema::Object === doc1 && Schema::Object === doc2
42
+ merge_object(doc1, doc2)
43
+ when Schema::Object === doc1 && Schema::Anything === doc2
44
+ merge_object(doc1, Schema::Object::EMPTY_WITH_ADDITIONAL)
45
+ when Schema::Anything === doc1 && Schema::Object === doc2
46
+ merge_object(Schema::Object::EMPTY_WITH_ADDITIONAL, doc2)
47
+ else
48
+ raise Errors::InvalidTemplateError, 'Only objects can be merged'
49
+ end
50
+ end
51
+
52
+ # Merge two documention when Schema::AnyOf is involved either
53
+ # on left, right or both sides.
54
+ def merge_extended(doc1, doc2)
55
+ # Ensure doc1 & doc2 are both Schema::AnyOf
56
+ doc1ext = Schema::AnyOf.new([doc1])
57
+ doc2ext = Schema::AnyOf.new([doc2])
58
+
59
+ # Generate all possible combinations
60
+ all_docs = doc1ext.choices.product(doc2ext.choices)
61
+ .map { |choice1, choice2| merge_doc(choice1, choice2) }
62
+
63
+ # Analyze each property accross all combination to
64
+ # generate a Schema::Object::Property filled with
65
+ # accurate information.
66
+ final_properties = all_docs.flat_map(&:properties)
67
+ .group_by(&:name)
68
+ .map do |name, properties|
69
+ Schema::Object::Property.new(
70
+ name,
71
+ Schema::AnyOf.new(properties.map(&:value)).simplify,
72
+ properties.size == all_docs.size && properties.all?(&:required)
73
+ )
74
+ end
75
+
76
+ # Generate the final Schema::Object
77
+ Schema::Object.new(final_properties, all_docs.any?(&:additional), false)
78
+ end
79
+
80
+ # Merge two Schema::Object
81
+ def merge_object(doc1, doc2)
82
+ additional = doc1.additional || doc2.additional
83
+
84
+ properties = (
85
+ doc1.properties.map { |property| doc2.additional ? allow_anything(property) : property } +
86
+ doc2.properties
87
+ ).each_with_object({}) { |property, acc| acc[property.name] = property }.values
88
+
89
+ Schema::Object.new properties, additional, false
90
+ end
91
+
92
+ # Rewrite a property to allow Schema::Anything as value
93
+ def allow_anything(property)
94
+ Schema::Object::Property.new(
95
+ property.name,
96
+ Schema::AnyOf.new([property.value, Schema::Anything.instance]).simplify,
97
+ property.required
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,33 @@
1
+ require 'values'
2
+
3
+ module Babl
4
+ module Nodes
5
+ class Nav < Value.new(:through, :node)
6
+ def dependencies
7
+ { through => node.dependencies }
8
+ end
9
+
10
+ def schema
11
+ node.schema
12
+ end
13
+
14
+ def pinned_dependencies
15
+ node.pinned_dependencies
16
+ end
17
+
18
+ def render(ctx)
19
+ node.render(ctx.move_forward_block(through) { navigate(ctx.object) })
20
+ end
21
+
22
+ private
23
+
24
+ def navigate(object)
25
+ if ::Hash === object
26
+ object.fetch(through)
27
+ else
28
+ object.send(through)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end