active_model_serializers 0.10.0.rc2 → 0.10.0.rc3

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +82 -0
  4. data/.rubocop_todo.yml +315 -0
  5. data/.simplecov +99 -0
  6. data/.travis.yml +8 -0
  7. data/CHANGELOG.md +9 -3
  8. data/Gemfile +39 -8
  9. data/README.md +55 -31
  10. data/Rakefile +29 -2
  11. data/active_model_serializers.gemspec +37 -13
  12. data/appveyor.yml +25 -0
  13. data/docs/README.md +29 -0
  14. data/docs/general/adapters.md +110 -0
  15. data/docs/general/configuration_options.md +11 -0
  16. data/docs/general/getting_started.md +73 -0
  17. data/docs/howto/add_pagination_links.md +112 -0
  18. data/docs/howto/add_root_key.md +51 -0
  19. data/docs/howto/outside_controller_use.md +42 -0
  20. data/lib/action_controller/serialization.rb +24 -33
  21. data/lib/active_model/serializable_resource.rb +70 -0
  22. data/lib/active_model/serializer.rb +50 -131
  23. data/lib/active_model/serializer/adapter.rb +84 -21
  24. data/lib/active_model/serializer/adapter/flatten_json.rb +9 -9
  25. data/lib/active_model/serializer/adapter/fragment_cache.rb +10 -13
  26. data/lib/active_model/serializer/adapter/json.rb +25 -28
  27. data/lib/active_model/serializer/adapter/json/fragment_cache.rb +2 -12
  28. data/lib/active_model/serializer/adapter/json_api.rb +100 -98
  29. data/lib/active_model/serializer/adapter/json_api/fragment_cache.rb +4 -14
  30. data/lib/active_model/serializer/adapter/json_api/pagination_links.rb +50 -0
  31. data/lib/active_model/serializer/adapter/null.rb +2 -8
  32. data/lib/active_model/serializer/array_serializer.rb +22 -17
  33. data/lib/active_model/serializer/association.rb +20 -0
  34. data/lib/active_model/serializer/associations.rb +97 -0
  35. data/lib/active_model/serializer/belongs_to_reflection.rb +10 -0
  36. data/lib/active_model/serializer/collection_reflection.rb +7 -0
  37. data/lib/active_model/serializer/configuration.rb +1 -0
  38. data/lib/active_model/serializer/fieldset.rb +7 -7
  39. data/lib/active_model/serializer/has_many_reflection.rb +10 -0
  40. data/lib/active_model/serializer/has_one_reflection.rb +10 -0
  41. data/lib/active_model/serializer/lint.rb +129 -0
  42. data/lib/active_model/serializer/railtie.rb +7 -0
  43. data/lib/active_model/serializer/reflection.rb +74 -0
  44. data/lib/active_model/serializer/singular_reflection.rb +7 -0
  45. data/lib/active_model/serializer/utils.rb +35 -0
  46. data/lib/active_model/serializer/version.rb +1 -1
  47. data/lib/active_model_serializers.rb +28 -14
  48. data/lib/generators/serializer/serializer_generator.rb +7 -7
  49. data/lib/generators/serializer/templates/{serializer.rb → serializer.rb.erb} +2 -2
  50. data/lib/tasks/rubocop.rake +0 -0
  51. data/test/action_controller/adapter_selector_test.rb +3 -3
  52. data/test/action_controller/explicit_serializer_test.rb +9 -9
  53. data/test/action_controller/json_api/linked_test.rb +179 -0
  54. data/test/action_controller/json_api/pagination_test.rb +116 -0
  55. data/test/action_controller/serialization_scope_name_test.rb +10 -6
  56. data/test/action_controller/serialization_test.rb +149 -112
  57. data/test/active_record_test.rb +9 -0
  58. data/test/adapter/fragment_cache_test.rb +11 -1
  59. data/test/adapter/json/belongs_to_test.rb +4 -5
  60. data/test/adapter/json/collection_test.rb +30 -21
  61. data/test/adapter/json/has_many_test.rb +20 -9
  62. data/test/adapter/json_api/belongs_to_test.rb +38 -38
  63. data/test/adapter/json_api/collection_test.rb +22 -23
  64. data/test/adapter/json_api/has_many_embed_ids_test.rb +2 -2
  65. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +4 -4
  66. data/test/adapter/json_api/has_many_test.rb +54 -19
  67. data/test/adapter/json_api/has_one_test.rb +28 -8
  68. data/test/adapter/json_api/json_api_test.rb +37 -0
  69. data/test/adapter/json_api/linked_test.rb +75 -75
  70. data/test/adapter/json_api/pagination_links_test.rb +115 -0
  71. data/test/adapter/json_api/resource_type_config_test.rb +59 -0
  72. data/test/adapter/json_test.rb +18 -5
  73. data/test/adapter_test.rb +10 -11
  74. data/test/array_serializer_test.rb +63 -5
  75. data/test/capture_warnings.rb +65 -0
  76. data/test/fixtures/active_record.rb +56 -0
  77. data/test/fixtures/poro.rb +60 -29
  78. data/test/generators/scaffold_controller_generator_test.rb +1 -2
  79. data/test/generators/serializer_generator_test.rb +17 -12
  80. data/test/lint_test.rb +37 -0
  81. data/test/logger_test.rb +18 -0
  82. data/test/poro_test.rb +9 -0
  83. data/test/serializable_resource_test.rb +27 -0
  84. data/test/serializers/adapter_for_test.rb +123 -3
  85. data/test/serializers/association_macros_test.rb +36 -0
  86. data/test/serializers/associations_test.rb +70 -47
  87. data/test/serializers/attribute_test.rb +28 -4
  88. data/test/serializers/attributes_test.rb +8 -14
  89. data/test/serializers/cache_test.rb +58 -31
  90. data/test/serializers/fieldset_test.rb +3 -4
  91. data/test/serializers/meta_test.rb +42 -28
  92. data/test/serializers/root_test.rb +21 -0
  93. data/test/serializers/serializer_for_test.rb +1 -1
  94. data/test/support/rails_app.rb +21 -0
  95. data/test/support/serialization_testing.rb +13 -0
  96. data/test/support/simplecov.rb +6 -0
  97. data/test/support/stream_capture.rb +50 -0
  98. data/test/support/test_case.rb +5 -0
  99. data/test/test_helper.rb +41 -29
  100. data/test/utils/include_args_to_hash_test.rb +79 -0
  101. metadata +123 -17
  102. data/test/action_controller/json_api_linked_test.rb +0 -179
  103. data/test/action_controller/rescue_from_test.rb +0 -32
  104. data/test/serializers/urls_test.rb +0 -26
