-
Notifications
You must be signed in to change notification settings - Fork 363
Integrate with Active Model Attributes #410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
d1d2bca to
d789946
Compare
408844c to
5e5344a
Compare
2e56983 to
da59d07
Compare
b15f777 to
2676f37
Compare
Replace all access of `@attributes` and `attributes` key-value pairs with calls to `read_attribute` and `write_attribute`. The `read_attribute` and `write_attribute` implementations draw inspiration from [ActiveRecord::AttributeMethods::Read][] and [ActiveRecord::AttributeMethods::Write][], respectively. [rails/rails#53886][] proposes implementing each method at the Active Model layer. While that proposal is considered, this commit implements each method in terms of accessing the underlying `@attributes` hash instance. This change is also in support of a first-party integration with [ActiveModel::Attributes][] proposed in [rails#410][], and aims to be compatible with its attribute reading and writing interfaces. ```ruby person = Person.find(1) person.read_attribute("name") # => "Matz" person.name # => "Matz" person.write_attribute("name", "matz") person.name # => "matz" ``` [ActiveRecord::AttributeMethods::Read]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Read.html#method-i-read_attribute [ActiveRecord::AttributeMethods::Write]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Write.html#method-i-write_attribute [rails/rails#53886]: rails/rails#53886 [ActiveModel::Attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html [rails#410]: rails#410
2676f37 to
0389ff5
Compare
The Base class defines an `attr_accessor :attributes` that is marked with `# :nodoc:`. Technically, this means that any interaction with `Base#attributes` or `Base#attributes=` is not part of the public interface, and is free to be changed or removed without breaking the public API. However, given the project's age and the long-term period of time with minimal changes to the public interface, this PR proposes that there be a public deprecation of **writing** to the Hash instance returned by the `Base#attributes` method. The migration to `ActiveModel::Attributes` proposed in [rails#410][] will involve changes to the `Base#attributes` method (due to [ActiveModel::Attributes#attributes][]). Reading from the value returned by `ActiveModel::Attributes#attributes` will remain the same, but writing to that value will have no affect since the value is a Hash copy created by [ActiveModel::AttributeSet#to_hash][], and not the Hash instance used internally. Once deprecated and part of a release cycle, the `ActiveResource::ReadOnlyHash` class can be removed, and the Active Model migration's backwards compatibility burden can be reduced. [rails#410]: rails#410 [ActiveModel::Attributes#attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html#method-i-attributes [ActiveModel::AttributeSet#to_hash]: https://github.com/rails/rails/blob/v8.1.1/activemodel/lib/active_model/attribute_set.rb#L36-L39
The Base class defines an `attr_accessor :attributes` that is marked with `# :nodoc:`. Technically, this means that any interaction with `Base#attributes` or `Base#attributes=` is not part of the public interface, and is free to be changed or removed without breaking the public API. However, given the project's age and the long-term period of time with minimal changes to the public interface, this PR proposes that there be a public deprecation of **writing** to the Hash instance returned by the `Base#attributes` method. The migration to `ActiveModel::Attributes` proposed in [rails#410][] will involve changes to the `Base#attributes` method (due to [ActiveModel::Attributes#attributes][]). Reading from the value returned by `ActiveModel::Attributes#attributes` will remain the same, but writing to that value will have no affect since the value is a Hash copy created by [ActiveModel::AttributeSet#to_hash][], and not the Hash instance used internally. Similarly, `ActiveModel::Attributes` does not expose a corresponding `#attributes=` method. By deprecating `#attributes=`, changes made that incorporate `ActiveModel::Attributes` will not need to add an otherwise unnecessary `#attributes=` implementation. Once deprecated and part of a release cycle, the `ActiveResource::AttributeSet` class can be removed, and the Active Model migration's backwards compatibility burden can be reduced. [rails#410]: rails#410 [ActiveModel::Attributes#attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html#method-i-attributes [ActiveModel::AttributeSet#to_hash]: https://github.com/rails/rails/blob/v8.1.1/activemodel/lib/active_model/attribute_set.rb#L36-L39
Replace all access of `@attributes` and `attributes` key-value pairs with calls to `read_attribute` and `write_attribute`. The `read_attribute` and `write_attribute` implementations draw inspiration from [ActiveRecord::AttributeMethods::Read][] and [ActiveRecord::AttributeMethods::Write][], respectively. [rails/rails#53886][] proposes implementing each method at the Active Model layer. While that proposal is considered, this commit implements each method in terms of accessing the underlying `@attributes` hash instance. This change is also in support of a first-party integration with [ActiveModel::Attributes][] proposed in [rails#410][], and aims to be compatible with its attribute reading and writing interfaces. ```ruby person = Person.find(1) person.read_attribute("name") # => "Matz" person.name # => "Matz" person.write_attribute("name", "matz") person.name # => "matz" ``` [ActiveRecord::AttributeMethods::Read]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Read.html#method-i-read_attribute [ActiveRecord::AttributeMethods::Write]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Write.html#method-i-write_attribute [rails/rails#53886]: rails/rails#53886 [ActiveModel::Attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html [rails#410]: rails#410
The Base class defines an `attr_accessor :attributes` that is marked with `# :nodoc:`. Technically, this means that any interaction with `Base#attributes` or `Base#attributes=` is not part of the public interface, and is free to be changed or removed without breaking the public API. However, given the project's age and the long-term period of time with minimal changes to the public interface, this PR proposes that there be a public deprecation of **writing** to the Hash instance returned by the `Base#attributes` method. The migration to `ActiveModel::Attributes` proposed in [rails#410][] will involve changes to the `Base#attributes` method (due to [ActiveModel::Attributes#attributes][]). Reading from the value returned by `ActiveModel::Attributes#attributes` will remain the same, but writing to that value will have no affect since the value is a Hash copy created by [ActiveModel::AttributeSet#to_hash][], and not the Hash instance used internally. Similarly, `ActiveModel::Attributes` does not expose a corresponding `#attributes=` method. By deprecating `#attributes=`, changes made that incorporate `ActiveModel::Attributes` will not need to add an otherwise unnecessary `#attributes=` implementation. Once deprecated and part of a release cycle, the `ActiveResource::AttributeSet` class can be removed, and the Active Model migration's backwards compatibility burden can be reduced. [rails#410]: rails#410 [ActiveModel::Attributes#attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html#method-i-attributes [ActiveModel::AttributeSet#to_hash]: https://github.com/rails/rails/blob/v8.1.1/activemodel/lib/active_model/attribute_set.rb#L36-L39
The Base class defines an `attr_accessor :attributes` that is marked with `# :nodoc:`. Technically, this means that any interaction with `Base#attributes` or `Base#attributes=` is not part of the public interface, and is free to be changed or removed without breaking the public API. However, given the project's age and the long-term period of time with minimal changes to the public interface, this PR proposes that there be a public deprecation of **writing** to the Hash instance returned by the `Base#attributes` method. The migration to `ActiveModel::Attributes` proposed in [rails#410][] will involve changes to the `Base#attributes` method (due to [ActiveModel::Attributes#attributes][]). Reading from the value returned by `ActiveModel::Attributes#attributes` will remain the same, but writing to that value will have no affect since the value is a Hash copy created by [ActiveModel::AttributeSet#to_hash][], and not the Hash instance used internally. Similarly, `ActiveModel::Attributes` does not expose a corresponding `#attributes=` method. By deprecating `#attributes=`, changes made that incorporate `ActiveModel::Attributes` will not need to add an otherwise unnecessary `#attributes=` implementation. Once deprecated and part of a release cycle, the `ActiveResource::AttributeSet` class can be removed, and the Active Model migration's backwards compatibility burden can be reduced. [rails#410]: rails#410 [ActiveModel::Attributes#attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html#method-i-attributes [ActiveModel::AttributeSet#to_hash]: https://github.com/rails/rails/blob/v8.1.1/activemodel/lib/active_model/attribute_set.rb#L36-L39
The Base class defines an `attr_accessor :attributes` that is marked with `# :nodoc:`. Technically, this means that any interaction with `Base#attributes` or `Base#attributes=` is not part of the public interface, and is free to be changed or removed without breaking the public API. However, given the project's age and the long-term period of time with minimal changes to the public interface, this PR proposes that there be a public deprecation of **writing** to the Hash instance returned by the `Base#attributes` method. The migration to `ActiveModel::Attributes` proposed in [rails#410][] will involve changes to the `Base#attributes` method (due to [ActiveModel::Attributes#attributes][]). Reading from the value returned by `ActiveModel::Attributes#attributes` will remain the same, but writing to that value will have no affect since the value is a Hash copy created by [ActiveModel::AttributeSet#to_hash][], and not the Hash instance used internally. Similarly, `ActiveModel::Attributes` does not expose a corresponding `#attributes=` method. By deprecating `#attributes=`, changes made that incorporate `ActiveModel::Attributes` will not need to add an otherwise unnecessary `#attributes=` implementation. Once deprecated and part of a release cycle, the `ActiveResource::AttributeSet` class can be removed, and the Active Model migration's backwards compatibility burden can be reduced. [rails#410]: rails#410 [ActiveModel::Attributes#attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html#method-i-attributes [ActiveModel::AttributeSet#to_hash]: https://github.com/rails/rails/blob/v8.1.1/activemodel/lib/active_model/attribute_set.rb#L36-L39
Replace all access of `@attributes` and `attributes` key-value pairs with calls to `read_attribute` and `write_attribute`. The `read_attribute` and `write_attribute` implementations draw inspiration from [ActiveRecord::AttributeMethods::Read][] and [ActiveRecord::AttributeMethods::Write][], respectively. [rails/rails#53886][] proposes implementing each method at the Active Model layer. While that proposal is considered, this commit implements each method in terms of accessing the underlying `@attributes` hash instance. This change is also in support of a first-party integration with [ActiveModel::Attributes][] proposed in [rails#410][], and aims to be compatible with its attribute reading and writing interfaces. ```ruby person = Person.find(1) person.read_attribute("name") # => "Matz" person.name # => "Matz" person.write_attribute("name", "matz") person.name # => "matz" ``` [ActiveRecord::AttributeMethods::Read]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Read.html#method-i-read_attribute [ActiveRecord::AttributeMethods::Write]: https://edgeapi.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Write.html#method-i-write_attribute [rails/rails#53886]: rails/rails#53886 [ActiveModel::Attributes]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Attributes.html [rails#410]: rails#410
|
@seanpdoyle do you mind rebasing this? |
0389ff5 to
6c5b5fa
Compare
|
@guilleiguaran thank you for providing review and helping to merge open PRs! I've rebased this to incorporate the latest changes. I'm happy that it's functional, and attribute integration (casting, method hooks, etc) will be a big win! I'm still not entirely satisfied with the integration touchpoint. Ideally, I'm curious if @rafaelfranca has an opinion on how this currently proposed implementation compares to a more direct integration. Are there tasks included in rails/rails#50568 (currently marked for Rails' 8.2.0 milestone) that are worthwhile to consider as part of this integration? |
635d6d8 to
f6b5a24
Compare
The `schema { ... }` interface pre-dates the Active Model Attributes API
(defined as early as [v5.2.0][]), but clearly draws inspiration from
Active Record's Database Schema and Attribute casting (which was
extracted into `ActiveModel::Attributes`).
However, the type information captured in `schema { ... }` blocks or
assigned as `Hash` arguments to `schema=` is purely inert metadata.
Proposal
---
This commit aims to integrate with [ActiveModel::Model][] and
[ActiveModel::Attributes][]. Through the introduction of both modules,
subclasses of `ActiveResource::Schema` can benefit from type casting
attributes and constructing instances with default values.
This commit makes minimally incremental changes, prioritizing backwards
compatibility. The reliance on `#respond_to_missing?` and
`#method_missing` is left largely unchanged. Similarly, the `Schema`
interface continues to provide metadata about its attributes through the
`Schema#attr` method (instead of reading from
`ActiveModel::Attributes#attribute_names` or
`ActiveModel::Attributes.attribute_types`).
API Changes
---
To cast values to their specified types, declare the Schema with the
`:cast_values` set to true.
```ruby
class Person < ActiveResource::Base
schema cast_values: true do
integer 'age'
end
end
p = Person.new
p.age = "18"
p.age # => 18
```
To configure inheriting resources to cast values, set the `cast_values`
class attribute:
```ruby
class ApplicationResource < ActiveResource::Base
self.cast_values = true
end
class Person < ApplicationResource
schema do
integer 'age'
end
end
p = Person.new
p.age = "18"
p.age # => 18
```
To set all resources application-wide to cast values, set
`config.active_resource.cast_values`:
```ruby
# config/application.rb
config.active_resource.cast_values = true
```
[v5.2.0]: https://api.rubyonrails.org/v5.2.0/classes/ActiveModel/Attributes/ClassMethods.html
[ActiveModel::Model]: https://api.rubyonrails.org/classes/ActiveModel/Model.html
[ActiveModel::Attributes]: https://api.rubyonrails.org/classes/ActiveModel/Attributes/ClassMethods.html
f6b5a24 to
f414cf9
Compare
The
schema { ... }interface pre-dates the Active Model Attributes API (defined as early as v5.2.0), but clearly draws inspiration from Active Record's Database Schema and Attribute casting (which was extracted intoActiveModel::Attributes).However, the type information captured in
schema { ... }blocks or assigned asHasharguments toschema=is purely inert metadata.Proposal
This commit aims to integrate with ActiveModel::Model and ActiveModel::Attributes. Through the introduction of both modules, subclasses of
ActiveResource::Basecan benefit from type casting attributes and constructing instances with default values.This commit makes minimally incremental changes, prioritizing backwards compatibility. The reliance on
#respond_to_missing?and#method_missingis left largely unchanged. Similarly, theSchemainterface continues to provide metadata about its attributes through theSchema#attrmethod (instead of reading fromActiveModel::Attributes#attribute_namesorActiveModel::Attributes.attribute_types).API Changes
To cast values to their specified types, declare the Schema with the
:cast_valuesset to true.To configure inheriting resources to cast values, set the
cast_valuesclass attribute:To set all resources application-wide to cast values, set
config.active_resource.cast_values: