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