Skip to content

Commit 93ca27f

Browse files
authored
Merge 0-10-stable into master (to fix breaking change). (#2023)
* Merge pull request #1990 from mxie/mx-result-typo Fix typos and capitalization in Relationship Links docs [ci skip] * Merge pull request #1992 from ojiry/bump_ruby_versions Run tests by Ruby 2.2.6 and 2.3.3 * Merge pull request #1994 from bf4/promote_architecture Promote important architecture description that answers a lot of questions we get Conflicts: docs/ARCHITECTURE.md * Merge pull request #1999 from bf4/typos Fix typos [ci skip] * Merge pull request #2000 from berfarah/patch-1 Link to 0.10.3 tag instead of `master` branch * Merge pull request #2007 from bf4/check_ci Test was failing due to change in JSON exception message when parsing empty string * Swap out KeyTransform for CaseTransform (#1993) * delete KeyTransform, use CaseTransform * added changelog Conflicts: CHANGELOG.md * Merge pull request #2005 from kofronpi/support-ruby-2.4 Update jsonapi runtime dependency to 0.1.1.beta6 * Bump to v0.10.4 * Merge pull request #2018 from rails-api/bump_version Bump to v0.10.4 [ci skip] Conflicts: CHANGELOG.md * Merge pull request #2019 from bf4/fix_method_redefined_warning Fix AMS warnings * Merge pull request #2020 from bf4/silence_grape_warnings Silence Grape warnings * Merge pull request #2017 from bf4/remove_warnings Fix mt6 assert_nil warnings * Updated isolated tests to assert correct behavior. (#2010) * Updated isolated tests to assert correct behavior. * Added check to get unsafe params if rails version is great than 5 * Merge pull request #2012 from bf4/cleanup_isolated_jsonapi_renderer_tests_a_bit Cleanup assertions in isolated jsonapi renderer tests a bit * Add Model#attributes helper; make test attributes explicit * Fix model attributes accessors * Fix typos * Randomize testing of compatibility layer against regressions * Test bugfix * Add CHANGELOG * Merge pull request #1981 from groyoh/link_doc Fix relationship links doc Conflicts: CHANGELOG.md
1 parent 2a6d373 commit 93ca27f

File tree

14 files changed

+335
-83
lines changed

14 files changed

+335
-83
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ Breaking changes:
66

77
Features:
88

9-
- [#1982](https://github.com/rails-api/active_model_serializers/pull/1982) Add ActiveModelSerializers::Model.attributes to configure PORO attributes. (@bf4)
9+
- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) ActiveModelSerializers::Model#attributes. Originally in [#1982](https://github.com/rails-api/active_model_serializers/pull/1982). (@bf4)
1010

1111
Fixes:
1212

13-
- [#1984](https://github.com/rails-api/active_model_serializers/pull/1984) Mutation of ActiveModelSerializers::Model now changes the attributes. (@bf4)
13+
- [#2022](https://github.com/rails-api/active_model_serializers/pull/2022) Mutation of ActiveModelSerializers::Model now changes the attributes. Originally in [#1984](https://github.com/rails-api/active_model_serializers/pull/1984). (@bf4)
1414

1515
Misc:
1616

17+
- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) Make test attributes explicit. Tests have Model#associations. (@bf4)
1718
- [#1981](https://github.com/rails-api/active_model_serializers/pull/1981) Fix relationship link documentation. (@groyoh)
18-
- [#1984](https://github.com/rails-api/active_model_serializers/pull/1984) Make test attributes explicit. Test models have 'associations' support. (@bf4)
1919

2020
### [v0.10.4 (2017-01-06)](https://github.com/rails-api/active_model_serializers/compare/v0.10.3...v0.10.4)
2121

docs/howto/serialize_poro.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
# How to serialize a Plain-Old Ruby Object (PORO)
44

5-
When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable, but pretty much any object can be serializable with ActiveModelSerializers. Here is an example of a PORO that is serializable:
5+
When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable,
6+
but pretty much any object can be serializable with ActiveModelSerializers.
7+
Here is an example of a PORO that is serializable in most situations:
8+
69
```ruby
710
# my_model.rb
811
class MyModel
912
alias :read_attribute_for_serialization :send
1013
attr_accessor :id, :name, :level
11-
14+
1215
def initialize(attributes)
1316
@id = attributes[:id]
1417
@name = attributes[:name]
@@ -21,12 +24,22 @@ class MyModel
2124
end
2225
```
2326

24-
Fortunately, ActiveModelSerializers provides a [`ActiveModelSerializers::Model`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb) which you can use in production code that will make your PORO a lot cleaner. The above code now becomes:
27+
The [ActiveModel::Serializer::Lint::Tests](../../lib/active_model/serializer/lint.rb)
28+
define and validate which methods ActiveModelSerializers expects to be implemented.
29+
30+
An implementation of the complete spec is included either for use or as reference:
31+
[`ActiveModelSerializers::Model`](../../lib/active_model_serializers/model.rb).
32+
You can use in production code that will make your PORO a lot cleaner.
33+
34+
The above code now becomes:
35+
2536
```ruby
2637
# my_model.rb
2738
class MyModel < ActiveModelSerializers::Model
2839
attributes :id, :name, :level
2940
end
3041
```
3142

32-
The default serializer would be `MyModelSerializer`.
43+
The default serializer would be `MyModelSerializer`.
44+
45+
For more information, see [README: What does a 'serializable resource' look like?](../../README.md#what-does-a-serializable-resource-look-like).

lib/active_model_serializers/model.rb

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
1-
# ActiveModelSerializers::Model is a convenient
2-
# serializable class to inherit from when making
3-
# serializable non-activerecord objects.
1+
# ActiveModelSerializers::Model is a convenient superclass for making your models
2+
# from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation
3+
# that satisfies ActiveModel::Serializer::Lint::Tests.
44
module ActiveModelSerializers
55
class Model
66
include ActiveModel::Serializers::JSON
77
include ActiveModel::Model
88

9-
class_attribute :attribute_names
9+
# Declare names of attributes to be included in +sttributes+ hash.
10+
# Is only available as a class-method since the ActiveModel::Serialization mixin in Rails
11+
# uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here.
12+
#
13+
# @overload attribute_names
14+
# @return [Array<Symbol>]
15+
class_attribute :attribute_names, instance_writer: false, instance_reader: false
1016
# Initialize +attribute_names+ for all subclasses. The array is usually
1117
# mutated in the +attributes+ method, but can be set directly, as well.
1218
self.attribute_names = []
1319

20+
# Easily declare instance attributes with setters and getters for each.
21+
#
22+
# All attributes to initialize an instance must have setters.
23+
# However, the hash turned by +attributes+ instance method will ALWAYS
24+
# be the value of the initial attributes, regardless of what accessors are defined.
25+
# The only way to change the change the attributes after initialization is
26+
# to mutate the +attributes+ directly.
27+
# Accessor methods do NOT mutate the attributes. (This is a bug).
28+
#
29+
# @note For now, the Model only supports the notion of 'attributes'.
30+
# In the tests, there is a special Model that also supports 'associations'. This is
31+
# important so that we can add accessors for values that should not appear in the
32+
# attributes hash when modeling associations. It is not yet clear if it
33+
# makes sense for a PORO to have associations outside of the tests.
34+
#
35+
# @overload attributes(names)
36+
# @param names [Array<String, Symbol>]
37+
# @param name [String, Symbol]
1438
def self.attributes(*names)
1539
self.attribute_names |= names.map(&:to_sym)
1640
# Silence redefinition of methods warnings
@@ -19,44 +43,97 @@ def self.attributes(*names)
1943
end
2044
end
2145

46+
# Opt-in to breaking change
47+
def self.derive_attributes_from_names_and_fix_accessors
48+
unless included_modules.include?(DeriveAttributesFromNamesAndFixAccessors)
49+
prepend(DeriveAttributesFromNamesAndFixAccessors)
50+
end
51+
end
52+
53+
module DeriveAttributesFromNamesAndFixAccessors
54+
def self.included(base)
55+
# NOTE that +id+ will always be in +attributes+.
56+
base.attributes :id
57+
end
58+
59+
# Override the initialize method so that attributes aren't processed.
60+
#
61+
# @param attributes [Hash]
62+
def initialize(attributes = {})
63+
@errors = ActiveModel::Errors.new(self)
64+
super
65+
end
66+
67+
# Override the +attributes+ method so that the hash is derived from +attribute_names+.
68+
#
69+
# The the fields in +attribute_names+ determines the returned hash.
70+
# +attributes+ are returned frozen to prevent any expectations that mutation affects
71+
# the actual values in the model.
72+
def attributes
73+
self.class.attribute_names.each_with_object({}) do |attribute_name, result|
74+
result[attribute_name] = public_send(attribute_name).freeze
75+
end.with_indifferent_access.freeze
76+
end
77+
end
78+
79+
# Support for validation and other ActiveModel::Errors
80+
# @return [ActiveModel::Errors]
2281
attr_reader :errors
23-
# NOTE that +updated_at+ isn't included in +attribute_names+,
24-
# which means it won't show up in +attributes+ unless a subclass has
25-
# either <tt>attributes :updated_at</tt> which will redefine the methods
26-
# or <tt>attribute_names << :updated_at</tt>.
82+
83+
# (see #updated_at)
2784
attr_writer :updated_at
28-
# NOTE that +id+ will always be in +attributes+.
29-
attributes :id
3085

86+
# The only way to change the attributes of an instance is to directly mutate the attributes.
87+
# @example
88+
#
89+
# model.attributes[:foo] = :bar
90+
# @return [Hash]
91+
attr_reader :attributes
92+
93+
# @param attributes [Hash]
3194
def initialize(attributes = {})
95+
attributes ||= {} # protect against nil
96+
@attributes = attributes.symbolize_keys.with_indifferent_access
3297
@errors = ActiveModel::Errors.new(self)
3398
super
3499
end
35100

36-
# The the fields in +attribute_names+ determines the returned hash.
37-
# +attributes+ are returned frozen to prevent any expectations that mutation affects
38-
# the actual values in the model.
39-
def attributes
40-
attribute_names.each_with_object({}) do |attribute_name, result|
41-
result[attribute_name] = public_send(attribute_name).freeze
42-
end.with_indifferent_access.freeze
101+
# Defaults to the downcased model name.
102+
# This probably isn't a good default, since it's not a unique instance identifier,
103+
# but that's what is currently implemented \_('-')_/.
104+
#
105+
# @note Though +id+ is defined, it will only show up
106+
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
107+
# such as <tt>attributes[:id] = 5</tt>.
108+
# @return [String, Numeric, Symbol]
109+
def id
110+
attributes.fetch(:id) do
111+
defined?(@id) ? @id : self.class.model_name.name && self.class.model_name.name.downcase
112+
end
113+
end
114+
115+
# When not set, defaults to the time the file was modified.
116+
#
117+
# @note Though +updated_at+ and +updated_at=+ are defined, it will only show up
118+
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
119+
# such as <tt>attributes[:updated_at] = Time.current</tt>.
120+
# @return [String, Numeric, Time]
121+
def updated_at
122+
attributes.fetch(:updated_at) do
123+
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
124+
end
43125
end
44126

45127
# To customize model behavior, this method must be redefined. However,
46128
# there are other ways of setting the +cache_key+ a serializer uses.
129+
# @return [String]
47130
def cache_key
48131
ActiveSupport::Cache.expand_cache_key([
49132
self.class.model_name.name.downcase,
50133
"#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}"
51134
].compact)
52135
end
53136

54-
# When no set, defaults to the time the file was modified.
55-
# See NOTE by attr_writer :updated_at
56-
def updated_at
57-
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
58-
end
59-
60137
# The following methods are needed to be minimally implemented for ActiveModel::Errors
61138
# :nocov:
62139
def self.human_attribute_name(attr, _options = {})

test/action_controller/adapter_selector_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
module ActionController
44
module Serialization
55
class AdapterSelectorTest < ActionController::TestCase
6+
class Profile < Model
7+
attributes :id, :name, :description
8+
associations :comments
9+
end
10+
class ProfileSerializer < ActiveModel::Serializer
11+
type 'profiles'
12+
attributes :name, :description
13+
end
14+
615
class AdapterSelectorTestController < ActionController::Base
716
def render_using_default_adapter
817
@profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1')

test/action_controller/namespace_lookup_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ActionController
44
module Serialization
55
class NamespaceLookupTest < ActionController::TestCase
66
class Book < ::Model
7-
attributes :title, :body
7+
attributes :id, :title, :body
88
associations :writer, :chapters
99
end
1010
class Chapter < ::Model
@@ -86,15 +86,15 @@ def explicit_namespace_as_string
8686
book = Book.new(title: 'New Post', body: 'Body')
8787

8888
# because this is a string, ruby can't auto-lookup the constant, so otherwise
89-
# the looku things we mean ::Api::V2
89+
# the lookup thinks we mean ::Api::V2
9090
render json: book, namespace: 'ActionController::Serialization::NamespaceLookupTest::Api::V2'
9191
end
9292

9393
def explicit_namespace_as_symbol
9494
book = Book.new(title: 'New Post', body: 'Body')
9595

9696
# because this is a string, ruby can't auto-lookup the constant, so otherwise
97-
# the looku things we mean ::Api::V2
97+
# the lookup thinks we mean ::Api::V2
9898
render json: book, namespace: :'ActionController::Serialization::NamespaceLookupTest::Api::V2'
9999
end
100100

test/active_model_serializers/model_test.rb

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,56 @@ def test_attributes_can_be_read_for_serialization
2424
attributes :one, :two, :three
2525
end
2626
original_attributes = { one: 1, two: 2, three: 3 }
27-
instance = klass.new(original_attributes)
27+
original_instance = klass.new(original_attributes)
2828

2929
# Initial value
30-
expected_attributes = { id: nil, one: 1, two: 2, three: 3 }.with_indifferent_access
30+
instance = original_instance
31+
expected_attributes = { one: 1, two: 2, three: 3 }.with_indifferent_access
3132
assert_equal expected_attributes, instance.attributes
3233
assert_equal 1, instance.one
3334
assert_equal 1, instance.read_attribute_for_serialization(:one)
3435

35-
# Change via accessor
36+
# FIXME: Change via accessor has no effect on attributes.
37+
instance = original_instance.dup
3638
instance.one = :not_one
39+
assert_equal expected_attributes, instance.attributes
40+
assert_equal :not_one, instance.one
41+
assert_equal :not_one, instance.read_attribute_for_serialization(:one)
42+
43+
# FIXME: Change via mutating attributes
44+
instance = original_instance.dup
45+
instance.attributes[:one] = :not_one
46+
expected_attributes = { one: :not_one, two: 2, three: 3 }.with_indifferent_access
47+
assert_equal expected_attributes, instance.attributes
48+
assert_equal 1, instance.one
49+
assert_equal 1, instance.read_attribute_for_serialization(:one)
50+
end
51+
52+
def test_attributes_can_be_read_for_serialization_with_attributes_accessors_fix
53+
klass = Class.new(ActiveModelSerializers::Model) do
54+
derive_attributes_from_names_and_fix_accessors
55+
attributes :one, :two, :three
56+
end
57+
original_attributes = { one: 1, two: 2, three: 3 }
58+
original_instance = klass.new(original_attributes)
59+
60+
# Initial value
61+
instance = original_instance
62+
expected_attributes = { one: 1, two: 2, three: 3 }.with_indifferent_access
63+
assert_equal expected_attributes, instance.attributes
64+
assert_equal 1, instance.one
65+
assert_equal 1, instance.read_attribute_for_serialization(:one)
3766

38-
expected_attributes = { id: nil, one: :not_one, two: 2, three: 3 }.with_indifferent_access
67+
expected_attributes = { one: :not_one, two: 2, three: 3 }.with_indifferent_access
68+
# Change via accessor
69+
instance = original_instance.dup
70+
instance.one = :not_one
3971
assert_equal expected_attributes, instance.attributes
4072
assert_equal :not_one, instance.one
4173
assert_equal :not_one, instance.read_attribute_for_serialization(:one)
74+
75+
# Attributes frozen
76+
assert instance.attributes.frozen?
4277
end
4378

4479
def test_id_attribute_can_be_read_for_serialization
@@ -47,21 +82,59 @@ def test_id_attribute_can_be_read_for_serialization
4782
end
4883
self.class.const_set(:SomeTestModel, klass)
4984
original_attributes = { id: :ego, one: 1, two: 2, three: 3 }
50-
instance = klass.new(original_attributes)
85+
original_instance = klass.new(original_attributes)
5186

5287
# Initial value
88+
instance = original_instance.dup
5389
expected_attributes = { id: :ego, one: 1, two: 2, three: 3 }.with_indifferent_access
5490
assert_equal expected_attributes, instance.attributes
55-
assert_equal 1, instance.one
56-
assert_equal 1, instance.read_attribute_for_serialization(:one)
91+
assert_equal :ego, instance.id
92+
assert_equal :ego, instance.read_attribute_for_serialization(:id)
5793

58-
# Change via accessor
94+
# FIXME: Change via accessor has no effect on attributes.
95+
instance = original_instance.dup
5996
instance.id = :superego
97+
assert_equal expected_attributes, instance.attributes
98+
assert_equal :superego, instance.id
99+
assert_equal :superego, instance.read_attribute_for_serialization(:id)
60100

101+
# FIXME: Change via mutating attributes
102+
instance = original_instance.dup
103+
instance.attributes[:id] = :superego
61104
expected_attributes = { id: :superego, one: 1, two: 2, three: 3 }.with_indifferent_access
62105
assert_equal expected_attributes, instance.attributes
106+
assert_equal :ego, instance.id
107+
assert_equal :ego, instance.read_attribute_for_serialization(:id)
108+
ensure
109+
self.class.send(:remove_const, :SomeTestModel)
110+
end
111+
112+
def test_id_attribute_can_be_read_for_serialization_with_attributes_accessors_fix
113+
klass = Class.new(ActiveModelSerializers::Model) do
114+
derive_attributes_from_names_and_fix_accessors
115+
attributes :id, :one, :two, :three
116+
end
117+
self.class.const_set(:SomeTestModel, klass)
118+
original_attributes = { id: :ego, one: 1, two: 2, three: 3 }
119+
original_instance = klass.new(original_attributes)
120+
121+
# Initial value
122+
instance = original_instance.dup
123+
expected_attributes = { id: :ego, one: 1, two: 2, three: 3 }.with_indifferent_access
124+
assert_equal expected_attributes, instance.attributes
125+
assert_equal :ego, instance.id
126+
assert_equal :ego, instance.read_attribute_for_serialization(:id)
127+
128+
expected_attributes = { id: :superego, one: 1, two: 2, three: 3 }.with_indifferent_access
129+
# Change via accessor
130+
instance = original_instance.dup
131+
instance.id = :superego
132+
assert_equal expected_attributes, instance.attributes
63133
assert_equal :superego, instance.id
64134
assert_equal :superego, instance.read_attribute_for_serialization(:id)
135+
136+
# Attributes frozen
137+
assert instance.attributes.frozen?
65138
ensure
66139
self.class.send(:remove_const, :SomeTestModel)
67140
end

0 commit comments

Comments
 (0)