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
Internal: Iterating arrays is faster than iterating hashes.
Internal: Iterating arrays is faster than iterating hashes.
Protected Class Methods
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
Syntax Sugar: Allows to use it before a method name.
Example:
attribute \ def full_name "#{ first_name } #{ last_name }" end
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
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
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
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
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
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
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
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
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
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
Private Class Methods
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
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
# File lib/oj_serializers/serializer.rb, line 364 def add_association(name, options) _associations[name.to_s.freeze] = options end
# File lib/oj_serializers/serializer.rb, line 360 def add_attribute(name, options) _attributes[name.to_s.freeze] = options end
# File lib/oj_serializers/serializer.rb, line 356 def add_attributes(names, options) names.each { |name| add_attribute(name, options) } end
Internal: Will alias the object according to the name of the wrapper class.
# 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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