active_model_serializers 0.10.0.rc4 → 0.10.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +29 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +19 -1
  6. data/.rubocop_todo.yml +30 -103
  7. data/.simplecov +0 -1
  8. data/.travis.yml +20 -8
  9. data/CHANGELOG.md +89 -5
  10. data/CONTRIBUTING.md +54 -179
  11. data/Gemfile +7 -2
  12. data/{LICENSE.txt → MIT-LICENSE} +0 -0
  13. data/README.md +27 -5
  14. data/Rakefile +44 -16
  15. data/active_model_serializers.gemspec +9 -1
  16. data/appveyor.yml +1 -0
  17. data/bin/bench +171 -0
  18. data/bin/bench_regression +316 -0
  19. data/bin/serve_benchmark +39 -0
  20. data/docs/ARCHITECTURE.md +13 -7
  21. data/docs/README.md +5 -1
  22. data/docs/STYLE.md +58 -0
  23. data/docs/general/adapters.md +99 -16
  24. data/docs/general/configuration_options.md +87 -14
  25. data/docs/general/deserialization.md +100 -0
  26. data/docs/general/getting_started.md +35 -0
  27. data/docs/general/instrumentation.md +1 -1
  28. data/docs/general/key_transforms.md +40 -0
  29. data/docs/general/rendering.md +115 -13
  30. data/docs/general/serializers.md +138 -6
  31. data/docs/howto/add_pagination_links.md +36 -18
  32. data/docs/howto/outside_controller_use.md +4 -4
  33. data/docs/howto/passing_arbitrary_options.md +27 -0
  34. data/docs/jsonapi/errors.md +56 -0
  35. data/docs/jsonapi/schema.md +29 -18
  36. data/docs/rfcs/0000-namespace.md +106 -0
  37. data/docs/rfcs/template.md +15 -0
  38. data/lib/action_controller/serialization.rb +10 -19
  39. data/lib/active_model/serializable_resource.rb +4 -65
  40. data/lib/active_model/serializer.rb +73 -18
  41. data/lib/active_model/serializer/adapter.rb +15 -82
  42. data/lib/active_model/serializer/adapter/attributes.rb +5 -56
  43. data/lib/active_model/serializer/adapter/base.rb +5 -47
  44. data/lib/active_model/serializer/adapter/json.rb +6 -12
  45. data/lib/active_model/serializer/adapter/json_api.rb +5 -213
  46. data/lib/active_model/serializer/adapter/null.rb +7 -3
  47. data/lib/active_model/serializer/array_serializer.rb +3 -3
  48. data/lib/active_model/serializer/association.rb +4 -5
  49. data/lib/active_model/serializer/attributes.rb +1 -1
  50. data/lib/active_model/serializer/caching.rb +56 -5
  51. data/lib/active_model/serializer/collection_serializer.rb +30 -13
  52. data/lib/active_model/serializer/configuration.rb +7 -0
  53. data/lib/active_model/serializer/error_serializer.rb +10 -0
  54. data/lib/active_model/serializer/errors_serializer.rb +27 -0
  55. data/lib/active_model/serializer/links.rb +4 -2
  56. data/lib/active_model/serializer/lint.rb +14 -0
  57. data/lib/active_model/serializer/meta.rb +29 -0
  58. data/lib/active_model/serializer/null.rb +17 -0
  59. data/lib/active_model/serializer/reflection.rb +57 -1
  60. data/lib/active_model/serializer/type.rb +1 -1
  61. data/lib/active_model/serializer/version.rb +1 -1
  62. data/lib/active_model_serializers.rb +17 -0
  63. data/lib/active_model_serializers/adapter.rb +92 -0
  64. data/lib/active_model_serializers/adapter/attributes.rb +94 -0
  65. data/lib/active_model_serializers/adapter/base.rb +90 -0
  66. data/lib/active_model_serializers/adapter/json.rb +11 -0
  67. data/lib/active_model_serializers/adapter/json_api.rb +513 -0
  68. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +213 -0
  69. data/lib/active_model_serializers/adapter/json_api/error.rb +96 -0
  70. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +49 -0
  71. data/lib/active_model_serializers/adapter/json_api/link.rb +83 -0
  72. data/lib/active_model_serializers/adapter/json_api/meta.rb +37 -0
  73. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +57 -0
  74. data/lib/active_model_serializers/adapter/json_api/relationship.rb +52 -0
  75. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +37 -0
  76. data/lib/active_model_serializers/adapter/null.rb +10 -0
  77. data/lib/active_model_serializers/cached_serializer.rb +87 -0
  78. data/lib/active_model_serializers/callbacks.rb +1 -1
  79. data/lib/active_model_serializers/deprecate.rb +55 -0
  80. data/lib/active_model_serializers/deserialization.rb +2 -2
  81. data/lib/active_model_serializers/fragment_cache.rb +118 -0
  82. data/lib/active_model_serializers/json_pointer.rb +14 -0
  83. data/lib/active_model_serializers/key_transform.rb +70 -0
  84. data/lib/active_model_serializers/logging.rb +4 -1
  85. data/lib/active_model_serializers/model.rb +11 -1
  86. data/lib/active_model_serializers/railtie.rb +9 -1
  87. data/lib/active_model_serializers/register_jsonapi_renderer.rb +64 -0
  88. data/lib/active_model_serializers/serializable_resource.rb +81 -0
  89. data/lib/active_model_serializers/serialization_context.rb +24 -2
  90. data/lib/active_model_serializers/test/schema.rb +2 -2
  91. data/lib/grape/formatters/active_model_serializers.rb +1 -1
  92. data/test/action_controller/adapter_selector_test.rb +1 -1
  93. data/test/action_controller/json_api/deserialization_test.rb +56 -3
  94. data/test/action_controller/json_api/errors_test.rb +41 -0
  95. data/test/action_controller/json_api/linked_test.rb +10 -9
  96. data/test/action_controller/json_api/pagination_test.rb +2 -2
  97. data/test/action_controller/json_api/transform_test.rb +180 -0
  98. data/test/action_controller/serialization_scope_name_test.rb +201 -35
  99. data/test/action_controller/serialization_test.rb +39 -7
  100. data/test/active_model_serializers/adapter_for_test.rb +208 -0
  101. data/test/active_model_serializers/cached_serializer_test.rb +80 -0
  102. data/test/active_model_serializers/fragment_cache_test.rb +34 -0
  103. data/test/active_model_serializers/json_pointer_test.rb +20 -0
  104. data/test/active_model_serializers/key_transform_test.rb +263 -0
  105. data/test/active_model_serializers/logging_test.rb +8 -8
  106. data/test/active_model_serializers/railtie_test_isolated.rb +6 -0
  107. data/test/active_model_serializers/serialization_context_test_isolated.rb +58 -0
  108. data/test/adapter/deprecation_test.rb +100 -0
  109. data/test/adapter/json/belongs_to_test.rb +32 -34
  110. data/test/adapter/json/collection_test.rb +73 -75
  111. data/test/adapter/json/has_many_test.rb +36 -38
  112. data/test/adapter/json/transform_test.rb +93 -0
  113. data/test/adapter/json_api/belongs_to_test.rb +127 -129
  114. data/test/adapter/json_api/collection_test.rb +80 -82
  115. data/test/adapter/json_api/errors_test.rb +78 -0
  116. data/test/adapter/json_api/fields_test.rb +68 -70
  117. data/test/adapter/json_api/has_many_embed_ids_test.rb +32 -34
  118. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +75 -77
  119. data/test/adapter/json_api/has_many_test.rb +121 -123
  120. data/test/adapter/json_api/has_one_test.rb +59 -61
  121. data/test/adapter/json_api/json_api_test.rb +28 -30
  122. data/test/adapter/json_api/linked_test.rb +319 -321
  123. data/test/adapter/json_api/links_test.rb +75 -50
  124. data/test/adapter/json_api/pagination_links_test.rb +115 -82
  125. data/test/adapter/json_api/parse_test.rb +114 -116
  126. data/test/adapter/json_api/relationship_test.rb +161 -0
  127. data/test/adapter/json_api/relationships_test.rb +199 -0
  128. data/test/adapter/json_api/resource_identifier_test.rb +85 -0
  129. data/test/adapter/json_api/resource_meta_test.rb +100 -0
  130. data/test/adapter/json_api/toplevel_jsonapi_test.rb +61 -63
  131. data/test/adapter/json_api/transform_test.rb +500 -0
  132. data/test/adapter/json_api/type_test.rb +61 -0
  133. data/test/adapter/json_test.rb +35 -37
  134. data/test/adapter/null_test.rb +13 -15
  135. data/test/adapter/polymorphic_test.rb +72 -0
  136. data/test/adapter_test.rb +27 -29
  137. data/test/array_serializer_test.rb +7 -8
  138. data/test/benchmark/app.rb +65 -0
  139. data/test/benchmark/benchmarking_support.rb +67 -0
  140. data/test/benchmark/bm_caching.rb +117 -0
  141. data/test/benchmark/bm_transform.rb +34 -0
  142. data/test/benchmark/config.ru +3 -0
  143. data/test/benchmark/controllers.rb +77 -0
  144. data/test/benchmark/fixtures.rb +167 -0
  145. data/test/cache_test.rb +388 -0
  146. data/test/collection_serializer_test.rb +10 -0
  147. data/test/fixtures/active_record.rb +12 -0
  148. data/test/fixtures/poro.rb +28 -3
  149. data/test/grape_test.rb +5 -5
  150. data/test/lint_test.rb +9 -0
  151. data/test/serializable_resource_test.rb +59 -3
  152. data/test/serializers/associations_test.rb +8 -8
  153. data/test/serializers/attribute_test.rb +7 -7
  154. data/test/serializers/caching_configuration_test_isolated.rb +170 -0
  155. data/test/serializers/meta_test.rb +74 -6
  156. data/test/serializers/read_attribute_for_serialization_test.rb +79 -0
  157. data/test/serializers/serialization_test.rb +55 -0
  158. data/test/support/isolated_unit.rb +3 -0
  159. data/test/support/rails5_shims.rb +26 -8
  160. data/test/support/rails_app.rb +38 -18
  161. data/test/support/serialization_testing.rb +5 -5
  162. data/test/test_helper.rb +6 -10
  163. metadata +132 -37
  164. data/docs/DESIGN.textile +7 -1
  165. data/lib/active_model/serializer/adapter/cached_serializer.rb +0 -45
  166. data/lib/active_model/serializer/adapter/fragment_cache.rb +0 -111
  167. data/lib/active_model/serializer/adapter/json/fragment_cache.rb +0 -13
  168. data/lib/active_model/serializer/adapter/json_api/deserialization.rb +0 -207
  169. data/lib/active_model/serializer/adapter/json_api/fragment_cache.rb +0 -21
  170. data/lib/active_model/serializer/adapter/json_api/link.rb +0 -44
  171. data/lib/active_model/serializer/adapter/json_api/pagination_links.rb +0 -58
  172. data/test/active_model_serializers/serialization_context_test.rb +0 -18
  173. data/test/adapter/fragment_cache_test.rb +0 -38
  174. data/test/adapter/json_api/resource_type_config_test.rb +0 -71
  175. data/test/serializers/adapter_for_test.rb +0 -166
  176. data/test/serializers/cache_test.rb +0 -209
  177. data/test/support/simplecov.rb +0 -6
  178. data/test/support/stream_capture.rb +0 -50
  179. data/test/support/test_case.rb +0 -19
