nesaulov / surrealist Goto Github PK
View Code? Open in Web Editor NEWto_json but I wrote it myself
Home Page: https://nesaulov.github.io/surrealist
License: MIT License
to_json but I wrote it myself
Home Page: https://nesaulov.github.io/surrealist
License: MIT License
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 :)
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?
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)
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?
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:
Human.new('John').surrealize(camelize: true)
# => '{"name": "John", "nameLength": 4}'
HumanSerializer.new(Human.new('Alfred')).surrealize(camelize: true)
# => '{"name": "Alfred", "nameLength": 6}'
method definitions for in-nested parameters cannot be loaded in
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" } } } }'
We need specs to make sure that Surrealist works fine with DataMapper objects.
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$" } } } }'
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\"}}"
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
It's not covered 😱
We need specs to make sure that Surrealist works fine with Sequel objects.
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" }'
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}"
It appears that I've missed to implement Surrealist::Serializer#build_schema
for collections, while this is a necessary thing to have.
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.
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)
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.
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.
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
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:
camelize: true
is passed to #surrealize
, root should also be converted to camelBack.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.
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.
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.
And so do mixins.
We need to add comprehensive benchmarks (after #37 is done) to measure speed in comparison to AMS (and something else)
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.
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.
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.
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" }
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.
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"}
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.
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.
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.
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?
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
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?
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?
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.