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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
@@ -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,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
|