@@ -1,5 +1,12 @@
1
+ require 'rails/railtie'
1
2
  module ActiveModel
2
3
  class Railtie < Rails::Railtie
4
+ initializer 'active_model_serializers.logger' do
5
+ ActiveSupport.on_load(:action_controller) do
6
+ ActiveModelSerializers.logger = ActionController::Base.logger
7
+ end
8
+ end
9
+
3
10
  initializer 'generators' do |app|
4
11
  app.load_generators
5
12
  require 'generators/serializer/resource_override'
@@ -0,0 +1,74 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # Holds all the meta-data about an association as it was specified in the
4
+ # ActiveModel::Serializer class.
5
+ #
6
+ # @example
7
+ # class PostSerializer < ActiveModel::Serializer
8
+ # has_one :author, serializer: AuthorSerializer
9
+ # has_many :comments
10
+ # end
11
+ #
12
+ # PostSerializer._reflections #=>
13
+ # # [
14
+ # # HasOneReflection.new(:author, serializer: AuthorSerializer),
15
+ # # HasManyReflection.new(:comments)
16
+ # # ]
17
+ #
18
+ # So you can inspect reflections in your Adapters.
19
+ #
20
+ Reflection = Struct.new(:name, :options) do
21
+ # Build association. This method is used internally to
22
+ # build serializer's association by its reflection.
23
+ #
24
+ # @param [Serializer] subject is a parent serializer for given association
25
+ # @param [Hash{Symbol => Object}] parent_serializer_options
26
+ #
27
+ # @example
28
+ # # Given the following serializer defined:
29
+ # class PostSerializer < ActiveModel::Serializer
30
+ # has_many :comments, serializer: CommentSummarySerializer
31
+ # end
32
+ #
33
+ # # Then you instantiate your serializer
34
+ # post_serializer = PostSerializer.new(post, foo: 'bar') #
35
+ # # to build association for comments you need to get reflection
36
+ # comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments }
37
+ # # and #build_association
38
+ # comments_reflection.build_association(post_serializer, foo: 'bar')
39
+ #
40
+ # @api private
41
+ #
42
+ def build_association(subject, parent_serializer_options)
43
+ association_value = subject.send(name)
44
+ reflection_options = options.dup
45
+ serializer_class = ActiveModel::Serializer.serializer_for(association_value, reflection_options)
46
+
47
+ if serializer_class
48
+ begin
49
+ serializer = serializer_class.new(
50
+ association_value,
51
+ serializer_options(parent_serializer_options, reflection_options)
52
+ )
53
+ rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
54
+ reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
55
+ end
56
+ elsif !association_value.nil? && !association_value.instance_of?(Object)
57
+ reflection_options[:virtual_value] = association_value
58
+ end
59
+
60
+ Association.new(name, serializer, reflection_options)
61
+ end
62
+
63
+ private
64
+
65
+ def serializer_options(parent_serializer_options, reflection_options)
66
+ serializer = reflection_options.fetch(:serializer, nil)
67
+
68
+ serializer_options = parent_serializer_options.except(:serializer)
69
+ serializer_options[:serializer] = serializer if serializer
70
+ serializer_options
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ class SingularReflection < Reflection
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ module ActiveModel::Serializer::Utils
2
+ module_function
3
+
4
+ # Translates a comma separated list of dot separated paths (JSONAPI format) into a Hash.
5
+ # Example: `'posts.author, posts.comments.upvotes, posts.comments.author'` would become `{ posts: { author: {}, comments: { author: {}, upvotes: {} } } }`.
6
+ #
7
+ # @param [String] included
8
+ # @return [Hash] a Hash representing the same tree structure
9
+ def include_string_to_hash(included)
10
+ included.delete(' ').split(',').inject({}) do |hash, path|
11
+ hash.deep_merge!(path.split('.').reverse_each.inject({}) { |a, e| { e.to_sym => a } })
12
+ end
13
+ end
14
+
15
+ # Translates the arguments passed to the include option into a Hash. The format can be either
16
+ # a String (see #include_string_to_hash), an Array of Symbols and Hashes, or a mix of both.
17
+ # Example: `posts: [:author, comments: [:author, :upvotes]]` would become `{ posts: { author: {}, comments: { author: {}, upvotes: {} } } }`.
18
+ #
19
+ # @param [Symbol, Hash, Array, String] included
20
+ # @return [Hash] a Hash representing the same tree structure
21
+ def include_args_to_hash(included)
22
+ case included
23
+ when Symbol
24
+ { included => {} }
25
+ when Hash
26
+ included.each_with_object({}) { |(key, value), hash| hash[key] = include_args_to_hash(value) }
27
+ when Array
28
+ included.inject({}) { |a, e| a.merge!(include_args_to_hash(e)) }
29
+ when String
30
+ include_string_to_hash(included)
31
+ else
32
+ {}
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
- VERSION = "0.10.0.rc2"
3
+ VERSION = '0.10.0.rc3'
4
4
  end
