activemodel 5.2.0.beta2 → 5.2.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f8bb0c3867c3dfe9215c08be8eb1e993ade817b4
4
- data.tar.gz: 13b15b80b683e29fc4b3b603afd762a90264b8a8
2
+ SHA256:
3
+ metadata.gz: 666a46bf39ff339f606b0d60219d61138a7de2def4808f6135255ee2a2b43ff0
4
+ data.tar.gz: 5de9942d5e2dfd21df46a9ac538bc83b4bc879c57884e3f26192f4204dac6d33
5
5
  SHA512:
6
- metadata.gz: 753ce129d560dc612f06f6e159c7ad2300181f9d28391e50c1697b310c377c76385fe0db72edfcb31961731b568eff6bb4d8a35cb4b988145718d41f538b4c36
7
- data.tar.gz: ca6bbdaa75becdb24782a026c82fc88d68f15da756914639ff26c31b390496758833f76bdf7d5725be7dc397a9d2bc166ef107624f044e0977b0e1b7465dd4d2
6
+ metadata.gz: f1f362e75e4a2cf7f08d2a89c2c8465516a72f014fc9d5d230a5cbc18195a4c734deb1b0c5bdede79d7126c7450154218b33824b5ab7b778ce033a800f156c68
7
+ data.tar.gz: b00a94be4f5c30d482412abfb0e395c46804195b6c64ab57ba0f4cb4f01481bf2ff6ae4b32f0b51177859b496f8253413bbe8672602390f80b71df7a31b8a500
@@ -1,3 +1,16 @@
1
+ ## Rails 5.2.0.rc1 (January 30, 2018) ##
2
+
3
+ * Models using the attributes API with a proc default can now be marshalled.
4
+
5
+ Fixes #31216.
6
+
7
+ *Sean Griffin*
8
+
9
+ * Fix to working before/after validation callbacks on multiple contexts.
10
+
11
+ *Yoshiyuki Hirano*
12
+
13
+
1
14
  ## Rails 5.2.0.beta2 (November 28, 2017) ##
2
15
 
3
16
  * No changes.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2004-2017 David Heinemeier Hansson
1
+ Copyright (c) 2004-2018 David Heinemeier Hansson
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -251,11 +251,11 @@ Active Model is released under the MIT license:
251
251
 
252
252
  == Support
253
253
 
254
- API documentation is at
254
+ API documentation is at:
255
255
 
256
256
  * http://api.rubyonrails.org
257
257
 
258
- Bug reports can be filed for the Ruby on Rails project here:
258
+ Bug reports for the Ruby on Rails project can be filed here:
259
259
 
260
260
  * https://github.com/rails/rails/issues
261
261
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2004-2017 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2018 David Heinemeier Hansson
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -233,6 +233,10 @@ module ActiveModel
233
233
  false
234
234
  end
235
235
 
236
+ def forgetting_assignment
237
+ dup
238
+ end
239
+
236
240
  def with_type(type)
237
241
  self.class.new(name, type)
238
242
  end
@@ -22,6 +22,28 @@ module ActiveModel
22
22
  self.class.new(name, user_provided_value, type, original_attribute)
23
23
  end
24
24
 
25
+ def marshal_dump
26
+ result = [
27
+ name,
28
+ value_before_type_cast,
29
+ type,
30
+ original_attribute,
31
+ ]
32
+ result << value if defined?(@value)
33
+ result
34
+ end
35
+
36
+ def marshal_load(values)
37
+ name, user_provided_value, type, original_attribute, value = values
38
+ @name = name
39
+ @user_provided_value = user_provided_value
40
+ @type = type
41
+ @original_attribute = original_attribute
42
+ if values.length == 5
43
+ @value = value
44
+ end
45
+ end
46
+
25
47
  protected
26
48
 
27
49
  attr_reader :user_provided_value
@@ -35,6 +35,10 @@ module ActiveModel
35
35
  end
36
36
  end
37
37
 
38
+ def changed_attribute_names
39
+ attr_names.select { |attr| changed?(attr) }
40
+ end
41
+
38
42
  def any_changes?
39
43
  attr_names.any? { |attr| changed?(attr) }
