activeentity 0.0.1.beta14 → 0.0.1.beta15

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +4 -4
  4. data/Rakefile +7 -7
  5. data/lib/active_entity.rb +29 -7
  6. data/lib/active_entity/aggregations.rb +2 -1
  7. data/lib/active_entity/associations.rb +46 -24
  8. data/lib/active_entity/associations/{embedded → embeds}/association.rb +2 -2
  9. data/lib/active_entity/associations/{embedded → embeds}/builder/association.rb +1 -1
  10. data/lib/active_entity/associations/{embedded → embeds}/builder/collection_association.rb +1 -1
  11. data/lib/active_entity/associations/{embedded → embeds}/builder/embedded_in.rb +1 -1
  12. data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_many.rb +1 -1
  13. data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_one.rb +1 -1
  14. data/lib/active_entity/associations/{embedded → embeds}/builder/singular_association.rb +1 -1
  15. data/lib/active_entity/associations/{embedded → embeds}/collection_association.rb +1 -1
  16. data/lib/active_entity/associations/{embedded → embeds}/collection_proxy.rb +2 -2
  17. data/lib/active_entity/associations/{embedded → embeds}/embedded_in_association.rb +1 -1
  18. data/lib/active_entity/associations/{embedded → embeds}/embeds_many_association.rb +1 -1
  19. data/lib/active_entity/associations/{embedded → embeds}/embeds_one_association.rb +2 -1
  20. data/lib/active_entity/associations/{embedded → embeds}/singular_association.rb +1 -1
  21. data/lib/active_entity/attribute_assignment.rb +9 -3
  22. data/lib/active_entity/attribute_methods.rb +12 -11
  23. data/lib/active_entity/attribute_methods/before_type_cast.rb +1 -1
  24. data/lib/active_entity/attribute_methods/dirty.rb +13 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +1 -1
  26. data/lib/active_entity/attribute_methods/query.rb +11 -4
  27. data/lib/active_entity/attribute_methods/read.rb +1 -3
  28. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +2 -0
  29. data/lib/active_entity/attribute_methods/write.rb +4 -6
  30. data/lib/active_entity/attributes.rb +76 -2
  31. data/lib/active_entity/base.rb +3 -12
  32. data/lib/active_entity/core.rb +97 -39
  33. data/lib/active_entity/define_callbacks.rb +4 -0
  34. data/lib/active_entity/enum.rb +30 -4
  35. data/lib/active_entity/errors.rb +0 -11
  36. data/lib/active_entity/gem_version.rb +1 -1
  37. data/lib/active_entity/inheritance.rb +4 -106
  38. data/lib/active_entity/integration.rb +1 -1
  39. data/lib/active_entity/model_schema.rb +0 -12
  40. data/lib/active_entity/nested_attributes.rb +5 -12
  41. data/lib/active_entity/railtie.rb +61 -1
  42. data/lib/active_entity/readonly_attributes.rb +9 -1
  43. data/lib/active_entity/reflection.rb +22 -19
  44. data/lib/active_entity/serialization.rb +9 -6
  45. data/lib/active_entity/store.rb +51 -2
  46. data/lib/active_entity/type.rb +8 -8
  47. data/lib/active_entity/type/registry.rb +5 -5
  48. data/lib/active_entity/{validate_embedded_association.rb → validate_embeds_association.rb} +6 -6
  49. data/lib/active_entity/validations.rb +2 -6
  50. data/lib/active_entity/validations/associated.rb +1 -1
  51. data/lib/active_entity/validations/{uniqueness_in_embedding.rb → uniqueness_in_embeds.rb} +1 -1
  52. data/lib/active_entity/validations/uniqueness_on_active_record.rb +46 -40
  53. metadata +27 -30
  54. data/lib/active_entity/type/decimal_without_scale.rb +0 -15
  55. data/lib/active_entity/type/hash_lookup_type_map.rb +0 -25
  56. data/lib/active_entity/type/type_map.rb +0 -62
  57. data/lib/tasks/active_entity_tasks.rake +0 -6
@@ -16,6 +16,7 @@ module ActiveEntity
16
16
  include Query
17
17
  include PrimaryKey
18
18
  include TimeZoneConversion
19
+ include Dirty
19
20
  include Serialization
20
21
  end
21
22
 
@@ -32,7 +33,8 @@ module ActiveEntity
32
33
  end
33
34
 
34
35
  def initialize_generated_modules # :nodoc:
35
- @generated_attribute_methods = GeneratedAttributeMethods.new
36
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
37
+ private_constant :GeneratedAttributeMethods
36
38
  @attribute_methods_generated = false
37
39
  include @generated_attribute_methods
38
40
 
@@ -111,13 +113,11 @@ module ActiveEntity
111
113
  # A class method is 'dangerous' if it is already (re)defined by Active Entity, but
112
114
  # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
113
115
  def dangerous_class_method?(method_name)
114
- RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
115
- end
116
+ return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s)
116
117
 
117
- def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
118
- if klass.respond_to?(name, true)
119
- if superklass.respond_to?(name, true)
120
- klass.method(name).owner != superklass.method(name).owner
118
+ if Base.respond_to?(method_name, true)
119
+ if Object.respond_to?(method_name, true)
120
+ Base.method(method_name).owner != Object.method(method_name).owner
121
121
  else
122
122
  true
123
123
  end
@@ -126,8 +126,8 @@ module ActiveEntity
126
126
  end
127
127
  end
128
128
 
129
- # Returns an array of column names as strings if it's not an abstract class and
130
- # table exists. Otherwise it returns an empty array.
129
+ # Returns an array of column names as strings if it's not an abstract class.
130
+ # Otherwise it returns an empty array.
131
131
  #
132
132
  # class Person < ActiveEntity::Base
133
133
  # end
@@ -302,6 +302,7 @@ module ActiveEntity
302
302
  end
303
303
 
304
304
  private
305
+
305
306
  def attribute_method?(attr_name)
306
307
  # We check defined? because Syck calls respond_to? before actually calling initialize.
307
308
  defined?(@attributes) && @attributes.key?(attr_name)
@@ -323,8 +324,8 @@ module ActiveEntity
323
324
  end
324
325
  end
325
326
 
326
- def readonly_attribute?(name)
327
- self.class.readonly_attributes.include?(name)
327
+ def pk_attribute?(name)
328
+ name == @primary_key
328
329
  end
329
330
  end
330
331
  end
@@ -65,7 +65,7 @@ module ActiveEntity
65
65
 
66
66
  private
67
67
 
68
- # Handle *_before_type_cast for method_missing.
68
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
69
69
  def attribute_before_type_cast(attribute_name)
70
70
  read_attribute_before_type_cast(attribute_name)
71
71
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/attribute_accessors"
4
+
5
+ module ActiveEntity
6
+ module AttributeMethods
7
+ module Dirty
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveModel::Dirty
11
+ end
12
+ end
13
+ end
@@ -90,7 +90,7 @@ module ActiveEntity
90
90
  #
91
91
  # Project.primary_key # => "foo_id"
92
92
  def primary_key=(value)
93
- @primary_key = value&.to_s
93
+ @primary_key = value && -value.to_s
94
94
  end
95
95
  end
96
96
  end
@@ -16,17 +16,24 @@ module ActiveEntity
16
16
  when true then true
17
17
  when false, nil then false
18
18
  else
19
- if Numeric === value || value !~ /[^0-9]/
20
- !value.to_i.zero?
19
+ if !type_for_attribute(attr_name) { false }
20
+ if Numeric === value || !value.match?(/[^0-9]/)
21
+ !value.to_i.zero?
22
+ else
23
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
24
+ !value.blank?
25
+ end
26
+ elsif value.respond_to?(:zero?)
27
+ !value.zero?
21
28
  else
22
- return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
23
29
  !value.blank?
24
30
  end
25
31
  end
26
32
  end
27
33
 
28
34
  private
29
- # Handle *? for method_missing.
35
+
36
+ # Dispatch target for <tt>*?</tt> attribute methods.
30
37
  def attribute?(attribute_name)
31
38
  query_attribute(attribute_name)
32
39
  end
@@ -27,9 +27,7 @@ module ActiveEntity
27
27
  # to a date object, like Date.new(2004, 12, 12)).
28
28
  def read_attribute(attr_name, &block)
29
29
  name = attr_name.to_s
30
- if self.class.attribute_alias?(name)
31
- name = self.class.attribute_alias(name)
32
- end
30
+ name = self.class.attribute_aliases[name] || name
33
31
 
34
32
  _read_attribute(name, &block)
35
33
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/try"
4
+
3
5
  module ActiveEntity
4
6
  module AttributeMethods
5
7
  module TimeZoneConversion
@@ -31,9 +31,7 @@ module ActiveEntity
31
31
  # turned into +nil+.
32
32
  def write_attribute(attr_name, value)
33
33
  name = attr_name.to_s
34
- if self.class.attribute_alias?(name)
35
- name = self.class.attribute_alias(name)
36
- end
34
+ name = self.class.attribute_aliases[name] || name
37
35
 
38
36
  _write_attribute(name, value)
39
37
  end
@@ -48,13 +46,13 @@ module ActiveEntity
48
46
  end
49
47
 
50
48
  private
49
+
51
50
  def write_attribute_without_type_cast(attr_name, value)
52
- name = attr_name.to_s
53
- @attributes.write_cast_value(name, value)
51
+ @attributes.write_cast_value(attr_name.to_s, value)
54
52
  value
55
53
  end
56
54
 
57
- # Handle *= for method_missing.
55
+ # Dispatch target for <tt>*=</tt> attribute methods.
58
56
  def attribute=(attribute_name, value)
59
57
  _write_attribute(attribute_name, value)
60
58
  end
@@ -12,7 +12,13 @@ module ActiveEntity
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- # Defines an attribute with a type on this model.
15
+ # Defines an attribute with a type on this model. It will override the
16
+ # type of existing attributes if needed. This allows control over how
17
+ # values are converted to and from SQL when assigned to a model. It also
18
+ # changes the behavior of values passed to
19
+ # {ActiveEntity::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
20
+ # your domain objects across much of Active Entity, without having to
21
+ # rely on implementation details or monkey patching.
16
22
  #
17
23
  # +name+ The name of the methods to define attribute methods for, and the
18
24
  # column which this will persist to.
@@ -29,11 +35,66 @@ module ActiveEntity
29
35
  # is not passed, the previous default value (if any) will be used.
30
36
  # Otherwise, the default will be +nil+.
31
37
  #
32
- # +array+ Specifies that the type should be an array (see the
38
+ # +array+ (PostgreSQL only) specifies that the type should be an array (see the
33
39
  # examples below).
34
40
  #
41
+ # +range+ (PostgreSQL only) specifies that the type should be a range (see the
42
+ # examples below).
43
+ #
44
+ # When using a symbol for +cast_type+, extra options are forwarded to the
45
+ # constructor of the type object.
46
+ #
35
47
  # ==== Examples
36
48
  #
49
+ # The type detected by Active Entity can be overridden.
50
+ #
51
+ # # db/schema.rb
52
+ # create_table :store_listings, force: true do |t|
53
+ # t.decimal :price_in_cents
54
+ # end
55
+ #
56
+ # # app/models/store_listing.rb
57
+ # class StoreListing < ActiveEntity::Base
58
+ # end
59
+ #
60
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
61
+ #
62
+ # # before
63
+ # store_listing.price_in_cents # => BigDecimal(10.1)
64
+ #
65
+ # class StoreListing < ActiveEntity::Base
66
+ # attribute :price_in_cents, :integer
67
+ # end
68
+ #
69
+ # # after
70
+ # store_listing.price_in_cents # => 10
71
+ #
72
+ # A default can also be provided.
73
+ #
74
+ # # db/schema.rb
75
+ # create_table :store_listings, force: true do |t|
76
+ # t.string :my_string, default: "original default"
77
+ # end
78
+ #
79
+ # StoreListing.new.my_string # => "original default"
80
+ #
81
+ # # app/models/store_listing.rb
82
+ # class StoreListing < ActiveEntity::Base
83
+ # attribute :my_string, :string, default: "new default"
84
+ # end
85
+ #
86
+ # StoreListing.new.my_string # => "new default"
87
+ #
88
+ # class Product < ActiveEntity::Base
89
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
90
+ # end
91
+ #
92
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
93
+ # sleep 1
94
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
95
+ #
96
+ # \Attributes do not need to be backed by a database column.
97
+ #
37
98
  # # app/models/my_model.rb
38
99
  # class MyModel < ActiveEntity::Base
39
100
  # attribute :my_string, :string
@@ -54,6 +115,16 @@ module ActiveEntity
54
115
  # my_float_range: 1.0..3.5
55
116
  # }
56
117
  #
118
+ # Passing options to the type constructor
119
+ #
120
+ # # app/models/my_model.rb
121
+ # class MyModel < ActiveEntity::Base
122
+ # attribute :small_int, :integer, limit: 2
123
+ # end
124
+ #
125
+ # MyModel.create(small_int: 65537)
126
+ # # => Error: 65537 is out of range for the limit of two bytes
127
+ #
57
128
  # ==== Creating Custom Types
