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
@@ -0,0 +1,50 @@
1
+ class ActiveModel::Serializer::Adapter::JsonApi::PaginationLinks
2
+ FIRST_PAGE = 1
3
+
4
+ attr_reader :collection, :context
5
+
6
+ def initialize(collection, context)
7
+ @collection = collection
8
+ @context = context
9
+ end
10
+
11
+ def serializable_hash(options = {})
12
+ pages_from.each_with_object({}) do |(key, value), hash|
13
+ params = query_parameters.merge(page: { number: value, size: collection.size }).to_query
14
+
15
+ hash[key] = "#{url(options)}?#{params}"
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def pages_from
22
+ return {} if collection.total_pages == FIRST_PAGE
23
+
24
+ {}.tap do |pages|
25
+ pages[:self] = collection.current_page
26
+
27
+ unless collection.current_page == FIRST_PAGE
28
+ pages[:first] = FIRST_PAGE
29
+ pages[:prev] = collection.current_page - FIRST_PAGE
30
+ end
31
+
32
+ unless collection.current_page == collection.total_pages
33
+ pages[:next] = collection.current_page + FIRST_PAGE
34
+ pages[:last] = collection.total_pages
35
+ end
36
+ end
37
+ end
38
+
39
+ def url(options)
40
+ @url ||= options.fetch(:links, {}).fetch(:self, nil) || original_url
41
+ end
42
+
43
+ def original_url
44
+ @original_url ||= context.original_url[/\A[^?]+/]
45
+ end
46
+
47
+ def query_parameters
48
+ @query_parameters ||= context.query_parameters
49
+ end
50
+ end
@@ -1,11 +1,5 @@
1
- module ActiveModel
2
- class Serializer
3
- class Adapter
4
- class Null < Adapter
5
- def serializable_hash(options = {})
1
+ class ActiveModel::Serializer::Adapter::Null < ActiveModel::Serializer::Adapter
2
+ def serializable_hash(options = nil)
6
3
  {}
7
4
  end
8
- end
9
- end
10
- end
11
5
  end
@@ -1,34 +1,39 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
3
  class ArraySerializer
4
+ NoSerializerError = Class.new(StandardError)
4
5
  include Enumerable
5
- delegate :each, to: :@objects
6
+ delegate :each, to: :@serializers
6
7
 
7
- attr_reader :meta, :meta_key
8
+ attr_reader :object, :root, :meta, :meta_key
8
9
 
9
- def initialize(objects, options = {})
10
- @resource = objects
11
- @objects = objects.map do |object|
12
- serializer_class = options.fetch(
13
- :serializer,
14
- ActiveModel::Serializer.serializer_for(object)
15
- )
16
- serializer_class.new(object, options.except(:serializer))
10
+ def initialize(resources, options = {})
11
+ @root = options[:root]
12
+ @object = resources
13
+ @serializers = resources.map do |resource|
14
+ serializer_class = options.fetch(:serializer) {
15
+ ActiveModel::Serializer.serializer_for(resource)
16
+ }
17
+
18
+ if serializer_class.nil?
19
+ fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"
20
+ else
21
+ serializer_class.new(resource, options.except(:serializer))
22
+ end
17
23
  end
18
24
  @meta = options[:meta]
19
25
  @meta_key = options[:meta_key]
20
26
  end
21
27
 
22
28
  def json_key
23
- if @objects.first
24
- @objects.first.json_key.pluralize
25
- else
26
- @resource.name.downcase.pluralize if @resource.try(:name)
27
- end
29
+ key = root || @serializers.first.try(:json_key) || object.try(:name).try(:underscore)
30
+ key.try(:pluralize)
28
31
  end
29
32
 
30
- def root=(root)
31
- @objects.first.root = root if @objects.first
33
+ def paginated?
34
+ object.respond_to?(:current_page) &&
35
+ object.respond_to?(:total_pages) &&
36
+ object.respond_to?(:size)
32
37
  end
33
38
  end
34
39
  end
