jsonapi_serializer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/Rakefile +6 -0
- data/bin/benchmark +23 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/jsonapi_serializer.gemspec +30 -0
- data/lib/jsonapi_serializer.rb +56 -0
- data/lib/jsonapi_serializer/aux/converters.rb +22 -0
- data/lib/jsonapi_serializer/base.rb +105 -0
- data/lib/jsonapi_serializer/common.rb +43 -0
- data/lib/jsonapi_serializer/dsl/common.rb +77 -0
- data/lib/jsonapi_serializer/dsl/polymorphic.rb +44 -0
- data/lib/jsonapi_serializer/polymorphic.rb +62 -0
- data/lib/jsonapi_serializer/utils.rb +36 -0
- data/lib/jsonapi_serializer/version.rb +3 -0
- data/perf/fast_jsonapi_test.rb +41 -0
- data/perf/jsonapi_serializer_test.rb +42 -0
- data/perf/models.rb +86 -0
- data/perf/runner.rb +76 -0
- metadata +181 -0
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jsonapi_serializer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jsonapi_serializer"
|
8
|
+
spec.version = JsonapiSerializer::VERSION
|
9
|
+
spec.authors = ["Ivan Yurov"]
|
10
|
+
spec.email = ["ivan.youroff@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Alternative JSONApi serializer}
|
13
|
+
spec.description = %q{Alternative JSONApi serializer inspired by Netflix's fast_jsonapi serializer}
|
14
|
+
spec.homepage = "https://github.com/youroff/jsonapi_serializer"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "activesupport", ">= 4.2"
|
23
|
+
spec.add_development_dependency "oj", "~> 3.3"
|
24
|
+
spec.add_development_dependency "multi_json", "~> 1.0"
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
spec.add_development_dependency "ffaker", "~> 2.8.1"
|
29
|
+
spec.add_development_dependency "fast_jsonapi", "~> 1.0"
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "active_support/inflector"
|
2
|
+
require "jsonapi_serializer/version"
|
3
|
+
require "jsonapi_serializer/aux/converters"
|
4
|
+
require "jsonapi_serializer/base"
|
5
|
+
require "jsonapi_serializer/polymorphic"
|
6
|
+
|
7
|
+
module JsonapiSerializer
|
8
|
+
extend JsonapiSerializer::AUX::Converters
|
9
|
+
|
10
|
+
TRANSFORMS = {
|
11
|
+
dasherize: lambda { |str| str.to_s.underscore.dasherize.to_sym },
|
12
|
+
underscore: lambda { |str| str.to_s.underscore.to_sym },
|
13
|
+
camelize: lambda { |str| str.to_s.underscore.camelize(:lower).to_sym }
|
14
|
+
}
|
15
|
+
|
16
|
+
@@key_transform = TRANSFORMS[:underscore]
|
17
|
+
@@type_transform = TRANSFORMS[:underscore]
|
18
|
+
@@type_namespace_separator = "_"
|
19
|
+
|
20
|
+
def self.key_transform(key)
|
21
|
+
@@key_transform.call(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.type_transform(klass)
|
25
|
+
segments = klass.split("::").map { |segment| @@type_transform.call(segment) }
|
26
|
+
if @@type_namespace_separator == :ignore
|
27
|
+
segments.last
|
28
|
+
else
|
29
|
+
segments.join(@@type_namespace_separator)
|
30
|
+
end.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.set_key_transform(name = nil, &block)
|
34
|
+
if name.nil? && block_given?
|
35
|
+
@@key_transform = block
|
36
|
+
elsif name.is_a?(Symbol) && !block_given?
|
37
|
+
@@key_transform = TRANSFORMS[name]
|
38
|
+
else
|
39
|
+
raise ArgumentError, "set_key_transform accepts either a standard transform symbol (:dasherize, :underscore or :camelize) or a block"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.set_type_namespace_separator(separator)
|
44
|
+
@@type_namespace_separator = separator
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.set_type_transform(name = nil, &block)
|
48
|
+
if name.nil? && block_given?
|
49
|
+
@@type_transform = block
|
50
|
+
elsif name.is_a?(Symbol) && !block_given?
|
51
|
+
@@type_transform = TRANSFORMS[name]
|
52
|
+
else
|
53
|
+
raise ArgumentError, "set_type_transform accepts either a standard transform symbol (:dasherize, :underscore or :camelize) or a block"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module JsonapiSerializer::AUX
|
2
|
+
module Converters
|
3
|
+
|
4
|
+
# http://jsonapi.org/format/#fetching-includes
|
5
|
+
# This method converts include string into hash accepted by serializer
|
6
|
+
def convert_include(include_string)
|
7
|
+
include_string.split(",").each_with_object({}) do |path, includes|
|
8
|
+
path.split(".").reduce(includes) do |ref, segment|
|
9
|
+
ref[segment.to_sym] ||= {}
|
10
|
+
ref[segment.to_sym]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# http://jsonapi.org/format/#fetching-sparse-fieldsets
|
16
|
+
def convert_fields(fields)
|
17
|
+
Hash[fields.map do |type, fields|
|
18
|
+
[type.to_sym, fields.split(",").map(&:to_sym)]
|
19
|
+
end]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'jsonapi_serializer/dsl/common'
|
4
|
+
require 'jsonapi_serializer/utils'
|
5
|
+
require 'jsonapi_serializer/common'
|
6
|
+
require 'set'
|
7
|
+
|
8
|
+
module JsonapiSerializer::Base
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include JsonapiSerializer::DSL::Common
|
11
|
+
include JsonapiSerializer::Utils
|
12
|
+
include JsonapiSerializer::Common
|
13
|
+
|
14
|
+
def initialize(opts = {})
|
15
|
+
super(opts)
|
16
|
+
@id = self.class.meta_id
|
17
|
+
unless opts[:id_only]
|
18
|
+
@attributes = []
|
19
|
+
@relationships = []
|
20
|
+
@includes = normalize_includes(opts.fetch(:include, []))
|
21
|
+
prepare_fields(opts)
|
22
|
+
prepare_attributes
|
23
|
+
prepare_relationships
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def id_hash(record)
|
28
|
+
{id: @id.call(record), type: @type}
|
29
|
+
end
|
30
|
+
|
31
|
+
def attributes_hash(record)
|
32
|
+
@attributes.each_with_object({}) do |(key, val), hash|
|
33
|
+
hash[key] = val.call(record)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def relationships_hash(record, context = {})
|
38
|
+
@relationships.each_with_object({}) do |(key, type, from, serializer), hash|
|
39
|
+
if rel = record.public_send(from)
|
40
|
+
if rel.respond_to?(:each)
|
41
|
+
hash[key] = {data: []}
|
42
|
+
rel.each do |item|
|
43
|
+
id = serializer.id_hash(item)
|
44
|
+
hash[key][:data] << id
|
45
|
+
add_included(serializer, item, id, context) if @includes.has_key?(key)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
id = serializer.id_hash(rel)
|
49
|
+
hash[key] = {data: id}
|
50
|
+
add_included(serializer, rel, id, context) if @includes.has_key?(key)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
hash[key] = {data: nil}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def record_hash(record, context = {})
|
59
|
+
hash = id_hash(record)
|
60
|
+
if context[:tracker]
|
61
|
+
(context[:tracker][hash[:type]] ||= Set.new).add?(hash[:id])
|
62
|
+
end
|
63
|
+
hash[:attributes] = attributes_hash(record) if @attributes.present?
|
64
|
+
hash[:relationships] = relationships_hash(record, context) if @relationships.present?
|
65
|
+
hash
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def prepare_fields(opts)
|
70
|
+
@fields = opts.fetch(:fields, {})
|
71
|
+
if opts[:poly_fields].present? || @fields[@type].present?
|
72
|
+
@fields[@type] = opts.fetch(:poly_fields, []) + [*@fields.fetch(@type, [])].map { |f| JsonapiSerializer.key_transform(f) }
|
73
|
+
@fields[@type].uniq!
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def prepare_attributes
|
78
|
+
key_intersect(@fields[@type], self.class.meta_attributes.keys).each do |key|
|
79
|
+
@attributes << [key, self.class.meta_attributes[key]]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepare_relationships
|
84
|
+
key_intersect(@fields[@type], self.class.meta_relationships.keys).each do |key|
|
85
|
+
rel = self.class.meta_relationships[key]
|
86
|
+
serializer = rel[:serializer].new(
|
87
|
+
fields: @fields,
|
88
|
+
include: @includes.fetch(key, []),
|
89
|
+
id_only: !@includes.has_key?(key)
|
90
|
+
)
|
91
|
+
@relationships << [key, rel[:type], rel[:from], serializer]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_included(serializer, item, id, context)
|
96
|
+
if (context[:tracker][id[:type]] ||= Set.new).add?(id[:id]).present?
|
97
|
+
attributes = serializer.attributes_hash(item)
|
98
|
+
relationships = serializer.relationships_hash(item, context)
|
99
|
+
inc = id.clone
|
100
|
+
inc[:attributes] = attributes if attributes.present?
|
101
|
+
inc[:relationships] = relationships if relationships.present?
|
102
|
+
context[:included] << inc
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module JsonapiSerializer::Common
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@type = self.class.meta_type || guess_type
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
@type
|
14
|
+
end
|
15
|
+
|
16
|
+
def serializable_hash(payload, opts = {})
|
17
|
+
hash = {}
|
18
|
+
context = {tracker: {}, included: []}
|
19
|
+
|
20
|
+
if payload.respond_to?(:map)
|
21
|
+
hash[:data] = payload.map { |p| record_hash(p, context) }
|
22
|
+
else
|
23
|
+
hash[:data] = record_hash(payload, context)
|
24
|
+
end
|
25
|
+
|
26
|
+
hash[:meta] = opts[:meta] if opts.has_key? :meta
|
27
|
+
hash[:included] = context[:included] if context[:included].present?
|
28
|
+
hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialized_json(payload, opts = {})
|
32
|
+
MultiJson.dump(serializable_hash payload, opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def guess_type
|
37
|
+
if self.class.name.end_with?('Serializer')
|
38
|
+
JsonapiSerializer.type_transform(self.class.name.chomp('Serializer'))
|
39
|
+
else
|
40
|
+
raise "Serializer class must end with `Serializer` in order to be able to guess type"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
module JsonapiSerializer::DSL
|
5
|
+
module Common
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
@meta_id = lambda { |obj| obj.id.to_s }
|
10
|
+
@meta_attributes = {}
|
11
|
+
@meta_relationships = {}
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_reader :meta_type, :meta_id, :meta_attributes, :meta_relationships
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
def type(name)
|
20
|
+
@meta_type = name.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def id(attr = nil, &block)
|
24
|
+
case
|
25
|
+
when attr.nil? && block_given?
|
26
|
+
@meta_id = block
|
27
|
+
when attr.present? && !block_given?
|
28
|
+
@meta_id = lambda { |obj| obj.public_send(attr) }
|
29
|
+
else
|
30
|
+
raise ArgumentError, "ID hook requires either attribute name or block"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def attributes(*attrs)
|
35
|
+
attrs.each do |attr|
|
36
|
+
case attr
|
37
|
+
when Symbol, String
|
38
|
+
@meta_attributes[attr.to_sym] = lambda { |obj| obj.public_send(attr.to_sym) }
|
39
|
+
when Hash
|
40
|
+
attr.each do |key, val|
|
41
|
+
@meta_attributes[key] = lambda { |obj| obj.public_send(val) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def attribute(attr, &block)
|
48
|
+
@meta_attributes[attr.to_sym] = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_many(name, opts = {})
|
52
|
+
@meta_relationships[name] = {
|
53
|
+
type: :has_many,
|
54
|
+
from: opts.fetch(:from, name),
|
55
|
+
serializer: opts[:serializer] || guess_serializer(name.to_s.singularize)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def belongs_to(name, opts = {})
|
60
|
+
@meta_relationships[name] = {
|
61
|
+
type: :belongs_to,
|
62
|
+
from: opts.fetch(:from, name),
|
63
|
+
serializer: opts[:serializer] || guess_serializer(name.to_s)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def inherited(subclass)
|
68
|
+
raise "You attempted to inherit regular serializer class, if you want to create Polymorphic serializer, include Polymorphic mixin"
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def guess_serializer(name)
|
73
|
+
"#{name.classify}Serializer".constantize
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'jsonapi_serializer/dsl/common'
|
3
|
+
|
4
|
+
module JsonapiSerializer::DSL
|
5
|
+
module Polymorphic
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include JsonapiSerializer::DSL::Common
|
8
|
+
|
9
|
+
included do
|
10
|
+
@meta_poly = []
|
11
|
+
@meta_resolver = lambda { |record| JsonapiSerializer.type_transform(record.class.name) }
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_reader :meta_poly, :meta_resolver, :meta_inherited
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
def resolver(&block)
|
20
|
+
if block_given?
|
21
|
+
@meta_resolver = block
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Resolver hook requires a block that takes record and returns its type."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def polymorphic_for(*serializers)
|
28
|
+
@meta_poly += serializers
|
29
|
+
end
|
30
|
+
|
31
|
+
def inherited(subclass)
|
32
|
+
parent = self
|
33
|
+
subclass.class_eval do
|
34
|
+
include JsonapiSerializer::Base
|
35
|
+
@meta_attributes = parent.meta_attributes.clone
|
36
|
+
@meta_relationships = parent.meta_relationships.clone
|
37
|
+
@meta_id = parent.meta_id
|
38
|
+
@meta_inherited = true
|
39
|
+
end
|
40
|
+
@meta_poly << subclass
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'jsonapi_serializer/dsl/polymorphic'
|
5
|
+
require 'jsonapi_serializer/utils'
|
6
|
+
require 'jsonapi_serializer/common'
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module JsonapiSerializer::Polymorphic
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include JsonapiSerializer::DSL::Polymorphic
|
12
|
+
include JsonapiSerializer::Utils
|
13
|
+
include JsonapiSerializer::Common
|
14
|
+
|
15
|
+
def initialize(opts = {})
|
16
|
+
super(opts)
|
17
|
+
unless self.class.meta_inherited
|
18
|
+
unless self.class.meta_resolver.respond_to? :call
|
19
|
+
raise "Polymorphic serializer must implement a block resolving an object into type."
|
20
|
+
end
|
21
|
+
|
22
|
+
poly_fields = [*opts.dig(:fields, @type)].map { |f| JsonapiSerializer.key_transform(f) }
|
23
|
+
if self.class.meta_poly.present?
|
24
|
+
@poly = self.class.meta_poly.each_with_object({}) do |poly_class, hash|
|
25
|
+
serializer = poly_class.new(opts.merge poly_fields: poly_fields)
|
26
|
+
hash[serializer.type] = serializer
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise "You have to create at least one children serializer for polymorphic #{self.class.name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def id_hash(record)
|
35
|
+
serializer_for(record).id_hash(record)
|
36
|
+
end
|
37
|
+
|
38
|
+
def attributes_hash(record)
|
39
|
+
serializer_for(record).attributes_hash(record)
|
40
|
+
end
|
41
|
+
|
42
|
+
def relationships_hash(record, context = {})
|
43
|
+
serializer_for(record).relationships_hash(record, context)
|
44
|
+
end
|
45
|
+
|
46
|
+
def record_hash(record, context = {})
|
47
|
+
hash = id_hash(record)
|
48
|
+
|
49
|
+
attributes = attributes_hash(record)
|
50
|
+
hash[:attributes] = attributes if attributes.present?
|
51
|
+
|
52
|
+
relationships = relationships_hash(record, context)
|
53
|
+
hash[:relationships] = relationships if relationships.present?
|
54
|
+
|
55
|
+
hash
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def serializer_for(record)
|
60
|
+
@poly[self.class.meta_resolver.call(record)] || (raise "Could not resolve serializer for #{record} associated with #{self.class.name}")
|
61
|
+
end
|
62
|
+
end
|