json-schema_dsl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'json/schema_dsl'
6
+ require 'pry'
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'json/schema_dsl/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'json-schema_dsl'
9
+ spec.version = Json::SchemaDsl::VERSION
10
+ spec.authors = ['Paul Martensen']
11
+ spec.email = ['paul.martensen@gmx.de']
12
+
13
+ spec.summary = 'A builder dsl to programatically build json-schemas'
14
+ spec.description = 'A builder dsl to programatically build
15
+ json-schemas that are composable and reusable.'
16
+ spec.homepage = 'https://github.com/lokalportal/json-schema_dsl'
17
+ spec.license = 'MIT'
18
+
19
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata['homepage_uri'] = spec.homepage
23
+ else
24
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
25
+ 'public gem pushes.'
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = 'exe'
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ['lib']
36
+
37
+ spec.add_dependency 'activesupport', '< 6.0'
38
+ spec.add_dependency 'docile', '< 2'
39
+ spec.add_dependency 'dry-struct', '~> 1.0'
40
+ spec.add_dependency 'dry-types', '~> 1.0'
41
+ spec.add_development_dependency 'bundler', '~> 2.0'
42
+ spec.add_development_dependency 'pry'
43
+ spec.add_development_dependency 'pry-byebug'
44
+ spec.add_development_dependency 'rake', '~> 10.0'
45
+ spec.add_development_dependency 'rspec', '~> 3.0'
46
+ spec.add_development_dependency 'rubocop', '0.74.0'
47
+ spec.add_development_dependency 'rubocop-rspec', '1.36.0'
48
+ spec.add_development_dependency 'spring'
49
+ spec.add_development_dependency 'stackprof'
50
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'active_support/core_ext/object'
5
+ require 'docile'
6
+ require 'dry-struct'
7
+ require 'dry-types'
8
+
9
+ require 'json/schema_dsl/version'
10
+ require 'json/schema_dsl/types'
11
+ require 'json/schema_dsl/configuration'
12
+ require 'json/schema_dsl/ast_node'
13
+ require 'json/schema_dsl/entity'
14
+
15
+ %w[null boolean numeric integer string object array].each do |type|
16
+ require "json/schema_dsl/#{type}"
17
+ end
18
+
19
+ require 'json/schema_dsl/builder'
20
+ require 'json/schema_dsl/renderer'
21
+ require 'json/schema_dsl/proxy'
22
+
23
+ module JSON
24
+ # This module provides the base that it includes with the methods to build new json-schemas.
25
+ module SchemaDsl
26
+ class Error < StandardError; end
27
+
28
+ class << self
29
+ delegate(:type_defaults,
30
+ :reset_type_defaults!,
31
+ :add_defaults_for,
32
+ to: ::JSON::SchemaDsl::Builder)
33
+
34
+ DEFAULT_TYPES = JSON::SchemaDsl::Entity
35
+ .descendants.dup.push(JSON::SchemaDsl::Entity).freeze
36
+ DEFAULT_RENDERERS = [Renderers::Desugar,
37
+ Renderers::Multiplexer,
38
+ Renderers::Alias,
39
+ Renderers::Filter].freeze
40
+
41
+ attr_writer :registered_renderers
42
+
43
+ # @return [Array<Class>] The renderer classes that schema_dsl will use in the renderer
44
+ def registered_renderers
45
+ @registered_renderers ||= DEFAULT_RENDERERS.dup
46
+ end
47
+
48
+ # Resets the registered_renderers to the default settings
49
+ # @return [Array<Class>] The renderer classes that schema_dsl will use in the renderer
50
+ def reset_registered_renderers!
51
+ @registered_renderers = DEFAULT_RENDERERS.dup
52
+ end
53
+
54
+ # @return [Array<Class>] The registered types. These are used to add new dsl
55
+ # and builder methods.
56
+ def registered_types
57
+ @registered_types ||= DEFAULT_TYPES.dup
58
+ end
59
+
60
+ # @param [Class] type A new type to be registered. This will define new builder and dsl
61
+ # methods for that type.
62
+ # @return [Array<Class>] The registered types.
63
+ def register_type(type)
64
+ registered_types.push(type).tap { define_type_methods(type) }
65
+ end
66
+
67
+ # Resets schema_dsl back to default. Removes all dsl methods and redefines
68
+ # them with the default types.
69
+ def reset_schema_dsl!
70
+ type_methods.each { |tm| remove_method tm }
71
+ @registered_types = DEFAULT_TYPES.dup
72
+ define_schema_dsl!
73
+ end
74
+
75
+ # Defines the dsl for all registered types.
76
+ def define_schema_dsl!
77
+ registered_types.map { |t| define_type_methods(t) }
78
+ end
79
+
80
+ # Defines builder methods for the given type.
81
+ # @param [Class] type A class that is a {JSON::SchemaDsl::AstNode}
82
+ def define_type_methods(type)
83
+ JSON::SchemaDsl::Builder.define_builder_method(type)
84
+ builder = JSON::SchemaDsl::Builder[type]
85
+ define_method(type_method_name(type)) do |name = nil, **attributes, &block|
86
+ builder.build(name, **attributes, scope: self, &block)
87
+ end
88
+ end
89
+
90
+ # Reset all settings to default.
91
+ def reset!
92
+ reset_registered_renderers!
93
+ reset_type_defaults!
94
+ reset_schema_dsl!
95
+ end
96
+
97
+ # @return [JSON::SchemaDsl::Proxy] a new proxy to build schemas.
98
+ def proxy
99
+ ::JSON::SchemaDsl::Proxy.new
100
+ end
101
+
102
+ # @return [Array<Symbol>] An array of all type methods
103
+ def type_methods
104
+ registered_types.map { |t| type_method_name(t).to_sym } & instance_methods
105
+ end
106
+
107
+ # @param [Class] type The class for which a method will be defined.
108
+ # @return [String] the name of the new method.
109
+ def type_method_name(type)
110
+ type.type_method_name || 'entity'
111
+ end
112
+ end
113
+
114
+ define_schema_dsl!
115
+ end
116
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON
4
+ module SchemaDsl
5
+ # Type that validates a json entity to be an array.
6
+ #
7
+ # @see https://json-schema.org/understanding-json-schema/reference/array.html
8
+ class Array < Entity
9
+ attribute?(:unique_items, Types::Bool)
10
+ attribute?(:additional_items, Types::Bool)
11
+ attribute?(:min_items, Types::Bool)
12
+ attribute?(:max_items, Types::Bool)
13
+ attribute?(:items, Types::Any)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON
4
+ module SchemaDsl
5
+ # Methods for an object to be used as an ast node by the renderer
6
+ # Include this module to define your own types that are not descendants of
7
+ # Entity. You should still implement two methods to be compatible with the
8
+ # normal builder class:
9
+ #
10
+ # #initialize: Hash -> Self
11
+ # #to_h: Self -> Hash
12
+ # .has_attribute?: Symbol -> Boolean
13
+ module AstNode
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ # @param [Symbol] attribute_name The name of the attribute to update.
19
+ # @param [Object] value The value that will be set for the attribute.
20
+ # @return [Entity] Since entities themselves are immutable, this method returns a new
21
+ # entity with the attribute_name and value pair added.
22
+ def update(attribute_name, value = nil)
23
+ self.class.new(to_h.merge(attribute_name => value))
24
+ end
25
+
26
+ # Used to do a simple render of the entity. Since this has no sensible scope while
27
+ # rendering, use Builder#render instead.
28
+ # @see JSON::SchemaDsl::Builder#render
29
+ def render
30
+ ::JSON::SchemaDsl::Renderer.new(self).render
31
+ end
32
+
33
+ # The class methods that ast nodes should have
34
+ module ClassMethods
35
+ # @return [String] The type that will be used in I.E. `type: 'object'` attributes.
36
+ # Also used to give names to the dsl and builder methods.
37
+ def infer_type
38
+ type = name.split('::').last.underscore
39
+ type == 'entity' ? nil : type
40
+ end
41
+
42
+ # @method! type_method_name
43
+ # Override this method to set the name of the dsl method for this type.
44
+ alias type_method_name infer_type
45
+
46
+ # Override this to set a custom builder for your type.
47
+ # @return [Class] A new builder class for this type.
48
+ def builder
49
+ ::JSON::SchemaDsl::Builder.define_builder(self)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON
4
+ module SchemaDsl
5
+ # Boolean primitive of json schema.
6
+ #
7
+ # @see https://json-schema.org/understanding-json-schema/reference/boolean.html
8
+ class Boolean < Entity
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON
4
+ module SchemaDsl
5
+ # Builders are used to build entity-structs. They handle building this raw data so
6
+ # it can then be given to the renderers which modify the structure to be valid json-schema.
7
+ #
8
+ # Each type has an associated Builder that is a dynamically generated class.
9
+ # Since entity-structs are immutable, the builder updates the struct and keeps track of the
10
+ # latest version of the struct.
11
+ #
12
+ # Entity definitions can mostly be treated as data input while most of the logic of building
13
+ # the entity tree resides in the builders.
14
+ # @todo Refactor class and remove rubocop exception
15
+ # rubocop:disable Metrics/ClassLength
16
+ class Builder
17
+ class << self
18
+ attr_accessor :inner_class
19
+
20
+ # @param [Class] klass A class that is a subclass of {JSON::SchemaDsl::Entity}.
21
+ # @return A new builder class that subclasses {JSON::SchemaDsl::Builder}
22
+ def [](klass)
23
+ raise ArgumentError, "#{klass} is not a struct." unless klass < AstNode
24
+
25
+ registered_builders[klass] ||= klass.builder
26
+ end
27
+
28
+ # @return [Array<JSON::SchemaDsl::Builder>] All builders that have been
29
+ # registered so far. Usually builders get automatically registered when
30
+ # the associated type is registered.
31
+ def registered_builders
32
+ @registered_builders ||= {}
33
+ end
34
+
35
+ # @return [Hash<Symbol, Hash>] Defaults that are applied when a new struct of a
36
+ # given type is contsructed. The type symbol is the key and the defaults the value
37
+ # of this hash.
38
+ def type_defaults
39
+ @type_defaults ||= Hash.new { {} }
40
+ end
41
+
42
+ # Clears the registered type defaults and returns an empty hash.
43
+ # @return [Hash<Symbol, Hash>]
44
+ # @see #type_defaults
45
+ def reset_type_defaults!
46
+ type_defaults.clear
47
+ end
48
+
49
+ # Adds new defaults for the given type.
50
+ # @param [Symbol] type The type symbol for the type. Usually the name underscored and
51
+ # symbolized. I.e. {JSON::SchemaDsl::Object} => `:object`.
52
+ # @param [Hash<Symbol, Object>] defaults New defaults that will be merged to
53
+ # the existing ones.
54
+ # @return [Hash<Symbol, Hash>]
55
+ # @see #type_defaults
56
+ def add_defaults_for(type, defaults)
57
+ if type_defaults[type].empty?
58
+ type_defaults[type] = type_defaults[type].merge(defaults)
59
+ else
60
+ type_defaults[type].merge!(defaults)
61
+ end
62
+ end
63
+
64
+ # Instantiates a new builder instance with a corresponding entity and
65
+ # applies the attributes and block to construct a complete entity.
66
+ # @param [#to_s] name The name of the new entity. This is important for the entity
67
+ # to be added to properties later. Usually is the name of a property or the pattern
68
+ # for a pattern property.
69
+ # @param [Object] scope The scope will be used as a fallback to evaluate the block.
70
+ # If there are any methods that the block does not understand, the scope will
71
+ # be called instead.
72
+ # @param [Hash] attributes The initial attributes that the entity will start with
73
+ # before the block is applied.
74
+ # @param [Proc] block Will be evaluated in the context of the builder. Should contain
75
+ # setter methods.
76
+ #
77
+ def build(name = nil, scope: nil, **attributes, &block)
78
+ type = (attributes[:type] || inner_class.infer_type)&.to_sym
79
+ defaults = ::JSON::SchemaDsl::Builder
80
+ .type_defaults[type].merge(name: name, type: type)
81
+ builder = new(inner_class.new(defaults), scope: scope)
82
+ Docile.dsl_eval(builder, &config_block(attributes, &block))
83
+ end
84
+
85
+ # nodoc
86
+ def inspect
87
+ "#<#{class_name} inner_class=#{inner_class}>"
88
+ end
89
+
90
+ # nodoc
91
+ def class_name
92
+ name || inner_class.name + 'Builder'
93
+ end
94
+
95
+ # Defines a new method for the builder instance that mirrors the dsl method
96
+ # for the given type.
97
+ # @param [Class] type A class that is a subclass of {JSON::SchemaDsl::Entity}.
98
+ def define_builder_method(type)
99
+ type_param = type.type_method_name || 'entity'
100
+ define_method(type_param) do |name = nil, **attributes, &block|
101
+ new_child = build_struct(type, name, **attributes, &block)
102
+ add_child(new_child)
103
+ end
104
+ end
105
+
106
+ # @param [Class] klass A class that is a subclass of {JSON::SchemaDsl::Entity}.
107
+ # @return A new builder class that subclasses {JSON::SchemaDsl::Builder}
108
+ # @see #[]
109
+ def define_builder(klass)
110
+ Class.new(self) do
111
+ self.inner_class = klass
112
+ klass.schema.keys.map(&:name).each do |name|
113
+ define_method(name) do |*args, **opts, &block|
114
+ set(name, *args, **opts, &block)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ # Combines a set of attributes and a block into a single proc.
123
+ def config_block(attributes, &block)
124
+ proc do
125
+ attributes.each { |k, v| send(k, v) }
126
+ instance_exec(&block) if block_given?
127
+ end
128
+ end
129
+ end
130
+
131
+ attr_reader :inner, :scope
132
+ delegate :as_json, to: :render
133
+ delegate :to_h, to: :inner
134
+
135
+ # @param [JSON::SchemaDsl::Entity] inner The struct that the builder is supposed
136
+ # to update and build up.
137
+ # @param [Object] scope The scope is used for as a fallback for helper methods.
138
+ # @see JSON::SchemaDsl::Builder.build
139
+ def initialize(inner, scope: nil)
140
+ @inner = inner
141
+ @scope = scope
142
+ end
143
+
144
+ # Renders the given tree structure into a hash. Note that this hash still has symbol keys.
145
+ # The scope used for the render is the same as the builder.
146
+ # @see JSON::SchemaDsl::Renderer#render
147
+ def render
148
+ ::JSON::SchemaDsl::Renderer.new(inner, scope).render
149
+ end
150
+
151
+ private
152
+
153
+ def update(type, *args)
154
+ args = args.first if args.count == 1 && args.is_a?(::Array)
155
+ @inner = if args.is_a?(::Array)
156
+ inner.update(type, *args)
157
+ else
158
+ inner.update(type, args)
159
+ end
160
+ end
161
+
162
+ def build_struct(type, name = nil, **attributes, &block)
163
+ builder = self.class[type || attributes[:type].constantize]
164
+ builder.build(name, **attributes, scope: scope, &block)
165
+ end
166
+
167
+ def method_missing(meth, *args, &block)
168
+ return super unless scope&.respond_to?(meth, true)
169
+
170
+ maybe_child = scope.send(meth, *args, &block)
171
+ maybe_child.respond_to?(:render) &&
172
+ add_child(maybe_child)
173
+ maybe_child
174
+ end
175
+
176
+ def respond_to_missing?(meth, priv)
177
+ return super unless scope
178
+
179
+ scope.respond_to?(meth, priv)
180
+ end
181
+
182
+ def inspect
183
+ "#<#{self.class.class_name} \n scope = #{scope}\n inner = #{inner}> "
184
+ end
185
+
186
+ def set(name, *args, **opts, &block)
187
+ args = extract_args(name, args, opts, &block)
188
+ return inner.send(name) unless args
189
+
190
+ @inner = update(name, args)
191
+ end
192
+
193
+ def extract_args(name, args, opts, &block)
194
+ if block.present? || !inner.class.has_attribute?(name) || opts[:type].present?
195
+ return build_struct(Object, **opts, &block)
196
+ end
197
+
198
+ args.presence || opts.presence
199
+ end
200
+
201
+ def add_child(child)
202
+ children(children | [child])
203
+ child
204
+ end
205
+ end
206
+ # rubocop:enable Metrics/ClassLength
207
+ end
208
+ end