@@ -0,0 +1,20 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # This class hold all information about serializer's association.
4
+ #
5
+ # @param [Symbol] name
6
+ # @param [ActiveModel::Serializer] serializer
7
+ # @param [Hash{Symbol => Object}] options
8
+ #
9
+ # @example
10
+ # Association.new(:comments, CommentSummarySerializer)
11
+ #
12
+ Association = Struct.new(:name, :serializer, :options) do
13
+ # @return [Symbol]
14
+ #
15
+ def key
16
+ options.fetch(:key, name)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,97 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # Defines an association in the object should be rendered.
4
+ #
5
+ # The serializer object should implement the association name
6
+ # as a method which should return an array when invoked. If a method
7
+ # with the association name does not exist, the association name is
8
+ # dispatched to the serialized object.
9
+ #
10
+ module Associations
11
+ extend ActiveSupport::Concern
12
+
13
+ included do |base|
14
+ class << base
15
+ attr_accessor :_reflections
16
+ end
17
+
18
+ autoload :Association
19
+ autoload :Reflection
20
+ autoload :SingularReflection
21
+ autoload :CollectionReflection
22
+ autoload :BelongsToReflection
23
+ autoload :HasOneReflection
24
+ autoload :HasManyReflection
25
+ end
26
+
27
+ module ClassMethods
28
+ def inherited(base)
29
+ base._reflections = self._reflections.try(:dup) || []
30
+ end
31
+
32
+ # @param [Symbol] name of the association
33
+ # @param [Hash<Symbol => any>] options for the reflection
34
+ # @return [void]
35
+ #
36
+ # @example
37
+ # has_many :comments, serializer: CommentSummarySerializer
38
+ #
39
+ def has_many(name, options = {})
40
+ associate HasManyReflection.new(name, options)
41
+ end
42
+
43
+ # @param [Symbol] name of the association
44
+ # @param [Hash<Symbol => any>] options for the reflection
45
+ # @return [void]
46
+ #
47
+ # @example
48
+ # belongs_to :author, serializer: AuthorSerializer
49
+ #
50
+ def belongs_to(name, options = {})
51
+ associate BelongsToReflection.new(name, options)
52
+ end
53
+
54
+ # @param [Symbol] name of the association
55
+ # @param [Hash<Symbol => any>] options for the reflection
56
+ # @return [void]
57
+ #
58
+ # @example
59
+ # has_one :author, serializer: AuthorSerializer
60
+ #
61
+ def has_one(name, options = {})
62
+ associate HasOneReflection.new(name, options)
63
+ end
64
+
65
+ private
66
+
67
+ # Add reflection and define {name} accessor.
68
+ # @param [ActiveModel::Serializer::Reflection] reflection
69
+ # @return [void]
70
+ #
71
+ # @api private
72
+ #
73
+ def associate(reflection)
74
+ self._reflections = _reflections.dup
75
+
76
+ define_method reflection.name do
77
+ object.send reflection.name
78
+ end unless method_defined?(reflection.name)
79
+
80
+ self._reflections << reflection
81
+ end
82
+ end
83
+
84
+ # @return [Enumerator<Association>]
85
+ #
86
+ def associations
87
+ return unless object
88
+
89
+ Enumerator.new do |y|
90
+ self.class._reflections.each do |reflection|
91
+ y.yield reflection.build_association(self, options)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ class BelongsToReflection < SingularReflection
5
+ def macro
6
+ :belongs_to
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ class CollectionReflection < Reflection
5
+ end
6
+ end
7
+ end
@@ -7,6 +7,7 @@ module ActiveModel
7
7
  included do |base|
8
8
  base.config.array_serializer = ActiveModel::Serializer::ArraySerializer
9
9
  base.config.adapter = :flatten_json
10
+ base.config.jsonapi_resource_type = :plural
10
11
  end
11
12
  end
12
13
  end
@@ -1,7 +1,6 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
3
  class Fieldset
4
-
5
4
  def initialize(fields, root = nil)
6
5
  @root = root
7
6
  @raw_fields = fields
@@ -16,16 +15,18 @@ module ActiveModel
16
15
  fields[key.to_sym] || fields[key.pluralize.to_sym]
17
16
  end
18
17
 
19
- private
18
+ private
20
19
 
21
- attr_reader :raw_fields, :root
20
+ ActiveModelSerializers.silence_warnings do
21
+ attr_reader :raw_fields, :root
22
+ end
22
23
 
23
24
  def parsed_fields
24
25
  if raw_fields.is_a?(Hash)
25
- raw_fields.inject({}) { |h,(k,v)| h[k.to_sym] = v.map(&:to_sym); h}
26
+ raw_fields.inject({}) { |h, (k, v)| h[k.to_sym] = v.map(&:to_sym); h }
26
27
  elsif raw_fields.is_a?(Array)
27
28
  if root.nil?
28
- raise ArgumentError, 'The root argument must be specified if the fileds argument is an array.'
29
+ raise ArgumentError, 'The root argument must be specified if the fields argument is an array.'
29
30
  end
30
31
  hash = {}
31
32
  hash[root.to_sym] = raw_fields.map(&:to_sym)
@@ -34,7 +35,6 @@ module ActiveModel
34
35
  {}
35
36
  end
36
37
  end
37
-
38
38
  end
39
39
  end