5
5
  end
@@ -1,19 +1,33 @@
1
+ require 'logger'
1
2
  require 'active_model'
2
- require 'active_model/serializer/version'
3
- require 'active_model/serializer'
4
- require 'active_model/serializer/fieldset'
5
- require 'active_model/serializer/railtie'
3
+ require 'active_support/railtie'
4
+ require 'action_controller'
5
+ require 'action_controller/railtie'
6
+ module ActiveModelSerializers
7
+ mattr_accessor :logger
8
+ self.logger = Rails.logger || Logger.new(IO::NULL)
6
9
 
7
- begin
8
- require 'action_controller'
9
- require 'action_controller/serialization'
10
+ module_function
10
11
 
11
- ActiveSupport.on_load(:action_controller) do
12
- include ::ActionController::Serialization
13
- ActionDispatch::Reloader.to_prepare do
14
- ActiveModel::Serializer.serializers_cache.clear
15
- end
12
+ def silence_warnings
13
+ verbose = $VERBOSE
14
+ $VERBOSE = nil
15
+ yield
16
+ ensure
17
+ $VERBOSE = verbose
16
18
  end
17
- rescue LoadError
18
- # rails not installed, continuing
19
19
  end
20
+
21
+ require 'active_model/serializer'
22
+ require 'active_model/serializable_resource'
23
+ require 'active_model/serializer/version'
24
+
25
+ require 'action_controller/serialization'
26
+ ActiveSupport.on_load(:action_controller) do
27
+ include ::ActionController::Serialization
28
+ ActionDispatch::Reloader.to_prepare do
29
+ ActiveModel::Serializer.serializers_cache.clear
30
+ end
31
+ end
32
+
33
+ require 'active_model/serializer/railtie'
@@ -1,15 +1,15 @@
1
1
  module Rails
