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.
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