40
44
  end
@@ -109,8 +113,5 @@ module ActiveModel
109
113
 
110
114
  def original_value(*)
111
115
  end
112
-
113
- def force_change(*)
114
- end
115
116
  end
116
117
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/deep_dup"
3
4
  require "active_model/attribute_set/builder"
4
5
  require "active_model/attribute_set/yaml_encoder"
5
6
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/object/deep_dup"
4
3
  require "active_model/attribute_set"
5
4
  require "active_model/attribute/user_provided_default"
6
5
 
@@ -24,7 +23,7 @@ module ActiveModel
24
23
  end
25
24
  self.attribute_types = attribute_types.merge(name => type)
26
25
  define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
27
- define_attribute_methods(name)
26
+ define_attribute_method(name)
28
27
  end
29
28
 
30
29
  private
@@ -3,6 +3,7 @@
3
3
  require "active_support/hash_with_indifferent_access"
4
4
  require "active_support/core_ext/object/duplicable"
5
5
  require "active_model/attribute_mutation_tracker"
6
+ require "active_model/attribute_set"
6
7
 
7
8
  module ActiveModel
8
9
  # == Active \Model \Dirty
@@ -142,9 +143,8 @@ module ActiveModel
142
143
  end
143
144
 
144
145
  def changes_applied # :nodoc:
145
- @previously_changed = changes
146
+ _prepare_changes
146
147
  @mutations_before_last_save = mutations_from_database
147
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
148
148
  forget_attribute_assignments
149
149
  @mutations_from_database = nil
150
150
  end
@@ -155,7 +155,7 @@ module ActiveModel
155
155
  # person.name = 'bob'
156
156
  # person.changed? # => true
157
157
  def changed?
158
- changed_attributes.present?
158
+ mutations_from_database.any_changes?
159
159
  end
160
160
 
161
161
  # Returns an array with the name of the attributes with unsaved changes.
@@ -164,24 +164,24 @@ module ActiveModel
164
164
  # person.name = 'bob'
165
165
  # person.changed # => ["name"]
166
166
  def changed
167
- changed_attributes.keys
167
+ mutations_from_database.changed_attribute_names
168
168
  end
169
169
 
170
170
  # Handles <tt>*_changed?</tt> for +method_missing+.
171
171
  def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
172
- !!changes_include?(attr) &&
172
+ !!mutations_from_database.changed?(attr) &&
173
173
  (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
174
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
174
+ (from == OPTION_NOT_GIVEN || from == attribute_was(attr))
175
175
  end
176
176
 
177
177
  # Handles <tt>*_was</tt> for +method_missing+.
178
178
  def attribute_was(attr) # :nodoc:
179
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
179
+ mutations_from_database.original_value(attr)
180
180
  end
181
181
 
182
182
  # Handles <tt>*_previously_changed?</tt> for +method_missing+.
183
183
  def attribute_previously_changed?(attr) #:nodoc:
184
- previous_changes_include?(attr)
184
+ mutations_before_last_save.changed?(attr)
185
185
  end
186
186
 
187
187
  # Restore all previous data of the provided attributes.
@@ -191,15 +191,12 @@ module ActiveModel
191
191
 
192
192
  # Clears all dirty data: current changes and previous changes.
193
193
  def clear_changes_information
194
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
195
194
  @mutations_before_last_save = nil
196
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
197
195
  forget_attribute_assignments
198
196
  @mutations_from_database = nil
199
197
  end
200
198
 
201
199
  def clear_attribute_changes(attr_names)
202
- attributes_changed_by_setter.except!(*attr_names)
203
200
  attr_names.each do |attr_name|
204
201
  clear_attribute_change(attr_name)
205
202
  end
@@ -212,13 +209,7 @@ module ActiveModel
212
209
  # person.name = 'robert'
213
210
  # person.changed_attributes # => {"name" => "bob"}
214
211
  def changed_attributes
215
- # This should only be set by methods which will call changed_attributes
216
- # multiple times when it is known that the computed value cannot change.
217
- if defined?(@cached_changed_attributes)
218
- @cached_changed_attributes
219
- else
220
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
221
- end
212
+ mutations_from_database.changed_values.freeze
222
213
  end
223
214
 
224
215
  # Returns a hash of changed attributes indicating their original
@@ -228,9 +219,8 @@ module ActiveModel
228
219
  # person.name = 'bob'
229
220
  # person.changes # => { "name" => ["bill", "bob"] }
230
221
  def changes
231
- cache_changed_attributes do
232
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
233
- end
222
+ _prepare_changes
223
+ mutations_from_database.changes
234
224
  end
235
225
 
236
226
  # Returns a hash of attributes that were changed before the model was saved.
@@ -240,8 +230,7 @@ module ActiveModel
240
230
  # person.save
241
231
  # person.previous_changes # => {"name" => ["bob", "robert"]}
242
232
  def previous_changes
243
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
244
- @previously_changed.merge(mutations_before_last_save.changes)
233
+ mutations_before_last_save.changes
245
234
  end
246
235
 
247
236
  def attribute_changed_in_place?(attr_name) # :nodoc:
@@ -257,11 +246,17 @@ module ActiveModel
257
246
  unless defined?(@mutations_from_database)
258
247
  @mutations_from_database = nil
259
248
  end
260
- @mutations_from_database ||= if defined?(@attributes)
261
- ActiveModel::AttributeMutationTracker.new(@attributes)
262
- else
263
- NullMutationTracker.instance
249
+
250
+ unless defined?(@attributes)
251
+ @_pseudo_attributes = true
252
+ @attributes = AttributeSet.new(
253
+ Hash.new { |h, attr|
254
+ h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value)
255
+ }
256
+ )
264
257
  end
