class OjSerializers::Serializer

Public: Implementation of an “ActiveModelSerializer”-like DSL, but with a design that allows replacing the internal object, which greatly reduces object allocation.

Unlike ActiveModelSerializer, which builds a Hash which then gets encoded to JSON, this implementation allows to use Oj::StringWriter to write directly to JSON, greatly reducing the overhead of allocating and garbage collecting the hashes.

Constants

ALLOWED_INSTANCE_VARIABLES

Public: Used to validate incorrect memoization during development. Users of this library might add additional options as needed.

CACHE
DEFAULT_OPTIONS
DEV_MODE

Internal: Used to display warnings or detect misusage during development.

Attributes

_associations_entries[R]

Internal: Iterating arrays is faster than iterating hashes.

_attributes_entries[R]

Internal: Iterating arrays is faster than iterating hashes.

Protected Class Methods

ams_attributes(*method_names, **options) click to toggle source

Backwards Compatibility: Meant only to replace Active Model Serializers, calling a method in the serializer, or using `read_attribute_for_serialization`.

NOTE: Prefer to use `attributes` or `serializer_attributes` explicitly.

# File lib/oj_serializers/serializer.rb, line 347
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, strategy: :write_value_using_serializer_strategy)
end
attribute(*method_names, **options)

Syntax Sugar: Allows to use it before a method name.

Example:

attribute \
def full_name
  "#{ first_name } #{ last_name }"
end
attributes(*method_names, **options) click to toggle source

Public: Specify which attributes are going to be obtained by calling a method in the object.

# File lib/oj_serializers/serializer.rb, line 322
def attributes(*method_names, **options)
  add_attributes(method_names, **options, strategy: :write_value_using_method_strategy)
end
cached(cache_key_proc = :cache_key.to_proc) click to toggle source

Public: Allows to define a cache key strategy for the serializer. Defaults to calling cache_key in the object if no key is provided.

NOTE: Benchmark it, sometimes caching is actually SLOWER.

# File lib/oj_serializers/serializer.rb, line 239
def cached(cache_key_proc = :cache_key.to_proc)
  cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze

  # Internal: Redefine `write_one` to use the cache for the serialized JSON.
  define_singleton_method(:write_one) do |external_writer, item, options = nil|
    cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
      writer = new_json_writer
      non_cached_write_one(writer, item, options)
      writer.to_json
    end
    external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
  end

  # Internal: Redefine `write_many` to use fetch_multi from cache.
  define_singleton_method(:write_many) do |external_writer, items, options = nil|
    # We define a one-off method for the class to receive the entire object
    # inside the `fetch_multi` block. Otherwise we would only get the cache
    # key, and we would need to build a Hash to retrieve the object.
    #
    # NOTE: The assignment is important, as queries would return different
    # objects when expanding with the splat in fetch_multi.
    items = items.entries.each do |item|
      item_key = item_cache_key(item, cache_key_proc)
      item.define_singleton_method(:cache_key) { item_key }
    end

    # Fetch all items at once by leveraging `read_multi`.
    #
    # 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_options) do |item|
      writer = new_json_writer
      non_cached_write_one(writer, item, options)
      writer.to_json
    end.values
    external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
  end
end
Also aliased as: cached_with_key
cached_with_key(cache_key_proc = :cache_key.to_proc)
Alias for: cached
flat_one(name, root: false, serializer:, **options) click to toggle source

Public: Specify an object that should be serialized using the serializer, but unlike `has_one`, this one will write the attributes directly without wrapping it in an object.

# File lib/oj_serializers/serializer.rb, line 298
def flat_one(name, root: false, serializer:, **options)
  add_association(name, write_method: :write_flat, root: root, serializer: serializer, **options)
end
has_many(name, root: name, serializer:, **options) click to toggle source

Public: Specify a collection of objects that should be serialized using the specified serializer.

# File lib/oj_serializers/serializer.rb, line 286
def has_many(name, root: name, serializer:, **options)
  add_association(name, write_method: :write_many, root: root, serializer: serializer, **options)
end
has_one(name, root: name, serializer:, **options) click to toggle source

Public: Specify an object that should be serialized using the serializer.

# File lib/oj_serializers/serializer.rb, line 291
def has_one(name, root: name, serializer:, **options)
  add_association(name, write_method: :write_one, root: root, serializer: serializer, **options)
end
hash_attributes(*method_names, **options) click to toggle source

Public: Specify which attributes are going to be obtained from indexing the object.

# File lib/oj_serializers/serializer.rb, line 304
def hash_attributes(*method_names, **options)
  options = { **options, strategy: :write_value_using_hash_strategy }
  method_names.each { |name| _attributes[name] = options }
end
item_cache_key(item, cache_key_proc) click to toggle source

Internal: Calculates the cache_key used to cache one serialized item.

# File lib/oj_serializers/serializer.rb, line 231
def item_cache_key(item, cache_key_proc)
  ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
end
mongo_attributes(*method_names, **options) click to toggle source

Public: Specify which attributes are going to be obtained from indexing a Mongoid model's `attributes` hash directly, for performance.

Automatically renames `_id` to `id` for Mongoid models.

