babl-json 0.1.4 → 0.2.0

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