258
+
259
+ @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes)
265
260
  end
266
261
 
267
262
  def forget_attribute_assignments
@@ -272,68 +267,45 @@ module ActiveModel
272
267
  @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
273
268
  end
274
269
 
275
- def cache_changed_attributes
276
- @cached_changed_attributes = changed_attributes
277
- yield
278
- ensure
279
- clear_changed_attributes_cache
280
- end
281
-
282
- def clear_changed_attributes_cache
283
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
284
- end
285
-
286
- # Returns +true+ if attr_name is changed, +false+ otherwise.
287
- def changes_include?(attr_name)
288
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
289
- end
290
- alias attribute_changed_by_setter? changes_include?
291
-
292
- # Returns +true+ if attr_name were changed before the model was saved,
293
- # +false+ otherwise.
294
- def previous_changes_include?(attr_name)
295
- previous_changes.include?(attr_name)
296
- end
297
-
298
270
  # Handles <tt>*_change</tt> for +method_missing+.
299
271
  def attribute_change(attr)
300
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
272
+ [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr)
301
273
  end
302
274
 
303
275
  # Handles <tt>*_previous_change</tt> for +method_missing+.
304
276
  def attribute_previous_change(attr)
305
- previous_changes[attr] if attribute_previously_changed?(attr)
277
+ mutations_before_last_save.change_to_attribute(attr)
306
278
  end
307
279
 
308
280
  # Handles <tt>*_will_change!</tt> for +method_missing+.
309
281
  def attribute_will_change!(attr)
310
- unless attribute_changed?(attr)
311
- begin
312
- value = _read_attribute(attr)
313
- value = value.duplicable? ? value.clone : value
314
- rescue TypeError, NoMethodError
315
- end
316
-
317
- set_attribute_was(attr, value)
282
+ attr = attr.to_s
283
+ mutations_from_database.force_change(attr).tap do
284
+ @attributes[attr] if defined?(@_pseudo_attributes)
318
285
  end
319
- mutations_from_database.force_change(attr)
320
286
  end
321
287
 
322
288
  # Handles <tt>restore_*!</tt> for +method_missing+.
323
289
  def restore_attribute!(attr)
324
290
  if attribute_changed?(attr)
325
- __send__("#{attr}=", changed_attributes[attr])
291
+ __send__("#{attr}=", attribute_was(attr))
326
292
  clear_attribute_changes([attr])
327
293
  end
328
294
  end
329
295
 
330
- def attributes_changed_by_setter
331
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
296
+ def _prepare_changes
297
+ if defined?(@_pseudo_attributes)
298
+ changed.each do |attr|
299
+ @attributes.write_from_user(attr, _read_attribute(attr))
300
+ end
301
+ end
332
302
  end
