bronze 0.0.1.alpha → 0.1.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 +5 -5
- data/CHANGELOG.md +31 -0
- data/DEVELOPMENT.md +78 -0
- data/README.md +801 -14
- data/lib/bronze.rb +2 -11
- data/lib/bronze/entities.rb +10 -0
- data/lib/bronze/entities/attributes.rb +241 -0
- data/lib/bronze/entities/attributes/builder.rb +249 -0
- data/lib/bronze/entities/attributes/metadata.rb +87 -0
- data/lib/bronze/entities/normalization.rb +69 -0
- data/lib/bronze/entities/primary_key.rb +70 -0
- data/lib/bronze/entities/primary_keys.rb +8 -0
- data/lib/bronze/entities/primary_keys/uuid.rb +44 -0
- data/lib/bronze/entity.rb +14 -0
- data/lib/bronze/not_implemented_error.rb +18 -0
- data/lib/bronze/transform.rb +29 -0
- data/lib/bronze/transforms.rb +9 -0
- data/lib/bronze/transforms/attributes.rb +9 -0
- data/lib/bronze/transforms/attributes/big_decimal_transform.rb +40 -0
- data/lib/bronze/transforms/attributes/date_time_transform.rb +60 -0
- data/lib/bronze/transforms/attributes/date_transform.rb +58 -0
- data/lib/bronze/transforms/attributes/symbol_transform.rb +36 -0
- data/lib/bronze/transforms/attributes/time_transform.rb +38 -0
- data/lib/bronze/transforms/entities.rb +9 -0
- data/lib/bronze/transforms/entities/normalize_transform.rb +52 -0
- data/lib/bronze/transforms/identity_transform.rb +31 -0
- data/lib/bronze/version.rb +12 -11
- metadata +70 -41
data/lib/bronze.rb
CHANGED
@@ -1,14 +1,5 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
require 'sleeping_king_studios/tools/all'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
3
|
# A component-based application toolkit designed around dependency injection,
|
6
4
|
# composable objects, and modern design principles.
|
7
|
-
module Bronze
|
8
|
-
# The file path to the root of the Bronze directory.
|
9
|
-
def self.gem_path
|
10
|
-
@gem_path ||= __dir__.sub %r{/lib\z}, ''
|
11
|
-
end # method
|
12
|
-
end # module
|
13
|
-
|
14
|
-
require 'bronze/version'
|
5
|
+
module Bronze; end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze'
|
4
|
+
|
5
|
+
module Bronze
|
6
|
+
# Namespace for library classes and modules that implement or enhance data
|
7
|
+
# entities, which store information about business objects in a datastore-
|
8
|
+
# independent implementation.
|
9
|
+
module Entities; end
|
10
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tools/toolbox/mixin'
|
4
|
+
|
5
|
+
require 'bronze/entities'
|
6
|
+
require 'bronze/entities/attributes/builder'
|
7
|
+
|
8
|
+
module Bronze::Entities
|
9
|
+
# Module for defining attributes on an entity class.
|
10
|
+
module Attributes
|
11
|
+
extend SleepingKingStudios::Tools::Toolbox::Mixin
|
12
|
+
|
13
|
+
# Class methods to define when including Attributes in a class.
|
14
|
+
module ClassMethods
|
15
|
+
# Defines an attribute with the specified name and type.
|
16
|
+
#
|
17
|
+
# @example Defining an Attribute
|
18
|
+
# class Book
|
19
|
+
# include Bronze::Entities::Attributes
|
20
|
+
#
|
21
|
+
# attribute :title, String
|
22
|
+
# end # class
|
23
|
+
#
|
24
|
+
# book.title
|
25
|
+
# #=> nil
|
26
|
+
#
|
27
|
+
# book.title = 'Romance of the Three Kingdoms'
|
28
|
+
# book.title
|
29
|
+
# #=> 'Romance of the Three Kingdoms'
|
30
|
+
#
|
31
|
+
# @param (see Attributes::Builder#build)
|
32
|
+
#
|
33
|
+
# @option (see Attributes::Builder#build)
|
34
|
+
#
|
35
|
+
# @return (see Attributes::Builder#build)
|
36
|
+
#
|
37
|
+
# @raise (see Attributes::Builder#build)
|
38
|
+
def attribute(attribute_name, attribute_type, attribute_options = {})
|
39
|
+
metadata =
|
40
|
+
build_attribute(attribute_name, attribute_type, attribute_options)
|
41
|
+
|
42
|
+
(@attributes ||= {})[metadata.name] = metadata
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the metadata for the attributes defined for the current class.
|
46
|
+
#
|
47
|
+
# @return [Hash{Symbol => Attributes::Metadata}] the metadata for each
|
48
|
+
# attribute.
|
49
|
+
#
|
50
|
+
# @note This method allocates a new hash each time it is called and is not
|
51
|
+
# cached. To loop through the attributes, use the ::each_attribute
|
52
|
+
# method instead.
|
53
|
+
def attributes
|
54
|
+
each_attribute
|
55
|
+
.with_object({}) do |(name, metadata), hsh|
|
56
|
+
hsh[name] = metadata
|
57
|
+
end
|
58
|
+
.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
# @overload each_attribute
|
62
|
+
# Returns an enumerator that iterates through the attributes defined on
|
63
|
+
# the entity class and any parent classes.
|
64
|
+
#
|
65
|
+
# @return [Enumerator] the enumerator.
|
66
|
+
#
|
67
|
+
# @overload each_attribute
|
68
|
+
# Iterates through the attributes defined on the entity class and any
|
69
|
+
# parent classes, and yields the name and metadata of each attribute to
|
70
|
+
# the block.
|
71
|
+
#
|
72
|
+
# @yieldparam name [Symbol] The name of the attribute.
|
73
|
+
# @yieldparam name [Bronze::Entities::Attributes::Metadata] The metadata
|
74
|
+
# for the attribute.
|
75
|
+
def each_attribute
|
76
|
+
return enum_for(:each_attribute) unless block_given?
|
77
|
+
|
78
|
+
if superclass.respond_to?(:each_attribute)
|
79
|
+
superclass.each_attribute { |name, metadata| yield(name, metadata) }
|
80
|
+
end
|
81
|
+
|
82
|
+
(@attributes ||= {}).each { |name, metadata| yield(name, metadata) }
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def attribute_builder
|
88
|
+
Bronze::Entities::Attributes::Builder.new(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_attribute(attribute_name, attribute_type, attribute_options)
|
92
|
+
attribute_builder
|
93
|
+
.build(attribute_name, attribute_type, attribute_options)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param attributes [Hash] The default attributes with which to initialize
|
98
|
+
# the entity. Defaults to an empty hash.
|
99
|
+
def initialize(attributes = {})
|
100
|
+
initialize_attributes(attributes)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Compares with the other object.
|
104
|
+
#
|
105
|
+
# If the other object is a Hash, returns true if the entity attributes hash
|
106
|
+
# is equal to the given hash. Otherwise, returns true if the other object
|
107
|
+
# has the same class and attributes as the entity.
|
108
|
+
#
|
109
|
+
# @param other [Bronze::Entities::Attributes, Hash] The object to compare.
|
110
|
+
#
|
111
|
+
# @return [Boolean] true if the other object matches the entity, otherwise
|
112
|
+
# false.
|
113
|
+
def ==(other)
|
114
|
+
return attributes == other if other.is_a?(Hash)
|
115
|
+
|
116
|
+
other.class == self.class && other.attributes == attributes
|
117
|
+
end
|
118
|
+
|
119
|
+
# Updates the attributes with the given hash. If an attribute is not in the
|
120
|
+
# hash, it is unchanged.
|
121
|
+
#
|
122
|
+
# @raise ArgumentError if one of the keys is not a valid attribute
|
123
|
+
def assign_attributes(hash)
|
124
|
+
validate_attributes(hash)
|
125
|
+
|
126
|
+
self.class.each_attribute do |name, metadata|
|
127
|
+
next if metadata.read_only?
|
128
|
+
next unless hash.key?(name) || hash.key?(name.to_s)
|
129
|
+
|
130
|
+
set_attribute(name, hash[name] || hash[name.to_s])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
alias_method :assign, :assign_attributes
|
134
|
+
|
135
|
+
# @return true if the entity has an attribute with the given name, otherwise
|
136
|
+
# false.
|
137
|
+
def attribute?(name)
|
138
|
+
attribute_name = name.intern
|
139
|
+
|
140
|
+
self.class.each_attribute.any? { |key, _metadata| key == attribute_name }
|
141
|
+
rescue NoMethodError
|
142
|
+
raise ArgumentError, "invalid attribute #{name.inspect}"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns the current value of each attribute.
|
146
|
+
#
|
147
|
+
# @return [Hash{Symbol => Object}] the attribute values.
|
148
|
+
def attributes
|
149
|
+
each_attribute.with_object({}) do |attr_name, hsh|
|
150
|
+
hsh[attr_name] = get_attribute(attr_name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Replaces the attributes with the given hash. If a non-primary key
|
155
|
+
# attribute is not in the hash, it is set to nil.
|
156
|
+
#
|
157
|
+
# @raise ArgumentError if one of the keys is not a valid attribute
|
158
|
+
def attributes=(hash)
|
159
|
+
validate_attributes(hash)
|
160
|
+
|
161
|
+
self.class.each_attribute do |name, metadata|
|
162
|
+
next if metadata.primary_key?
|
163
|
+
|
164
|
+
@attributes[name] = hash[name] || hash[name.to_s]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# @param name [String] The name of the attribute.
|
169
|
+
#
|
170
|
+
# @return [Object] the value of the given attribute.
|
171
|
+
#
|
172
|
+
# @raise ArgumentError when the attribute name is not a valid attribute
|
173
|
+
def get_attribute(name)
|
174
|
+
unless attribute?(name)
|
175
|
+
raise ArgumentError, "invalid attribute #{name.inspect}"
|
176
|
+
end
|
177
|
+
|
178
|
+
@attributes[name.intern]
|
179
|
+
end
|
180
|
+
|
181
|
+
# @return [String] a human-readable representation of the entity, composed
|
182
|
+
# of the class name and the attribute keys and values.
|
183
|
+
def inspect # rubocop:disable Metrics/AbcSize
|
184
|
+
buffer = +'#<'
|
185
|
+
buffer << self.class.name
|
186
|
+
each_attribute.with_index do |(name, _metadata), index|
|
187
|
+
buffer << ',' unless index.zero?
|
188
|
+
buffer << ' ' << name.to_s << ': ' << get_attribute(name).inspect
|
189
|
+
end
|
190
|
+
buffer << '>'
|
191
|
+
end
|
192
|
+
|
193
|
+
# @param name [String] The name of the attribute.
|
194
|
+
# @param value [Object] The new value of the attribute.
|
195
|
+
#
|
196
|
+
# @return [Object] the new value of the given attribute.
|
197
|
+
#
|
198
|
+
# @raise ArgumentError when the attribute name is not a valid attribute
|
199
|
+
def set_attribute(name, value)
|
200
|
+
unless attribute?(name)
|
201
|
+
raise ArgumentError, "invalid attribute #{name.inspect}"
|
202
|
+
end
|
203
|
+
|
204
|
+
@attributes[name.intern] = value
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def each_attribute
|
210
|
+
return enum_for(:each_attribute) unless block_given?
|
211
|
+
|
212
|
+
self.class.each_attribute { |name, _metadata| yield(name) }
|
213
|
+
end
|
214
|
+
|
215
|
+
def initialize_attributes(data)
|
216
|
+
@attributes = {}
|
217
|
+
|
218
|
+
validate_attributes(data)
|
219
|
+
|
220
|
+
self.class.each_attribute do |name, metadata|
|
221
|
+
name = name.intern if name.is_a?(String)
|
222
|
+
value = data[name] || data[name.to_s] || metadata.default
|
223
|
+
|
224
|
+
@attributes[name] = value
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def validate_attributes(obj)
|
229
|
+
unless obj.is_a?(Hash)
|
230
|
+
raise ArgumentError,
|
231
|
+
"expected attributes to be a Hash, but was #{obj.inspect}"
|
232
|
+
end
|
233
|
+
|
234
|
+
obj.each_key do |name|
|
235
|
+
unless attribute?(name)
|
236
|
+
raise ArgumentError, "invalid attribute #{name.inspect}"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze/entities'
|
4
|
+
require 'bronze/entities/attributes/metadata'
|
5
|
+
require 'bronze/transforms/attributes/big_decimal_transform'
|
6
|
+
require 'bronze/transforms/attributes/date_time_transform'
|
7
|
+
require 'bronze/transforms/attributes/date_transform'
|
8
|
+
require 'bronze/transforms/attributes/symbol_transform'
|
9
|
+
require 'bronze/transforms/attributes/time_transform'
|
10
|
+
|
11
|
+
module Bronze::Entities::Attributes
|
12
|
+
# Service class to define attributes on an entity.
|
13
|
+
class Builder # rubocop:disable Metrics/ClassLength
|
14
|
+
# Provides a list of the valid options for the attribute_options parameter
|
15
|
+
# for Builder#build.
|
16
|
+
VALID_OPTIONS = %w[
|
17
|
+
allow_nil
|
18
|
+
default
|
19
|
+
default_transform
|
20
|
+
foreign_key
|
21
|
+
primary_key
|
22
|
+
read_only
|
23
|
+
transform
|
24
|
+
].map(&:freeze).freeze
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Registers a transform as the default transform for attributes with the
|
28
|
+
# specified type or a subtype of the specified type.
|
29
|
+
#
|
30
|
+
# This default is not retroactive - any attributes already defined will
|
31
|
+
# use their existing default transform, if any. If more than one
|
32
|
+
# registered transform has a matching type, the most recently defined
|
33
|
+
# transform will be used.
|
34
|
+
#
|
35
|
+
# @param type [Class] The attribute type. When defining an attribute, if
|
36
|
+
# the type of the attribute is this class or a subclass of this class
|
37
|
+
# and no :transform option is given, the transform for the attribute
|
38
|
+
# will be the transform passed to ::attribute_transform.
|
39
|
+
# @param transform [Class, Bronze::Transforms::Transform] The transform to
|
40
|
+
# use as the default. If this value is a transform instance, the default
|
41
|
+
# transform for matching attributes will be the given transform.
|
42
|
+
# Otherwise, will set the default transform to the result of ::instance
|
43
|
+
# (if defined) or ::new.
|
44
|
+
def attribute_transform(type, transform)
|
45
|
+
(@attribute_transforms ||= {})[type] = transform
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def transform_for_attribute(attribute_type)
|
51
|
+
(@attribute_transforms ||= {}).reverse_each do |type, transform|
|
52
|
+
return transform if attribute_type <= type
|
53
|
+
end
|
54
|
+
|
55
|
+
return super if superclass.respond_to?(:transform_for_attribute)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attribute_transform BigDecimal,
|
60
|
+
Bronze::Transforms::Attributes::BigDecimalTransform
|
61
|
+
|
62
|
+
attribute_transform Date,
|
63
|
+
Bronze::Transforms::Attributes::DateTransform
|
64
|
+
|
65
|
+
attribute_transform DateTime,
|
66
|
+
Bronze::Transforms::Attributes::DateTimeTransform
|
67
|
+
|
68
|
+
attribute_transform Symbol,
|
69
|
+
Bronze::Transforms::Attributes::SymbolTransform
|
70
|
+
|
71
|
+
attribute_transform Time,
|
72
|
+
Bronze::Transforms::Attributes::TimeTransform
|
73
|
+
|
74
|
+
# @param entity_class [Class] The entity class on which attributes will be
|
75
|
+
# defined.
|
76
|
+
def initialize(entity_class)
|
77
|
+
@entity_class = entity_class
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Class] the entity class on which attributes will be defined.
|
81
|
+
attr_reader :entity_class
|
82
|
+
|
83
|
+
# Defines an attribute on the entity class.
|
84
|
+
#
|
85
|
+
# @example Defining an Attribute
|
86
|
+
# class Book < Bronze::Entities::Entity; end
|
87
|
+
#
|
88
|
+
# book = Book.new
|
89
|
+
# book.title
|
90
|
+
# #=> NoMethodError: undefined method `title'
|
91
|
+
#
|
92
|
+
# builder = Bronze::Entities::Attributes::Builder.new(Book)
|
93
|
+
# builder.define_attribute :title, String
|
94
|
+
#
|
95
|
+
# book.title
|
96
|
+
# #=> nil
|
97
|
+
#
|
98
|
+
# book.title = 'Romance of the Three Kingdoms'
|
99
|
+
# book.title
|
100
|
+
# #=> 'Romance of the Three Kingdoms'
|
101
|
+
#
|
102
|
+
# @param attribute_name [Symbol, String] The name of the attribute to
|
103
|
+
# define.
|
104
|
+
# @param attribute_type [Class] The type of the attribute to define.
|
105
|
+
# @param attribute_options [Hash] Additional options for building the
|
106
|
+
# attribute.
|
107
|
+
#
|
108
|
+
# @option attribute_options [Object, Proc] :default The default value for
|
109
|
+
# the attribute. If the attribute value is nil or has not been set, the
|
110
|
+
# attribute will be set to the default. If the default is a Proc, the
|
111
|
+
# Proc will be called each time and the attribute set to the return value.
|
112
|
+
# Otherwise, the attribute will be set to the default value.
|
113
|
+
# @option attribute_options [Boolean] :default_transform If a transform is
|
114
|
+
# set, marks the transform as a default transform, which can be overriden
|
115
|
+
# when normalizing the attribute.
|
116
|
+
# @option attribute_options [Boolean] :foreign_key Marks the attribute as a
|
117
|
+
# foreign key. Will be set to true by association builders, and generally
|
118
|
+
# should not be set manually. Defaults to false.
|
119
|
+
# @option attribute_options [Boolean] :read_only If true, the writer method
|
120
|
+
# for the attribute will be set as private. Defaults to false.
|
121
|
+
# @option attribute_options [Class, Bronze::Transform] :transform If set,
|
122
|
+
# the attribute will be normalized using this transform. By default,
|
123
|
+
# certain attribute types will be transformed - BigDecimal, Date,
|
124
|
+
# DateTime, Symbol, and Time.
|
125
|
+
#
|
126
|
+
# @return [Attributes::Metadata] The generated metadata for the
|
127
|
+
# attribute.
|
128
|
+
#
|
129
|
+
# @raise Builder::Error if the attribute name or attribute type is missing
|
130
|
+
# or invalid.
|
131
|
+
def build(attribute_name, attribute_type, attribute_options = {})
|
132
|
+
validate_attribute_name(attribute_name)
|
133
|
+
validate_attribute_opts(attribute_options)
|
134
|
+
|
135
|
+
characterize(
|
136
|
+
attribute_name,
|
137
|
+
attribute_type,
|
138
|
+
attribute_options
|
139
|
+
)
|
140
|
+
.tap { |metadata| define_property_methods(metadata) }
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def attributes_module
|
146
|
+
@attributes_module ||= define_attributes_module
|
147
|
+
end
|
148
|
+
|
149
|
+
def characterize(attribute_name, attribute_type, attribute_options)
|
150
|
+
Bronze::Entities::Attributes::Metadata.new(
|
151
|
+
attribute_name,
|
152
|
+
attribute_type,
|
153
|
+
normalize_options(attribute_options, type: attribute_type)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
def define_attributes_module
|
158
|
+
mod =
|
159
|
+
if entity_class.const_defined?(:Attributes, false)
|
160
|
+
entity_class::Attributes
|
161
|
+
else
|
162
|
+
entity_class.const_set(:Attributes, Module.new)
|
163
|
+
end
|
164
|
+
|
165
|
+
entity_class.send(:include, mod) unless entity_class < mod
|
166
|
+
|
167
|
+
mod
|
168
|
+
end
|
169
|
+
|
170
|
+
def define_property_methods(metadata)
|
171
|
+
define_reader(metadata)
|
172
|
+
define_writer(metadata)
|
173
|
+
end
|
174
|
+
|
175
|
+
def define_reader(metadata)
|
176
|
+
attr_name = metadata.name
|
177
|
+
|
178
|
+
attributes_module.send :define_method,
|
179
|
+
metadata.reader_name,
|
180
|
+
-> { get_attribute(attr_name) }
|
181
|
+
end
|
182
|
+
|
183
|
+
def define_writer(metadata)
|
184
|
+
attr_name = metadata.name
|
185
|
+
|
186
|
+
attributes_module.send :define_method,
|
187
|
+
metadata.writer_name,
|
188
|
+
->(value) { set_attribute(attr_name, value) }
|
189
|
+
|
190
|
+
return unless metadata.read_only?
|
191
|
+
|
192
|
+
attributes_module.send(:private, metadata.writer_name)
|
193
|
+
end
|
194
|
+
|
195
|
+
def normalize_options(options, type:)
|
196
|
+
options = options.each.with_object({}) do |(key, value), hsh|
|
197
|
+
hsh[key.intern] = value
|
198
|
+
end
|
199
|
+
|
200
|
+
unless options.key?(:default_transform)
|
201
|
+
options[:default_transform] = !options[:transform]
|
202
|
+
end
|
203
|
+
|
204
|
+
options[:transform] =
|
205
|
+
normalize_transform(options[:transform], type: type)
|
206
|
+
|
207
|
+
options
|
208
|
+
end
|
209
|
+
|
210
|
+
def normalize_transform(transform, type:)
|
211
|
+
transform ||= self.class.send(:transform_for_attribute, type)
|
212
|
+
|
213
|
+
return nil if transform.nil?
|
214
|
+
|
215
|
+
transform_instance(transform)
|
216
|
+
end
|
217
|
+
|
218
|
+
def transform_instance(transform)
|
219
|
+
return transform unless transform.is_a?(Class)
|
220
|
+
|
221
|
+
return transform.instance if transform.respond_to?(:instance)
|
222
|
+
|
223
|
+
transform.new
|
224
|
+
end
|
225
|
+
|
226
|
+
def validate_attribute_name(attribute_name)
|
227
|
+
unless attribute_name.is_a?(String) || attribute_name.is_a?(Symbol)
|
228
|
+
message = 'expected attribute name to be a String or Symbol, but was ' \
|
229
|
+
"#{attribute_name.inspect}"
|
230
|
+
|
231
|
+
raise ArgumentError, message, caller[1..-1]
|
232
|
+
end
|
233
|
+
|
234
|
+
return unless attribute_name.to_s.empty?
|
235
|
+
|
236
|
+
raise ArgumentError, "attribute name can't be blank", caller[1..-1]
|
237
|
+
end
|
238
|
+
|
239
|
+
def validate_attribute_opts(attribute_options)
|
240
|
+
attribute_options.each do |key, _value|
|
241
|
+
next if VALID_OPTIONS.include?(key.to_s)
|
242
|
+
|
243
|
+
raise ArgumentError,
|
244
|
+
"invalid attribute option #{key.inspect}",
|
245
|
+
caller[1..-1]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|