ivy-serializers 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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +155 -0
  8. data/Rakefile +7 -0
  9. data/ivy-serializers.gemspec +24 -0
  10. data/lib/ivy-serializers.rb +1 -0
  11. data/lib/ivy/serializers.rb +5 -0
  12. data/lib/ivy/serializers/action_controller/serialization_support.rb +35 -0
  13. data/lib/ivy/serializers/attribute.rb +24 -0
  14. data/lib/ivy/serializers/documents.rb +17 -0
  15. data/lib/ivy/serializers/documents/document.rb +55 -0
  16. data/lib/ivy/serializers/documents/individual_resource.rb +18 -0
  17. data/lib/ivy/serializers/documents/resource_collection.rb +18 -0
  18. data/lib/ivy/serializers/formats.rb +3 -0
  19. data/lib/ivy/serializers/formats/active_model_serializers.rb +40 -0
  20. data/lib/ivy/serializers/formats/json.rb +96 -0
  21. data/lib/ivy/serializers/formats/json_api.rb +66 -0
  22. data/lib/ivy/serializers/mapping.rb +40 -0
  23. data/lib/ivy/serializers/railtie.rb +13 -0
  24. data/lib/ivy/serializers/registry.rb +33 -0
  25. data/lib/ivy/serializers/relationships.rb +2 -0
  26. data/lib/ivy/serializers/relationships/belongs_to.rb +13 -0
  27. data/lib/ivy/serializers/relationships/has_many.rb +13 -0
  28. data/lib/ivy/serializers/relationships/relationship.rb +23 -0
  29. data/lib/ivy/serializers/serializer.rb +35 -0
  30. data/lib/ivy/serializers/version.rb +5 -0
  31. data/spec/integration/formats/active_model_serializers_spec.rb +143 -0
  32. data/spec/integration/formats/json_api_spec.rb +254 -0
  33. data/spec/integration/serializer_spec.rb +47 -0
  34. data/spec/spec_helper.rb +93 -0
  35. metadata +165 -0
