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 Maintainability Test Coverage Gem Version -License +License

@@ -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