bronze 0.0.1.alpha → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|