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
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
|