diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d845803dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +### 0.10.0 + + * adds support for `meta` and `meta_key` [@kurko] diff --git a/README.md b/README.md index 50387ee2c..5dfb47315 100644 --- a/README.md +++ b/README.md @@ -54,18 +54,17 @@ end ``` Generally speaking, you as a user of AMS will write (or generate) these -serializer classes. By default, they will use the JsonApiAdapter, implemented -by AMS. If you want to use a different adapter, such as a HalAdapter, you can +serializer classes. If you want to use a different adapter, such as a JsonApi, you can change this in an initializer: ```ruby -ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::HalAdapter +ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi ``` or ```ruby -ActiveModel::Serializer.config.adapter = :hal +ActiveModel::Serializer.config.adapter = :json_api ``` You won't need to implement an adapter unless you wish to use a new format or @@ -99,15 +98,6 @@ end In this case, Rails will look for a serializer named `PostSerializer`, and if it exists, use it to serialize the `Post`. -### Built in Adapters - -The `:json_api` adapter will include the associated resources in the `"linked"` -member when the resource names are included in the `include` option. - -```ruby - render @posts, include: 'authors,comments' -``` - ### Specify a serializer If you wish to use a serializer other than the default, you can explicitly pass it to the renderer. @@ -129,6 +119,46 @@ render json: @posts, each_serializer: PostPreviewSerializer render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer ``` +### Meta + +If you want a `meta` attribute in your response, specify it in the `render` +call: + +```ruby +render json: @post, meta: { total: 10 } +``` + +The key can be customized using `meta_key` option. + +```ruby +render json: @post, meta: { total: 10 }, meta_key: "custom_meta" +``` + +`meta` will only be included in your response if there's a root. For instance, +it won't be included in array responses. + +### Root key + +If you want to define a custom root for your response, specify it in the `render` +call: + +```ruby +render json: @post, root: "articles" +``` + +### Built in Adapters + +#### JSONAPI + +This adapter follows the format specified in +[jsonapi.org/format](http://jsonapi.org/format). It will include the associated +resources in the `"linked"` member when the resource names are included in the +`include` option. + +```ruby + render @posts, include: 'authors,comments' +``` + ## Installation Add this line to your application's Gemfile: diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index f2a34c350..a98b092ce 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -114,11 +114,13 @@ def self.root_name name.demodulize.underscore.sub(/_serializer$/, '') if name end - attr_accessor :object, :root + attr_accessor :object, :root, :meta, :meta_key def initialize(object, options = {}) @object = object @root = options[:root] || (self.class._root ? self.class.root_name : false) + @meta = options[:meta] + @meta_key = options[:meta_key] end def json_key diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index bf546097e..b1efdae68 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -18,7 +18,8 @@ def serializable_hash(options = {}) end def as_json(options = {}) - serializable_hash(options) + hash = serializable_hash(options) + include_meta(hash) end def self.create(resource, options = {}) @@ -30,6 +31,25 @@ def self.create(resource, options = {}) def self.adapter_class(adapter) "ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize end + + private + + def meta + serializer.meta if serializer.respond_to?(:meta) + end + + def meta_key + serializer.meta_key || "meta" + end + + def root + serializer.json_key + end + + def include_meta(json) + json[meta_key] = meta if meta && root + json + end end end end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index 83693c978..2f679bbe0 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -4,6 +4,8 @@ class ArraySerializer include Enumerable delegate :each, to: :@objects + attr_reader :meta, :meta_key + def initialize(objects, options = {}) @objects = objects.map do |object| serializer_class = options.fetch( @@ -12,6 +14,8 @@ def initialize(objects, options = {}) ) serializer_class.new(object) end + @meta = options[:meta] + @meta_key = options[:meta_key] end def json_key diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index a4c20a54c..55ebf2367 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -37,6 +37,18 @@ def render_array_using_implicit_serializer ] render json: array end + + def render_array_using_implicit_serializer_and_meta + old_adapter = ActiveModel::Serializer.config.adapter + + ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi + array = [ + Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + ] + render json: array, meta: { total: 10 } + ensure + ActiveModel::Serializer.config.adapter = old_adapter + end end tests MyController @@ -87,6 +99,13 @@ def test_render_array_using_implicit_serializer assert_equal expected.to_json, @response.body end + + def test_render_array_using_implicit_serializer_and_meta + get :render_array_using_implicit_serializer_and_meta + + assert_equal 'application/json', @response.content_type + assert_equal '{"profiles":[{"name":"Name 1","description":"Description 1"}],"meta":{"total":10}}', @response.body + end end end end diff --git a/test/serializers/meta_test.rb b/test/serializers/meta_test.rb new file mode 100644 index 000000000..b226c13a8 --- /dev/null +++ b/test/serializers/meta_test.rb @@ -0,0 +1,77 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class MetaTest < Minitest::Test + def setup + @blog = Blog.new(id: 1, + name: 'AMS Hints', + writer: Author.new(id: 2, name: "Steve"), + articles: [Post.new(id: 3, title: "AMS")]) + end + + def test_meta_is_present_with_root + adapter = load_adapter(root: "blog", meta: {total: 10}) + expected = { + "blog" => { + id: 1, + title: "AMS Hints" + }, + "meta" => { + total: 10 + } + } + assert_equal expected, adapter.as_json + end + + def test_meta_is_not_included_when_root_is_missing + adapter = load_adapter(meta: {total: 10}) + expected = { + id: 1, + title: "AMS Hints" + } + assert_equal expected, adapter.as_json + end + + def test_meta_key_is_used + adapter = load_adapter(root: "blog", meta: {total: 10}, meta_key: "haha_meta") + expected = { + "blog" => { + id: 1, + title: "AMS Hints" + }, + "haha_meta" => { + total: 10 + } + } + assert_equal expected, adapter.as_json + end + + def test_meta_is_not_used_on_arrays + serializer = ArraySerializer.new([@blog], root: "blog", meta: {total: 10}, meta_key: "haha_meta") + adapter = ActiveModel::Serializer::Adapter::Json.new(serializer) + expected = [{ + id: 1, + name: "AMS Hints", + writer: { + id: 2, + name: "Steve" + }, + articles: [{ + id: 3, + title: "AMS", + body: nil + }] + }] + assert_equal expected, adapter.as_json + end + + private + + def load_adapter(options) + serializer = AlternateBlogSerializer.new(@blog, options) + ActiveModel::Serializer::Adapter::Json.new(serializer) + end + end + end +end