See ./benchmarks/document_benchmark.rb

# File lib/oj_serializers/serializer.rb, line 315
def mongo_attributes(*method_names, **options)
  add_attribute('id', **options, strategy: :write_value_using_id_strategy) if method_names.delete(:id)
  add_attributes(method_names, **options, strategy: :write_value_using_mongoid_strategy)
end
new_json_writer() click to toggle source

Internal: The writer to use to write to json

# File lib/oj_serializers/serializer.rb, line 280
def new_json_writer
  Oj::StringWriter.new(mode: :rails)
end
serializer_attributes(*method_names, **options) click to toggle source

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.

# File lib/oj_serializers/serializer.rb, line 330
def serializer_attributes(*method_names, **options)
  add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
end
Also aliased as: attribute

Private Class Methods

_associations() click to toggle source

Internal: List of associations to be serialized. Any associations defined in parent classes are inherited.

# File lib/oj_serializers/serializer.rb, line 220
def _associations
  @_associations = superclass.try(:_associations)&.dup || {} unless defined?(@_associations)
  @_associations
end
_attributes() click to toggle source

Internal: List of attributes to be serialized.

Any attributes defined in parent classes are inherited.

# File lib/oj_serializers/serializer.rb, line 213
def _attributes
  @_attributes = superclass.try(:_attributes)&.dup || {} unless defined?(@_attributes)
  @_attributes
end
add_association(name, options) click to toggle source
# File lib/oj_serializers/serializer.rb, line 364
def add_association(name, options)
  _associations[name.to_s.freeze] = options
end
add_attribute(name, options) click to toggle source
# File lib/oj_serializers/serializer.rb, line 360
def add_attribute(name, options)
  _attributes[name.to_s.freeze] = options
end
add_attributes(names, options) click to toggle source
# File lib/oj_serializers/serializer.rb, line 356
def add_attributes(names, options)
  names.each { |name| add_attribute(name, options) }
end
inherited(subclass) click to toggle source

Internal: Will alias the object according to the name of the wrapper class.

Calls superclass method
# File lib/oj_serializers/serializer.rb, line 204
def inherited(subclass)
  object_alias = subclass.name.demodulize.chomp('Serializer').underscore
  subclass.object_as(object_alias) unless method_defined?(object_alias)
  super
end
instance() click to toggle source

Internal: Allows to obtain a pre-existing instance and binds it to the specified object.

NOTE: Each class is only instantiated once to reduce object allocation. For that reason, serializers must be completely stateless (or use global state).

# File lib/oj_serializers/serializer.rb, line 447
def instance
  Thread.current[instance_key] ||= new
end
instance_key() click to toggle source

Internal: Cache key to set a thread-local instance.

# File lib/oj_serializers/serializer.rb, line 452
def instance_key
  unless defined?(@instance_key)
    @instance_key = "#{name.underscore}_instance".to_sym
    # We take advantage of the fact that this method will always be called
    # before instantiating a serializer to define the write_to_json method.
    class_eval(write_to_json_body)
    raise ArgumentError, "You must use `cached ->(object) { ... }` in order to specify a different cache key when subclassing #{name}." if method_defined?(:cache_key) || respond_to?(:cache_key)
  end
  @instance_key
end
many(items, options = nil) click to toggle source

Public: Serializes an array of items using this serializer.

items - Must respond to `each`. options - list of external options to pass to the sub class (available in `item.options`)

Returns an Oj::StringWriter instance, which is encoded as raw json.

# File lib/oj_serializers/serializer.rb, line 192
def many(items, options = nil)
  writer = new_json_writer
  write_many(writer, items, options)
  writer
end
object_as(name) click to toggle source

Public: Creates an alias for the internal object.

# File lib/oj_serializers/serializer.rb, line 199
def object_as(name)
  define_method(name) { @object }
end
one(item, options = nil) click to toggle source

Public: Serializes the configured attributes for the specified object.

item - the item to serialize options - list of external options to pass to the sub class (available in `item.options`)

Returns an Oj::StringWriter instance, which is encoded as raw json.

# File lib/oj_serializers/serializer.rb, line 180
def one(item, options = nil)
  writer = new_json_writer
  write_one(writer, item, options)
  writer
end
one_if(item, options = nil) click to toggle source

Helper: Serializes the item unless it's nil.

# File lib/oj_serializers/serializer.rb, line 170
def one_if(item, options = nil)
  one(item, options) if item
end
write_association_body(method_name, association_options) click to toggle source

Internal: Returns the code for the association method.