2
2
  module Generators
3
3
  class SerializerGenerator < NamedBase
4
- source_root File.expand_path("../templates", __FILE__)
5
- check_class_collision :suffix => "Serializer"
4
+ source_root File.expand_path('../templates', __FILE__)
5
+ check_class_collision :suffix => 'Serializer'
6
6
 
7
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
7
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
8
8
 
9
- class_option :parent, :type => :string, :desc => "The parent class for the generated serializer"
9
+ class_option :parent, :type => :string, :desc => 'The parent class for the generated serializer'
10
10
 
11
11
  def create_serializer_file
12
- template 'serializer.rb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
12
+ template 'serializer.rb.erb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
13
13
  end
14
14
 
15
15
  private
@@ -26,9 +26,9 @@ module Rails
26
26
  if options[:parent]
27
27
  options[:parent]
28
28
  elsif defined?(::ApplicationSerializer)
29
- "ApplicationSerializer"
29
+ 'ApplicationSerializer'
30
30
  else
31
- "ActiveModel::Serializer"
31
+ 'ActiveModel::Serializer'
32
32
  end
33
33
  end
34
34
  end
@@ -1,8 +1,8 @@
1
1
  <% module_namespacing do -%>
2
2
  class <%= class_name %>Serializer < <%= parent_class_name %>
3
3
  attributes <%= attributes_names.map(&:inspect).join(", ") %>
4
- end
5
4
  <% association_names.each do |attribute| -%>
6
- attribute :<%= attribute %>
5
+ has_one :<%= attribute %>
7
6
  <% end -%>
7
+ end
8
8
  <% end -%>
File without changes
@@ -33,10 +33,10 @@ module ActionController
33
33
  expected = {
34
34
  data: {
35
35
  id: assigns(:profile).id.to_s,
36
- type: "profiles",
36
+ type: 'profiles',
37
37
  attributes: {
38
- name: "Name 1",
39
- description: "Description 1",
38
+ name: 'Name 1',
39
+ description: 'Description 1',
40
40
  }
41
41
  }
42
42
  }
