Skip to content

creasty/jsonity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jsonity

The most sexy language for building JSON in Ruby

I'd been writing JSON API with Jbuilder, RABL and ActiveModel::Serializer, but nothing of them meet my requirement and use case.

  • Jbuilder is very verbose in syntax, and its functonalities of partial and mixin are actually weak
  • RABL has simple syntax, but writing complex data structure with it is not very readable
  • ActiveModel::Serializer is persuasive role in Rails architecture, but can get very useless when you need to fully control from controller what attributes of multi-nested (associated) object to be included

So I chose to create new one -- Jsonity, which is simple and powerful JSON builder especially for JSON API representations.

  • Simple and readable syntax even if it gets complex
  • Flexible and arbitrary nodes
  • Includable mixin
  • Declarative attributes inclusion

Installation

Make sure to add the gem to your Gemfile.

gem 'jsonity'

Overview

@meta_pagination_mixin = ->(t) {
  t.meta! { |meta|
    meta.total_pages
    meta.current_page
  }
}

Jsonity.build { |t|
  t[].users!(@users) { |user|
    user.id
    user.age
    user.full_name { |u| [u.first_name, u.last_name].join ' ' }

    user.avatar? { |avatar|
      avatar.image_url
    }
  }

  t.(@users, &@meta_pagination_mixin)
}
=begin
{
  "users": [
    {
      "id": 1,
      "age": 21,
      "full_name": "John Smith",
      "avatar": {
        "image_url": "http://example.com/john.png"
      }
    },
    {
      "id": 2,
      "age": 37,
      "full_name": "William Northington",
      "avatar": null
    }
  ],
  "meta": {
    "total_pages": 1,
    "current_page": 1
  }
}
=end

Usage

Data object assignment

To declare the data object for use:

Jsonity.build { |t|
  t <= @user
  # ...
}

Or passing as an argument:

Jsonity.build(@user) { |user|
  # ...
}

Attribute nodes

Basic usage of defining simple attributes:

Jsonity.build(@user) { |user|
  user.id   # @user.id
  user.age  # @user.age
}
=begin
{
  "id": 123,
  "age": 27
}
=end

Or you can use custom attributes in flexible ways:

Jsonity.build(@user) { |user|
  # create full_name from @user.first_name and @user.last_name
  user.full_name { |u| [u.first_name, u.last_name].join ' ' }

  # block parameter isn't required
  user.russian_roulette { rand(1..10) }

  # or with specified the data object
  user.hello('world') { |w| w.upcase }

  # block can be omitted if the value is constant
  user.seventeen 17
}
=begin
{
  "full_name": "John Smith",
  "russian_roulette": 4,
  "hello": "WORLD",
  "seventeen": 17
}
=end

Aliased attributes works well as you expected:

Jsonity.build(@user) { |user|
  user.my_id &:id
}
=begin
{
  "my_id": 123
}
=end

Hash nodes

With name suffixed with !, nested object can be included:

Jsonity.build(@user) { |user|
  user.name  # @user.name

  user.avatar! { |avatar|
    avatar.image_url  # @user.avatar.image_url
    avatar.width      # @user.avatar.width
    avatar.height     # @user.avatar.height
  }
}
=begin
{
  "name": "John Smith",
  "avatar": {
    "image_url": "http://www.example.com/avatar.png",
    "width": 512,
    "height": 512
  }
}
=end

Assume that @user.avatar is nil, the output will be:

=begin
{
  "name": "John Smith",
  "avatar": {
    "image_url": null,
    "width": null,
    "height": null
  }
}
=end

On the other hand, use ? as suffix, the whole object become null:

Jsonity.build(@user) { |user|
  user.name

  user.avatar? { |avatar|  # <-- look, prefix is `?`
    avatar.image_url
    avatar.width
    avatar.height
  }
}
=begin
Assume that `@user.avatar` is `nil`,

{
  "name": "John Smith",
  "avatar": null
}
=end

To specify the data object to use inside a block:

Jsonity.build { |t|
  t.home!(@user.hometown_address) { |home|
    home.street  # @user.hometown_address.street
    home.zip
    home.city
    home.state
  }
}
=begin
{
  "home": {
    "street": "4611 Armbrester Drive",
    "zip": "90017",
    "city": "Los Angeles",
    "state": "CA"
  }
}
=end

