jsonapi_serializer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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