@@ -100,11 +100,11 @@ module ActionController
100
100
  get :render_array_using_explicit_serializer_and_custom_serializers
101
101
 
102
102
  expected = [
103
- { "title" => "New Post",
104
- "body" => "Body",
105
- "id" => assigns(:post).id,
106
- "comments" => [{"id" => 1}, {"id" => 2}],
107
- "author" => { "id" => assigns(:author).id }
103
+ { 'title' => 'New Post',
104
+ 'body' => 'Body',
105
+ 'id' => assigns(:post).id,
106
+ 'comments' => [{ 'id' => 1 }, { 'id' => 2 }],
107
+ 'author' => { 'id' => assigns(:author).id }
108
108
  }
109
109
  ]
110
110
 
@@ -116,13 +116,13 @@ module ActionController
116
116
 
117
117
  expected = {
118
118
  id: 1337,
119
- name: "Amazing Place",
119
+ name: 'Amazing Place',
120
120
  locations: [
121
121
  {
122
122
  id: 42,
123
- lat: "-23.550520",
124
- lng: "-46.633309",
125
- place: "Nowhere" # is a virtual attribute on LocationSerializer
123
+ lat: '-23.550520',
124
+ lng: '-46.633309',
125
+ place: 'Nowhere' # is a virtual attribute on LocationSerializer
126
126
  }
127
127
  ]
128
128
  }
@@ -0,0 +1,179 @@
1
+ require 'test_helper'
2
+
3
+ module ActionController
4
+ module Serialization
5
+ class JsonApi
6
+ class LinkedTest < ActionController::TestCase
7
+ class LinkedTestController < ActionController::Base
8
+ def setup_post
9
+ ActionController::Base.cache_store.clear
10
+ @role1 = Role.new(id: 1, name: 'admin')
11
+ @role2 = Role.new(id: 2, name: 'colab')
12
+ @author = Author.new(id: 1, name: 'Steve K.')
13
+ @author.posts = []
14
+ @author.bio = nil
15
+ @author.roles = [@role1, @role2]
16
+ @role1.author = @author
17
+ @role2.author = @author
18
+ @author2 = Author.new(id: 2, name: 'Anonymous')
19
+ @author2.posts = []
20
+ @author2.bio = nil
21
+ @author2.roles = []
22
+ @post = Post.new(id: 1, title: 'New Post', body: 'Body')
23
+ @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
24
+ @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT')
25
+ @post.comments = [@first_comment, @second_comment]
26
+ @post.author = @author
27
+ @first_comment.post = @post
28
+ @first_comment.author = @author2
29
+ @second_comment.post = @post
30
+ @second_comment.author = nil
31
+ @post2 = Post.new(id: 2, title: 'Another Post', body: 'Body')
32
+ @post2.author = @author
33
+ @post2.comments = []
34
+ @blog = Blog.new(id: 1, name: 'My Blog!!')
35
+ @post.blog = @blog
36
+ @post2.blog = @blog
37
+ end
38
+
39
+ def render_resource_without_include
40
+ setup_post
41
+ render json: @post, adapter: :json_api
42
+ end
43
+
44
+ def render_resource_with_include
45
+ setup_post
46
+ render json: @post, include: [:author], adapter: :json_api
47
+ end
48
+
49
+ def render_resource_with_nested_include
50
+ setup_post
51
+ render json: @post, include: [comments: [:author]], adapter: :json_api
52
+ end
53
+
54
+ def render_resource_with_nested_has_many_include
55
+ setup_post
56
+ render json: @post, include: 'author.roles', adapter: :json_api
57
+ end
58
+
59
+ def render_resource_with_missing_nested_has_many_include
60
+ setup_post
61
+ @post.author = @author2 # author2 has no roles.
62
+ render json: @post, include: [author: [:roles]], adapter: :json_api
63
+ end
64
+
65
+ def render_collection_with_missing_nested_has_many_include
66
+ setup_post
67
+ @post.author = @author2
68
+ render json: [@post, @post2], include: [author: [:roles]], adapter: :json_api
69
+ end
70
+
71
+ def render_collection_without_include
72
+ setup_post
73
+ render json: [@post], adapter: :json_api
74
+ end
75
+
76
+ def render_collection_with_include
77
+ setup_post
78
+ render json: [@post], include: 'author, comments', adapter: :json_api
79
+ end
80
+ end
81
+
82
+ tests LinkedTestController
83
+
84
+ def test_render_resource_without_include
85
+ get :render_resource_without_include
86
+ response = JSON.parse(@response.body)
87
+ refute response.key? 'included'
88
+ end
89
+
90
+ def test_render_resource_with_include
91
+ get :render_resource_with_include
92
+ response = JSON.parse(@response.body)
93
+ assert response.key? 'included'
94
+ assert_equal 1, response['included'].size
95
+ assert_equal 'Steve K.', response['included'].first['attributes']['name']
96
+ end
97
+
98
+ def test_render_resource_with_nested_has_many_include
99
+ get :render_resource_with_nested_has_many_include
100
+ response = JSON.parse(@response.body)
101
+ expected_linked = [
102
+ {
103
+ 'id' => '1',
104
+ 'type' => 'authors',
105
+ 'attributes' => {
106
+ 'name' => 'Steve K.'
107
+ },
108
+ 'relationships' => {
109
+ 'posts' => { 'data' => [] },
110
+ 'roles' => { 'data' => [{ 'type' => 'roles', 'id' => '1' }, { 'type' => 'roles', 'id' => '2' }] },
111
+ 'bio' => { 'data' => nil }
112
+ }
113
+ }, {
114
+ 'id' => '1',
115
+ 'type' => 'roles',
116
+ 'attributes' => {
117
+ 'name' => 'admin',
118
+ 'description' => nil,
119
+ 'slug' => 'admin-1'
120
+ },
121
+ 'relationships' => {
122
+ 'author' => { 'data' => { 'type' => 'authors', 'id' => '1' } }
123
+ }
124
+ }, {
125
+ 'id' => '2',
126
+ 'type' => 'roles',
127
+ 'attributes' => {
128
+ 'name' => 'colab',
129
+ 'description' => nil,
130
+ 'slug' => 'colab-2'
131
+ },
132
+ 'relationships' => {
133
+ 'author' => { 'data' => { 'type' => 'authors', 'id' => '1' } }
134
+ }
135
+ }
136
+ ]
137
+ assert_equal expected_linked, response['included']
138
+ end
139
+
140
+ def test_render_resource_with_nested_include
141
+ get :render_resource_with_nested_include
142
+ response = JSON.parse(@response.body)
143
+ assert response.key? 'included'
144
+ assert_equal 3, response['included'].size
145
+ end
146
+
147
+ def test_render_collection_without_include
148
+ get :render_collection_without_include
149
+ response = JSON.parse(@response.body)
150
+ refute response.key? 'included'
151
+ end
152
+
153
+ def test_render_collection_with_include
154
+ get :render_collection_with_include
155
+ response = JSON.parse(@response.body)
156
+ assert response.key? 'included'
157
+ end
158
+
159
+ def test_render_resource_with_nested_attributes_even_when_missing_associations
160
+ get :render_resource_with_missing_nested_has_many_include
161
+ response = JSON.parse(@response.body)
162
+ assert response.key? 'included'
163
+ refute has_type?(response['included'], 'roles')
164
+ end
165
+
166
+ def test_render_collection_with_missing_nested_has_many_include
167
+ get :render_collection_with_missing_nested_has_many_include
168
+ response = JSON.parse(@response.body)
169
+ assert response.key? 'included'
170
+ assert has_type?(response['included'], 'roles')
171
+ end
172
+
173
+ def has_type?(collection, value)
174
+ collection.detect { |i| i['type'] == value }
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end