Primalize
Primalize lets you de-evolve your objects into primitive values for serialization. The primary use case is to serialize them into JSON, but once it's in its primitive state, it can be converted into other formats such as XML or CSV.
Primalizers support type checking by letting you specify the types of the resulting properties:
class OrderSerializer < Primalize::Single
attributes(
id: integer,
customer_id: integer,
product_ids: array(integer),
status: enum(
'requested',
'payment_processed',
'awaiting_shipment',
'shipped',
'delivered',
),
signature_required: boolean,
shipping_address: object(
address1: string,
address2: optional(string),
city: string,
state: string,
zip: string,
),
created_at: timestamp,
)
end
OrderSerializer.new(order).call
# { id: ... }
You can also primalize a nested structure of response objects with Primalize::Many
, replacing the type annotations with the classes of their respective serializers:
class PostResponseSerializer < Primalize::Many
attributes(
post: PostSerializer,
author: UserSerializer,
comments: enumerable(CommentSerializer), # Not just one comment, but *many*
)
end
# Instantiate it by passing in the pertinent values
serializer = PostResponseSerializer.new(
post: @post,
author: @post.author,
comments: @post.comments,
)
serializer.call
# {
# post: { ... },
# author: { ... },
# comments: [
# { ... },
# ],
# }
Installation
Add this line to your application's Gemfile:
gem 'primalize'
And then execute:
$ bundle
Or install it yourself as:
$ gem install primalize
Usage
If you need to primalize a single object, you subclass Primalize::Single
and specify the attributes and types of the result as in the example above.
Supported types
The complete list of supported types are:
-
integer
: whole numbers -
float
: floating-point numbers -
number
: any numeric value -
string
: text -
boolean
: explicitlytrue
orfalse
(not "truthy" or "falsy" values) -
array(*types)
: an array containing values of the specified types- Example:
array(string, integer)
- Example:
-
optional(*types)
: any of the specified types ornil
- Example:
optional(string)
, both"foo"
andnil
are acceptable values
- Example:
-
enum(*values)
: must be one of the specified values- Example:
enum('requested', 'shipped', 'delivered')
- Example:
-
timestamp
: aDate
,Time
, orDateTime
value -
any(*types)
: any value of the given types- Example:
any(string, integer)
will only match on strings and integers - If no types are specified, any value will match
- Example:
-
primalize(YourPrimalizerClass)
: primalizes the specified attribute with the givenPrimalize::Single
subclass- Example:
primalize(OrderSerializer)
- Example:
-
object(**types)
: a hash of the specified structure- Example:
object(id: integer, name: string)
- Only the required keys need to be specified. The rest of the hash will pass.
- If no keys are specified, all of them are optional and it will match any hash.
- Example:
Attribute coercion
Reducing the object's attributes to a hash isn't all you do in most apps. You may also need to do some coercion. For example, if you have an object whose city
isn't stored as a string but you need to translate it to one:
class ShipmentSerializer < Primalize::Single
attributes(
city: string { |city| city.name },
# ...
)
end
Virtual attributes
You can also generate attributes that don't exist on the object being primalized by defining methods on the primalizer:
class ShipmentSerializer < Primalize::Single
attributes(
payment_method: string,
)
def payment_method
if object.paid_with_card?
'credit_card'
elsif object.purchase_order?
'purchase_order'
elsif object.bill_later?
'invoice'
else
'unknown'
end
end
end
Type Checking
By default, subclasses of Primalize::Single
will raise an ArgumentError
if there is a mismatch between the types declared in its attributes
call and what is passed in to be primalized. In production, you might not want that to happen, so you can change that in your production config:
Primalize::Single.type_mismatch_handler = proc do |primalizer, attr, type, value|
msg = "Type mismatch: #{primalizer.name}##{attr} is expected to be #{type.inspect}, but is #{value.inspect}\n"
msg << caller.grep(Regexp.new(Rails.root)).join("\n") # Include application stack trace
Slack.notify '#bugs', msg
nil # Don't emit any value for an incorrect attribute
end
Note that this example returns nil
when there is a type mismatch after reporting the bug. The return value of the block becomes the value serialized into your payload, so you can allow the incorrect value to pass through, clear it, or anything else that makes sense for your use case.
Also note that type_mismatch_handler
is set on Primalize::Single
here. You can also set it for individual classes and it will be inherited by all subclasses. For example, if you have different use cases for serialization within the same application, such as API responses sent over HTTP and events passed through a message broker, you can perform different type-mismatch handling for each.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/jgaskins/primalize. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Primalize project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.