From 59bb3123f930799244237ed8595578f82ac7f34f Mon Sep 17 00:00:00 2001
From: Maximo Mussini
Date: Mon, 27 Mar 2023 13:54:44 -0300
Subject: [PATCH] feat: has_one and has_many can take a block, mongo_attributes
can define aliases
---
MIGRATION_GUIDE.md | 23 ++-
README.md | 307 +++++++++++++++++--------------
lib/oj_serializers/serializer.rb | 85 +++++----
3 files changed, 232 insertions(+), 183 deletions(-)
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
index de34fd2..b93440d 100644
--- a/MIGRATION_GUIDE.md
+++ b/MIGRATION_GUIDE.md
@@ -2,15 +2,16 @@
[request_store]: https://github.com/steveklabnik/request_store
[request_store_rails]: https://github.com/ElMassimo/request_store_rails
-[readme]: https://github.com/ElMassimo/oj_serializers/blob/master/README.md
+[readme]: https://github.com/ElMassimo/oj_serializers/blob/main/README.md
+[attributes dsl]: https://github.com/ElMassimo/oj_serializers/blob/main/README.md#attributes-dsl-
[oj]: https://github.com/ohler55/oj
[ams]: https://github.com/rails-api/active_model_serializers
[jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer
[panko]: https://github.com/panko-serializer/panko_serializer
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
-[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/master/benchmarks/document_benchmark.rb
-[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/master/MIGRATION_GUIDE.md
+[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
+[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
[raw_json]: https://github.com/ohler55/oj/issues/542
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
@@ -44,9 +45,19 @@ render json: {
### Attributes
-Have in mind that unlike in Active Model Serializers, `attributes` in `Oj::Serializer` will _not_ take into account methods defined in the serializer.
+If you read the [Attributes DSL] section, you might have noticed that you need
+to _explicitly_ tell when a method in the serializer should be used by
+specifying it with `attribute`.
-Specially in the beginning, you can replace `attributes` with `ams_attributes` to preserve the same behavior.
+This makes the serializers more predictable and more maintainable, but it can
+make it challenging to migrate from `active_model_serializers`.
+
+Specially in the beginning, you can replace `attributes` with `ams_attributes`
+to preserve the same behavior.
+
+`ams_attributes` works like `attributes` in `active_model_serializers`: by
+calling a method in the serializer if defined, or calling
+`read_attribute_for_serialization` in the model.
```ruby
class AlbumSerializer < ActiveModel::Serializer
@@ -96,7 +107,7 @@ class AlbumSerializer < Oj::Serializer
has_many :songs, serializer: SongSerializer
- attr if: -> { album.released? }
+ attribute if: -> { album.released? }
def release
album.release_date.strftime('%B %d, %Y')
end
diff --git a/README.md b/README.md
index 20425b7..f4f80bd 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Oj Serializers
-
+
@@ -19,14 +19,15 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
[panko]: https://github.com/panko-serializer/panko_serializer
[blueprinter]: https://github.com/procore/blueprinter
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
-[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/master/benchmarks/document_benchmark.rb
-[sugar]: https://github.com/ElMassimo/oj_serializers/blob/master/lib/oj_serializers/sugar.rb#L14
-[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/master/MIGRATION_GUIDE.md
+[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
+[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
+[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
[design]: https://github.com/ElMassimo/oj_serializers#design-
[raw_json]: https://github.com/ohler55/oj/issues/542
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
[render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
[sorbet]: https://sorbet.org/
+[Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
## Why? 🤔
@@ -39,7 +40,7 @@ Learn more about [how this library achieves its performance][design].
## Features ⚡️
-- Declaration syntax similar to Active Model Serializers
+- Declaration syntax [similar to Active Model Serializers][migration guide]
- Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
- Support for `has_one` and `has_many`, compose with `flat_one`
- Useful development checks to avoid typos and mistakes
@@ -139,48 +140,23 @@ end
```
-
-
-To use the serializer, the recommended approach is:
+You can then use your new serializer to render an object or collection:
```ruby
class AlbumsController < ApplicationController
def show
- album = Album.find(params[:id])
render json: AlbumSerializer.one(album)
end
def index
- albums = Album.all
render json: { albums: AlbumSerializer.many(albums) }
end
end
```
-If you are using Rails you can also use something closer to Active Model Serializers by adding [`sugar`][sugar]:
-
-```ruby
-require 'oj_serializers/sugar'
-
-class AlbumsController < ApplicationController
- def show
- album = Album.find(params[:id])
- render json: album, serializer: AlbumSerializer
- end
-
- def index
- albums = Album.all
- render json: albums, each_serializer: AlbumSerializer, root: :albums
- end
-end
-```
-
-It's recommended to create your own `BaseSerializer` class in order to easily
-add custom extensions, specially when migrating from `active_model_serializers`.
-
-## Render DSL 🛠
+## Rendering 🖨
-In order to efficiently reuse the instances, serializers can't be instantiated directly. Use `one` and `many` to serialize objects or enumerables:
+Use `one` to serialize objects, and `many` to serialize enumerables:
```ruby
render json: {
@@ -189,141 +165,120 @@ render json: {
}
```
-You can use these serializers inside arrays, hashes, or even inside `ActiveModel::Serializer` by using a method in the serializer.
+Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Serializer`
+by using a method in the serializer, making it very easy to combine with other
+libraries and migrate incrementally.
-## Attributes DSL 🛠
+You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
-Attributes methods can be used to define which model attributes should be serialized
-to JSON. Each method provides a different strategy to obtain the values to serialize.
-
-The internal design is simple and extensible, so creating new strategies requires very little code.
-Please open an issue if you need help 😃
+```ruby
+render json: {
+ favorite_album: AlbumSerializer.render(album),
+ purchased_albums: AlbumSerializer.render(albums),
+}
+```
-### `attributes`
+## Attributes DSL 🪄
-Obtains the attribute value by calling a method in the object being serialized.
+Specify which attributes should be rendered by calling a method in the object to serialize.
```ruby
class PlayerSerializer < Oj::Serializer
- attributes :full_name
+ attributes :first_name, :last_name, :full_name
end
```
-Have in mind that unlike Active Model Serializers, it will _not_ take into
-account methods defined in the serializer. Being explicit about where the
-attribute is coming from makes the serializers easier to understand and more
-maintainable.
-
-### `serializer_attributes`
-
-Obtains the attribute value by calling a method defined in the serializer.
-
-Simply call `attribute` right before defining the method, and it will be serialized:
+You can serialize custom values by specifying that a method is an `attribute`:
```ruby
class PlayerSerializer < Oj::Serializer
+ attributes :first_name, :last_name, :full_name
+
attribute
- def full_name
+ def name
"#{player.first_name} #{player.last_name}"
end
end
```
-> This inline syntax was inspired by how types are defined in [`sorbet`][sorbet].
+> **Note**
+>
+> In this example, `player` was inferred from `PlayerSerializer`.
+>
+> You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
-Instance methods can access the object by the serializer name without the
-`Serializer` suffix, `player` in the example above, or directly as `@object`.
-You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
+### Associations 🔗
-### `ams_attributes` 🐌
+Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
-Works like `attributes` in Active Model Serializers, by calling a method in the serializer if defined, or calling `read_attribute_for_serialization` in the model.
+You must specificy which serializer to use with the `serializer` option.
```ruby
-class AlbumSerializer < Oj::Serializer
- ams_attributes :name, :release
-
- def release
- album.release_date.strftime('%B %d, %Y')
- end
+class SongSerializer < Oj::Serializer
+ has_one :album, serializer: AlbumSerializer
+ has_many :composers, serializer: ComposerSerializer
end
```
-Should only be used when migrating from Active Model Serializers, as it's slower and can create confusion.
-
-Instead, use `attributes` for model methods, and the inline `attribute` for serializer attributes. Being explicit makes serializers easier to understand, and to maintain.
-
-Please refer to the [migration guide] for more information.
-
-### `hash_attributes` 🚀
-
-Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
+Provide a different value for the association by providing a block:
```ruby
-class PersonSerializer < Oj::Serializer
- hash_attributes 'first_name', :last_name
+class SongSerializer < Oj::Serializer
+ has_one :album, serializer: AlbumSerializer do
+ Album.find_by(song_ids: song.id)
+ end
end
-
-PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
-# {"first_name":"Mary","last_name":"Watson"}
```
-### `mongo_attributes` 🚀
-
-Reads data directly from `attributes` in a [Mongoid] document.
-
-By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
-
-Although there are some downsides, depending on how consistent your schema is,
-and which kind of consumer the API has, it can be really powerful.
+In case you need to pass options, you can call the serializer manually:
```ruby
-class AlbumSerializer < Oj::Serializer
- mongo_attributes :id, :name
+class SongSerializer < Oj::Serializer
+ attribute
+ def album
+ AlbumSerializer.one(song.album, for_song: song)
+ end
end
```
-## Associations DSL 🛠
-
-Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
-
-The value for the association is obtained from a serializer method if defined,
-or by calling the method in the object being serialized.
+### Aliasing or renaming attributes ↔️
-You must specificy which serializer to use with the `serializer` option.
+You can pass `as` when defining an attribute or association to serialize it
+using a different key:
```ruby
class SongSerializer < Oj::Serializer
- has_one :album, serializer: AlbumSerializer
- has_many :composers, serializer: ComposerSerializer
+ has_one :album, as: :first_release, serializer: AlbumSerializer
- # You can also compose serializers using `flat_one`.
- flat_one :song, serializer: SongMetadataSerializer
+ attributes title: {as: :name}
+
+ # or as a shortcut
+ attributes title: :name
end
```
-The associations DSL is essentially a shortcut for defining attributes manually:
+### Conditional Attributes ❔
+
+You can render attributes and associations conditionally by using `:if`.
```ruby
-class SongSerializer < SongMetadataSerializer
- attribute
- def album
- AlbumSerializer.one(song.album)
- end
+class PlayerSerializer < Oj::Serializer
+ attributes :first_name, :last_name, if: -> { player.display_name? }
- attribute
- def composers
- ComposerSerializer.many(song.composers)
- end
+ has_one :album, serializer: AlbumSerializer, if: -> { player.album }
end
```
-## Other DSL 🛠
+This is useful in cases where you don't want to `null` values to be in the response.
+
+## Advanced Usage 🧙♂️
### Using a different alias for the internal object
-You can use `object_as` to create an alias for the serialized object to access it from instance methods:
+In most cases, the default alias for the `object` will be convenient enough.
+
+However, if you would like to specify it manually, use `object_as`:
```ruby
class DiscographySerializer < Oj::Serializer
@@ -337,39 +292,82 @@ class DiscographySerializer < Oj::Serializer
end
```
-### Aliasing or renaming attributes
+### Identifier Attributes
-You can pass `as` when defining an attribute or association to serialize it
-using a different key:
+The `identifier` method allows you to only include an identifier if the record
+or document has been persisted.
```ruby
-class SongSerializer < Oj::Serializer
- has_one :album, as: :latest_album, serializer: AlbumSerializer
+class AlbumSerializer < Oj::Serializer
+ identifier
- attributes title: {as: :name}
+ # or if it's a different field
+ identifier :uuid
end
```
-### Rendering an attribute conditionally
+Additionally, identifier fields are always rendered first, even when sorting
+fields alphabetically.
+
+### Transforming Attribute Keys 🗝
+
+When serialized data will be consumed from a client language that has different
+naming conventions, it can be convenient to transform keys accordingly.
-All the attributes and association methods can take an `if` option to render conditionally.
+For example, when rendering an API to be consumed from the browser via JavaScript,
+where properties are traditionally named using camel case.
+
+Use `transform_keys` to handle that conversion.
```ruby
-class AlbumSerializer < Oj::Serializer
- mongo_attributes :release_date, if: -> { album.released? }
+class BaseSerializer < Oj::Serializer
+ transform_keys :camelize
+
+ # shortcut for
+ transform_keys -> (key) { key.to_s.camelize(:lower) }
+end
+```
+
+This has no performance impact, as keys will be transformed at load time.
+
+### Sorting Attributes 📶
+
+By default attributes are rendered in the order they are defined.
+
+If you would like to sort attributes alphabetically, you can specify it at a
+serializer level:
+
+```ruby
+class BaseSerializer < Oj::Serializer
+ sort_attributes_by :name # or a Proc
+end
+```
+
+This has no performance impact, as attributes will be sorted at load time.
- has_many :songs, serializer: SongSerializer, if: -> { album.songs.any? }
+### Path Helpers 🛣
- # You can achieve the same by manually defining a method:
- def include_songs?
- album.songs.any?
+In case you need to access path helpers in your serializers, you can use the
+following:
+
+```ruby
+class BaseSerializer < Oj::Serializer
+ include Rails.application.routes.url_helpers
+
+ def default_url_options
+ Rails.application.routes.default_url_options
end
end
```
+One slight variation that might make it easier to maintain in the long term is
+to use a separate singleton service to provide the url helpers and options, and
+make it available as `urls`.
+
### Memoization & Local State
-Serializers are designed to be stateless so that an instanced can be reused, but sometimes it's convenient to store intermediate calculations.
+Serializers are designed to be stateless so that an instanced can be reused, but
+sometimes it's convenient to store intermediate calculations.
Use `memo` for memoization and storing temporary information.
@@ -391,6 +389,35 @@ private
end
end
```
+
+### `hash_attributes` 🚀
+
+Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
+
+```ruby
+class PersonSerializer < Oj::Serializer
+ hash_attributes 'first_name', :last_name
+end
+
+PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
+# {"first_name":"Mary","last_name":"Watson"}
+```
+
+### `mongo_attributes` 🚀
+
+Reads data directly from `attributes` in a [Mongoid] document.
+
+By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
+
+Although there are some downsides, depending on how consistent your schema is,
+and which kind of consumer the API has, it can be really powerful.
+
+```ruby
+class AlbumSerializer < Oj::Serializer
+ mongo_attributes :id, :name
+end
+```
+
### Caching 📦
Use `cached` to leverage key-based caching, which calls `cache_key` in the object.
@@ -415,12 +442,13 @@ Usually serialization happens so fast that __turning caching on can be slower__.
Always benchmark to make sure it's worth it, and use caching only for
time-consuming or deeply nested structures with unpredictable query patterns.
-### Writing directly to JSON
+### Writing to JSON
-In some corner cases it might be faster to serialize using a `Oj::StringWriter`.
+In some corner cases it might be faster to serialize using a `Oj::StringWriter`,
+which you can access by using `one_as_json` and `many_as_json`.
-You can toggle this mode at a serializer level by using `default_format :json`,
-or configure it globally from your base serializer.
+Alternatively, you can toggle this mode at a serializer level by using
+`default_format :json`, or configure it globally from your base serializer:
```ruby
class BaseSerializer < Oj::Serializer
@@ -431,8 +459,6 @@ end
This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
so that the serializer writes directly to JSON instead of returning a Hash.
-Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
-
Example Output
@@ -517,6 +543,7 @@ Follow [this discussion][raw_json] to find out more about [the `raw_json` extens
```
+
## Design 📐
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
@@ -526,6 +553,10 @@ greatly reducing the overhead of allocating and garbage collecting the hashes.
It also allocates a single instance per serializer class, which makes it easy
to use, while keeping memory usage under control.
+The internal design is simple and extensible, and because the library is written
+in Ruby, creating new serialization strategies requires very little code.
+Please open a [Discussion] if you need help 😃
+
### Comparison with other libraries
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
@@ -539,6 +570,8 @@ mixins must be applied to the class itself.
`Oj::Serializer` combines some of these ideas, by using instances, but reusing them to avoid object allocations. Serializing 10,000 items instantiates a single serializer. Unlike `panko-serializer`, it doesn't suffer from [double encoding problems](https://panko.dev/docs/response-bag) so it's easier to use.
+Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
+
As a result, migrating from `active_model_serializers` is relatively
straightforward because instance methods, inheritance, and mixins work as usual.
@@ -549,6 +582,12 @@ This library includes some [benchmarks] to compare performance with similar libr
See [this pull request](https://github.com/ElMassimo/oj_serializers/pull/9) for a quick comparison,
or check the CI to see the latest results.
+### Migrating from other libraries
+
+This library provides a few different compatibility modes that make it
+easier to migrate from `active_model_serializers` and similar libraries, please
+refer to the [migration guide] for a full discussion.
+
## Formatting 📏
Even though most of the examples above use a single-line style to be succint, I highly recommend writing one attribute per line, sorting them alphabetically (most editors can do it for you), and [always using a trailing comma][trailing_commas].
diff --git a/lib/oj_serializers/serializer.rb b/lib/oj_serializers/serializer.rb
index 659751a..ef5f8a3 100644
--- a/lib/oj_serializers/serializer.rb
+++ b/lib/oj_serializers/serializer.rb
@@ -199,7 +199,7 @@ def many_as_hash(items, options = nil)
# Internal: Will alias the object according to the name of the wrapper class.
def inherited(subclass)
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
- subclass.object_as(object_alias) unless method_defined?(object_alias)
+ subclass.object_as(object_alias) unless method_defined?(object_alias) || object_alias == "base"
super
end
@@ -249,7 +249,7 @@ def cached(cache_key_proc = :cache_key.to_proc)
#
# NOTE: Memcached does not support `write_multi`, if we switch the cache
# store to use Redis performance would improve a lot for this case.
- cached_items = CACHE.fetch_multi(*items, cache_hash_options) do |item|
+ CACHE.fetch_multi(*items, cache_hash_options) do |item|
instance.render_as_hash(item, options)
end.values
end
@@ -309,18 +309,23 @@ def new_json_writer
end
# Public: Identifiers are always serialized first.
+ #
+ # NOTE: We skip the id for non-persisted documents, since it doesn't
+ # actually identify the document (it will change once it's persisted).
def identifier(name = :id, **options)
- add_attribute(name, **options, attribute: :method, identifier: true, if: -> { !@object.new_record? })
+ add_attribute(name, attribute: :method, if: -> { !@object.new_record? }, **options, identifier: true)
end
# Public: Specify a collection of objects that should be serialized using
# the specified serializer.
- def has_many(name, serializer:, root: name, as: root, **options)
+ def has_many(name, serializer:, root: name, as: root, **options, &block)
+ define_method(name, &block) if block
add_attribute(name, association: :many, as: as, serializer: serializer, **options)
end
# Public: Specify an object that should be serialized using the serializer.
- def has_one(name, serializer:, root: name, as: root, **options)
+ def has_one(name, serializer:, root: name, as: root, **options, &block)
+ define_method(name, &block) if block
add_attribute(name, association: :one, as: as, serializer: serializer, **options)
end
# Alias: From a serializer perspective, the association type does not matter.
@@ -347,15 +352,19 @@ def hash_attributes(*method_names, **options)
#
# See ./benchmarks/document_benchmark.rb
def mongo_attributes(*method_names, **options)
- add_attribute('id', **options, attribute: :id, identifier: true) if method_names.delete(:id)
- add_attributes(method_names, **options, attribute: :mongoid)
+ identifier(:_id, as: :id, attribute: :mongoid, **options) if method_names.delete(:id)
+ attributes(*method_names, **options, attribute: :mongoid)
end
# Public: Specify which attributes are going to be obtained by calling a
# method in the object.
def attributes(*method_names, **methods_with_options)
- attr_options = methods_with_options.extract!(:if, :as)
- add_attributes(method_names, **attr_options, attribute: :method)
+ attr_options = methods_with_options.extract!(:if, :as, :attribute)
+ attr_options[:attribute] ||= :method
+
+ method_names.each do |name|
+ add_attribute(name, attr_options)
+ end
methods_with_options.each do |name, options|
options = { as: options } if options.is_a?(Symbol)
@@ -365,10 +374,8 @@ def attributes(*method_names, **methods_with_options)
# Public: Specify which attributes are going to be obtained by calling a
# method in the serializer.
- #
- # NOTE: This can be one of the slowest strategies, when in doubt, measure.
def serializer_attributes(*method_names, **options)
- add_attributes(method_names, **options, attribute: :serializer)
+ attributes(*method_names, **options, attribute: :serializer)
end
# Syntax Sugar: Allows to use it before a method name.
@@ -379,10 +386,11 @@ def serializer_attributes(*method_names, **options)
# "#{ first_name } #{ last_name }"
# end
def attribute(name = nil, **options)
+ options[:attribute] = :serializer
if name
- serializer_attributes(name, **options)
+ add_attribute(name, options)
else
- @_current_attribute = options
+ @_current_attribute_options = options
end
end
alias_method :attr, :attribute
@@ -391,9 +399,9 @@ def attribute(name = nil, **options)
# previously specified to the name of the attribute.
def method_added(name)
super(name)
- if @_current_attribute
- serializer_attributes(name, **@_current_attribute)
- @_current_attribute = nil
+ if @_current_attribute_options
+ add_attribute(name, @_current_attribute_options)
+ @_current_attribute_options = nil
end
end
@@ -405,7 +413,7 @@ def ams_attributes(*method_names, **options)
method_names.each do |method_name|
define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
end
- add_attributes(method_names, **options, attribute: :serializer)
+ attributes(*method_names, **options, attribute: :serializer)
end
# Internal: The default format to use for `render`, `one`, and `many`.
@@ -432,10 +440,6 @@ def _transform_keys
private
- def add_attributes(names, options)
- names.each { |name| add_attribute(name, options) }
- end
-
def add_attribute(name, options)
_attributes[name.to_s.freeze] = options
end
@@ -524,18 +528,27 @@ def code_to_rescue_no_method
RESCUE_NO_METHOD
end
+ # Internal: Detects any include methods defined in the serializer, or defines
+ # one by using the lambda passed in the `if` option, if any.
+ def check_conditional_method(method_name, options)
+ include_method_name = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
+ if render_if = options[:if]
+ if render_if.is_a?(Symbol)
+ alias_method(include_method_name, render_if)
+ else
+ define_method(include_method_name, &render_if)
+ end
+ end
+ include_method_name if method_defined?(include_method_name)
+ end
+
# Internal: Returns the code to render an attribute or association
# conditionally.
#
# NOTE: Detects any include methods defined in the serializer, or defines
# one by using the lambda passed in the `if` option, if any.
def code_to_write_conditional(method_name, options)
- include_method_name = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
- if render_if = options[:if]
- define_method(include_method_name, &render_if)
- end
-
- if method_defined?(include_method_name)
+ if (include_method_name = check_conditional_method(method_name, options))
"if #{include_method_name};#{yield};end\n"
else
yield
@@ -559,12 +572,6 @@ def code_to_write_attribute(method_name, options)
when :mongoid
# Writes an Mongoid attribute to JSON, this is the fastest strategy.
"writer.push_value(@object.attributes['#{method_name}'], #{key})"
- when :id
- # Writes an _id value to JSON using `id` as the key instead.
- #
- # NOTE: We skip the id for non-persisted documents, since it doesn't actually
- # identify the document (it will change once it's persisted).
- "writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?"
else
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
end
@@ -605,13 +612,7 @@ def code_to_write_association(method_name, options)
# NOTE: Detects any include methods defined in the serializer, or defines
# one by using the lambda passed in the `if` option, if any.
def code_to_render_conditionally(method_name, options)
- include_method_name = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
-
- if render_if = options[:if]
- define_method(include_method_name, &render_if)
- end
-
- if method_defined?(include_method_name)
+ if (include_method_name = check_conditional_method(method_name, options))
"**(#{include_method_name} ? {#{yield}} : {})"
else
yield
@@ -630,8 +631,6 @@ def code_to_render_attribute(method_name, options)
"#{key}: @object[#{method_name.inspect}]"
when :mongoid
"#{key}: @object.attributes['#{method_name}']"
- when :id
- "**(@object.new_record? ? {} : {id: @object.attributes['_id']})"
else
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
end