Code Monkey home page Code Monkey logo

surrealist's Introduction

Surrealist

Build Status Coverage Status Inline docs Gem Version Open Source Helpers Reviewed by Hound

Surrealist

Surrealist is a schema-driven object serialization ruby library. It also provides a trivial type checking in the runtime before serialization. Yard documentation

Motivation

A typical use case for this gem could be, for example, serializing a (decorated) object outside of the view context. The schema is described through a hash, so you can build the structure of serialized object independently of its methods and attributes, while also having possibility to serialize nested objects and structures. Introductory blogpost.

Installation

Add this line to your application's Gemfile:

gem 'surrealist'

And then execute:

$ bundle

Or install it yourself as:

$ gem install surrealist

Usage

Schema should be defined with a block that contains a hash. Every key of the schema should be either a name of a method of the surrealizable object (or it's ancestors/mixins), or - in case you want to build json structure independently from object's structure - a symbol. Every value of the hash should be a constant that represents a Ruby class, that will be used for type-checks.

Simple example

  • Include Surrealist in your class.
  • Define a schema with methods that need to be serialized.
class Person
  include Surrealist

  json_schema do
    { name: String, age: Integer }
  end

  def name
    'John Doe'
  end

  def age
    42
  end
end
  • Surrealize it.
Person.new.surrealize
# => '{ "name": "John Doe", "age": 42 }'

Nested structures

class Person
  include Surrealist

  json_schema do
    {
      foo: String,
      name: String,
      nested: {
        at: {
          any: Integer,
          level: Bool,
        },
      },
    }
  end
  # ... method definitions
end

Person.find_by(email: '[email protected]').surrealize
# => '{ "foo": "Some string", "name": "John Doe", "nested": { "at": { "any": 42, "level": true } } }'

Nested objects

If you need to serialize nested objects and their attributes, you should define a method that calls nested object:

class User
  include Surrealist

  json_schema do
    {
      name: String,
      credit_card: {
        number: Integer,
        cvv: Integer,
      },
    }
  end

  def name
    'John Doe'
  end

  def credit_card
    # Assuming that instance of a CreditCard has methods #number and #cvv defined
    CreditCard.find_by(holder: name)
  end
end

User.new.surrealize
# => '{ "name": "John Doe", "credit_card": { "number": 1234, "cvv": 322 } }'

Collection Surrealization

Since 0.2.0 Surrealist has API for collection serialization. Example for ActiveRecord:

class User < ActiveRecord::Base
  include Surrealist

  json_schema do
    { name: String, age: Integer }
  end
end

users = User.all
# => [#<User:0x007fa1485de878 id: 1, name: "Nikita", age: 23>, #<User:0x007fa1485de5f8 id: 2, name: "Alessandro", age: 24>]

Surrealist.surrealize_collection(users)
# => '[{ "name": "Nikita", "age": 23 }, { "name": "Alessandro", "age": 24 }]'

You can find motivation behind introducing new API versus monkey-patching here. #surrealize_collection works for all data structures that behave like Enumerable. All ActiveRecord features (like associations, inheritance etc) are supported and covered. Further reading: working with ORMs. All optional arguments (camelize, include_root etc) are also supported.

An additional and unique argument for #surrealize_collection is raw which is evaluated as a Boolean. If this option is 'truthy' then the results will be an array of surrealized hashes (i.e. NOT a JSON string).

Surrealist.surrealize_collection(users, raw: true)
# => [{ "name": "Nikita", "age": 23 }, { "name": "Alessandro", "age": 24 }]

Defining custom serializers

If you need to keep serialization logic separately from the model, you can define a class that will inherit from Surrealist::Serializer. To point to that class from the model use a class method .surrealize_with. Example usage:

class CatSerializer < Surrealist::Serializer
  json_schema { { age: Integer, age_group: String } }

  def age_group
    age <= 5 ? 'kitten' : 'cat'
  end
end

class Cat
  include Surrealist
  attr_reader :age

  surrealize_with CatSerializer

  def initialize(age)
    @age = age
  end
end

Cat.new(12).surrealize # Implicit usage through .surrealize_with
# => '{ "age": 12, "age_group": "cat" }'

CatSerializer.new(Cat.new(3)).surrealize # explicit usage of CatSerializer
# => '{ "age": 3, "age_group": "kitten" }'

The constructor of Surrealist::Serializer takes two arguments: serializable model (or collection) and a context hash. So if there is an object that is not coupled to serializable model but it is still necessary for constructing JSON, you can pass it to constructor as a hash. It will be available in the serializer in the context hash.

class IncomeSerializer < Surrealist::Serializer
  json_schema { { amount: Integer } }

  def amount
    current_user.guest? ? 100000000 : object.amount
  end

  def current_user
    context[:current_user]
  end
end

class Income
  include Surrealist
  surrealize_with IncomeSerializer

  attr_reader :amount

  def initialize(amount)
    @amount = amount
  end
end

income = Income.new(200)
IncomeSerializer.new(income, current_user: GuestUser.new).surrealize
# => '{ "amount": 100000000 }'

IncomeSerializer.new(income, current_user: User.find(3)).surrealize
# => '{ "amount": 200 }'

If you happen to pass a context to a serializer, there is a handy DSL to reduce the number of methods you have to define yourself. DSL looks as follows

class IncomeSerializer < Surrealist::Serializer
  serializer_context :current_user
  json_schema { { amount: Integer } }

  def amount
    current_user.guest? ? 100000000 : object.amount
  end
end

.serializer_context takes an array of symbols and dynamically defines instance methods that read values from the context hash. So .serializer_context :current_user will become

def current_user
  context[:current_user]
end

There is also an alias in the plural form: .serializer_contexts.

Multiple serializers

You can define several custom serializers for one object and use it in different cases. Just mark it with a tag:

class PostSerializer < Surrealist::Serializer
  json_schema { { id: Integer, title: String, author: { name: String } } }
end

class PreviewSerializer < Surrealist::Serializer
  json_schema { { id: Integer, title: String } }
end

class Post
  include Surrealist

  surrealize_with PostSerializer
  surrealize_with PreviewSerializer, tag: :preview

  attr_reader :id, :title, :author
end

And then specify serializer's tag with for argument:

author = Struct.new(:name).new("John")
post = Post.new(1, "Ruby is awesome", author)
post.surrealize # => '{ "id": 1, "title": "Ruby is awesome", author: { name: "John" } }'

post.surrealize(for: :preview) # => '{ "id": 1, "title": "Ruby is awesome" }'

Or specify serializer explicitly with serializer argument:

post.surrealize(serializer: PreviewSerializer) # => '{ "id": 1, "title": "Ruby is awesome" }'

Build schema

If you don't need to dump the hash to json, you can use #build_schema method on the instance. It calculates values and checks types, but returns a hash instead of a JSON string. From the previous example:

Car.new.build_schema
# => { age: 7, brand: "Toyota", doors: nil, horsepower: 140, fuel_system: "Direct injection", previous_owner: "John Doe" }

Defined schema

Use the .defined_schema method to get the schema that has been defined with json_schema:

User.defined_schema
# => { name: String, age: Integer }

Working with ORMs

There are two kinds of return values of ORM methods: some return collections of objects, while others return instances. For the first ones one should use instance#surrealize, whereas for the second ones Surrealist.surrealize_collection(collection) Please keep in mind that if your serialization logic is kept in a separate class which is inherited from Surrealist::Serializer, than usage boils down to YourSerializer.new(instance || collection).surrealize.

ActiveRecord

All associations work as expected: .has_many, .has_and_belongs_to_many return collections, .has_one, .belongs_to return instances.

Methods that return instances:

.find
.find_by
.find_by!
.take!
.first
.first!
.second
.second!
.third
.third!
.fourth
.fourth!
.fifth
.fifth!
.forty_two
.forty_two!
.last
.last!
.third_to_last
.third_to_last!
.second_to_last
.second_to_last!

Methods that return collections:

.all
.where
.where_not
.order
.take
.limit
.offset
.lock
.readonly
.reorder
.distinct
.find_each
.select
.group
.order
.except
.extending
.having
.references
.includes
.joins

ROM

For detailed usage example (covering ROM 3.x and ROM 4.x) please see spec/orms/rom/. Under the hood ROM uses Sequel, and Sequel returns instances only on .first, .last, .[] and .with_pk!. Collections are returned for all other methods.

container = ROM.container(:sql, ['sqlite::memory']) do |conf|
  conf.default.create_table(:users) do
    primary_key :id
    column :name, String, null: false
    column :email, String, null: false
  end
  # ...
end

users = UserRepo.new(container).users
# => #<ROM::Relation[Users] name=ROM::Relation::Name(users) dataset=#<Sequel::SQLite::Dataset: "SELECT `users`.`id`, `users`.`name`, `users`.`email` FROM `users` ORDER BY `users`.`id`">>

Basically, there are several ways to fetch/represent data in ROM:

# With json_schema defined in ROM::Struct::User
class ROM::Struct::User < ROM::Struct
  include Surrealist

  json_schema { { name: String } }
end

users.to_a.first # => #<ROM::Struct::User id=1 name="Jane Struct" email="[email protected]">
users.to_a.first.surrealize # => "{\"name\":\"Jane Struct\"}"

users.where(id: 1).first # => #<ROM::Struct::User id=1 name="Jane Struct" email="[email protected]">
users.where(id: 1).first.surrealize # => "{\"name\":\"Jane Struct\"}"

Surrealist.surrealize_collection(users.to_a) # => "[{\"name\":\"Jane Struct\"},{\"name\":\"Dane As\"},{\"name\":\"Jack Mapper\"}]"

# using ROM::Struct::Model#as(Representative) with json_schema defined in representative
class RomUser < Dry::Struct
  include Surrealist

  attribute :name, String
  attribute :email, String

  json_schema { { email: String } }
end

# ROM 3.x
rom_users = users.as(RomUser).to_a

# ROM 4.x
rom_users = users.map_to(RomUser).to_a

rom_users[1].surrealize # => "{\"email\":\"[email protected]\"}"
Surrealist.surrealize_collection(rom_users) # => "[{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"}]"

# using Mappers
class UserModel
  include Surrealist

  json_schema { { id: Integer, email: String } }

  attr_reader :id, :name, :email

  def initialize(attributes)
    @id, @name, @email = attributes.values_at(:id, :name, :email)
  end
end

class UsersMapper < ROM::Mapper
  register_as :user_obj
  relation :users
  model UserModel
end

# ROM 3.x
mapped = users.as(:user_obj)
# ROM 4.x
mapped = users.map_with(:user_obj)

mapped.to_a[2] # => #<UserModel:0x00007f8ec19fb3c8 @email="[email protected]", @id=3, @name="Jack Mapper">
mapped.where(id: 3).first # => #<UserModel:0x00007f8ec19fb3c8 @email="[email protected]", @id=3, @name="Jack Mapper">
mapped.to_a[2].surrealize # => "{\"id\":3,\"email\":\"[email protected]\"}"
Surrealist.surrealize_collection(mapped.to_a) # => "[{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"}]"
Surrealist.surrealize_collection(mapped.where { id < 4 }.to_a) # => "[{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"},{\"email\":\"[email protected]\"}]"

Sequel

Basically, Sequel returns instances only on .first, .last, .[] and .with_pk!. Collections are returned for all other methods. Most of them are covered in spec/orms/sequel specs, please refer to them for code examples. Associations serialization works the same way as it does with ActiveRecord.

Usage with Dry::Types

You can use Dry::Types for type checking. Note that Surrealist does not ship with dry-types by default, so you should do the installation and configuration by yourself. All built-in features of dry-types work, so if you use, say, Types::Coercible::String, your data will be coerced if it is able to, otherwise you will get a TypeError. Assuming that you have defined module called Types:

require 'dry-types'

class Car
  include Surrealist

  json_schema do
    {
      age:            Types::Coercible::Integer,
      brand:          Types::Coercible::String,
      doors:          Types::Integer.optional,
      horsepower:     Types::Strict::Integer.constrained(gteq: 20),
      fuel_system:    Types::Any,
      previous_owner: Types::String,
    }
  end

  def age;
    '7'
  end

  def previous_owner;
    'John Doe'
  end

  def horsepower;
    140
  end

  def brand;
    'Toyota'
  end

  def doors; end

  def fuel_system;
    'Direct injection'
  end
end

Car.new.surrealize
# => '{ "age": 7, "brand": "Toyota", "doors": null, "horsepower": 140, "fuel_system": "Direct injection", "previous_owner": "John Doe" }'

Delegating surrealization

You can share the json_schema between classes:

class Host
  include Surrealist

  json_schema do
    { name: String }
  end

  def name
    'Host'
  end
end

class Guest
  delegate_surrealization_to Host

  def name
    'Guest'
  end
end

Host.new.surrealize
# => '{ "name": "Host" }'
Guest.new.surrealize
# => '{ "name": "Guest" }'

Schema delegation works without inheritance as well, so if you wish you can delegate surrealization not only to parent classes, but to any class. Please note that in this case you have to include Surrealist in class that delegates schema as well.

class Potato
  include Surrealist
  delegate_surrealization_to Host

  def name
    'Potato'
  end
end

Potato.new.surrealize
# => '{ "name": "Potato" }'

Optional arguments

Camelization

If you need to have keys in camelBack, you can pass optional camelize argument to #surrealize or #build_schema. From the previous example:

Car.new.surrealize(camelize: true)
# => '{ "age": 7, "brand": "Toyota", "doors": null, "horsepower": 140, "fuelSystem": "Direct injection", "previousOwner": "John Doe" }'

Include root

If you want to wrap the resulting JSON into a root key, you can pass optional include_root argument to #surrealize or #build_schema. The root key in this case will be taken from the class name of the surrealizable object.

class Cat
  include Surrealist

  json_schema do
    { weight: String }
  end

  def weight
    '3 kilos'
  end
end

Cat.new.surrealize(include_root: true)
# => '{ "cat": { "weight": "3 kilos" } }'

With nested classes the last namespace will be taken as root key:

class Animal
  class Dog
    include Surrealist

    json_schema do
      { breed: String }
    end

    def breed
      'Collie'
    end
  end
end

Animal::Dog.new.surrealize(include_root: true)
# => '{ "dog": { "breed": "Collie" } }'

Root

If you want to wrap the resulting JSON into a specified root key, you can pass optional root argument to #surrealize or #build_schema. The root argument will be stripped of whitespaces.

class Cat
  include Surrealist

  json_schema do
    { weight: String }
  end

  def weight
    '3 kilos'
  end
end

Cat.new.surrealize(root: :kitten)
# => '{ "kitten": { "weight": "3 kilos" } }'
Cat.new.surrealize(root: ' kitten ')
# => '{ "kitten": { "weight": "3 kilos" } }'

This overrides the include_root and include_namespaces arguments.

Animal::Cat.new.surrealize(include_root: true, root: :kitten)
# => '{ "kitten": { "weight": "3 kilos" } }'
Animal::Cat.new.surrealize(include_namespaces: true, root: 'kitten')
# => '{ "kitten": { "weight": "3 kilos" } }'

Include namespaces

You can build wrap schema into a nested hash from namespaces of the object's class.

class BusinessSystem::Cashout::ReportSystem::Withdraws
  include Surrealist

  json_schema do
    { withdraws_amount: Integer }
  end

  def withdraws_amount
    34
  end
end

withdraws = BusinessSystem::Cashout::ReportSystem::Withdraws.new

withdraws.surrealize(include_namespaces: true)
# => '{ "business_system": { "cashout": { "report_system": { "withdraws": { "withdraws_amount": 34 } } } } }'

By default all namespaces will be taken. If you want you can explicitly specify the level of nesting:

withdraws.surrealize(include_namespaces: true, namespaces_nesting_level: 2)
# => '{ "report_system": { "withdraws": { "withdraws_amount": 34 } } }'

Configuration

There are two ways of setting default arguments for serialization, by passing a block to Surrealist.configure:

Surrealist.configure do |config|
  config.camelize = true
  config.namespaces_nesting_level = 2
end

And by passing a hash:

Surrealist.configure(camelize: true, include_root: true)

These arguments will be applied to all calls of #build_schema and #surrealize. If these methods will be called with arguments, they will be merged with respect to explicitly passed ones:

Surrealist.configure(camelize: true, include_root: true)

Something.new.surrealize(camelize: false)
# will result in Something.new.surrealize(camelize: false, include_root: true)

Hash serialization

You can pass a hash to serializer and it will use the keys instead of methods.

class HashSerializer < Surrealist::Serializer
  json_schema { { string: String, int: Integer } }
end

HashSerializer.new(string: 'string', int: 4).surrealize
# => '{ "string": "string", "int": 4}'

HashSerializer.new(string: 'string', int: 'not int').surrealize
# => Surrealist::InvalidTypeError: Wrong type for key `int`. Expected Integer, got String.

Bool and Any

If you have a parameter that is of boolean type, or if you don't care about the type, you can use Bool and Any respectively.

class User
  include Surrealist

  json_schema do
    {
      age: Any,
      admin: Bool,
    }
  end
end

Type Errors

Surrealist::InvalidTypeError is thrown if types (and dry-types) mismatch.

class CreditCard
  include Surrealist

  json_schema do
    { number: Integer }
  end

  def number
    'string'
  end
end

CreditCard.new.surrealize
# => Surrealist::InvalidTypeError: Wrong type for key `number`. Expected Integer, got String.

Undefined methods in schema

Surrealist::UndefinedMethodError is thrown if a key defined in the schema does not have a corresponding method defined in the object.

class Car
  include Surrealist

  json_schema do
    { weight: Integer }
  end
end

Car.new.surrealize
# => Surrealist::UndefinedMethodError: undefined method `weight' for #<Car:0x007f9bc1dc7fa8>. You have probably defined a key in the schema that doesn't have a corresponding method.

Other notes

  • nil values are allowed by default, so if you have, say, age: String, but the actual value is nil, type check will be passed. If you want to be strict about nils consider using Dry::Types.
  • Surrealist officially supports MRI Ruby 2.3+ but should be working on other platforms as well.

Roadmap

Here is a list of features that are not implemented yet (contributions are welcome):

  • Automatic endpoint documentation
  • API for validating (contracts) without actually serializing to JSON (maybe with deserialization from JSON)

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/nesaulov/surrealist. 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.

Credits

The icon was created by Simon Child from Noun Project and is published under Creative Commons License

Authors

Created by Nikita Esaulov with help from Alessandro Minali and Alexey Bespalov.

Supported by Umbrellio

License

The gem is available as open source under the terms of the MIT License.

surrealist's People

Contributors

akxcv avatar alessandrominali avatar azhi avatar billgloff avatar chrisatanasian avatar codetriage-readme-bot avatar depfu[bot] avatar gjhenrique avatar glaucocustodio avatar kolasss avatar krzysiek1507 avatar nesaulov avatar nulldef avatar ojab avatar past-one avatar salbertson avatar stefkin avatar wildkain avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

surrealist's Issues

Add integration for Dry::Types

Add possibility to specify types via dry-types, e.g.

class User
  include Dry::Types.module
  include Surrealist

  json_schema do
    {
      email: Types::Coercible::String,
      age: Types::Strict::Int
    }
  end
end

DSL for serializer contexts

As @AlessandroMinali suggested in #61:

Maybe this could be another DSL (at the cost of adding more complexity for a low use case, maybe not worth)?

Right now we have to access context variables via context[:variable_name]. Suggested is the following DSL:

class IncomeSerializer < Surrealist::Serializer
  json_schema { { amount: Integer } }
  serializer_context :current_user, :something, :else
  
  def amount
    current_user.guest? ? 100000000 : object.amount
  end
end

I suggest to implement this feature (if we decide to implement it in the first place) after 1.0.

Question: how to create list of entities

Hey,

I use surrealist for json schema in my presenters and I found interesting problem. I use hanami entioty for building data objects and I have no idea how to use Surrealist.surrealize_collection with it (and can I use it?).

Also, I want to use list of objects and I have no idea how to do it without Dry-Types. I thought that I can do something like this:

users: ArrayOf(Entities::User.defined_schema)

where Entities::User is surrealist schema for entity object. WDYT can we update documentation for this cases?

conditional tags for json_schema

Currently, if I want to serialize only some fields from a serializer(for example for authorization purposes) I do it like this:

module SerializerMixin
  def first_field
    []
  end
end

class CompositeSerializer < Surrealist::Serializer
  include SerializerMixin

  json_schema do
    { first_field: Array, second_field: Hash, third_field: String }
  end

  def second_field
    {}
  end

  def third_field
    "awesome issue"
  end
end

class GuestSerializer < Surrealist::Serializer
  include SerializerMixin

  json_schema do
    { first_field: Array }
  end
end

GuestSerializer.new(object).build_schema

I think it would be great, if we can use one serializer with different json schemas

class CompositeSerializer < Surrealist::Serializer
  json_schema do
    { first_field: Array, second_field: Hash, third_field: String }
  end
 
  json_schema(:guest_user) do
    { first_field: Array }
  end

  def first_field
    []
  end

  def second_field
    {}
  end

  def third_field
    "awesome issue"
  end
end

CompositeSerializer.new(object).build_schema(:guest_user)

ActiveRecord_Relation serialization

Right now objects that are created from methods that return ActiveRecord_Relation (like #where) can not be surrealized:

User.where(id: 3).surrealize
# => NoMethodError: undefined method `surrealize' for #<User::ActiveRecord_Relation:0x007f90ae569758>

We need to have some kind of schema delegation to the initial object here.

Conflict when root and and object property share the same name

There seems to be a problem with serializing an object with a root that is the same as one of it's properties.

Foo = Struct.new(:foo, :bar)
Foo.include Surrealist
Foo.json_schema { { bar: String } }

foo = Foo.new('foo', 'bar')

foo.surrealize
# => "{\"bar\":\"bar\"}"

foo.surrealize(include_root: true)
# Surrealist::UndefinedMethodError: undefined method `bar' for "foo":String. You have probably defined a key in the schema that doesn't have a corresponding method.

foo.surrealize(root: 'foo')
# Surrealist::UndefinedMethodError: undefined method `bar' for "foo":String. You have probably defined a key in the schema that doesn't have a corresponding method.

foo.surrealize(root: 'fooz')
# => "{\"fooz\":{\"bar\":\"bar\"}}"

How I can set a root key?

Hello,
I have models:

class Country
  include Surrealist
  surrealize_with CountrySerializer

  belongs_to :region
end

class Region
  include Surrealist
  surrealize_with RegionSerializer

  has_many :countries
end

And searializers:

class RegionSerializer < Surrealist::Serializer
    json_schema do
      {
        name: String,
        slug: String,
        countries: Array
      }
    end

    def countries
      object.countries.to_a
    end
end

class CountrySerializer < Surrealist::Serializer
  include Location

  json_schema do
    {
      name: String,
      slug: String
    }
  end
end

And in controller I do this:

Regions::RegionSerializer.new(Region.includes(:countries)).surrealize(root: :regions)

And I want that result will be:

{
  regions: [
    {
      name: '...',
      slug: '...',
      countries: [
         {
           name: '...',
           slug: '...',
         },
         ...
      ]
    },
    ...
  ]
}

But I had root key for each result of regions...
How I can set the real root key which can wrap all results? Please don't tell me that I need to create a new serializer :)

Having class's name as a root key in JSON

I think it will be useful to have an optional argument that places class's name as a first key in JSON. For example:

class Cat
  include Surrealist

  json_schema do
    { weight: String }
  end

  def weight
    '3 kilos'
  end
end

Cat.new.surrealize(include_root: true)
# => '{ "cat": { "weight": "3 kilos" } }'

A few things that have to be taken care of:

  1. If camelize: true is passed to #surrealize, root should also be converted to camelBack.
  2. Root key should not be placed inside nested objects.

Aliases

What's up, dude? Would you like to create alias feature for schema? Something like that

class Person
  include Surrealist
 
  json_schema do
    {
      name: String,
      {avatar: :image}: String
    }
  end
 
  def name
    'John Doe'
  end
 
  def image
   'http://some-image.host/avatars/123.jpeg'
  end
end
Person.new.surrealize 
# => { name: "John Doe", avatar: "http://some-image.host/avatars/123.jpeg" }

Benchmarks

We need to add comprehensive benchmarks (after #37 is done) to measure speed in comparison to AMS (and something else)

Delegate surrealization to parent class

Considering this example:

class Parent
  include Surrealist

  json_schema do
    { name: String }
  end

  def name
    'Parent'
  end
end

class Child < Parent
  def name
    'Child'
  end
end

If we surrealize Parent, everything is fine:
Parent.new.surrealize # => '{ "name": "Parent" }'
But if we surrealize Child, error is thrown:

Child.new.surrealize
# => Surrealist::UnknownSchemaError: Can't serialize Child - no schema was provided.

I guess it would be useful to have a class method that would delegate surrealization to specified class, something like:

class Child < Parent
  
  delegate_surrealization_to Parent
  
  def name
    'Child'
  end
end
Child.new.surrealize # => '{ "name": "Child" }'

Serialize from hash

Suppose I have:

class Foo
  include Surrealist
  json_schema do
    { foo: String }
  end
end

I can't serialize passing a hash (because the gem expect methods):

Foo.new({ foo: "bar" }).surrealize # crash!

Do you plan add support for hash? I've created the thin wrapper below and it works:

class HashBased
  include Surrealist

  attr_reader :hash

  def initialize(hash = {})
    @hash = hash.deep_symbolize_keys
  end

  def method_missing(method_name, *args)
    if hash.key?(method_name)
      hash[method_name]
    else
      super
    end
  end

  def respond_to_missing?(method_name, _include_private = false)
    hash.key?(method_name)
  end
end
class Foo < HashBased
  json_schema do
    # same as before
  end
end
Foo.new({ foo: "bar" }).surrealize # now it works!

With a simple piece of code like that we can open a bunch of new possibilities.

Introduce an abstract serializer class

I think that in most of the cases the logic of serialization should be separated from the model itself. So I suggest having a class that will represent abstract serializer which will be inherited by particular ones. API that I propose:

class HumanSerializer < Surrealist::Serializer
  json_schema do
    { name: String, name_length: Integer }
  end

  def name_length
    name.length
  end
end

class Human
  include Surrealist

  attr_reader :name

  surrealize_with HumanSerializer

  def initialize(name)
    @name = name
  end
end

And two ways of serializing:

  • Implicit (through DSL):
Human.new('John').surrealize(camelize: true)
# => '{"name": "John", "nameLength": 4}'
  • Explicit:
HumanSerializer.new(Human.new('Alfred')).surrealize(camelize: true)
# => '{"name": "Alfred", "nameLength": 6}'

Nil values for Boolean type

Hi, @nesaulov.

It would be nice to allow nil values for attributes of boolean type.

class User
  include Surrealist

  json_schema do
    {
      admin: Bool,
    }
  end
end
Surrealist::InvalidTypeError:
       Wrong type for key `admin`. Expected Bool, got NilClass.

Optional transform of keys to camelCase

class User
  include Surrealist

  attr_reader :full_name, :credit_card

  json_schema do
    {
      full_name: String,
      credit_card: Integer,
    }
  end

  def initialize(full_name:, credit_card:)
    @full_name = full_name
    @credit_card = credit_card
  end
end

Then:

user = User.new(full_name: 'John Doe', credit_card: 1234_5678_9012_3456)

user.surrealize # => "{\"full_name\":\"John Doe\",\"credit_card\":1234567890123456}"
user.surrealize(camel_case: true) # => "{\"fullName\":\"John Doe\",\"creditCard\":1234567890123456}"
# or
user.surrealize(camelize: true) # => "{\"fullName\":\"John Doe\",\"creditCard\":1234567890123456}"

Collection serialization

I guess we need to have a way to serialize collections. Let's take AR for example:

class User < ActiveRecord::Base
  include Surrealist

  json_schema do
    { id: Integer, name: String }
  end
end

It would be convenient to have something like

Surrealist.surrealize_collection(User.all, include_root: true)
# => '{ "users": [ { "id": 1, "name": "John" }, { "id": 2, "name": "Chris" } ... ] }'

We also need to take care of other ORMs and mappers, e.g. sequel, rom.

Methods from included module which conflict with methods from the model don't work.

I defined several serializers for one object.
Then I moved shared methods to a module and they aren't called.
Methods from the model are called instead.

class Person < ApplicationRecord
  attr_accessor :name, :lastname
end


class PersonSerializer < Surrealist::Serializer
  include PersonMethods

  alias person object

  json_schema { { name: String } }

  # def name
  #   "#{person.name} #{person.lastname}"
  # end
end


module PersonMethods
  def name
    "#{person.name} #{person.lastname}
  end
end
PersonSerializer.new(Person.new(name: "John", lastname: "Smith")).build_schema
=> {:name=>"John"}

Sequel Integration

We need specs to make sure that Surrealist works fine with Sequel objects.

Inherited Surrealist::Serializer does not accept array as object

I'm using Hanami as backend and I have this serializer here inheriting `Surrealist::Serializer)

When I tried to use a collection, it returns a weird result with the object stringifed.

module Serializers
  class Impression < Surrealist::Serializer
    json_schema do
      {
        id: Integer,
        entity: String,
        entity_id: Integer,
        user_id: Integer
      }
    end
  end
end

# returns an Array of Impressions
impressions = ImpressionRepository.new.all

Serializers::Impression.new(impressions).surrealize

#ย => "[\"#<Impression:0x00007f91aaa8cf80>\",\"#<Impression:0x00007f91aa27f400>\",\"#<Impression:0x00007f91aa27e3c0>\",\"#<Impression:0x00007f91aa27d380>\",\"#<Impression:0x00007f91aa27c340>\",\"#<Impression:0x00007f91aa2414c0>\",\"#<Impression:0x00007f91aa287768>\",\"#<Impression:0x00007f91aa286728>\",\"#<Impression:0x00007f91aa2856e8>\",\"#<Impression:0x00007f91aa2846a8>\",\"#<Impression:0x00007f91aa23a120>\",\"#<Impression:0x00007f91aa2381e0>\"]"

What works is mapping them into the serializer then using it as the object argument.
I know this portion of docs is meant for AR but maybe I misread about accepting a collection?

https://github.com/nesaulov/surrealist#activerecord

Unable to override method in 1.2

gem "surrealist", "1.2.0"
require "surrealist"

class KekSerializer < Surrealist::Serializer
  json_schema do
    {
      kek: String,
      cheburek: String,
    }
  end

  def kek
    "kek"
  end

  def cheburek
    "cheburek"
  end
end

class A
  def kek
    "smth"
  end
end

kek = A.new
p KekSerializer.new(kek).build_schema # => {:kek=>"smth", :cheburek=>"cheburek"}

The output was {:kek=>"kek", :cheburek=>"cheburek"} in version 1.1.2.

ActiveModel validations?

Would be great to have some way to return ActiveModel standard .errors responses when the type checking fails. Plug in to a .valid? call and return the errors collection with fields that were in breach of contract definition.

Would also love to stitch swagger spec output from this as well.

Any thoughts on these ideas? I love the concept of this lib.

Hound Necessary?

Hound is really annoying currently. Is it needed since rubocop is in Travis?

If so then we have two places now that we need to update any time with decide on new rules for linting.

ROM 4.x Integration

Right now we have only ROM 3.x covered, it would be great if we were sure that Surrealist plays well with ROM 4.x as well. ROM 4.x can be left in the main Gemfile, while ROM 3.x can be transferred to gemfiles/activerecord42.gemfile (it may be as well renamed to ruby_2_2.gemfile as it is used for bundle only for Ruby 2.2.0)

Add `include_namespaces` optional argument

include_namespaces will be responsible for breaking down a nested class to keys in hash, for example:

module Business
  class Cashout
    class Reports
      class Withdraws
        include surrealist

        json_schema do
          { amount: Types::Strict::String }
        end

        def amount
          '120$'
        end
      end
    end
  end
end

To include all namespaces:

withdraws = Business::Cashout::Reports::Withdraws.new

withdraws.surrealize(include_namespaces: true)
# =>  '{ "business":  { "cashout": { "reports": { "withdraws": { "amount": "120$" } } } } }'

To specify the level of nesting we need to have something like namespaces_nesting_level:

withdraws.surrealize(include_namespaces: true, namespaces_nesting_level: 3)
# =>  '{ "cashout": { "reports": { "withdraws": { "amount": "120$" } } } }'

Add Oj dependency

I am going to add Oj dependency. It's not that good to have a dependency, but on the other hand Oj is 2.5x faster than stdlib JSON.

Comparison:
                  oj:   265888.1 i/s
           multi_json:   176083.1 i/s - 1.51x  slower
                json:   100783.9 i/s - 2.64x  slower

`root_key` optional argument

Right now Surrealist has optional include_root key word argument that works like this:

module Animal
  class Cat
    include Surrealist

    json_schema do
      { weight: String }
    end

    def weight
      '3 kilos'
    end
  end
end

Animal::Cat.new.surrealize(include_root: true)
# => '{ "cat": { "weight": "3 kilos" } }'

It takes the bottom-level class name and sets it as a root key to resulting JSON. I think we need yet another kwarg: root:, so that JSON can be wrapped into any key that user provides. I imagine API like this:

Animal::Cat.new.surrealize(root: :kitten)
# => '{ "kitten": { "weight": "3 kilos" } }'
Animal::Cat.new.surrealize(root: 'kitten')
# => '{ "kitten": { "weight": "3 kilos" } }'

It should work without include_root and include_namespaces.
And when both include_root || include_namespaces and root are passed, the hash that is returned from include_root or include_namespaces should be wrapped into root:

Animal::Cat.new.surrealize(include_root: true, root: :kitten)
# => '{ "kitten": { "cat": { "weight": "3 kilos" } } }'
Animal::Cat.new.surrealize(include_namespaces: true, root: 'kitten')
# => '{ "kitten": { "animal": { "cat": { "weight": "3 kilos" } } } }'

Some methods are not being correctly delegated

To reproduce:

require 'surrealist'

class Foo
  def test
    'test'
  end
end

class FooSerializer < Surrealist::Serializer
  json_schema { { test: String } }
end

FooSerializer.new(Foo.new).build_schema

# => ArgumentError (wrong number of arguments (given 0, expected 2..3))

The method Surrealist is calling is Kernel#test, while it should have called Foo.new.test.

The bug was introduced by commit 9f89b2d.

Pluggable type systems

It would be nice if Surrealist supported plug-ins for type systems like thy. Perpahs to achieve this more cleanly (and to make type systems uniform within Surrealist), it's a good idea to also make dry-types pluggable instead of half-built-in.
wdyt?

Default options

I think of having a possibility to define default options for serialization. Like Oj has. For example, for most of applications camel case conversions are either true or false for all endpoints. I suggest API to be as simple as follows:

Surrealist.default_options = { camelize: true, include_root: true }

@AlessandroMinali @nulldef what do you think?

Sequel models do not work with custom serializers

Minimal reproduction:

require 'sequel'
require 'surrealist'

FileUtils.rm('test.sqlite') if File.exists?('test.sqlite')
DB = Sequel.sqlite('test.sqlite')
DB.create_table('users') do
  primary_key :id
  String :name
end

class UserSerializer < Surrealist::Serializer
  json_schema {
    {id: Integer, name: String}
  }
end

class User < Sequel::Model
  include Surrealist

  surrealize_with UserSerializer
end

User.create(name: 'Name')

User.first.surrealize
UserSerializer.new(User.first).surrealize

Last two lines both failing with:

Traceback (most recent call last):
	3: from test.rb:25:in `<main>'
	2: from .../gems/surrealist-35c87f58b098/lib/surrealist/instance_methods.rb:55:in `surrealize'
	1: from .../gems/surrealist-35c87f58b098/lib/surrealist/serializer.rb:45:in `surrealize'
.../gems/surrealist-35c87f58b098/lib/surrealist.rb:59:in `surrealize_collection': undefined method `map' for #<User @values={:id=>1, :name=>"Name"}> (NoMethodError)
Did you mean?  tap

The cause is here. Same check is performed in some other places in source code.

Can't redefine object method with inheritance

If parent serializer redefines object's method it's not invoked in child serializer and calls object's method (same behaviour with modules)

require 'surrealist'

class User
  def first
    2
  end
end

class User::BaseSerializer < Surrealist::Serializer
  def first
    object.first * 2
  end
end

class UserSerializer < User::BaseSerializer
  json_schema { { first: Integer } }
end

user = User.new

p UserSerializer.new(user).build_schema
# prints {:first=>2} 
# should be {:first=>4} 

I believe this is because of false argument for instance_methods here
Bug or feature?

Easy access to defined schema and schema sample

Suppose we have the following code:

class MySerializer
  include Surrealist
  json_schema do
    { name: String, age: Int }
  end
end
get '/foo' do
  # returns json using MySerializer
  present AnyClass.get_data(params), with: MySerializer 
end

In my request test I want to test the api without overtesting AnyClass, so I mock the return of get_data:

it do
  expect(AnyClass).to receive(:get_data).and_return(data)
  get '/foo'
end

the data variable has to match the schema defined in MySerializer, so, I created a piece of code that generates a hash (because I am using my hack to accept hash) based on my defined schema (set with json_schema) to be DRY (so I dont have to mock manually).

I had to do instance_variable_get('@__surrealist_schema') to get the defined schema..

What if we could do just MySerializer.defined_schema to get the defined schema?

Going further..

What if we could do just MySerializer.schema_sample to get a sample of the schema, something like:

{ name: "", age: 0 }

Then I could do this in my test instead of having to define by myself a schema matching hash for the mock:

let(:data) { MySerializer.schema_sample } 

Full demo: https://gist.github.com/glaucocustodio/85217a4e153407f053ae0cb60144a189

Refactor Surrealist::Builder

Surrealist::Builder requires refactoring, for instance method #take_values_from_instance takes in 6 arguments, that seems to be too much. Specs are already present, so it should not be too hard.

1.0.0 release

I think it's time to release 1.0.0 version of this gem. I will write some docs on how to work with ORMs and the nuances that should be kept in mind when doing so. Unfortunately, I have no time right now to write benchmarks, I guess I'll do that a bit later. I will also write a blog post about what's new in 1.0.0. @AlessandroMinali is there anything that should be done before release from your point of view?

Multiple serializers for one model

Hi all!
As I already suggested in #59, we can use tag option for splitting serializers. I propose this API:

class HumanSerializer < Surrealist::Serializer
  json_schema do
    { name: String, name_length: Integer }
  end

  def name_length
    name.length
  end
end

class SmallHumanSerializer < Surrealist::Serializer
  json_schema do
    { name: String }
  end
end

class Human
  include Surrealist

  attr_reader :name

  surrealize_with HumanSerializer # default
  surrealize_with SmallHumanSerializer, tag: :small

  def initialize(name)
    @name = name
  end
end
# default behaviour
Human.new('John').surrealize(camelize: true)
# => '{"name": "John", "nameLength": 4}'

# using tags
Human.new('John').surrealize(camelize: true, tag: :small)
# => '{"name": "John"}'

I can realize this functionality and I know that it will be very useful feature.

Change API for multiple serializers

What we have right now:

class Something
  include Surrealist
  surrealize_with SomethingSerializer
  surrealize_with SomethingShortSerializer, tag: :short
end

Something.new.surrealize
Something.new.surrealize(tag: :short)

This tag option still bothers me, I think passing tag as an option is not semantically correct here. Specifying a serializer with tag is alright, but serializing with tag seems wrong. I suggest having
Something.new.surrealize(via: :short) or Something.new.surrealize(using: :short) or Something.new.surrealize(serializer: :short) or Something.new.surrealize(format: :short) or something else.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.