333
303
 
334
- # Force an attribute to have a particular "before" value
335
- def set_attribute_was(attr, old_value)
336
- attributes_changed_by_setter[attr] = old_value
304
+ def _clone_attribute(attr)
305
+ value = _read_attribute(attr)
306
+ value.duplicable? ? value.clone : value
307
+ rescue TypeError, NoMethodError
308
+ value
337
309
  end
338
310
  end
339
311
  end
@@ -10,7 +10,7 @@ module ActiveModel
10
10
  MAJOR = 5
11
11
  MINOR = 2
12
12
  TINY = 0
13
- PRE = "beta2"
13
+ PRE = "rc1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -29,7 +29,7 @@ module ActiveModel
29
29
  # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
30
30
  # of the model, and is used to a generate unique DOM id for the object.
31
31
  def test_to_key
32
- assert model.respond_to?(:to_key), "The model should respond to to_key"
32
+ assert_respond_to model, :to_key
33
33
  def model.persisted?() false end
34
34
  assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
35
35
  end
@@ -44,7 +44,7 @@ module ActiveModel
44
44
  # tests for this behavior in lint because it doesn't make sense to force
45
45
  # any of the possible implementation strategies on the implementer.
46
46
  def test_to_param
47
- assert model.respond_to?(:to_param), "The model should respond to to_param"
47
+ assert_respond_to model, :to_param
48
48
  def model.to_key() [1] end
49
49
  def model.persisted?() false end
50
50
  assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
@@ -56,7 +56,7 @@ module ActiveModel
56
56
  # <tt>to_partial_path</tt> is used for looking up partials. For example,
57
57
  # a BlogPost model might return "blog_posts/blog_post".
58
58
  def test_to_partial_path
59
- assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
59
+ assert_respond_to model, :to_partial_path
60
60
  assert_kind_of String, model.to_partial_path
61
61
  end
62
62
 
@@ -68,7 +68,7 @@ module ActiveModel
68
68
  # will route to the create action. If it is persisted, a form for the
69
69
  # object will route to the update action.
70
70
  def test_persisted?
71
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
71
+ assert_respond_to model, :persisted?
72
72
  assert_boolean model.persisted?, "persisted?"
73
73
  end
74
74
 
@@ -79,14 +79,14 @@ module ActiveModel
79
79
  #
80
80
  # Check ActiveModel::Naming for more information.
81
81
  def test_model_naming
82
- assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
82
+ assert_respond_to model.class, :model_name
83
83
  model_name = model.class.model_name
84
- assert model_name.respond_to?(:to_str)
85
- assert model_name.human.respond_to?(:to_str)
86
- assert model_name.singular.respond_to?(:to_str)
87
- assert model_name.plural.respond_to?(:to_str)
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
88
 
89
- assert model.respond_to?(:model_name), "The model instance should respond to model_name"
89
+ assert_respond_to model, :model_name
90
90
  assert_equal model.model_name, model.class.model_name
91
91
  end
92
92
 
@@ -100,13 +100,13 @@ module ActiveModel
100
100
  # If localization is used, the strings should be localized for the current
101
101
  # locale. If no error is present, the method should return an empty array.
102
102
  def test_errors_aref
103
- assert model.respond_to?(:errors), "The model should respond to errors"
103
+ assert_respond_to model, :errors
104
104
  assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
105
105
  end
106
106
 
107
107
  private
108
108
  def model
109
- assert @model.respond_to?(:to_model), "The object should respond to to_model"
109
+ assert_respond_to @model, :to_model
110
110
  @model.to_model
111
111
  end
112
112
 
@@ -24,7 +24,7 @@ module ActiveModel
24
24
  class << self
25
25
  attr_accessor :registry # :nodoc:
26
26
 
27
- # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup
27
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
28
28
  def register(type_name, klass = nil, **options, &block)
29
29
  registry.register(type_name, klass, **options, &block)
30
30
  end
@@ -164,14 +164,14 @@ module ActiveModel
164
164
 
165
165
  if options.key?(:on)