40
- end
40
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ class HasManyReflection < CollectionReflection
5
+ def macro
6
+ :has_many
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ class HasOneReflection < SingularReflection
5
+ def macro
6
+ :has_one
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,129 @@
1
+ module ActiveModel::Serializer::Lint
2
+ # == Active \Model \Serializer \Lint \Tests
3
+ #
4
+ # You can test whether an object is compliant with the Active \Model \Serializers
5
+ # API by including <tt>ActiveModel::Serializer::Lint::Tests</tt> in your TestCase.
6
+ # It will include tests that tell you whether your object is fully compliant,
7
+ # or if not, which aspects of the API are not implemented.
8
+ #
9
+ # Note an object is not required to implement all APIs in order to work
10
+ # with Active \Model \Serializers. This module only intends to provide guidance in case
11
+ # you want all features out of the box.
12
+ #
13
+ # These tests do not attempt to determine the semantic correctness of the
14
+ # returned values. For instance, you could implement <tt>serializable_hash</tt> to
15
+ # always return +{}+, and the tests would pass. It is up to you to ensure
16
+ # that the values are semantically meaningful.
17
+ module Tests
18
+ # Passes if the object responds to <tt>serializable_hash</tt> and if it takes
19
+ # zero or one arguments.
20
+ # Fails otherwise.
21
+ #
22
+ # <tt>serializable_hash</tt> returns a hash representation of a object's attributes.
23
+ # Typically, it is implemented by including ActiveModel::Serialization.
24
+ def test_serializable_hash
25
+ assert_respond_to resource, :serializable_hash, 'The resource should respond to serializable_hash'
26
+ resource.serializable_hash
27
+ resource.serializable_hash(nil)
28
+ end
29
+
30
+ # Passes if the object responds to <tt>read_attribute_for_serialization</tt>
31
+ # and if it requires one argument (the attribute to be read).
32
+ # Fails otherwise.
33
+ #
34
+ # <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization
35
+ # Typically, it is implemented by including ActiveModel::Serialization.
36
+ def test_read_attribute_for_serialization
37
+ assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization'
38
+ actual_arity = resource.method(:read_attribute_for_serialization).arity
39
+ if defined?(::Rubinius)
40
+ # 1 for def read_attribute_for_serialization(name); end
41
+ # -2 for alias :read_attribute_for_serialization :send for rbx because :shrug:
42
+ assert_includes [1, -2], actual_arity, "expected #{actual_arity.inspect} to be 1 or -2"
43
+ else
44
+ # using absolute value since arity is:
45
+ # 1 for def read_attribute_for_serialization(name); end
46
+ # -1 for alias :read_attribute_for_serialization :send
47
+ assert_includes [1, -1], actual_arity, "expected #{actual_arity.inspect} to be 1 or -1"
48
+ end
49
+ end
50
+
51
+ # Passes if the object responds to <tt>as_json</tt> and if it takes
52
+ # zero or one arguments.
53
+ # Fails otherwise.
54
+ #
55
+ # <tt>as_json</tt> returns a hash representation of a serialized object.
56
+ # It may delegate to <tt>serializable_hash</tt>
57
+ # Typically, it is implemented either by including ActiveModel::Serialization
58
+ # which includes ActiveModel::Serializers::JSON.
59
+ # or by the JSON gem when required.
60
+ def test_as_json
61
+ assert_respond_to resource, :as_json
62
+ resource.as_json
63
+ resource.as_json(nil)
64
+ end
65
+
66
+ # Passes if the object responds to <tt>to_json</tt> and if it takes
67
+ # zero or one arguments.
68
+ # Fails otherwise.
69
+ #
70
+ # <tt>to_json</tt> returns a string representation (JSON) of a serialized object.
71
+ # It may be called on the result of <tt>as_json</tt>.
72
+ # Typically, it is implemented on all objects when the JSON gem is required.
73
+ def test_to_json
74
+ assert_respond_to resource, :to_json
75
+ resource.to_json
76
+ resource.to_json(nil)
77
+ end
78
+
79
+ # Passes if the object responds to <tt>cache_key</tt> and if it takes no
80
+ # arguments (Rails 4.0) or a splat (Rails 4.1+).
81
+ # Fails otherwise.
82
+ #
83
+ # <tt>cache_key</tt> returns a (self-expiring) unique key for the object,
84
+ # which is used by the adapter.
85
+ # It is not required unless caching is enabled.
86
+ def test_cache_key
87
+ assert_respond_to resource, :cache_key
88
+ actual_arity = resource.method(:cache_key).arity
89
+ # using absolute value since arity is:
90
+ # 0 for Rails 4.1+, *timestamp_names
91
+ # -1 for Rails 4.0, no arguments
92
+ assert_includes [-1, 0], actual_arity, "expected #{actual_arity.inspect} to be 0 or -1"
93
+ end
94
+
95
+ # Passes if the object responds to <tt>id</tt> and if it takes no
96
+ # arguments.
97
+ # Fails otherwise.
98
+ #
99
+ # <tt>id</tt> returns a unique identifier for the object.
100
+ # It is not required unless caching is enabled.
101
+ def test_id
102
+ assert_respond_to resource, :id
103
+ assert_equal resource.method(:id).arity, 0
104
+ end
105
+
106
+ # Passes if the object's class responds to <tt>model_name</tt> and if it
107
+ # is in an instance of +ActiveModel::Name+.
108
+ # Fails otherwise.
109
+ #
110
+ # <tt>model_name</tt> returns an ActiveModel::Name instance.
111
+ # It is used by the serializer to identify the object's type.
112
+ # It is not required unless caching is enabled.
113
+ def test_model_name
114
+ resource_class = resource.class
115
+ assert_respond_to resource_class, :model_name
116
+ assert_instance_of resource_class.model_name, ActiveModel::Name
117
+ end
118
+
119
+ private
120
+
121
+ def resource
122
+ @resource or fail "'@resource' must be set as the linted object"
123
+ end
124
+
125
+ def assert_instance_of(result, name)
126
+ assert result.instance_of?(name), "#{result} should be an instance of #{name}"
127
+ end
128
+ end
129
+ end