Or a block can inherit the parent data object:

Jsonity.build { |t|
  t.user!(@user) { |user|
    user.profile!(inherit: true) { |profile|
      profile.name  # @user.name
    }
  }
}
=begin
{
  "user": {
    "profile": {
      "name": "John Smith"
    }
  }
}
=end

Array nodes

Including a collection of objects, just use [] and write the same syntax of hash node:

Jsonity.build(@user) { |user|
  user[].friends! { |friend|
    friend.name  # @user.friends[i].name
  }
}
=begin
{
  "friends": [
    { "name": "John Smith" },
    { "name": "William Northington" }
  ]
}
=end

Similar to hash nodes in naming convention,
if @user.friends = nil nodes suffix with ! will be an empty array [], in contrast, some with ? will be null.

Also passing the data object or inheritance can be done in the same way as hash nodes.

Automatic attributes inclusion

If you set attr_json in any class, the specified attributes will automatically be included:

class Sample < Struct.new(:id, :foo, :bar)
  attr_json :id, :foo

  attr_json { |sample|
    sample.hello_from 'attr_json!'
  }
end

@sample = Sample.new 123, 'foo!', 'bar!!'

and then,

Jsonity.build { |t|
  t.sample! @sample
}
=begin
{
  "sample": {
    "id": 123,
    "foo": "foo!",
    "hello_from": "attr_json!"
  }
}
=end

Still you can create any kinds of nodes with a block:

Jsonity.build { |t|
  t.sample!(@sample) { |sample|
    sample.bar { |s| "this is #{s.bar}" }
  }
}
=begin
{
  "sample": {
    "id": 123,
    "foo": "foo!",
    "hello_from": "attr_json!",
    "bar": "this is bar!!"
  }
}
=end

Mixin / Scope

Since Jsonity aim to be simple and light, use plain Proc to fullfill functonality of mixin.

@timestamps_mixin = ->(t) {
  t.created_at
  t.updated_at
}

and then,

Jsonity.build { |t|
  t.user!(@user) { |user|
    user.(&@timestamps_mixin)
  }
}
=begin
{
  "user": {
    "created_at": "2014-09-10 10:41:07 +0900",
    "updated_at": "2014-09-13 12:55:56 +0900"
  }
}
=end

To explicitly specify the data object to use in mixin, you can do by passing it in the first argument:

Jsonity.build { |t|
  t.(@user, &@timestamps_mixin)
}
=begin
{
  "created_at": "2014-09-10 10:41:07 +0900",
  "updated_at": "2014-09-13 12:55:56 +0900"
}
=end

So you can take this functonality for scope:

Jsonity.build { |t|
  t.(@user) { |user|
    user.name
  }
}
=begin
{
  "name": "John Smith"
}
=end

Mixining nested object and merging

@meta_pagination_mixin = ->(t) {
  t.meta! { |meta|
    meta.total_pages
    meta.current_page
  }
}

and use this mixin like:

Jsonity.build { |t|
  t.(@people, &@meta_pagination_mixin)

  t.meta! { |meta|
    meta.total_count @people.count
  }
}
=begin
Notice that two objects `meta!` got merged.

{
  "meta": {
    "total_pages": 5,
    "current_page": 1,
    "total_count": 123
  }
}
=end

Conditional

You can use if: and unless: options.

Jsonity.build { |t|
  t[].people!(@people) { |person|
    # if:
    person.cv!(if: &:looking_for_job?) { |cv|
      cv.title
      cv.download_link
    }

    # unless:
    person.name unless: &:anonymous?
  }
}

Getting data object

You can get the data object by calling get.

Jsonity.build { |t|
  t[].people!(@people) { |person|
    # debugging object
    p person.get
  }
}

With Rails

Helper method is available in controller for rendering with Jsonity:

render_json(status: :ok) { |t|
  # ...
}

License

This project is copyright by Creasty, released under the MIT lisence.
See LICENSE file for details.

About

The most sexy language for building JSON in Ruby

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages