omg-activemodel 8.0.0.alpha1

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Lint
5
+ # == Active \Model \Lint \Tests
6
+ #
7
+ # You can test whether an object is compliant with the Active \Model API by
8
+ # including +ActiveModel::Lint::Tests+ in your TestCase. It will
9
+ # include tests that tell you whether your object is fully compliant,
10
+ # or if not, which aspects of the API are not implemented.
11
+ #
12
+ # Note an object is not required to implement all APIs in order to work
13
+ # with Action Pack. This module only intends to provide guidance in case
14
+ # you want all features out of the box.
15
+ #
16
+ # These tests do not attempt to determine the semantic correctness of the
17
+ # returned values. For instance, you could implement <tt>valid?</tt> to
18
+ # always return +true+, and the tests would pass. It is up to you to ensure
19
+ # that the values are semantically meaningful.
20
+ #
21
+ # Objects you pass in are expected to return a compliant object from a call
22
+ # to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
23
+ # +self+.
24
+ module Tests
25
+ # Passes if the object's model responds to <tt>to_key</tt> and if calling
26
+ # this method returns +nil+ when the object is not persisted.
27
+ # Fails otherwise.
28
+ #
29
+ # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
30
+ # of the model, and is used to a generate unique DOM id for the object.
31
+ def test_to_key
32
+ assert_respond_to model, :to_key
33
+ def model.persisted?() false end
34
+ assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
35
+ end
36
+
37
+ # Passes if the object's model responds to <tt>to_param</tt> and if
38
+ # calling this method returns +nil+ when the object is not persisted.
39
+ # Fails otherwise.
40
+ #
41
+ # <tt>to_param</tt> is used to represent the object's key in URLs.
42
+ # Implementers can decide to either raise an exception or provide a
43
+ # default in case the record uses a composite primary key. There are no
44
+ # tests for this behavior in lint because it doesn't make sense to force
45
+ # any of the possible implementation strategies on the implementer.
46
+ def test_to_param
47
+ assert_respond_to model, :to_param
48
+ def model.to_key() [1] end
49
+ def model.persisted?() false end
50
+ assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
51
+ end
52
+
53
+ # Passes if the object's model responds to <tt>to_partial_path</tt> and if
54
+ # calling this method returns a string. Fails otherwise.
55
+ #
56
+ # <tt>to_partial_path</tt> is used for looking up partials. For example,
57
+ # a BlogPost model might return "blog_posts/blog_post".
58
+ def test_to_partial_path
59
+ assert_respond_to model, :to_partial_path
60
+ assert_kind_of String, model.to_partial_path
61
+ end
62
+
63
+ # Passes if the object's model responds to <tt>persisted?</tt> and if
64
+ # calling this method returns either +true+ or +false+. Fails otherwise.
65
+ #
66
+ # <tt>persisted?</tt> is used when calculating the URL for an object.
67
+ # If the object is not persisted, a form for that object, for instance,
68
+ # will route to the create action. If it is persisted, a form for the
69
+ # object will route to the update action.
70
+ def test_persisted?
71
+ assert_respond_to model, :persisted?
72
+ assert_boolean model.persisted?, "persisted?"
73
+ end
74
+
75
+ # Passes if the object's model responds to <tt>model_name</tt> both as
76
+ # an instance method and as a class method, and if calling this method
77
+ # returns a string with some convenience methods: <tt>:human</tt>,
78
+ # <tt>:singular</tt> and <tt>:plural</tt>.
79
+ #
80
+ # Check ActiveModel::Naming for more information.
81
+ def test_model_naming
82
+ assert_respond_to model.class, :model_name
83
+ model_name = model.class.model_name
84
+ assert_respond_to model_name, :to_str
85
+ assert_respond_to model_name.human, :to_str
86
+ assert_respond_to model_name.singular, :to_str
87
+ assert_respond_to model_name.plural, :to_str
88
+
89
+ assert_respond_to model, :model_name
90
+ assert_equal model.model_name, model.class.model_name
91
+ end
92
+
93
+ # Passes if the object's model responds to <tt>errors</tt> and if calling
94
+ # <tt>[](attribute)</tt> on the result of this method returns an array.
95
+ # Fails otherwise.
96
+ #
97
+ # <tt>errors[attribute]</tt> is used to retrieve the errors of a model
98
+ # for a given attribute. If errors are present, the method should return
99
+ # an array of strings that are the errors for the attribute in question.
100
+ # If localization is used, the strings should be localized for the current
101
+ # locale. If no error is present, the method should return an empty array.
102
+ def test_errors_aref
103
+ assert_respond_to model, :errors
104
+ assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
105
+ end
106
+
107
+ private
108
+ def model
109
+ assert_respond_to @model, :to_model
110
+ @model.to_model
111
+ end
112
+
113
+ def assert_boolean(result, name)
114
+ assert result == true || result == false, "#{name} should be a boolean"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,38 @@
1
+ en:
2
+ errors:
3
+ # The default format to use in full error messages.
4
+ format: "%{attribute} %{message}"
5
+
6
+ # The values :model, :attribute and :value are always available for interpolation
7
+ # The value :count is available when applicable. Can be used for pluralization.
8
+ messages:
9
+ model_invalid: "Validation failed: %{errors}"
10
+ inclusion: "is not included in the list"
11
+ exclusion: "is reserved"
12
+ invalid: "is invalid"
13
+ confirmation: "doesn't match %{attribute}"
14
+ accepted: "must be accepted"
15
+ empty: "can't be empty"
16
+ blank: "can't be blank"
17
+ present: "must be blank"
18
+ too_long:
19
+ one: "is too long (maximum is 1 character)"
20
+ other: "is too long (maximum is %{count} characters)"
21
+ password_too_long: "is too long"
22
+ too_short:
23
+ one: "is too short (minimum is 1 character)"
24
+ other: "is too short (minimum is %{count} characters)"
25
+ wrong_length:
26
+ one: "is the wrong length (should be 1 character)"
27
+ other: "is the wrong length (should be %{count} characters)"
28
+ not_a_number: "is not a number"
29
+ not_an_integer: "must be an integer"
30
+ greater_than: "must be greater than %{count}"
31
+ greater_than_or_equal_to: "must be greater than or equal to %{count}"
32
+ equal_to: "must be equal to %{count}"
33
+ less_than: "must be less than %{count}"
34
+ less_than_or_equal_to: "must be less than or equal to %{count}"
35
+ other_than: "must be other than %{count}"
36
+ in: "must be in %{count}"
37
+ odd: "must be odd"
38
+ even: "must be even"
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ # = Active \Model \Basic \Model
5
+ #
6
+ # Allows implementing models similar to ActiveRecord::Base.
7
+ # Includes ActiveModel::API for the required interface for an
8
+ # object to interact with Action Pack and Action View, but can be
9
+ # extended with other functionalities.
10
+ #
11
+ # A minimal implementation could be:
12
+ #
13
+ # class Person
14
+ # include ActiveModel::Model
15
+ # attr_accessor :name, :age
16
+ # end
17
+ #
18
+ # person = Person.new(name: 'bob', age: '18')
19
+ # person.name # => "bob"
20
+ # person.age # => "18"
21
+ #
22
+ # If for some reason you need to run code on <tt>initialize</tt>, make
23
+ # sure you call +super+ if you want the attributes hash initialization to
24
+ # happen.
25
+ #
26
+ # class Person
27
+ # include ActiveModel::Model
28
+ # attr_accessor :id, :name, :omg
29
+ #
30
+ # def initialize(attributes={})
31
+ # super
32
+ # @omg ||= true
33
+ # end
34
+ # end
35
+ #
36
+ # person = Person.new(id: 1, name: 'bob')
37
+ # person.omg # => true
38
+ #
39
+ # For more detailed information on other functionalities available, please
40
+ # refer to the specific modules included in +ActiveModel::Model+
41
+ # (see below).
42
+ module Model
43
+ extend ActiveSupport::Concern
44
+ include ActiveModel::API
45
+ include ActiveModel::Access
46
+
47
+ ##
48
+ # :method: slice
49
+ #
50
+ # :call-seq: slice(*methods)
51
+ #
52
+ # Returns a hash of the given methods with their names as keys and returned
53
+ # values as values.
54
+ #
55
+ # person = Person.new(id: 1, name: "bob")
56
+ # person.slice(:id, :name)
57
+ # => { "id" => 1, "name" => "bob" }
58
+ #
59
+ #--
60
+ # Implemented by ActiveModel::Access#slice.
61
+
62
+ ##
63
+ # :method: values_at
64
+ #
65
+ # :call-seq: values_at(*methods)
66
+ #
67
+ # Returns an array of the values returned by the given methods.
68
+ #
69
+ # person = Person.new(id: 1, name: "bob")
70
+ # person.values_at(:id, :name)
71
+ # => [1, "bob"]
72
+ #
73
+ #--
74
+ # Implemented by ActiveModel::Access#values_at.
75
+ end
76
+
77
+ ActiveSupport.run_load_hooks(:active_model, Model)
78
+ end
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/except"
4
+ require "active_support/core_ext/module/introspection"
5
+ require "active_support/core_ext/module/redefine_method"
6
+ require "active_support/core_ext/module/delegation"
7
+
8
+ module ActiveModel
9
+ class Name
10
+ include Comparable
11
+
12
+ attr_accessor :singular, :plural, :element, :collection,
13
+ :singular_route_key, :route_key, :param_key, :i18n_key,
14
+ :name
15
+
16
+ alias_method :cache_key, :collection
17
+
18
+ ##
19
+ # :method: ==
20
+ #
21
+ # :call-seq:
22
+ # ==(other)
23
+ #
24
+ # Equivalent to <tt>String#==</tt>. Returns +true+ if the class name and
25
+ # +other+ are equal, otherwise +false+.
26
+ #
27
+ # class BlogPost
28
+ # extend ActiveModel::Naming
29
+ # end
30
+ #
31
+ # BlogPost.model_name == 'BlogPost' # => true
32
+ # BlogPost.model_name == 'Blog Post' # => false
33
+
34
+ ##
35
+ # :method: ===
36
+ #
37
+ # :call-seq:
38
+ # ===(other)
39
+ #
40
+ # Equivalent to <tt>#==</tt>.
41
+ #
42
+ # class BlogPost
43
+ # extend ActiveModel::Naming
44
+ # end
45
+ #
46
+ # BlogPost.model_name === 'BlogPost' # => true
47
+ # BlogPost.model_name === 'Blog Post' # => false
48
+
49
+ ##
50
+ # :method: <=>
51
+ #
52
+ # :call-seq:
53
+ # <=>(other)
54
+ #
55
+ # Equivalent to <tt>String#<=></tt>.
56
+ #
57
+ # class BlogPost
58
+ # extend ActiveModel::Naming
59
+ # end
60
+ #
61
+ # BlogPost.model_name <=> 'BlogPost' # => 0
62
+ # BlogPost.model_name <=> 'Blog' # => 1
63
+ # BlogPost.model_name <=> 'BlogPosts' # => -1
64
+
65
+ ##
66
+ # :method: =~
67
+ #
68
+ # :call-seq:
69
+ # =~(regexp)
70
+ #
71
+ # Equivalent to <tt>String#=~</tt>. Match the class name against the given
72
+ # regexp. Returns the position where the match starts or +nil+ if there is
73
+ # no match.
74
+ #
75
+ # class BlogPost
76
+ # extend ActiveModel::Naming
77
+ # end
78
+ #
79
+ # BlogPost.model_name =~ /Post/ # => 4
80
+ # BlogPost.model_name =~ /\d/ # => nil
81
+
82
+ ##
83
+ # :method: !~
84
+ #
85
+ # :call-seq:
86
+ # !~(regexp)
87
+ #
88
+ # Equivalent to <tt>String#!~</tt>. Match the class name against the given
89
+ # regexp. Returns +true+ if there is no match, otherwise +false+.
90
+ #
91
+ # class BlogPost
92
+ # extend ActiveModel::Naming
93
+ # end
94
+ #
95
+ # BlogPost.model_name !~ /Post/ # => false
96
+ # BlogPost.model_name !~ /\d/ # => true
97
+
98
+ ##
99
+ # :method: eql?
100
+ #
101
+ # :call-seq:
102
+ # eql?(other)
103
+ #
104
+ # Equivalent to <tt>String#eql?</tt>. Returns +true+ if the class name and
105
+ # +other+ have the same length and content, otherwise +false+.
106
+ #
107
+ # class BlogPost
108
+ # extend ActiveModel::Naming
109
+ # end
110
+ #
111
+ # BlogPost.model_name.eql?('BlogPost') # => true
112
+ # BlogPost.model_name.eql?('Blog Post') # => false
113
+
114
+ ##
115
+ # :method: match?
116
+ #
117
+ # :call-seq:
118
+ # match?(regexp)
119
+ #
120
+ # Equivalent to <tt>String#match?</tt>. Match the class name against the
121
+ # given regexp. Returns +true+ if there is a match, otherwise +false+.
122
+ #
123
+ # class BlogPost
124
+ # extend ActiveModel::Naming
125
+ # end
126
+ #
127
+ # BlogPost.model_name.match?(/Post/) # => true
128
+ # BlogPost.model_name.match?(/\d/) # => false
129
+
130
+ ##
131
+ # :method: to_s
132
+ #
133
+ # :call-seq:
134
+ # to_s()
135
+ #
136
+ # Returns the class name.
137
+ #
138
+ # class BlogPost
139
+ # extend ActiveModel::Naming
140
+ # end
141
+ #
142
+ # BlogPost.model_name.to_s # => "BlogPost"
143
+
144
+ ##
145
+ # :method: to_str
146
+ #
147
+ # :call-seq:
148
+ # to_str()
149
+ #
150
+ # Equivalent to +to_s+.
151
+ delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
152
+ :to_str, :as_json, to: :name
153
+
154
+ # Returns a new ActiveModel::Name instance. By default, the +namespace+
155
+ # and +name+ option will take the namespace and name of the given class
156
+ # respectively.
157
+ # Use +locale+ argument for singularize and pluralize model name.
158
+ #
159
+ # module Foo
160
+ # class Bar
161
+ # end
162
+ # end
163
+ #
164
+ # ActiveModel::Name.new(Foo::Bar).to_s
165
+ # # => "Foo::Bar"
166
+ def initialize(klass, namespace = nil, name = nil, locale = :en)
167
+ @name = name || klass.name
168
+
169
+ raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
170
+
171
+ @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
172
+ @klass = klass
173
+ @singular = _singularize(@name)
174
+ @plural = ActiveSupport::Inflector.pluralize(@singular, locale)
175
+ @uncountable = @plural == @singular
176
+ @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
177
+ @human = ActiveSupport::Inflector.humanize(@element)
178
+ @collection = ActiveSupport::Inflector.tableize(@name)
179
+ @param_key = (namespace ? _singularize(@unnamespaced) : @singular)
180
+ @i18n_key = @name.underscore.to_sym
181
+
182
+ @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key, locale) : @plural.dup)
183
+ @singular_route_key = ActiveSupport::Inflector.singularize(@route_key, locale)
184
+ @route_key << "_index" if @uncountable
185
+ end
186
+
187
+ # Transform the model name into a more human format, using I18n. By default,
188
+ # it will underscore then humanize the class name.
189
+ #
190
+ # class BlogPost
191
+ # extend ActiveModel::Naming
192
+ # end
193
+ #
194
+ # BlogPost.model_name.human # => "Blog post"
195
+ #
196
+ # Specify +options+ with additional translating options.
197
+ def human(options = {})
198
+ return @human if i18n_keys.empty? || i18n_scope.empty?
199
+
200
+ key, *defaults = i18n_keys
201
+ defaults << options[:default] if options[:default]
202
+ defaults << MISSING_TRANSLATION
203
+
204
+ translation = I18n.translate(key, scope: i18n_scope, count: 1, **options, default: defaults)
205
+ translation = @human if translation == MISSING_TRANSLATION
206
+ translation
207
+ end
208
+
209
+ def uncountable?
210
+ @uncountable
211
+ end
212
+
213
+ private
214
+ MISSING_TRANSLATION = -(2**60) # :nodoc:
215
+
216
+ def _singularize(string)
217
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
218
+ end
219
+
220
+ def i18n_keys
221
+ @i18n_keys ||= if @klass.respond_to?(:lookup_ancestors)
222
+ @klass.lookup_ancestors.map { |klass| klass.model_name.i18n_key }
223
+ else
224
+ []
225
+ end
226
+ end
227
+
228
+ def i18n_scope
229
+ @i18n_scope ||= @klass.respond_to?(:i18n_scope) ? [@klass.i18n_scope, :models] : []
230
+ end
231
+ end
232
+
233
+ # = Active \Model \Naming
234
+ #
235
+ # Creates a +model_name+ method on your object.
236
+ #
237
+ # To implement, just extend ActiveModel::Naming in your object:
238
+ #
239
+ # class BookCover
240
+ # extend ActiveModel::Naming
241
+ # end
242
+ #
243
+ # BookCover.model_name.name # => "BookCover"
244
+ # BookCover.model_name.human # => "Book cover"
245
+ #
246
+ # BookCover.model_name.i18n_key # => :book_cover
247
+ # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
248
+ #
249
+ # Providing the functionality that ActiveModel::Naming provides in your object
250
+ # is required to pass the \Active \Model Lint test. So either extending the
251
+ # provided method below, or rolling your own is required.
252
+ module Naming
253
+ def self.extended(base) # :nodoc:
254
+ base.silence_redefinition_of_method :model_name
255
+ base.delegate :model_name, to: :class
256
+ end
257
+
258
+ # Returns an ActiveModel::Name object for module. It can be
259
+ # used to retrieve all kinds of naming-related information
260
+ # (See ActiveModel::Name for more information).
261
+ #
262
+ # class Person
263
+ # extend ActiveModel::Naming
264
+ # end
265
+ #
266
+ # Person.model_name.name # => "Person"
267
+ # Person.model_name.class # => ActiveModel::Name
268
+ # Person.model_name.singular # => "person"
269
+ # Person.model_name.plural # => "people"
270
+ def model_name
271
+ @_model_name ||= begin
272
+ namespace = module_parents.detect do |n|
273
+ n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
274
+ end
275
+ ActiveModel::Name.new(self, namespace)
276
+ end
277
+ end
278
+
279
+ # Returns the plural class name of a record or class.
280
+ #
281
+ # ActiveModel::Naming.plural(post) # => "posts"
282
+ # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
283
+ def self.plural(record_or_class)
284
+ model_name_from_record_or_class(record_or_class).plural
285
+ end
286
+
287
+ # Returns the singular class name of a record or class.
288
+ #
289
+ # ActiveModel::Naming.singular(post) # => "post"
290
+ # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
291
+ def self.singular(record_or_class)
292
+ model_name_from_record_or_class(record_or_class).singular
293
+ end
294
+
295
+ # Identifies whether the class name of a record or class is uncountable.
296
+ #
297
+ # ActiveModel::Naming.uncountable?(Sheep) # => true
298
+ # ActiveModel::Naming.uncountable?(Post) # => false
299
+ def self.uncountable?(record_or_class)
300
+ model_name_from_record_or_class(record_or_class).uncountable?
301
+ end
302
+
303
+ # Returns string to use while generating route names. It differs for
304
+ # namespaced models regarding whether it's inside isolated engine.
305
+ #
306
+ # # For isolated engine:
307
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
308
+ #
309
+ # # For shared engine:
310
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
311
+ def self.singular_route_key(record_or_class)
312
+ model_name_from_record_or_class(record_or_class).singular_route_key
313
+ end
314
+
315
+ # Returns string to use while generating route names. It differs for
316
+ # namespaced models regarding whether it's inside isolated engine.
317
+ #
318
+ # # For isolated engine:
319
+ # ActiveModel::Naming.route_key(Blog::Post) # => "posts"
320
+ #
321
+ # # For shared engine:
322
+ # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
323
+ #
324
+ # The route key also considers if the noun is uncountable and, in
325
+ # such cases, automatically appends _index.
326
+ def self.route_key(record_or_class)
327
+ model_name_from_record_or_class(record_or_class).route_key
328
+ end
329
+
330
+ # Returns string to use for params names. It differs for
331
+ # namespaced models regarding whether it's inside isolated engine.
332
+ #
333
+ # # For isolated engine:
334
+ # ActiveModel::Naming.param_key(Blog::Post) # => "post"
335
+ #
336
+ # # For shared engine:
337
+ # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
338
+ def self.param_key(record_or_class)
339
+ model_name_from_record_or_class(record_or_class).param_key
340
+ end
341
+
342
+ def self.model_name_from_record_or_class(record_or_class) # :nodoc:
343
+ if record_or_class.respond_to?(:to_model)
344
+ record_or_class.to_model.model_name
345
+ else
346
+ record_or_class.model_name
347
+ end
348
+ end
349
+ private_class_method :model_name_from_record_or_class
350
+
351
+ private
352
+ def inherited(base)
353
+ super
354
+ base.class_eval do
355
+ @_model_name = nil
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/error"
4
+ require "forwardable"
5
+
6
+ module ActiveModel
7
+ class NestedError < Error
8
+ def initialize(base, inner_error, override_options = {})
9
+ @base = base
10
+ @inner_error = inner_error
11
+ @attribute = override_options.fetch(:attribute) { inner_error.attribute }
12
+ @type = override_options.fetch(:type) { inner_error.type }
13
+ @raw_type = inner_error.raw_type
14
+ @options = inner_error.options
15
+ end
16
+
17
+ attr_reader :inner_error
18
+
19
+ extend Forwardable
20
+ def_delegators :@inner_error, :message
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "rails"
5
+
6
+ module ActiveModel
7
+ class Railtie < Rails::Railtie # :nodoc:
8
+ config.eager_load_namespaces << ActiveModel
9
+
10
+ config.active_model = ActiveSupport::OrderedOptions.new
11
+
12
+ initializer "active_model.deprecator", before: :load_environment_config do |app|
13
+ app.deprecators[:active_model] = ActiveModel.deprecator
14
+ end
15
+
16
+ initializer "active_model.secure_password" do
17
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
18
+ end
19
+
20
+ initializer "active_model.i18n_customize_full_message" do
21
+ ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
22
+ end
23
+ end
24
+ end