Skip to content

Ruby object serialization and parsing lib

License

Notifications You must be signed in to change notification settings

jsteinberg/ializer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Gem Version

{De | Ser} Ializer

A fast serializer/deserializer for Ruby Objects.

Table of Contents

Design Goals

  • Simple Singular DSL for defining object-to-data and data-to-object mappings
  • Support for nested object relationships
  • Isolate serialization/parsing code from object to not pollute method space
  • speed

Installation

Add this line to your application's Gemfile:

gem 'ializer'

Execute:

bundle install

Require:

require 'ializer'

Usage

Configuration

Ializer.setup do |config|
  config.key_transform = :dasherize # change serailized key names
  # or
  config.key_transformer = ->(key) {
    key.lowercase.undsercore + '1'
  }
  config.warn_on_default = true # outputs a warning to STDOUT(puts) if DefaultDeSer is used
  config.raise_on_default = false # raises an exception if the DefaultDeSer is used
end

For more information, see Key Transforms and Attribute Types sections.

Model Definitions

class Order
  attr_accessor :id, :created_at, :items, :customer

  def initialize(attr = {})
    @id = attr[:id]
    @created_at = attr[:created_at]
    @items = attr[:items] || []
  end
end

class OrderItem
  attr_accessor :name, :price, :in_stock

  def initialize(attr = {})
    @name = attr[:name]
    @price = attr[:price]
    @in_stock = attr[:in_stock]
  end
end

class Customer
  attr_accessor :name, :email

  def initialize(attr = {})
    @name = attr[:name]
    @email = attr[:email]
  end
end

Serializer Definition

class OrderDeSer < Ser::Ializer
  integer    :id
  timestamp  :created_at

  nested     :items,       deser: OrderItemDeSer
  nested     :customer,    deser: CustomerDeSer
end

class OrderItemDeSer < Ser::Ializer
  string     :name
  decimal    :price
  boolean    :in_stock
end

class CustomerDeSer < Ser::Ializer
  string     :name
  string     :email
end

DeSerializer Definition

De::Ser::Ializers can deserialize from JSON and serialize to JSON. If you only need serialization capabilities, you can inherit from, Ser::Italizer instead.

class OrderDeSer < De::Ser::Ializer
  integer    :id
  timestamp  :created_at

  nested     :items,       deser: OrderItemDeSer,   model_class: OrderItem
  nested     :customer,    deser: CustomerDeSer,    model_class: Customer
end

class OrderItemDeSer < De::Ser::Ializer
  string     :name
  decimal    :price
  boolean    :in_stock
end

class CustomerDeSer < De::Ser::Ializer
  string     :name
  string     :email
end

De/Ser::Ializer Configuration

You can override the global config for a specific Ser::Ializer or De::Ser::Ializer by calling the setup command.

Note: setup must be called at the beginning of the definition otherwise the default config will be used.

class OrderDeSer < De::Ser::Ializer
  setup do |config|
    config.key_transform = :dasherize
  end

  integer    :id
  timestamp  :created_at

  nested     :items,       deser: OrderItemDeSer,   model_class: OrderItem
  nested     :customer,    deser: CustomerDeSer,    model_class: Customer
end

Sample Object

order = Order.new(id: 4, created_at: Time.now, items: [])
order.items << OrderItem.new(name: 'Baseball', price: BigDecimal('4.99'), in_stock: true)
order.items << OrderItem.new(name: 'Football', price: BigDecimal('14.99'), in_stock: false)
order.customer = Customer.new(name: 'Bowser', email: '[email protected]')

Object Serialization

Return a hash

data = OrderDeSer.serialize(order)

Return Serialized JSON

Ializer relies on the MultiJson gem for json serialization/parsing

json_string = OrderDeser.serialize_json(order)

Serialized Output

{
  "id": 4,
  "created_at": "2019-12-01T00:00:00.000-06:00",
  "items": [
    {
       "name": "Baseball",
       "decimal": "4.99",
       "in_stock": true
     },
     {
       "name": "Football",
       "decimal": "14.99",
       "in_stock": false
     }
  ],
  "customer": {
    "name": "Bowser",
    "email": "[email protected]"
  }
}

Serialize a collection

data = OrderDeSer.serialize([order, order2])