166
166
  options = options.dup
167
+ options[:on] = Array(options[:on])
167
168
  options[:if] = Array(options[:if])
168
169
  options[:if].unshift ->(o) {
169
- !(Array(options[:on]) & Array(o.validation_context)).empty?
170
+ !(options[:on] & Array(o.validation_context)).empty?
170
171
  }
171
172
  end
172
173
 
173
- args << options
174
- set_callback(:validate, *args, &block)
174
+ set_callback(:validate, *args, options, &block)
175
175
  end
176
176
 
177
177
  # List all validators that are being used to validate the model using
@@ -54,15 +54,18 @@ module ActiveModel
54
54
  # person.valid? # => true
55
55
  # person.name # => "bob"
56
56
  def before_validation(*args, &block)
57
- options = args.last
58
- if options.is_a?(Hash) && options[:on]
59
- options[:if] = Array(options[:if])
57
+ options = args.extract_options!
58
+
59
+ if options.key?(:on)
60
+ options = options.dup
60
61
  options[:on] = Array(options[:on])
62
+ options[:if] = Array(options[:if])
61
63
  options[:if].unshift ->(o) {
62
- options[:on].include? o.validation_context
64
+ !(options[:on] & Array(o.validation_context)).empty?
63
65
  }
64
66
  end
65
- set_callback(:validation, :before, *args, &block)
67
+
68
+ set_callback(:validation, :before, *args, options, &block)
66
69
  end
67
70
 
68
71
  # Defines a callback that will get called right after validation.
@@ -93,15 +96,18 @@ module ActiveModel
93
96
  # person.status # => true
94
97
  def after_validation(*args, &block)
95
98
  options = args.extract_options!
99
+ options = options.dup
96
100
  options[:prepend] = true
97
- options[:if] = Array(options[:if])
98
- if options[:on]
101
+
102
+ if options.key?(:on)
99
103
  options[:on] = Array(options[:on])
104
+ options[:if] = Array(options[:if])
100
105
  options[:if].unshift ->(o) {
101
- options[:on].include? o.validation_context
106
+ !(options[:on] & Array(o.validation_context)).empty?
102
107
  }
103
108
  end
104
- set_callback(:validation, :after, *(args << options), &block)
109
+
110
+ set_callback(:validation, :after, *args, options, &block)
105
111
  end
106
112
  end
107
113
 
@@ -154,7 +154,7 @@ module ActiveModel
154
154
  # When creating custom validators, it might be useful to be able to specify
155
155
  # additional default keys. This can be done by overwriting this method.
156
156
  def _validates_default_keys
157
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
157
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
158
158
  end
159
159
 
160
160
  def _parse_validates_options(options)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.0.beta2
4
+ version: 5.2.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-28 00:00:00.000000000 Z
11
+ date: 2018-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 5.2.0.beta2
19
+ version: 5.2.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 5.2.0.beta2
26
+ version: 5.2.0.rc1
27
27
  description: A toolkit for building modeling frameworks like Active Record. Rich support
28
28
  for attributes, callbacks, validations, serialization, internationalization, and
29
29
  testing.
@@ -100,8 +100,8 @@ homepage: http://rubyonrails.org
100
100
  licenses:
101
101
  - MIT
102
102
  metadata:
103
- source_code_uri: https://github.com/rails/rails/tree/v5.2.0.beta2/activemodel
104
- changelog_uri: https://github.com/rails/rails/blob/v5.2.0.beta2/activemodel/CHANGELOG.md
103
+ source_code_uri: https://github.com/rails/rails/tree/v5.2.0.rc1/activemodel
104
+ changelog_uri: https://github.com/rails/rails/blob/v5.2.0.rc1/activemodel/CHANGELOG.md
105
105
  post_install_message:
106
106
  rdoc_options: []
107
107
  require_paths:
@@ -118,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
118
  version: 1.3.1
119
119
  requirements: []
120
120
  rubyforge_project:
121
- rubygems_version: 2.6.12
121
+ rubygems_version: 2.7.3
122
122
  signing_key:
123
123
  specification_version: 4
124
124
  summary: A toolkit for building modeling frameworks (part of Rails).