58
129
  #
59
130
  # Users may also define their own custom types, as long as they respond
@@ -121,6 +192,8 @@ module ActiveEntity
121
192
  # is not passed, the previous default value (if any) will be used.
122
193
  # Otherwise, the default will be +nil+. A proc can also be passed, and
123
194
  # will be called once each time a new value is needed.
195
+ #
196
+ # +cast+ or +deserialize+.
124
197
  def define_attribute(
125
198
  name,
126
199
  cast_type,
@@ -158,6 +231,7 @@ module ActiveEntity
158
231
  _default_attributes.fetch(name.to_s) { nil },
159
232
  )
160
233
  end
234
+
161
235
  _default_attributes[name] = default_attribute
162
236
  end
163
237
  end
@@ -1,22 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
3
  require "active_support/benchmarkable"
5
4
  require "active_support/dependencies"
6
5
  require "active_support/descendants_tracker"
7
6
  require "active_support/time"
8
- require "active_support/core_ext/module/attribute_accessors"
9
- require "active_support/core_ext/array/extract_options"
10
- require "active_support/core_ext/hash/deep_merge"
11
- require "active_support/core_ext/hash/slice"
12
- require "active_support/core_ext/string/behavior"
13
- require "active_support/core_ext/kernel/singleton_class"
14
- require "active_support/core_ext/module/introspection"
15
- require "active_support/core_ext/object/duplicable"
16
7
  require "active_support/core_ext/class/subclasses"
17
8
  require "active_entity/attribute_decorators"
18
9
  require "active_entity/define_callbacks"
19
- require "active_entity/errors"
20
10
  require "active_entity/attributes"
21
11
 
22
12
  module ActiveEntity #:nodoc:
@@ -28,7 +18,7 @@ module ActiveEntity #:nodoc:
28
18
  # Active Entity objects. The mapping that binds a given Active Entity class to a certain
29
19
  # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
30
20
  #
31
- # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
21
+ # See the mapping rules in table_name and the full example in link:files/activeentity/README_rdoc.html for more insight.
32
22
  #
33
23
  # == Creation
34
24
  #
@@ -292,8 +282,9 @@ module ActiveEntity #:nodoc:
292
282
  include AttributeDecorators
293
283
  include DefineCallbacks
294
284
  include AttributeMethods
285
+ include Callbacks
295
286
  include Associations
296
- include ValidateEmbeddedAssociation
287
+ include ValidateEmbedsAssociation
297
288
  include NestedAttributes
298
289
  include Reflection
299
290
  include Serialization
@@ -58,11 +58,9 @@ module ActiveEntity
58
58
  def inspect # :nodoc:
59
59
  if self == Base
60
60
  super
61
- elsif abstract_class?
62
- "#{super}(abstract)"
63
61
  else
64
62
  attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
65
- "#{super}(#{attr_list})"
63
+ "#{super}#{ "(abstract)" if abstract_class? }(#{attr_list})"
66
64
  end
67
65
  end
68
66
 
@@ -81,7 +79,6 @@ module ActiveEntity
81
79
  # # Instantiates a single new object
82
80
  # User.new(first_name: 'Jamie')
83
81
  def initialize(attributes = nil)
84
- self.class.define_attribute_methods
85
82
  @attributes = self.class._default_attributes.deep_dup
86
83
 
87
84
  init_internals
@@ -95,6 +92,42 @@ module ActiveEntity
95
92
  enable_attr_readonly!
96
93
  end
97
94
 
95
+ # Initialize an empty model object from +coder+. +coder+ should be
96
+ # the result of previously encoding an Active Entity model, using
97
+ # #encode_with.
98
+ #
99
+ # class Post < ActiveEntity::Base
100
+ # end
101
+ #
102
+ # old_post = Post.new(title: "hello world")
103
+ # coder = {}
104
+ # old_post.encode_with(coder)
105
+ #
106
+ # post = Post.allocate
107
+ # post.init_with(coder)
108
+ # post.title # => 'hello world'
109
+ def init_with(coder, &block)
110
+ attributes = self.class.yaml_encoder.decode(coder)
111
+ init_with_attributes(attributes, coder["new_record"], &block)
112
+ end
113
+
114
+ ##
115
+ # Initialize an empty model object from +attributes+.
116
+ # +attributes+ should be an attributes object, and unlike the
117
+ # `initialize` method, no assignment calls are made per attribute.
118
+ def init_with_attributes(attributes) # :nodoc:
119
+ @attributes = attributes
120
+
121
+ init_internals
122
+
123
+ yield self if block_given?
124
+
125
+ _run_find_callbacks
126
+ _run_initialize_callbacks
127
+
128
+ self
129
+ end
130
+
98
131
  ##