# File lib/oj_serializers/serializer.rb, line 413
    def write_association_body(method_name, association_options)
      # Use a serializer method if defined, else call the association in the object.
      association_method = method_defined?(method_name) ? method_name : "@object.#{method_name}"
      association_root = association_options[:root]
      serializer_class = association_options.fetch(:serializer)

      case write_method = association_options.fetch(:write_method)
      when :write_one
        <<-WRITE_ONE
        if associated_object = #{association_method}
          writer.push_key(#{association_root.to_s.inspect})
          #{serializer_class}.write_one(writer, associated_object)
        end
        WRITE_ONE
      when :write_many
        <<-WRITE_MANY
        writer.push_key(#{association_root.to_s.inspect})
        #{serializer_class}.write_many(writer, #{association_method})
        WRITE_MANY
      when :write_flat
        <<-WRITE_FLAT
        #{serializer_class}.write_flat(writer, #{association_method})
        WRITE_FLAT
      else
        raise ArgumentError, "Unknown write_method #{write_method}"
      end
    end
write_conditional_body(method_name, options) { || ... } click to toggle source

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.

# File lib/oj_serializers/serializer.rb, line 399
def write_conditional_body(method_name, options)
  include_method_name = "include_#{method_name}?"
  if render_if = options[:if]
    define_method(include_method_name, &render_if)
  end

  if method_defined?(include_method_name)
    "if #{include_method_name};#{yield};end\n"
  else
    yield
  end
end
write_to_json_body() click to toggle source

Internal: We generate code for the serializer to avoid the overhead of using variables for method names, having to iterate the list of attributes and associations, and the overhead of using `send` with dynamic methods.

As a result, the performance is the same as writing the most efficient code by hand.

# File lib/oj_serializers/serializer.rb, line 374
    def write_to_json_body
      <<~WRITE_TO_JSON
        # Public: Writes this serializer content to a provided Oj::StringWriter.
        def write_to_json(writer)
          #{ _attributes.map { |method_name, attribute_options|
            write_conditional_body(method_name, attribute_options) {
              <<-WRITE_ATTRIBUTE
                #{attribute_options.fetch(:strategy)}(writer, #{method_name.inspect})
              WRITE_ATTRIBUTE
            }
          }.join }
          #{ _associations.map { |method_name, association_options|
            write_conditional_body(method_name, association_options) {
              write_association_body(method_name, association_options)
            }
          }.join}
        end
      WRITE_TO_JSON
    end

Public Instance Methods

options() click to toggle source

Backwards Compatibility: Allows to access options passed through `render json`, in the same way than ActiveModel::Serializers.

# File lib/oj_serializers/serializer.rb, line 37
def options
  @object.try(:options) || DEFAULT_OPTIONS
end
write_flat(writer, item) click to toggle source

Internal: Used internally to write attributes and associations to JSON.

NOTE: Binds this instance to the specified object and options and writes to json using the provided writer.

# File lib/oj_serializers/serializer.rb, line 45
def write_flat(writer, item)
  @memo.clear if defined?(@memo)
  @object = item
  write_to_json(writer)
end
write_many(writer, items, options = nil) click to toggle source

Internal: Used internally to write an array of objects to JSON.

writer - writer used to serialize results items - items to serialize results for options - list of external options to pass to the serializer (available as `options`)

# File lib/oj_serializers/serializer.rb, line 84
def write_many(writer, items, options = nil)
  writer.push_array
  items.each do |item|
    write_one(writer, item, options)
  end
  writer.pop
end
write_one(writer, item, options = nil) click to toggle source

Internal: Used internally to write a single object to JSON.

writer - writer used to serialize results item - item to serialize results for options - list of external options to pass to the serializer (available as `options`)

NOTE: Binds this instance to the specified object and options and writes to json using the provided writer.

# File lib/oj_serializers/serializer.rb, line 72
def write_one(writer, item, options = nil)
  item.define_singleton_method(:options) { options } if options
  writer.push_object
  write_flat(writer, item)
  writer.pop
end

Protected Instance Methods

memo() click to toggle source

Internal: An internal cache that can be used for temporary memoization.

# File lib/oj_serializers/serializer.rb, line 95
def memo
  defined?(@memo) ? @memo : @memo = OjSerializers::Memo.new
end

Private Instance Methods

original_write_value_using_method_strategy(writer, key)
original_write_value_using_mongoid_strategy(writer, key)
write_value_using_hash_strategy(writer, key) click to toggle source

Strategy: Writes a Hash value to JSON, works with String or Symbol keys.

# File lib/oj_serializers/serializer.rb, line 114
def write_value_using_hash_strategy(writer, key)
  writer.push_value(@object[key], key.to_s)
end
write_value_using_id_strategy(writer, _key) click to toggle source

Strategy: 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).

# File lib/oj_serializers/serializer.rb, line 104
def write_value_using_id_strategy(writer, _key)
  writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?
end
write_value_using_method_strategy(writer, key) click to toggle source

Strategy: Obtains the value by calling a method in the object, and writes it.

# File lib/oj_serializers/serializer.rb, line 119
def write_value_using_method_strategy(writer, key)
  writer.push_value(@object.send(key), key)
end
write_value_using_mongoid_strategy(writer, key) click to toggle source

Strategy: Writes an Mongoid attribute to JSON, this is the fastest strategy.

# File lib/oj_serializers/serializer.rb, line 109
def write_value_using_mongoid_strategy(writer, key)
  writer.push_value(@object.attributes[key], key)
end
write_value_using_serializer_strategy(writer, key) click to toggle source

Strategy: Obtains the value by calling a method in the serializer.

# File lib/oj_serializers/serializer.rb, line 124
def write_value_using_serializer_strategy(writer, key)
  writer.push_value(send(key), key)
end