@@ -0,0 +1,3 @@
1
+ require 'ivy/serializers/formats/json'
2
+ require 'ivy/serializers/formats/active_model_serializers'
3
+ require 'ivy/serializers/formats/json_api'
@@ -0,0 +1,40 @@
1
+ require 'ivy/serializers/formats/json'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ module Formats
6
+ class ActiveModelSerializers < JSON
7
+ def belongs_to(name, resource, options={})
8
+ if options[:polymorphic]
9
+ if resource
10
+ @hash_gen.store_object(name) { polymorphic_resource(resource) }
11
+ else
12
+ @hash_gen.store(name, nil)
13
+ end
14
+ else
15
+ id(:"#{name}_id", resource)
16
+ end
17
+ end
18
+
19
+ def has_many(name, resources, options={})
20
+ if options[:polymorphic]
21
+ @hash_gen.store_array(name) { polymorphic_resources(resources) }
22
+ else
23
+ ids(:"#{ActiveSupport::Inflector.singularize(name.to_s)}_ids", resources)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def polymorphic_resource(resource)
30
+ id(:id, resource)
31
+ type(:type, resource)
32
+ end
33
+
34
+ def polymorphic_resources(resources)
35
+ resources.each { |resource| @hash_gen.push_object { polymorphic_resource(resource) } }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,96 @@
1
+ require 'active_support/inflector'
2
+ require 'hash_generator'
3
+
4
+ module Ivy
5
+ module Serializers
6
+ module Formats
7
+ class JSON
8
+ def initialize(document)
9
+ @document = document
10
+ @hash_gen = HashGenerator.new
11
+ end
12
+
13
+ def as_json(*)
14
+ @document.generate(self)
15
+ @hash_gen.to_h
16
+ end
17
+
18
+ def attribute(key, value)
19
+ @hash_gen.store(key, value)
20
+ end
21
+
22
+ def belongs_to(name, resource, options={})
23
+ id(name, resource)
24
+ end
25
+
26
+ def document(document)
27
+ document.generate_primary_resource(self)
28
+ document.generate_linked(self)
29
+ end
30
+
31
+ def has_many(name, resources, options={})
32
+ ids(name, resources)
33
+ end
34
+
35
+ def id(key, resource)
36
+ attribute(key, extract_id(resource))
37
+ end
38
+
39
+ def ids(key, resources)
40
+ attribute(key, resources.map { |resource| extract_id(resource) })
41
+ end
42
+
43
+ def linked(document)
44
+ document.generate_linked_resources(self)
45
+ end
46
+
47
+ def linked_resources(resource_class, resources)
48
+ key = key_for_collection(resource_class).to_sym
49
+ @hash_gen.store_array(key) { resources(resources) }
50
+ end
51
+
52
+ def links(resource)
53
+ @document.generate_links(self, resource)
54
+ end
55
+
56
+ def primary_resource(primary_resource_name, primary_resource)
57
+ @hash_gen.store_object(primary_resource_name) { resource(primary_resource) }
58
+ end
59
+
60
+ def primary_resources(primary_resources_name, primary_resources)
61
+ @hash_gen.store_array(primary_resources_name) { resources(primary_resources) }
62
+ end
63
+
64
+ def resource(resource)
65
+ @document.generate_resource(self, resource)
66
+ end
67
+
68
+ def resources(resources)
69
+ resources.each { |resource| @hash_gen.push_object { resource(resource) } }
70
+ end
71
+
72
+ def type(key, resource)
73
+ attribute(key, extract_type(resource))
74
+ end
75
+
76
+ private
77
+
78
+ def extract_id(resource)
79
+ resource.id
80
+ end
81
+
82
+ def extract_type(resource)
83
+ ActiveSupport::Inflector.dasherize(ActiveSupport::Inflector.underscore(resource.class.name))
84
+ end
85
+
86
+ def key_for_collection(resource_class)
87
+ ActiveSupport::Inflector.pluralize(key_for_individual(resource_class))
88
+ end
89
+
90
+ def key_for_individual(resource_class)
91
+ ActiveSupport::Inflector.underscore(resource_class.name)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,66 @@
1
+ require 'ivy/serializers/formats/json'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ module Formats
6
+ class JSONAPI < JSON
7
+ def attribute(key, value)
8
+ value = coerce_id(value) if key == :id
9
+ super
10
+ end
11
+
12
+ def belongs_to(name, resource, options={})
13
+ @hash_gen.store_object(name) { linkage(resource) }
14
+ end
15
+
16
+ def has_many(name, resources, options={})
17
+ @hash_gen.store_object(name) { linkages(resources) }
18
+ end
19
+
20
+ def linked(document)
21
+ @hash_gen.store_object(:included) { super }
22
+ end
23
+
24
+ def links(document)
25
+ @hash_gen.store_object(:links) { super }
26
+ end
27
+
28
+ def primary_resource(primary_resource_name, primary_resource)
29
+ @hash_gen.store_object(:data) { resource(primary_resource) }
30
+ end
31
+
32
+ def resource(resource)
33
+ super
34
+ type(:type, resource)
35
+ end
36
+
37
+ private
38
+
39
+ def coerce_id(id)
40
+ id.to_s
41
+ end
42
+
43
+ def extract_id(resource)
44
+ coerce_id(super)
45
+ end
46
+
47
+ def linkage(resource)
48
+ @hash_gen.store_object(:linkage) { linkage_object(resource) }
49
+ end
50
+
51
+ def linkage_object(resource)
52
+ id(:id, resource)
53
+ type(:type, resource)
54
+ end
55
+
56
+ def linkages(resources)
57
+ @hash_gen.store_array(:linkage) { linkage_objects(resources) }
58
+ end
59
+
60
+ def linkage_objects(resources)
61
+ resources.each { |resource| @hash_gen.push_object { linkage_object(resource) } }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ require 'ivy/serializers/attribute'
2
+ require 'ivy/serializers/relationships'
3
+
4
+ module Ivy
5
+ module Serializers
6
+ class Mapping
7
+ def initialize(klass)
8
+ @attrs = {}
9
+ @links = {}
10
+ @klass = klass
11
+
12
+ attribute(:id)
13
+ end
14
+
15
+ def attribute(name, &block)
16
+ @attrs[name] = Attribute.new(name, &block)
17
+ end
18
+
19
+ def attributes(*names)
20
+ names.each { |name| attribute(name) }
21
+ end
22
+
23
+ def belongs_to(name, options={}, &block)
24
+ @links[name] = Relationships::BelongsTo.new(name, options, &block)
25
+ end
26
+
27
+ def has_many(name, options={}, &block)
28
+ @links[name] = Relationships::HasMany.new(name, options, &block)
29
+ end
30
+
31
+ def links(generator, resource)
32
+ @links.each_value { |link| link.generate(generator, resource) }
33
+ end
34
+
35
+ def resource(generator, resource)
36
+ @attrs.each_value { |attr| attr.generate(generator, resource) }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ require 'ivy/serializers/action_controller/serialization_support'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ class Railtie < ::Rails::Railtie
6
+ initializer 'ivy.serializers.controllers' do
7
+ ActiveSupport.on_load(:action_controller) do
8
+ include ::Ivy::Serializers::ActionController::SerializationSupport
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ require 'ivy/serializers/mapping'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ class Registry
6
+ def initialize
7
+ @mappings = Hash.new { |hash, klass| hash[klass] = new_mapping(klass) }
8
+ end
9
+
10
+ def links(generator, resource)
11
+ mapping_for(resource.class).links(generator, resource)
12
+ end
13
+
14
+ def map(klass, &block)
15
+ mapping_for(klass).instance_eval(&block)
16
+ end
17
+
18
+ def resource(generator, resource)
19
+ mapping_for(resource.class).resource(generator, resource)
20
+ end
21
+
22
+ private
23
+
24
+ def mapping_for(klass)
25
+ @mappings[klass]
26
+ end
27
+
28
+ def new_mapping(klass)
29
+ Mapping.new(klass)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,2 @@
1
+ require 'ivy/serializers/relationships/belongs_to'
2
+ require 'ivy/serializers/relationships/has_many'
@@ -0,0 +1,13 @@
1
+ require 'ivy/serializers/relationships/relationship'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ module Relationships
6
+ class BelongsTo < Relationship
7
+ def generate(generator, resource)
8
+ generator.belongs_to(@name, get(resource), @options)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'ivy/serializers/relationships/relationship'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ module Relationships
6
+ class HasMany < Relationship
7
+ def generate(generator, resource)
8
+ generator.has_many(@name, get(resource), @options)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Ivy
2
+ module Serializers
3
+ module Relationships
4
+ class Relationship
5
+ def initialize(name, options={}, &getter)
6
+ @name = name
7
+ @options = options
8
+ @getter = getter || method(:default_getter)
9
+ end
10
+
11
+ private
12
+
13
+ def default_getter(resource)
14
+ resource.public_send(@name)
15
+ end
16
+
17
+ def get(resource)
18
+ @getter.call(resource)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ require 'ivy/serializers/registry'
2
+
3
+ module Ivy
4
+ module Serializers
5
+ class Serializer
6
+ class << self
7
+ attr_accessor :_registry
8
+
9
+ def inherited(base)
10
+ base._registry = Registry.new
11
+ end
12
+
13
+ def links(generator, resource)
14
+ _registry.links(generator, resource)
15
+ end
16
+
17
+ def map(klass, &block)
18
+ _registry.map(klass, &block)
19
+ end
20
+
21
+ def resource(generator, resource)
22
+ _registry.resource(generator, resource)
23
+ end
24
+ end
25
+
26
+ def links(generator, links)
27
+ self.class.links(generator, links)
28
+ end
29
+
30
+ def resource(generator, resource)
31
+ self.class.resource(generator, resource)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Ivy
2
+ module Serializers
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,143 @@
1
+ require 'ivy/serializers'
2
+
3
+ RSpec.describe Ivy::Serializers::Formats::ActiveModelSerializers do
4
+ let(:format) { described_class.new(document) }
5
+
6
+ describe '#as_json' do
7
+ let(:registry) { Ivy::Serializers::Registry.new }
8
+ let(:document) { Ivy::Serializers::Documents.create(registry, :post, post) }
9
+
10
+ subject { format.as_json }
11
+
12
+ context 'with default mapping' do
13
+ let(:post) { double('post', :id => 1) }
14
+
15
+ it { should eq(:post => {:id => 1}) }
16
+ end
17
+
18
+ context 'with an attribute' do
19
+ let(:post_class) { double('Post') }
20
+ let(:post) { double('post', :class => post_class, :id => 1, :title => 'title') }
21
+
22
+ context 'with default options' do
23
+ before do
24
+ registry.map post_class do
25
+ attribute :title
26
+ end
27
+ end
28
+
29
+ it { should eq(:post => {:id => 1, :title => 'title'}) }
30
+ end
31
+
32
+ context 'with a block provided' do
33
+ before do
34
+ registry.map post_class do
35
+ attribute(:headline) { |post| post.title }
36
+ end
37
+ end
38
+
39
+ it { should eq(:post => {:id => 1, :headline => 'title'}) }
40
+ end
41
+ end
42
+
43
+ context 'with a belongs_to relationship' do
44
+ let(:author_class) { double('Author', :name => 'Author') }
45
+ let(:author) { double('author', :class => author_class, :id => 1) }
46
+ let(:post_class) { double('Post') }
47
+ let(:post) { double('post', :author => author, :class => post_class, :id => 1) }
48
+
49
+ context 'with default options' do
50
+ before do
51
+ registry.map post_class do
52
+ belongs_to :author
53
+ end
54
+ end
55
+
56
+ it { should eq(:post => {:author_id => 1, :id => 1}) }
57
+ end
58
+
59
+ context 'with a block provided' do
60
+ before do
61
+ registry.map post_class do
62
+ belongs_to(:user) { |post| post.author }
63
+ end
64
+ end
65
+
66
+ it { should eq(:post => {:id => 1, :user_id => 1}) }
67
+ end
68
+
69
+ context 'with the :embed_in_root option' do
70
+ before do
71
+ registry.map post_class do
72
+ belongs_to :author, :embed_in_root => true
73
+ end
74
+ end
75
+
76
+ it { should eq(
77
+ :authors => [{:id => 1}],
78
+ :post => {:author_id => 1, :id => 1}
79
+ ) }
80
+ end
81
+
82
+ context 'with the :polymorphic option' do
83
+ before do
84
+ registry.map post_class do
85
+ belongs_to :author, :polymorphic => true
86
+ end
87
+ end
88
+
89
+ it { should eq(:post => {:author => {:id => 1, :type => 'author'}, :id => 1}) }
90
+ end
91
+ end
92
+
93
+ context 'with a has_many relationship' do
94
+ let(:comment_class) { double('Comment', :name => 'Comment') }
95
+ let(:comment) { double('comment', :class => comment_class, :id => 1) }
96
+ let(:post_class) { double('Post') }
97
+ let(:post) { double('post', :class => post_class, :comments => [comment], :id => 1) }
98
+
99
+ context 'with default options' do
100
+ before do
101
+ registry.map post_class do
102
+ has_many :comments
103
+ end
104
+ end
105
+
106
+ it { should eq(:post => {:comment_ids => [1], :id => 1}) }
107
+ end
108
+
109
+ context 'with a block provided' do
110
+ before do
111
+ registry.map post_class do
112
+ has_many(:replies) { |post| post.comments }
113
+ end
114
+ end
115
+
116
+ it { should eq(:post => {:id => 1, :reply_ids => [1]}) }
117
+ end
118
+
119
+ context 'with the :embed_in_root option' do
120
+ before do
121
+ registry.map post_class do
122
+ has_many :comments, :embed_in_root => true
123
+ end
124
+ end
125
+
126
+ it { should eq(
127
+ :comments => [{:id => 1}],
128
+ :post => {:comment_ids => [1], :id => 1}
129
+ ) }
130
+ end
131
+
132
+ context 'with the :polymorphic option' do
133
+ before do
134
+ registry.map post_class do
135
+ has_many :comments, :polymorphic => true
136
+ end
137
+ end
138
+
139
+ it { should eq(:post => {:comments => [{:id => 1, :type => 'comment'}], :id => 1}) }
140
+ end
141
+ end
142
+ end
143
+ end