99
132
  # :method: clone
100
133
  # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
@@ -147,6 +180,33 @@ module ActiveEntity
147
180
  coder["active_entity_yaml_version"] = 2
148
181
  end
149
182
 
183
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
184
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
185
+ #
186
+ # Note that new records are different from any other record by definition, unless the
187
+ # other record is the receiver itself. Besides, if you fetch existing records with
188
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
189
+ #
190
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
191
+ # models are still comparable.
192
+ def ==(comparison_object)
193
+ super ||
194
+ comparison_object.instance_of?(self.class) &&
195
+ !id.nil? &&
196
+ comparison_object.id == id
197
+ end
198
+ alias :eql? :==
199
+
200
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
201
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
202
+ def hash
203
+ if id
204
+ self.class.hash ^ id.hash
205
+ else
206
+ super
207
+ end
208
+ end
209
+
150
210
  # Clone and freeze the attributes hash such that associations are still
151
211
  # accessible, even on destroyed records, but cloned models will not be
152
212
  # frozen.
@@ -169,6 +229,14 @@ module ActiveEntity
169
229
  end
170
230
  end
171
231
 
232
+ def present? # :nodoc:
233
+ true
234
+ end
235
+
236
+ def blank? # :nodoc:
237
+ false
238
+ end
239
+
172
240
  # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
173
241
  # attributes will be marked as read only since they cannot be saved.
174
242
  def readonly?
@@ -184,24 +252,22 @@ module ActiveEntity
184
252
  def inspect
185
253
  # We check defined?(@attributes) not to issue warnings if the object is
186
254
  # allocated but not initialized.
187
- inspection =
188
- if defined?(@attributes) && @attributes
189
- self.class.attribute_names.collect do |name|
190
- if has_attribute?(name)
191
- attr = _read_attribute(name)
192
- value =
193
- if attr.nil?
194
- attr.inspect
195
- else
196
- attr = format_for_inspect(attr)
197
- inspection_filter.filter_param(name, attr)
198
- end
199
- "#{name}: #{value}"
255
+ inspection = if defined?(@attributes) && @attributes
256
+ self.class.attribute_names.collect do |name|
257
+ if has_attribute?(name)
258
+ attr = _read_attribute(name)
259
+ value = if attr.nil?
260
+ attr.inspect
261
+ else
262
+ attr = format_for_inspect(attr)
263
+ inspection_filter.filter_param(name, attr)
200
264
  end
201
- end.compact.join(", ")
202
- else
203
- "not initialized"
204
- end
265
+ "#{name}: #{value}"
266
+ end
267
+ end.compact.join(", ")
268
+ else
269
+ "not initialized"
270
+ end
205
271
 
206
272
  "#<#{self.class} #{inspection}>"
207
273
  end
@@ -236,14 +302,6 @@ module ActiveEntity
236
302
  Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
237
303
  end
238
304
 
239
- def present? # :nodoc:
240
- true
241
- end
242
-
243
- def blank? # :nodoc:
244
- false
245
- end
246
-
247
305
  private
248
306
 
249
307
  # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
@@ -261,27 +319,27 @@ module ActiveEntity
261
319
  def init_internals
262
320
  @readonly = false
263
321
  @marked_for_destruction = false
264
- end
265
322
 
266
- def initialize_internals_callback
323
+ self.class.define_attribute_methods
267
324
  end
268
325
 
269
- def thaw
270
- if frozen?
271
- @attributes = @attributes.dup
272
- end
326
+ def initialize_internals_callback
273
327
  end
274
328
 
275
329
  def custom_inspect_method_defined?
276
330
  self.class.instance_method(:inspect).owner != ActiveEntity::Base.instance_method(:inspect).owner
277
331
  end
278
332
 
333
+ class InspectionMask < DelegateClass(::String)
334
+ def pretty_print(pp)
335
+ pp.text __getobj__
336
+ end
337
+ end
338
+ private_constant :InspectionMask
339
+
279
340
  def inspection_filter
280
341
  @inspection_filter ||= begin
281
- mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
282
- def mask.pretty_print(pp)
283
- pp.text __getobj__
284
- end
342
+ mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
285
343
  ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
286
344
  end
287
345
  end