Serialized Collection Output

[
  {
    "id": 3,
    ...
  },
  {
    "id": 4,
    ...
  }
]

Object Deserialization

Note: Objects that are parsed must have a zero argument initializer (ie: Object.new)

Parsing a hash

model = OrderDeSer.parse(data, Order)

 => #<Order:0x00007f9e44aabd80 @id=4, @created_at=Sun, 01 Dec 2019 00:00:00 -0600, @items=[#<OrderItem:0x00007f9e44aab6f0 @name="Baseball", @in_stock=true, @price=0.499e1>, #<OrderItem:0x00007f9e44aab628 @name="Football", @in_stock=false, @price=0.1499e2>], @customer=#<Customer:0x00007f9e44aab498 @name="Bowser", @email="[email protected]">>

Attributes

Attributes are defined in ializer using the property method.

By default, attributes are read directly from the model property of the same name. In this example, name is expected to be a property of the object being serialized:

class Customer < De::Ser::Ializer
  property :id,   type: :integer
  property :name, type: String
end

Custom typed methods also exist to provide a cleaner DSL.

class Customer < De::Ser::Ializer
  integer :id
  string :name
end

Nested Attributes

ializer was built for serialization and parsing of nested objects. You can create a nested object via the property method or a specialized nested method.

The nested method allows you to define a deser inline.

class OrderDeSer < De::Ser::Ializer
  integer    :id
  timestamp  :created_at

  nested     :items,       deser: OrderItemDeSer,   model_class: OrderItem
  # OR
  property   :items,       deser: OrderItemDeSer,   model_class: OrderItem

  nested     :customer,    model_class: Customer do
    string     :name
    string     :email
  end
end

The property method DOES NOT allow you to define a deser inline, but instead allows you to override the value of the field.

class OrderDeSer < De::Ser::Ializer
  integer    :id
  property   :items,       deser: OrderItemDeSer,   model_class: OrderItem do |object, _context|
    object.items.select(&:should_display?)
  end
end

Attribute Types

The following types are included with ializer

Type method alias mappings
BigDecimal decimal() :BigDecimal, :decimal
Boolean boolean() :Boolean, :boolean
Date date() Date, :date
Integer integer() Integer, :integer
Float float() Float, :float
Time millis() :Millis, :millis
String string() String, :millis
Symbol symbol() Symbol, :symbol, :sym
Time timestamp() Time, DateTime, :timestamp
Array array() :array
JSON json() :json
Default default() :default

Note: Array/JSON/Default just uses the current value of the field and will only properly deserialize if it is a standard json value type(number, string, boolean).

Default Attribute Configuration Options

There are a few settings for dealing with the DefaultDeSer.

Ializer.setup do |config|
  config.warn_on_default = true # outputs a warning to STDOUT(puts) if DefaultDeSer is used
  config.raise_on_default = false # raises an exception if the DefaultDeSer is used
end

Since De::Ser::Ializers are configured on load, raising an exception should halt the application from starting(instead of silently failing later). By default a warning is logged to STDOUT.

Registering Custom Attribute types

You can register your own types or override the provided types. A custom attribute type DeSer must implement the following methods. When registering, you must register with the base Ser::Ializer class.

Ser::Ializer.register(method_name, deser, *mappings)
class CustomDeSer
  def self.serialize(value, _context = nil)
    "#{value}_custom"
  end

  def self.parse(value)
    value.split("_")[0]
  end
end

Ser::Ializer.register(:custom, CustomDeSer, :custom)

Then you can use them as follows:

class Customer < De::Ser::Ializer
  integer :id
  string :name
  custom :custom_prop
  # or
  property :custom_prop, type: :custom
end

To override the provided type desers, you do the following:

class MyTimeDeSer
  def self.serialize(value, _context = nil)
    value.to_my_favorite_time_serialization_format
  end

  def self.parse(value)
    Time.parse_my_favorite_time_serialization_format(value)
  end
end

Ser::Ializer.register('timestamp', MyTimeDeSer, Time, DateTime, :timestamp)

Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax:

class Customer < De::Ser::Ializer
  string :full_name do |object, _context|
    "#{object.first_name} (#{object.last_name})"
  end
end

The block syntax can also be used to override the property on the object:

class Customer < De::Ser::Ializer
  string :name do |object, _context|
    "#{object.name} Part 2"
  end
end

You can also override the property on an object with a specially named method:

class Customer < De::Ser::Ializer
  string :name

  def self.name(object, _context) # overrides :name attribute
    "#{object.name} Part 2"
  end
end

Setters can also be overridden:

class Customer < De::Ser::Ializer
  string :name

  def self.name=(object, value)
    object.name = value.delete_suffix('Part 2')
  end
end

Attributes can also use a different name by passing the original method or accessor with a proc shortcut:

class Customer < De::Ser::Ializer
  string :name, key: 'customer-name' # Note: an explicitly set key will not be transformed by the configured key_transformer
end

Serialization Context

In some cases a Ser::Ializer might require more information than what is available on the record. A context object can be passed to serialization and used however necessary.

class CustomerSerializer < Ser::Ializer
  integer :id
  string :name

  string :phone_number do |object, context|
    if context.admin?
      object.phone_number
    else
      object.phone_number.last(4)
    end
  end
end

# ...

CustomerDeSer.serialize(order, current_user)

Using the context to serialize a subset of attributes

There are special keywords/method names on the Serialization Context that can be used to limit the attributes that are serialized. This is different from conditional attributes below. The conditions would still apply to the subset.

If your serialization context is a Hash, you can use the hash keys :attributes or :include to define the limited subset of attributes for serialization.

CustomerDeSer.serialize(order, attributes: [:name])

If your serialization context is a ruby object, a method named attributes that returns an array of attribute names can be used.

class AttributeSubsetContext
  attr_accessor :attributes
end

context = AttributeSubsetContext.new(attributes: [:name])
CustomerDeSer.serialize(order, context)

Conditional Attributes

Conditional attributes can be defined by passing a Proc to the if key on the property method. Return truthy if the attribute should be serialized, and falsey if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.

class CustomerSerializer < Ser::Ializer
  integer :id
  string :name

  string :phone_number if: ->(object, context) { context.admin? }
end

# ...
CustomerDeSer.serialize(order, current_user)

Note: instead of a Proc, any object that responds to call with arity 2 can be passed to :if.

class AdminChecker
  def self.admin?(_object, context)
    context.admin?
  end
end

class CustomerSerializer < Ser::Ializer
  integer :id
  string :name

  string :phone_number if: AdminChecker.method(:admin?)
end

# ...
CustomerDeSer.serialize(order, current_user)

Attribute Sharing

There are a couple of ways to share attributes from different desers.

Inheritance

class SimpleUserDeSer < De::Ser::Ializer
  integer :id
  string  :username
end

class UserDeSer < SimpleUserDeSer
  string  :email
end

Composition

class BaseApiDeSer < De::Ser::Ializer
  integer    :id
  timestamp  :created_at
end

class UserDeSer < De::Ser::Ializer
  with BaseApiDeSer
  string  :email
end

Note: Including a deser using with will include any method overrides.

class BaseApiDeSer < De::Ser::Ializer
  integer    :id
  timestamp  :created_at
  timestamp  :updated_at

  def self.created_at(object, context)
    # INCLUDED IN UserDeSer
  end

  def self.updated_at(object, context)
    # NOT INCLUDED IN UserDeSer(Overridden below)
  end
end

class UserDeSer < De::Ser::Ializer
  with BaseApiDeSer
  string  :email

  def self.updated_at(object, context)
    # INCLUDED IN UserDeSer.
  end
end

For more examples check the spec/support/deser folder.

Key Transforms

By default ializer uses object field names as the key name. You can override this setting by either specifying a string method for transforms or providing a proc for manual transformation.

Note: key_transformer will override any value set as the key_transform

Ializer.setup do |config|
  config.key_transform = :dasherize
end

# or

Ializer.setup do |config|
  config.key_transformer = ->(key) {
    key.lowercase.undsercore + '1'
  }
end

Thread Safety

Defining of desers is not thread safe. As long as defitions are preloaded then thread safety is not a concern. Note: because of this you should not create desers at runtime

Performance Comparison

TODO

Contributing

TODO

About

Ruby object serialization and parsing lib

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors 4

  •  
  •  
  •  
  •