json-schema_dsl 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.gitlab-ci.yml +30 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +13 -0
- data/.solargraph.yml +15 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +114 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/json-schema_dsl.gemspec +50 -0
- data/lib/json/schema_dsl.rb +116 -0
- data/lib/json/schema_dsl/array.rb +16 -0
- data/lib/json/schema_dsl/ast_node.rb +54 -0
- data/lib/json/schema_dsl/boolean.rb +11 -0
- data/lib/json/schema_dsl/builder.rb +208 -0
- data/lib/json/schema_dsl/configuration.rb +8 -0
- data/lib/json/schema_dsl/entity.rb +54 -0
- data/lib/json/schema_dsl/integer.rb +16 -0
- data/lib/json/schema_dsl/null.rb +11 -0
- data/lib/json/schema_dsl/numeric.rb +22 -0
- data/lib/json/schema_dsl/object.rb +15 -0
- data/lib/json/schema_dsl/proxy.rb +10 -0
- data/lib/json/schema_dsl/renderer.rb +48 -0
- data/lib/json/schema_dsl/renderers/alias.rb +40 -0
- data/lib/json/schema_dsl/renderers/base.rb +41 -0
- data/lib/json/schema_dsl/renderers/desugar.rb +87 -0
- data/lib/json/schema_dsl/renderers/filter.rb +34 -0
- data/lib/json/schema_dsl/renderers/multiplexer.rb +59 -0
- data/lib/json/schema_dsl/string.rb +15 -0
- data/lib/json/schema_dsl/types.rb +12 -0
- data/lib/json/schema_dsl/version.rb +7 -0
- metadata +265 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
# The basic entity type for json schemas.
|
6
|
+
#
|
7
|
+
# This is mostly used in cases where you don't exactly know what type a property will have,
|
8
|
+
# for example if the property is an `anyOf` of different types.
|
9
|
+
#
|
10
|
+
# Internally it is used as the superclass of all other types.
|
11
|
+
class Entity < Dry::Struct
|
12
|
+
include AstNode
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# nodoc
|
16
|
+
def required_type
|
17
|
+
(Types::Bool | Types::Coercible::Array.of(Types::Coercible::String).default { [] })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attribute(:enum, Types::Coercible::Array.default { [] })
|
22
|
+
attribute(:all_of, Types::Coercible::Array.default { [] })
|
23
|
+
attribute(:any_of, Types::Coercible::Array.default { [] })
|
24
|
+
attribute(:one_of, Types::Coercible::Array.default { [] })
|
25
|
+
attribute(:children, Types::Coercible::Array.default { [] })
|
26
|
+
attribute?(:nullable, Types::Bool.default(false))
|
27
|
+
attribute?(:name, (Types.Instance(Regexp) | Types::Coercible::String))
|
28
|
+
attribute?(:type, Types::Coercible::String)
|
29
|
+
attribute?(:title, Types::Coercible::String)
|
30
|
+
attribute?(:description, Types::Coercible::String)
|
31
|
+
attribute?(:default, Types::Coercible::String)
|
32
|
+
attribute?(:required, required_type)
|
33
|
+
attribute?(:not_a, Types::String)
|
34
|
+
attribute?(:ref, Types::String)
|
35
|
+
attribute?(:definitions, Types::String)
|
36
|
+
|
37
|
+
# @return [Hash<Symbol, Object>] Returns this entity as a hash and all children and
|
38
|
+
# properties as simple values. This structure is used to render the eventual
|
39
|
+
# schema by the renderer.
|
40
|
+
# @see JSON::SchemaDsl::Rederer#initialize
|
41
|
+
def to_h
|
42
|
+
super.transform_values do |v|
|
43
|
+
is_array = v.is_a?(::Array)
|
44
|
+
if (is_array ? v.first : v).respond_to?(:to_h)
|
45
|
+
is_array ? v.map(&:to_h) : v.to_h
|
46
|
+
else
|
47
|
+
v
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
delegate :as_json, to: :to_h
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
# The primitive integer type for json schema.
|
6
|
+
#
|
7
|
+
# @see https://json-schema.org/understanding-json-schema/reference/numeric.html
|
8
|
+
class Integer < Entity
|
9
|
+
attribute?(:multiple_of, Types::Integer)
|
10
|
+
attribute?(:minimum, Types::Integer)
|
11
|
+
attribute?(:maximum, Types::Integer)
|
12
|
+
attribute?(:exclusive_minimum, Types::Integer)
|
13
|
+
attribute?(:exclusive_maximum, Types::Integer)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
# Number primitive of Json-Schema
|
6
|
+
#
|
7
|
+
# @see https://json-schema.org/understanding-json-schema/reference/numeric.html#number
|
8
|
+
class Numeric < Entity
|
9
|
+
attribute?(:multiple_of, Types::Integer)
|
10
|
+
attribute?(:minimum, Types::Integer)
|
11
|
+
attribute?(:maximum, Types::Integer)
|
12
|
+
attribute?(:exclusive_minimum, Types::Integer)
|
13
|
+
attribute?(:exclusive_maximum, Types::Integer)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Type alias of numeric.
|
17
|
+
#
|
18
|
+
# @see https://json-schema.org/understanding-json-schema/reference/numeric.html#number
|
19
|
+
class Number < Numeric
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
# Object type of JSON-Schema
|
6
|
+
#
|
7
|
+
# @see https://json-schema.org/understanding-json-schema/reference/object.html
|
8
|
+
class Object < Entity
|
9
|
+
attribute(:pattern_properties, Types::Array.of(Types.Instance(Regexp)).default { [] })
|
10
|
+
attribute?(:min_properties, Types::Integer)
|
11
|
+
attribute?(:max_properties, Types::Integer)
|
12
|
+
attribute?(:additional_properties, Types::Bool)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
%w[base desugar multiplexer alias filter].each do |file|
|
4
|
+
require "json/schema_dsl/renderers/#{file}"
|
5
|
+
end
|
6
|
+
|
7
|
+
module JSON
|
8
|
+
module SchemaDsl
|
9
|
+
# Main entry point for the rendering chain of JSON::SchemaDsl.
|
10
|
+
#
|
11
|
+
# This will in turn apply the registered renderers one by one, updating the
|
12
|
+
# entity-tree to result in json-schema.
|
13
|
+
class Renderer
|
14
|
+
attr_reader :entity, :scope
|
15
|
+
|
16
|
+
# @param [Entity] entity The root entity-tree of this render run.
|
17
|
+
# @param [Object] scope Used as a fallback for renderers that need access
|
18
|
+
# to helper methods.
|
19
|
+
def initialize(entity, scope = nil)
|
20
|
+
@entity = entity.to_h
|
21
|
+
@scope = scope
|
22
|
+
end
|
23
|
+
|
24
|
+
# @see #render
|
25
|
+
def self.render(entity)
|
26
|
+
new(entity).render
|
27
|
+
end
|
28
|
+
|
29
|
+
# Applies the renderer chain in turn to produce valid json-schema.
|
30
|
+
# Each renderer traverses the whole tree before passing the resulting structure to
|
31
|
+
# the next renderer
|
32
|
+
# @return [Hash] The resulting json schema structure after each render is applied.
|
33
|
+
def render
|
34
|
+
render_chain.inject(entity) do |structure, renderer|
|
35
|
+
renderer.new(scope).visit(structure)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @return [Array<Class>] Each rendering class that will be applied to the given entity.
|
42
|
+
# @see ::JSON::SchemaDsl.registered_renderers
|
43
|
+
def render_chain
|
44
|
+
::JSON::SchemaDsl.registered_renderers
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
module Renderers
|
6
|
+
# Aliases certain attributes and camel-cases all others.
|
7
|
+
# The only exception are property names which are set by the user and
|
8
|
+
# will not be camel-cased.
|
9
|
+
class Alias < Base
|
10
|
+
ALIASES = {
|
11
|
+
'ref' => '$ref'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
# Camel-case and/or alias the attribute names of the given structure.
|
15
|
+
def visit(entity)
|
16
|
+
traverse(entity
|
17
|
+
.transform_keys { |key| ALIASES[key.to_s]&.to_sym || key }
|
18
|
+
.transform_keys { |key| camelize_snake_cased(key) })
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def camelize_snake_cased(key)
|
24
|
+
key = key.to_s
|
25
|
+
(key.capitalize == key ? key : key.camelize(:lower)).to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def traverse(entity)
|
29
|
+
entity.map do |key, value|
|
30
|
+
if key.to_s.match?(/properties$/i) && value.is_a?(Hash)
|
31
|
+
[key, value.transform_values { |v| visit(v) }]
|
32
|
+
else
|
33
|
+
[key, step(value)]
|
34
|
+
end
|
35
|
+
end.to_h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
module Renderers
|
6
|
+
# The abstract base renderer that provides common behaviour
|
7
|
+
# to all renderers like the depth-first-traversal and
|
8
|
+
# access to the scope.
|
9
|
+
# @abstract
|
10
|
+
class Base
|
11
|
+
attr_reader :scope
|
12
|
+
# @param [Object] scope The scope used as a fallback for helper methods.
|
13
|
+
def initialize(scope)
|
14
|
+
@scope = scope
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Hash] entity The entity-structure given as a tree.
|
18
|
+
# This method will recursively visit each value in the structure until
|
19
|
+
# all have been visited.
|
20
|
+
# @return [Hash] The hash-tree with all values visited.
|
21
|
+
def traverse(entity)
|
22
|
+
entity.transform_values { |v| step(v) }
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# @param [Object] value The value that should be visited. Behaves differently
|
28
|
+
# for each renderer, since #visit holds the core logic of each.
|
29
|
+
# @return [Object] The visited object.
|
30
|
+
def step(value)
|
31
|
+
case value
|
32
|
+
when ::Array
|
33
|
+
value.first.is_a?(Hash) ? value.map { |v| visit(v) } : value
|
34
|
+
when Hash then visit(value)
|
35
|
+
else value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
module Renderers
|
6
|
+
# By default the first renderer that visits the tree.
|
7
|
+
# This renderer will translate all kinds of `syntax sugar` to
|
8
|
+
# a uniform format that fits json-schema.
|
9
|
+
class Desugar < Base
|
10
|
+
# Desugars all syntax sugar in that entity. This resolves concepts
|
11
|
+
# like `children` and `nullable` that are not present in the json
|
12
|
+
# schema specification itself but are used by the builder to ease
|
13
|
+
# the writing of schemas. In turn it
|
14
|
+
#
|
15
|
+
# * Transforms children to properties
|
16
|
+
# * Translates the `:required` attribute to the parent if true.
|
17
|
+
# * Collapses the item attribute of arrays into a single entity.
|
18
|
+
def visit(entity)
|
19
|
+
traverse(expand_children(nullable(entity)))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def expand_children(entity)
|
25
|
+
return entity unless entity[:children]
|
26
|
+
return collapse_items(entity) if entity[:items]
|
27
|
+
|
28
|
+
entity
|
29
|
+
.merge(required_properties(entity))
|
30
|
+
.merge(properties_properties(entity))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Collapses the items into the first child for arrays.
|
34
|
+
def collapse_items(entity)
|
35
|
+
items = entity[:items]
|
36
|
+
items = items[:children].first if items[:children].to_a.count == 1
|
37
|
+
entity.merge(items: items)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Hash] entity An object entity-hash that has children.
|
41
|
+
# @return [Hash] The hash with children translated into properties and
|
42
|
+
# @todo Enable and fix rubocop warning about AbcSize
|
43
|
+
# rubocop:disable Metrics/AbcSize
|
44
|
+
def properties_properties(entity)
|
45
|
+
entity[:children]
|
46
|
+
.filter { |ch| ch[:name].present? }
|
47
|
+
.map { |ch| ch[:name] }
|
48
|
+
.zip(entity[:children].map { |c| visit(c) })
|
49
|
+
.map(&unrequire_property)
|
50
|
+
.group_by { |(name, _obj)| name.class }
|
51
|
+
.transform_keys { |k| k == Regexp ? :pattern_properties : :properties }
|
52
|
+
.transform_values(&:to_h)
|
53
|
+
.merge(children: nil)
|
54
|
+
end
|
55
|
+
# rubocop:enable Metrics/AbcSize
|
56
|
+
|
57
|
+
def unrequire_property
|
58
|
+
lambda do |(name, obj)|
|
59
|
+
obj = obj[:required] == true ? obj.merge(required: nil) : obj
|
60
|
+
[name, obj]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Translates the required-properties of children into the required array
|
65
|
+
# of the parent structure.
|
66
|
+
def required_properties(entity)
|
67
|
+
requireds = entity[:children]
|
68
|
+
.select { |ch| ch[:required] == true }
|
69
|
+
.map { |ch| ch[:name].to_s }
|
70
|
+
pre_req = entity[:required].is_a?(Array) ? entity[:required] : []
|
71
|
+
{ required: requireds | pre_req }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Translates nullable property into a any_of: [null...]
|
75
|
+
def nullable(entity)
|
76
|
+
return entity unless entity[:nullable]
|
77
|
+
|
78
|
+
entity.merge(nullable: nil, any_of: [{ type: 'null' }]) do |k, old, new|
|
79
|
+
next unless k == :any_of
|
80
|
+
|
81
|
+
old + new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
module Renderers
|
6
|
+
# Filters out properties that are either used internally only or
|
7
|
+
# which are redundant (I.e. set to nil).
|
8
|
+
class Filter < Base
|
9
|
+
INVISIBLES = %w[Children Nullable Name].freeze
|
10
|
+
|
11
|
+
# Filters out properties that are either used internally only or
|
12
|
+
# which are redundant (I.e. set to nil).
|
13
|
+
def visit(entity)
|
14
|
+
traverse(filter(entity))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def filter(entity)
|
20
|
+
entity
|
21
|
+
.except(*(INVISIBLES + INVISIBLES.map(&:underscore).map(&:to_sym)))
|
22
|
+
.transform_values { |v| presence_of(v, preserve: [false]) }
|
23
|
+
.compact
|
24
|
+
end
|
25
|
+
|
26
|
+
def presence_of(obj, preserve: [])
|
27
|
+
return obj if preserve.include? obj
|
28
|
+
|
29
|
+
obj.presence
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
module SchemaDsl
|
5
|
+
module Renderers
|
6
|
+
# Renderer that translates multiplexer properties (any_of, one_of or all_of)
|
7
|
+
# so that a new wrapping structure is returned that holds the original in
|
8
|
+
# one of these properties.
|
9
|
+
#
|
10
|
+
# @example Nullable object
|
11
|
+
# obj = object :james, any_of: [null]
|
12
|
+
# Multiplexer.new(nil).visit(obj)
|
13
|
+
# #=> { type: 'entity', any_of: [{type: 'null'}, {type: 'object', ...}] }
|
14
|
+
#
|
15
|
+
class Multiplexer < Base
|
16
|
+
KEYS = %i[any_of one_of all_of].freeze
|
17
|
+
|
18
|
+
# Boxes the entity into a new one with the multiplexer attribute set
|
19
|
+
# to the entity, if required.
|
20
|
+
def visit(entity)
|
21
|
+
traverse(box(entity))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Boxes the given entity unless it is of type 'entity' or has no
|
27
|
+
# multiplexer attributes. The entity itself will be added to the "box"
|
28
|
+
# if it has any other attributes set by the user.
|
29
|
+
# @param [Hash] entity The entity that may have a multiplexer attribute.
|
30
|
+
def box(entity)
|
31
|
+
present_key = entity[:type] != 'entity' && KEYS.find { |k| entity[k].present? }
|
32
|
+
return entity unless present_key
|
33
|
+
|
34
|
+
new_value = entity[present_key].map { |ch| visit(ch) }
|
35
|
+
unless container?(entity.except(present_key))
|
36
|
+
new_value.push(entity.except(present_key))
|
37
|
+
end
|
38
|
+
|
39
|
+
{ type: 'entity', present_key => new_value.reject(&:blank?) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Boolean] `true` if the object is only there to have the
|
43
|
+
# boxing attribute, false otherwise.
|
44
|
+
def container?(entity)
|
45
|
+
cleaned_up = clean_up(entity)
|
46
|
+
|
47
|
+
cleaned_up[:type].to_s == 'object' && cleaned_up.keys.count <= 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def clean_up(entity)
|
51
|
+
defaults = ::JSON::SchemaDsl.type_defaults[entity[:type].to_sym]
|
52
|
+
(entity.to_a - defaults.to_a).to_h.yield_self do |without_defaults|
|
53
|
+
Renderers::Filter.new(scope).visit(without_defaults)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|