@@ -0,0 +1,117 @@
1
+ require_relative './benchmarking_support'
2
+ require_relative './app'
3
+
4
+ # https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/actionpack_router.rb
5
+ class ApiAssertion
6
+ include Benchmark::ActiveModelSerializers::TestMethods
7
+ BadRevisionError = Class.new(StandardError)
8
+
9
+ def valid?
10
+ caching = get_caching
11
+ caching[:body].delete('meta')
12
+ non_caching = get_non_caching
13
+ non_caching[:body].delete('meta')
14
+ assert_responses(caching, non_caching)
15
+ rescue BadRevisionError => e
16
+ msg = { error: e.message }
17
+ STDERR.puts msg
18
+ STDOUT.puts msg
19
+ exit 1
20
+ end
21
+
22
+ def get_status(on_off = 'on'.freeze)
23
+ get("/status/#{on_off}")
24
+ end
25
+
26
+ def clear
27
+ get('/clear')
28
+ end
29
+
30
+ def get_caching(on_off = 'on'.freeze)
31
+ get("/caching/#{on_off}")
32
+ end
33
+
34
+ def get_non_caching(on_off = 'on'.freeze)
35
+ get("/non_caching/#{on_off}")
36
+ end
37
+
38
+ private
39
+
40
+ def assert_responses(caching, non_caching)
41
+ assert_equal(caching[:code], 200, "Caching response failed: #{caching}")
42
+ assert_equal(caching[:body], expected, "Caching response format failed: \n+ #{caching[:body]}\n- #{expected}")
43
+ assert_equal(caching[:content_type], 'application/json; charset=utf-8', "Caching response content type failed: \n+ #{caching[:content_type]}\n- application/json")
44
+ assert_equal(non_caching[:code], 200, "Non caching response failed: #{non_caching}")
45
+ assert_equal(non_caching[:body], expected, "Non Caching response format failed: \n+ #{non_caching[:body]}\n- #{expected}")
46
+ assert_equal(non_caching[:content_type], 'application/json; charset=utf-8', "Non caching response content type failed: \n+ #{non_caching[:content_type]}\n- application/json")
47
+ end
48
+
49
+ def get(url)
50
+ response = request(:get, url)
51
+ { code: response.status, body: JSON.load(response.body), content_type: response.content_type }
52
+ end
53
+
54
+ def expected
55
+ @expected ||=
56
+ {
57
+ 'post' => {
58
+ 'id' => 1337,
59
+ 'title' => 'New Post',
60
+ 'body' => 'Body',
61
+ 'comments' => [
62
+ {
63
+ 'id' => 1,
64
+ 'body' => 'ZOMG A COMMENT'
65
+ }
66
+ ],
67
+ 'blog' => {
68
+ 'id' => 999,
69
+ 'name' => 'Custom blog'
70
+ },
71
+ 'author' => {
72
+ 'id' => 42,
73
+ 'first_name' => 'Joao',
74
+ 'last_name' => 'Moura'
75
+ }
76
+ }
77
+ }
78
+ end
79
+
80
+ def assert_equal(expected, actual, message)
81
+ return true if expected == actual
82
+ if ENV['FAIL_ASSERTION'] =~ /\Atrue|on|0\z/i # rubocop:disable Style/GuardClause
83
+ fail BadRevisionError, message
84
+ else
85
+ STDERR.puts message unless ENV['SUMMARIZE']
86
+ end
87
+ end
88
+
89
+ def debug(msg = '')
90
+ if block_given? && ENV['DEBUG'] =~ /\Atrue|on|0\z/i
91
+ STDERR.puts yield
92
+ else
93
+ STDERR.puts msg
94
+ end
95
+ end
96
+ end
97
+ assertion = ApiAssertion.new
98
+ assertion.valid?
99
+ # STDERR.puts assertion.get_status
100
+
101
+ time = 10
102
+ {
103
+ 'caching on: caching serializers: gc off' => { disable_gc: true, send: [:get_caching, 'on'] },
104
+ # 'caching on: caching serializers: gc on' => { disable_gc: false, send: [:get_caching, 'on'] },
105
+ 'caching off: caching serializers: gc off' => { disable_gc: true, send: [:get_caching, 'off'] },
106
+ # 'caching off: caching serializers: gc on' => { disable_gc: false, send: [:get_caching, 'off'] },
107
+ 'caching on: non-caching serializers: gc off' => { disable_gc: true, send: [:get_non_caching, 'on'] },
108
+ # 'caching on: non-caching serializers: gc on' => { disable_gc: false, send: [:get_non_caching, 'on'] },
109
+ 'caching off: non-caching serializers: gc off' => { disable_gc: true, send: [:get_non_caching, 'off'] }
110
+ # 'caching off: non-caching serializers: gc on' => { disable_gc: false, send: [:get_non_caching, 'off'] }
111
+ }.each do |label, options|
112
+ assertion.clear
113
+ Benchmark.ams(label, time: time, disable_gc: options[:disable_gc]) do
114
+ assertion.send(*options[:send])
115
+ end
116
+ # STDERR.puts assertion.get_status(options[:send][-1])
117
+ end
@@ -0,0 +1,34 @@
1
+ require_relative './benchmarking_support'
2
+ require_relative './app'
3
+
4
+ time = 10
5
+ disable_gc = true
6
+ ActiveModelSerializers.config.key_transform = :unaltered
7
+ comments = (0..50).map do |i|
8
+ Comment.new(id: i, body: 'ZOMG A COMMENT')
9
+ end
10
+ author = Author.new(id: 42, first_name: 'Joao', last_name: 'Moura')
11
+ post = Post.new(id: 1337, title: 'New Post', blog: nil, body: 'Body', comments: comments, author: author)
12
+ serializer = PostSerializer.new(post)
13
+ adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
14
+ serialization = adapter.as_json
15
+
16
+ Benchmark.ams('camel', time: time, disable_gc: disable_gc) do
17
+ ActiveModelSerializers::KeyTransform.camel(serialization)
18
+ end
19
+
20
+ Benchmark.ams('camel_lower', time: time, disable_gc: disable_gc) do
21
+ ActiveModelSerializers::KeyTransform.camel_lower(serialization)
22
+ end
23
+
24
+ Benchmark.ams('dash', time: time, disable_gc: disable_gc) do
25
+ ActiveModelSerializers::KeyTransform.dash(serialization)
26
+ end
27
+
28
+ Benchmark.ams('unaltered', time: time, disable_gc: disable_gc) do
29
+ ActiveModelSerializers::KeyTransform.unaltered(serialization)
30
+ end
31
+
32
+ Benchmark.ams('underscore', time: time, disable_gc: disable_gc) do
33
+ ActiveModelSerializers::KeyTransform.underscore(serialization)
34
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path(['..', 'app'].join(File::SEPARATOR), __FILE__)
2
+
3
+ run Rails.application
@@ -0,0 +1,77 @@
1
+ class PostController < ActionController::Base
2
+ POST =
3
+ begin
4
+ if ENV['BENCH_STRESS']
5
+ comments = (0..50).map do |i|
6
+ Comment.new(id: i, body: 'ZOMG A COMMENT')
7
+ end
8
+ else
9
+ comments = [Comment.new(id: 1, body: 'ZOMG A COMMENT')]
10
+ end
11
+ author = Author.new(id: 42, first_name: 'Joao', last_name: 'Moura')
12
+ Post.new(id: 1337, title: 'New Post', blog: nil, body: 'Body', comments: comments, author: author)
13
+ end
14
+
15
+ def render_with_caching_serializer
16
+ toggle_cache_status
17
+ render json: POST, serializer: CachingPostSerializer, adapter: :json, meta: { caching: perform_caching }
18
+ end
19
+
20
+ def render_with_non_caching_serializer
21
+ toggle_cache_status
22
+ render json: POST, adapter: :json, meta: { caching: perform_caching }
23
+ end
24
+
25
+ def render_cache_status
26
+ toggle_cache_status
27
+ # Uncomment to debug
28
+ # STDERR.puts cache_store.class
29
+ # STDERR.puts cache_dependencies
30
+ # ActiveSupport::Cache::Store.logger.debug [ActiveModelSerializers.config.cache_store, ActiveModelSerializers.config.perform_caching, CachingPostSerializer._cache, perform_caching, params].inspect
31
+ render json: { caching: perform_caching, meta: { cache_log: cache_messages, cache_status: cache_status } }.to_json
32
+ end
33
+
34
+ def clear
35
+ ActionController::Base.cache_store.clear
36
+ # Test caching is on
37
+ # Uncomment to turn on logger; possible performance issue
38
+ # logger = BenchmarkLogger.new
39
+ # ActiveSupport::Cache::Store.logger = logger # seems to be the best way
40
+ #
41
+ # the below is used in some rails tests but isn't available/working in all versions, so far as I can tell
42
+ # https://github.com/rails/rails/pull/15943
43
+ # ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
44
+ # logger.debug ActiveSupport::Notifications::Event.new(*args)
45
+ # end
46
+ render json: 'ok'.to_json
47
+ end
48
+
49
+ private
50
+
51
+ def cache_status
52
+ {
53
+ controller: perform_caching,
54
+ app: Rails.configuration.action_controller.perform_caching,
55
+ serializers: Rails.configuration.serializers.each_with_object({}) { |serializer, data| data[serializer.name] = serializer._cache.present? }
56
+ }
57
+ end
58
+
59
+ def cache_messages
60
+ ActiveSupport::Cache::Store.logger.is_a?(BenchmarkLogger) && ActiveSupport::Cache::Store.logger.messages.split("\n")
61
+ end
62
+
63
+ def toggle_cache_status
64
+ case params[:on]
65
+ when 'on'.freeze then self.perform_caching = true
66
+ when 'off'.freeze then self.perform_caching = false
67
+ else nil # no-op
68
+ end
69
+ end
70
+ end
71
+
72
+ Rails.application.routes.draw do
73
+ get '/status(/:on)' => 'post#render_cache_status'
74
+ get '/clear' => 'post#clear'
75
+ get '/caching(/:on)' => 'post#render_with_caching_serializer'
76
+ get '/non_caching(/:on)' => 'post#render_with_non_caching_serializer'
77
+ end
@@ -0,0 +1,167 @@
1
+ Rails.configuration.serializers = []
2
+ class AuthorSerializer < ActiveModel::Serializer
3
+ attributes :id, :first_name, :last_name
4
+
5
+ has_many :posts, embed: :ids
6
+ has_one :bio
7
+ end
8
+ Rails.configuration.serializers << AuthorSerializer
9
+
10
+ class BlogSerializer < ActiveModel::Serializer
11
+ attributes :id, :name
12
+ end
13
+ Rails.configuration.serializers << BlogSerializer
14
+
15
+ class CommentSerializer < ActiveModel::Serializer
16
+ attributes :id, :body
17
+
18
+ belongs_to :post
19
+ belongs_to :author
20
+ end
21
+ Rails.configuration.serializers << CommentSerializer
22
+
23
+ class PostSerializer < ActiveModel::Serializer
24
+ attributes :id, :title, :body
25
+
26
+ has_many :comments, serializer: CommentSerializer
27
+ belongs_to :blog, serializer: BlogSerializer
28
+ belongs_to :author, serializer: AuthorSerializer
29
+
30
+ link(:post_authors) { 'https://example.com/post_authors' }
31
+
32
+ meta do
33
+ {
34
+ rating: 5,
35
+ favorite_count: 10
36
+ }
37
+ end
38
+
39
+ def blog
40
+ Blog.new(id: 999, name: 'Custom blog')
41
+ end
42
+ end
43
+ Rails.configuration.serializers << PostSerializer
44
+
45
+ class CachingAuthorSerializer < AuthorSerializer
46
+ cache key: 'writer', only: [:first_name, :last_name], skip_digest: true
47
+ end
48
+ Rails.configuration.serializers << CachingAuthorSerializer
49
+
50
+ class CachingCommentSerializer < CommentSerializer
51
+ cache expires_in: 1.day, skip_digest: true
52
+ end
53
+ Rails.configuration.serializers << CachingCommentSerializer
54
+
55
+ class CachingPostSerializer < PostSerializer
56
+ cache key: 'post', expires_in: 0.1, skip_digest: true
57
+ belongs_to :blog, serializer: BlogSerializer
58
+ belongs_to :author, serializer: CachingAuthorSerializer
59
+ has_many :comments, serializer: CachingCommentSerializer
60
+ end
61
+ Rails.configuration.serializers << CachingPostSerializer
62
+
63
+ if ENV['ENABLE_ACTIVE_RECORD'] == 'true'
64
+ require 'active_record'
65
+
66
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
67
+ ActiveRecord::Schema.define do
68
+ self.verbose = false
69
+
70
+ create_table :blogs, force: true do |t|
71
+ t.string :name
72
+ t.timestamps null: false
73
+ end
74
+ create_table :authors, force: true do |t|
75
+ t.string :first_name
76
+ t.string :last_name
77
+ t.timestamps null: false
78
+ end
79
+ create_table :posts, force: true do |t|
80
+ t.string :title
81
+ t.text :body
82
+ t.references :author
83
+ t.references :blog
84
+ t.timestamps null: false
85
+ end
86
+ create_table :comments, force: true do |t|
87
+ t.text :body
88
+ t.references :author
89
+ t.references :post
90
+ t.timestamps null: false
91
+ end
92
+ end
93
+
94
+ class Comment < ActiveRecord::Base
95
+ belongs_to :author
96
+ belongs_to :post
97
+ end
98
+
99
+ class Author < ActiveRecord::Base
100
+ has_many :posts
101
+ has_many :comments
102
+ end
103
+
104
+ class Post < ActiveRecord::Base
105
+ has_many :comments
106
+ belongs_to :author
107
+ belongs_to :blog
108
+ end
109
+
110
+ class Blog < ActiveRecord::Base
111
+ has_many :posts
112
+ end
113
+ else
114
+ # ActiveModelSerializers::Model is a convenient
115
+ # serializable class to inherit from when making
116
+ # serializable non-activerecord objects.
117
+ class BenchmarkModel
118
+ include ActiveModel::Model
119
+ include ActiveModel::Serializers::JSON
120
+
121
+ attr_reader :attributes
122
+
123
+ def initialize(attributes = {})
124
+ @attributes = attributes
125
+ super
126
+ end
127
+
128
+ # Defaults to the downcased model name.
129
+ def id
130
+ attributes.fetch(:id) { self.class.name.downcase }
131
+ end
132
+
133
+ # Defaults to the downcased model name and updated_at
134
+ def cache_key
135
+ attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}" }
136
+ end
137
+
138
+ # Defaults to the time the serializer file was modified.
139
+ def updated_at
140
+ @updated_at ||= attributes.fetch(:updated_at) { File.mtime(__FILE__) }
141
+ end
142
+
143
+ def read_attribute_for_serialization(key)
144
+ if key == :id || key == 'id'
145
+ attributes.fetch(key) { id }
146
+ else
147
+ attributes[key]
148
+ end
149
+ end
150
+ end
151
+
152
+ class Comment < BenchmarkModel
153
+ attr_accessor :id, :body
154
+ end
155
+
156
+ class Author < BenchmarkModel
157
+ attr_accessor :id, :first_name, :last_name, :posts
158
+ end
159
+
160
+ class Post < BenchmarkModel
161
+ attr_accessor :id, :title, :body, :comments, :blog, :author
162
+ end
163
+
164
+ class Blog < BenchmarkModel
165
+ attr_accessor :id, :name
166
+ end
167
+ end
@@ -0,0 +1,388 @@
1
+ require 'test_helper'
2
+ require 'tmpdir'
3
+ require 'tempfile'
4
+
5
+ module ActiveModelSerializers
6
+ class CacheTest < ActiveSupport::TestCase
7
+ UncachedAuthor = Class.new(Author) do
8
+ # To confirm cache_key is set using updated_at and cache_key option passed to cache
9
+ undef_method :cache_key
10
+ end
11
+
12
+ Article = Class.new(::Model) do
13
+ # To confirm error is raised when cache_key is not set and cache_key option not passed to cache
14
+ undef_method :cache_key
15
+ end
16
+
17
+ ArticleSerializer = Class.new(ActiveModel::Serializer) do
18
+ cache only: [:place], skip_digest: true
19
+ attributes :title
20
+ end
21
+
22
+ InheritedRoleSerializer = Class.new(RoleSerializer) do
23
+ cache key: 'inherited_role', only: [:name, :special_attribute]
24
+ attribute :special_attribute
25
+ end
26
+
27
+ setup do
28
+ cache_store.clear
29
+ @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
30
+ @post = Post.new(title: 'New Post', body: 'Body')
31
+ @bio = Bio.new(id: 1, content: 'AMS Contributor')
32
+ @author = Author.new(name: 'Joao M. D. Moura')
33
+ @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: [])
34
+ @role = Role.new(name: 'Great Author')
35
+ @location = Location.new(lat: '-23.550520', lng: '-46.633309')
36
+ @place = Place.new(name: 'Amazing Place')
37
+ @author.posts = [@post]
38
+ @author.roles = [@role]
39
+ @role.author = @author
40
+ @author.bio = @bio
41
+ @bio.author = @author
42
+ @post.comments = [@comment]
43
+ @post.author = @author
44
+ @comment.post = @post
45
+ @comment.author = @author
46
+ @post.blog = @blog
47
+ @location.place = @place
48
+
49
+ @location_serializer = LocationSerializer.new(@location)
50
+ @bio_serializer = BioSerializer.new(@bio)
51
+ @role_serializer = RoleSerializer.new(@role)
52
+ @post_serializer = PostSerializer.new(@post)
53
+ @author_serializer = AuthorSerializer.new(@author)
54
+ @comment_serializer = CommentSerializer.new(@comment)
55
+ @blog_serializer = BlogSerializer.new(@blog)
56
+ end
57
+
58
+ def test_explicit_cache_store
59
+ default_store = Class.new(ActiveModel::Serializer) do
60
+ cache
61
+ end
62
+ explicit_store = Class.new(ActiveModel::Serializer) do
63
+ cache cache_store: ActiveSupport::Cache::FileStore
64
+ end
65
+
66
+ assert ActiveSupport::Cache::MemoryStore, ActiveModelSerializers.config.cache_store
67
+ assert ActiveSupport::Cache::MemoryStore, default_store.cache_store
68
+ assert ActiveSupport::Cache::FileStore, explicit_store.cache_store
69
+ end
70
+
71
+ def test_inherited_cache_configuration
72
+ inherited_serializer = Class.new(PostSerializer)
73
+
74
+ assert_equal PostSerializer._cache_key, inherited_serializer._cache_key
75
+ assert_equal PostSerializer._cache_options, inherited_serializer._cache_options
76
+ end
77
+
78
+ def test_override_cache_configuration
79
+ inherited_serializer = Class.new(PostSerializer) do
80
+ cache key: 'new-key'
81
+ end
82
+
83
+ assert_equal PostSerializer._cache_key, 'post'
84
+ assert_equal inherited_serializer._cache_key, 'new-key'
85
+ end
86
+
87
+ def test_cache_definition
88
+ assert_equal(cache_store, @post_serializer.class._cache)
89
+ assert_equal(cache_store, @author_serializer.class._cache)
90
+ assert_equal(cache_store, @comment_serializer.class._cache)
91
+ end
92
+
93
+ def test_cache_key_definition
94
+ assert_equal('post', @post_serializer.class._cache_key)
95
+ assert_equal('writer', @author_serializer.class._cache_key)
96
+ assert_equal(nil, @comment_serializer.class._cache_key)
97
+ end
98
+
99
+ def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
100
+ uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
101
+ uncached_author_serializer = AuthorSerializer.new(uncached_author)
102
+
103
+ render_object_with_cache(uncached_author)
104
+ key = "#{uncached_author_serializer.class._cache_key}/#{uncached_author_serializer.object.id}-#{uncached_author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
105
+ key = "#{key}/#{adapter.cached_name}"
106
+ assert_equal(uncached_author_serializer.attributes.to_json, cache_store.fetch(key).to_json)
107
+ end
108
+
109
+ def test_default_cache_key_fallback
110
+ render_object_with_cache(@comment)
111
+ key = "#{@comment.cache_key}/#{adapter.cached_name}"
112
+ assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(key).to_json)
113
+ end
114
+
115
+ def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
116
+ article = Article.new(title: 'Must Read')
117
+ e = assert_raises ActiveModelSerializers::CachedSerializer::UndefinedCacheKey do
118
+ render_object_with_cache(article)
119
+ end
120
+ assert_match(/ActiveModelSerializers::CacheTest::Article must define #cache_key, or the 'key:' option must be passed into 'CachedActiveModelSerializers_CacheTest_ArticleSerializer.cache'/, e.message)
121
+ end
122
+
123
+ def test_cache_options_definition
124
+ assert_equal({ expires_in: 0.1, skip_digest: true }, @post_serializer.class._cache_options)
125
+ assert_equal(nil, @blog_serializer.class._cache_options)
126
+ assert_equal({ expires_in: 1.day, skip_digest: true }, @comment_serializer.class._cache_options)
127
+ end
128
+
129
+ def test_fragment_cache_definition
130
+ assert_equal([:name], @role_serializer.class._cache_only)
131
+ assert_equal([:content], @bio_serializer.class._cache_except)
132
+ end
133
+
134
+ def test_associations_separately_cache
135
+ cache_store.clear
136
+ assert_equal(nil, cache_store.fetch(@post.cache_key))
137
+ assert_equal(nil, cache_store.fetch(@comment.cache_key))
138
+
139
+ Timecop.freeze(Time.current) do
140
+ render_object_with_cache(@post)
141
+
142
+ key = "#{@post.cache_key}/#{adapter.cached_name}"
143
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
144
+ key = "#{@comment.cache_key}/#{adapter.cached_name}"
145
+ assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
146
+ end
147
+ end
148
+
149
+ def test_associations_cache_when_updated
150
+ Timecop.freeze(Time.current) do
151
+ # Generate a new Cache of Post object and each objects related to it.
152
+ render_object_with_cache(@post)
153
+
154
+ # Check if it cached the objects separately
155
+ key = "#{@post.cache_key}/#{adapter.cached_name}"
156
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
157
+ key = "#{@comment.cache_key}/#{adapter.cached_name}"
158
+ assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
159
+
160
+ # Simulating update on comments relationship with Post
161
+ new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
162
+ new_comment_serializer = CommentSerializer.new(new_comment)
163
+ @post.comments = [new_comment]
164
+
165
+ # Ask for the serialized object
166
+ render_object_with_cache(@post)
167
+
168
+ # Check if the the new comment was cached
169
+ key = "#{new_comment.cache_key}/#{adapter.cached_name}"
170
+ assert_equal(new_comment_serializer.attributes, cache_store.fetch(key))
171
+ key = "#{@post.cache_key}/#{adapter.cached_name}"
172
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
173
+ end
174
+ end
175
+
176
+ def test_fragment_fetch_with_virtual_associations
177
+ expected_result = {
178
+ id: @location.id,
179
+ lat: @location.lat,
180
+ lng: @location.lng,
181
+ place: 'Nowhere'
182
+ }
183
+
184
+ hash = render_object_with_cache(@location)
185
+
186
+ assert_equal(hash, expected_result)
187
+ key = "#{@location.cache_key}/#{adapter.cached_name}"
188
+ assert_equal({ place: 'Nowhere' }, cache_store.fetch(key))
189
+ end
190
+
191
+ def test_fragment_cache_with_inheritance
192
+ inherited = render_object_with_cache(@role, serializer: InheritedRoleSerializer)
193
+ base = render_object_with_cache(@role)
194
+
195
+ assert_includes(inherited.keys, :special_attribute)
196
+ refute_includes(base.keys, :special_attribute)
197
+ end
198
+
199
+ def test_uses_adapter_in_cache_key
200
+ render_object_with_cache(@post)
201
+ key = "#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"
202
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
203
+ end
204
+
205
+ # Based on original failing test by @kevintyll
206
+ # rubocop:disable Metrics/AbcSize
207
+ def test_a_serializer_rendered_by_two_adapter_returns_differently_cached_attributes
208
+ Object.const_set(:Alert, Class.new(ActiveModelSerializers::Model) do
209
+ attr_accessor :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
210
+ end)
211
+ Object.const_set(:UncachedAlertSerializer, Class.new(ActiveModel::Serializer) do
212
+ attributes :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
213
+ end)
214
+ Object.const_set(:AlertSerializer, Class.new(UncachedAlertSerializer) do
215
+ cache
216
+ end)
217
+
218
+ alert = Alert.new(
219
+ id: 1,
220
+ status: 'fail',
221
+ resource: 'resource-1',
222
+ started_at: Time.new(2016, 3, 31, 21, 36, 35, 0),
223
+ ended_at: nil,
224
+ updated_at: Time.new(2016, 3, 31, 21, 27, 35, 0),
225
+ created_at: Time.new(2016, 3, 31, 21, 37, 35, 0)
226
+ )
227
+
228
+ expected_cached_attributes = {
229
+ id: 1,
230
+ status: 'fail',
231
+ resource: 'resource-1',
232
+ started_at: alert.started_at,
233
+ ended_at: nil,
234
+ updated_at: alert.updated_at,
235
+ created_at: alert.created_at
236
+ }
237
+ expected_cached_jsonapi_attributes = {
238
+ id: '1',
239
+ type: 'alerts',
240
+ attributes: {
241
+ status: 'fail',
242
+ resource: 'resource-1',
243
+ started_at: alert.started_at,
244
+ ended_at: nil,
245
+ updated_at: alert.updated_at,
246
+ created_at: alert.created_at
247
+ }
248
+ }
249
+
250
+ # Assert attributes are serialized correctly
251
+ serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :attributes)
252
+ attributes_serialization = serializable_alert.as_json
253
+ assert_equal expected_cached_attributes, alert.attributes
254
+ assert_equal alert.attributes, attributes_serialization
255
+ attributes_cache_key = CachedSerializer.new(serializable_alert.adapter.serializer).cache_key(serializable_alert.adapter)
256
+ assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key)
257
+
258
+ serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :json_api)
259
+ jsonapi_cache_key = CachedSerializer.new(serializable_alert.adapter.serializer).cache_key(serializable_alert.adapter)
260
+ # Assert cache keys differ
261
+ refute_equal attributes_cache_key, jsonapi_cache_key
262
+ # Assert (cached) serializations differ
263
+ jsonapi_serialization = serializable_alert.as_json
264
+ assert_equal alert.status, jsonapi_serialization.fetch(:data).fetch(:attributes).fetch(:status)
265
+ serializable_alert = serializable(alert, serializer: UncachedAlertSerializer, adapter: :json_api)
266
+ assert_equal serializable_alert.as_json, jsonapi_serialization
267
+
268
+ cached_serialization = cache_store.fetch(jsonapi_cache_key)
269
+ assert_equal expected_cached_jsonapi_attributes, cached_serialization
270
+ ensure
271
+ Object.send(:remove_const, :Alert)
272
+ Object.send(:remove_const, :AlertSerializer)
273
+ Object.send(:remove_const, :UncachedAlertSerializer)
274
+ end
275
+ # rubocop:enable Metrics/AbcSize
276
+
277
+ def test_uses_file_digest_in_cache_key
278
+ render_object_with_cache(@blog)
279
+ key = "#{@blog.cache_key}/#{adapter.cached_name}/#{::Model::FILE_DIGEST}"
280
+ assert_equal(@blog_serializer.attributes, cache_store.fetch(key))
281
+ end
282
+
283
+ def test_cache_digest_definition
284
+ assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest)
285
+ end
286
+
287
+ def test_object_cache_keys
288
+ serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment])
289
+ include_tree = ActiveModel::Serializer::IncludeTree.from_include_args('*')
290
+
291
+ actual = CachedSerializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_tree)
292
+
293
+ assert_equal actual.size, 3
294
+ assert actual.any? { |key| key == "comment/1/#{serializable.adapter.cached_name}" }
295
+ assert actual.any? { |key| key =~ %r{post/post-\d+} }
296
+ assert actual.any? { |key| key =~ %r{author/author-\d+} }
297
+ end
298
+
299
+ def test_cached_attributes
300
+ serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
301
+
302
+ Timecop.freeze(Time.current) do
303
+ render_object_with_cache(@comment)
304
+
305
+ attributes = Adapter::Attributes.new(serializer)
306
+ attributes.send(:cache_attributes)
307
+ cached_attributes = attributes.instance_variable_get(:@cached_attributes)
308
+
309
+ assert_equal cached_attributes["#{@comment.cache_key}/#{attributes.cached_name}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
310
+ assert_equal cached_attributes["#{@comment.post.cache_key}/#{attributes.cached_name}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
311
+
312
+ writer = @comment.post.blog.writer
313
+ writer_cache_key = writer.cache_key
314
+
315
+ assert_equal cached_attributes["#{writer_cache_key}/#{attributes.cached_name}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
316
+ end
317
+ end
318
+
319
+ def test_serializer_file_path_on_nix
320
+ path = '/Users/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
321
+ caller_line = "#{path}:1:in `<top (required)>'"
322
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
323
+ end
324
+
325
+ def test_serializer_file_path_on_windows
326
+ path = 'c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
327
+ caller_line = "#{path}:1:in `<top (required)>'"
328
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
329
+ end
330
+
331
+ def test_serializer_file_path_with_space
332
+ path = '/Users/git/ember js/ember-crm-backend/app/serializers/lead_serializer.rb'
333
+ caller_line = "#{path}:1:in `<top (required)>'"
334
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
335
+ end
336
+
337
+ def test_serializer_file_path_with_submatch
338
+ # The submatch in the path ensures we're using a correctly greedy regexp.
339
+ path = '/Users/git/ember js/ember:123:in x/app/serializers/lead_serializer.rb'
340
+ caller_line = "#{path}:1:in `<top (required)>'"
341
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
342
+ end
343
+
344
+ def test_digest_caller_file
345
+ contents = "puts 'AMS rocks'!"
346
+ dir = Dir.mktmpdir('space char')
347
+ file = Tempfile.new('some_ruby.rb', dir)
348
+ file.write(contents)
349
+ path = file.path
350
+ caller_line = "#{path}:1:in `<top (required)>'"
351
+ file.close
352
+ assert_equal ActiveModel::Serializer.digest_caller_file(caller_line), Digest::MD5.hexdigest(contents)
353
+ ensure
354
+ file.unlink
355
+ FileUtils.remove_entry dir
356
+ end
357
+
358
+ def test_warn_on_serializer_not_defined_in_file
359
+ called = false
360
+ serializer = Class.new(ActiveModel::Serializer)
361
+ assert_output(nil, /_cache_digest/) do
362
+ serializer.digest_caller_file('')
363
+ called = true
364
+ end
365
+ assert called
366
+ end
367
+
368
+ private
369
+
370
+ def cache_store
371
+ ActiveModelSerializers.config.cache_store
372
+ end
373
+
374
+ def render_object_with_cache(obj, options = {})
375
+ @serializable_resource = serializable(obj, options)
376
+ @serializable_resource.serializable_hash
377
+ end
378
+
379
+ def adapter
380
+ @serializable_resource.adapter
381
+ end
382
+
383
+ def cached_serialization(serializer)
384
+ cache_key = CachedSerializer.new(serializer).cache_key(adapter)
385
+ cache_store.fetch(cache_key)
386
+ end
387
+ end
388
+ end