json_api_server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +7 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +35 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Dockerfile +9 -0
  9. data/Gemfile +10 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +432 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/config/locales/en.yml +32 -0
  16. data/docker-compose.yml +10 -0
  17. data/json_api_server.gemspec +50 -0
  18. data/lib/json_api_server.rb +76 -0
  19. data/lib/json_api_server/api_version.rb +8 -0
  20. data/lib/json_api_server/attributes_builder.rb +135 -0
  21. data/lib/json_api_server/base_serializer.rb +169 -0
  22. data/lib/json_api_server/builder.rb +201 -0
  23. data/lib/json_api_server/cast.rb +89 -0
  24. data/lib/json_api_server/configuration.rb +99 -0
  25. data/lib/json_api_server/controller/error_handling.rb +164 -0
  26. data/lib/json_api_server/engine.rb +5 -0
  27. data/lib/json_api_server/error.rb +64 -0
  28. data/lib/json_api_server/errors.rb +50 -0
  29. data/lib/json_api_server/exceptions.rb +6 -0
  30. data/lib/json_api_server/fields.rb +76 -0
  31. data/lib/json_api_server/filter.rb +255 -0
  32. data/lib/json_api_server/filter_builders.rb +135 -0
  33. data/lib/json_api_server/filter_config.rb +71 -0
  34. data/lib/json_api_server/filter_parser.rb +88 -0
  35. data/lib/json_api_server/include.rb +158 -0
  36. data/lib/json_api_server/meta_builder.rb +39 -0
  37. data/lib/json_api_server/mime_types.rb +21 -0
  38. data/lib/json_api_server/pagination.rb +189 -0
  39. data/lib/json_api_server/paginator.rb +134 -0
  40. data/lib/json_api_server/relationships_builder.rb +215 -0
  41. data/lib/json_api_server/resource_serializer.rb +245 -0
  42. data/lib/json_api_server/resources_serializer.rb +131 -0
  43. data/lib/json_api_server/serializer.rb +34 -0
  44. data/lib/json_api_server/sort.rb +156 -0
  45. data/lib/json_api_server/sort_configs.rb +63 -0
  46. data/lib/json_api_server/validation_errors.rb +51 -0
  47. data/lib/json_api_server/version.rb +3 -0
  48. metadata +259 -0
@@ -0,0 +1,39 @@
1
+ module JsonApiServer # :nodoc:
2
+ # Class for building meta element.
3
+ # http://jsonapi.org/format/#document-meta
4
+ #
5
+ # ==== Example
6
+ #
7
+ # MetaBuilder.new
8
+ # .add('copyright', "Copyright 2015 Example Corp.")
9
+ # .add('authors', ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt", "Tyler Kellen"])
10
+ # .merge({a: 'something', b: 'something else'})
11
+ # .meta # => { "copyright": "Copyright 2015 Example Corp.",
12
+ # "authors": ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt", "Tyler Kellen"],
13
+ # a: 'something',
14
+ # b: 'something else'
15
+ # }
16
+ #
17
+ class MetaBuilder
18
+ def initialize
19
+ @hash = {}
20
+ end
21
+
22
+ # Add key and value.
23
+ def add(key, value)
24
+ @hash[key] = value
25
+ self
26
+ end
27
+
28
+ # Push in multiple key/values with merge.
29
+ def merge(hash)
30
+ @hash.merge!(hash) if hash.respond_to?(:keys) && hash.any?
31
+ self
32
+ end
33
+
34
+ # Returns a hash if it has values, nil otherwise.
35
+ def meta
36
+ @hash.any? ? @hash : nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ #++
2
+ # NOTE: it can cause issues in browsers: https://github.com/json-api/json-api/issues/1048
3
+ # __
4
+ module JsonApiServer # :nodoc:
5
+ # http://jsonapi.org/format/#introduction -> JSON API requires use of the JSON API
6
+ # media type (application/vnd.api+json) for exchanging data.
7
+ #
8
+ # Include this module in your config/initializers/mime_types.rb
9
+ #
10
+ # i.e,:
11
+ # # in config/initializers/mime_types.rb
12
+ # include JsonApiServer::MimeTypes
13
+ module MimeTypes
14
+ api_mime_types = %w[
15
+ application/vnd.api+json
16
+ text/x-json
17
+ application/json
18
+ ]
19
+ Mime::Type.register 'application/vnd.api+json', :json, api_mime_types
20
+ end
21
+ end
@@ -0,0 +1,189 @@
1
+ module JsonApiServer # :nodoc:
2
+ # === Description
3
+ #
4
+ # JSON API Spec: http://jsonapi.org/format/#fetching-pagination - "Pagination links MUST
5
+ # appear in the links object that corresponds to a collection. To paginate the primary data,
6
+ # supply pagination links in the top-level links object."
7
+ #
8
+ # This class handles pagination. It (1) ensures <tt>page[number]</tt> is a positive integer,
9
+ # (2) <tt>page[limit]</tt> doesn't exceed a maximum value and (3) generates a pagination sub-query
10
+ # (to be merged into a master query). It uses the WillPaginate gem
11
+ # https://rubygems.org/gems/will_paginate/versions/3.1.6.
12
+ #
13
+ # === Usage:
14
+ #
15
+ # A paginating request will look like:
16
+ # /comments?page[number]=1&page[limit]=10
17
+ #
18
+ # Where:
19
+ #
20
+ # - <b><tt>page[number]</tt></b> is the current page
21
+ #
22
+ # - <b><tt>page[limit]</tt></b> is number of records per page
23
+ #
24
+ # The class takes an ActiveDispatch::Request object, an ActiveRecord::Base model
25
+ # (to generate a pagination sub-query) and two options:
26
+ #
27
+ # <b><tt>:max_per_page</tt></b> - maximum number of records per page which defaults to
28
+ # JsonApiServer::Configuration#default_max_per_page.
29
+ #
30
+ # <b><tt>:default_per_page</tt></b> - number of records to show if not specified in <tt>page[limit]</tt>
31
+ # defaults to JsonApiServer::Configuration#default_per_page).
32
+ #
33
+ # ===== Example:
34
+ #
35
+ # Create an instance in your controller:
36
+ #
37
+ # pagination = JsonApiServer::Pagination.new(request, Comment, max_per_page: 50, default_per_page: 10)
38
+ #
39
+ # Merge pagination sub-query into your ActiveRecord query:
40
+ #
41
+ # recent_comments = Comment.recent.merge(pagination.query)
42
+ #
43
+ # Get JsonApiServer::Paginator instance to create links in your JSON serializer:
44
+ #
45
+ # paginator = pagination.paginator_for(recent_comments)
46
+ # paginator.as_json # creates JSON API links
47
+ #
48
+ # Pass paginator as param to a class inheriting from JsonApiServer::ResourcesSerializer and
49
+ # it creates the links section for you.
50
+ #
51
+ # class CommentsSerializer < JsonApiServer::ResourcesSerializer
52
+ # serializer CommentSerializer
53
+ # end
54
+ # serializer = CommentsSerializer.new(recent_comments, paginator: paginator)
55
+ #
56
+ # ==== Note:
57
+ # JsonApiServer::Builder class provides an easier way to use this class.
58
+ #
59
+ class Pagination
60
+ # ActionDispatch::Request passed in constructor.
61
+ attr_reader :request
62
+
63
+ # ActiveRecord::Base model passed in constructor.
64
+ attr_reader :model
65
+
66
+ # Query parameters from #request.
67
+ attr_reader :params
68
+
69
+ # Maximum records per page. Prevents users from requesting too many records. Passed
70
+ # into constructor.
71
+ attr_reader :max_per_page
72
+
73
+ # Default number of records to show per page. If <tt>page[limit]</tt> is not present, it
74
+ # will use this value. If this is not set, it will use
75
+ # JsonApiServer.configuration.default_per_page.
76
+ attr_reader :default_per_page
77
+
78
+ # Arguments:
79
+ # - <tt>request</tt> - ActionDispatch::Request
80
+ # - <tt>model</tt> - ActiveRecord::Base model. Used to generate sub-query.
81
+ # - <tt>options</tt> - (Hash)
82
+ # - :max_per_page (Integer) - Optional. Defaults to JsonApiServer.configuration.default_max_per_page.
83
+ # - :default_per_page (Integer) - Optional. Defaults to JsonApiServer.configuration.default_per_page.
84
+ #
85
+ def initialize(request, model, **options)
86
+ @request = request
87
+ @model = model
88
+ @max_per_page = (options[:max_per_page] || self.class.default_max_per_page).to_i
89
+ @default_per_page = (options[:default_per_page] || self.class.default_per_page).to_i
90
+ @params = request.query_parameters
91
+ end
92
+
93
+ # Calls WillPaginate 'paginate' method with #page and #per_page. Returns an
94
+ # ActiveRecord::Relation object (a query fragment) which can be
95
+ # merged into another query with merge.
96
+ #
97
+ # ==== Example:
98
+ #
99
+ # pagination = JsonApiServer::Pagination.new(request, Comment, options)
100
+ # recent_comments = Comment.recent.merge(pagination.relation)
101
+ #
102
+ def relation
103
+ @relation ||= model.paginate(page: page, per_page: per_page)
104
+ end
105
+
106
+ alias query relation
107
+
108
+ class << self
109
+ # Default max per page. Defaults to JsonApiServer.configuration.default_max_per_page
110
+ def default_max_per_page
111
+ JsonApiServer.configuration.default_max_per_page
112
+ end
113
+
114
+ # Default per page. Defaults to JsonApiServer.configuration.default_per_page.
115
+ def default_per_page
116
+ JsonApiServer.configuration.default_per_page
117
+ end
118
+ end
119
+
120
+ # Create an instance of JsonApiServer::Paginator for a WillPaginate collection. Returns
121
+ # nil if not a WillPaginate collection.
122
+ #
123
+ # params:
124
+ # - <tt>collection</tt> (WillPaginate collection) - i.e., Comment.recent.paginate(page: x, per_page: y)
125
+ # - <tt>options</tt> (Hash):
126
+ # - <tt>per_page</tt> (Integer) - defaults to self.per_page.
127
+ # - <tt>base_url</tt> (String) - defaults to self.base_url (joins JsonApiServer.configuration.base_url with request.path).
128
+ def paginator_for(collection, options = {})
129
+ if collection.respond_to?(:current_page) && collection.respond_to?(:total_pages)
130
+ # call to_i on WillPaginate::PageNumber which DelegateClass(Integer)
131
+ # paginator(collection.current_page.to_i, collection.total_pages.to_i, options)
132
+ # HACK: collection.current_page.to_i disappears when merged? works w/o merge.
133
+ paginator(page, collection.total_pages.to_i, options)
134
+ end
135
+ end
136
+
137
+ # Number of records per page. From query parameter <tt>page[limit]</tt>.
138
+ def per_page
139
+ @per_page ||= begin
140
+ l = begin
141
+ params[:page][:limit].to_i
142
+ rescue
143
+ default_per_page
144
+ end
145
+ l = [max_per_page, l].min
146
+ l <= 0 ? default_per_page : l
147
+ end
148
+ end
149
+
150
+ alias limit per_page
151
+
152
+ # The current page number. From query parameter page[number]</tt>.
153
+ def page
154
+ @page ||= begin
155
+ n = begin
156
+ params[:page][:number].to_i
157
+ rescue
158
+ 1
159
+ end
160
+ n <= 0 ? 1 : n
161
+ end
162
+ end
163
+
164
+ alias number page
165
+
166
+ # Joins JsonApiServer::Configuration#base_url with request.path.
167
+ def base_url
168
+ @base_url ||= File.join(JsonApiServer.configuration.base_url, request.path)
169
+ end
170
+
171
+ protected
172
+
173
+ # Creates an instance of JsonApiServer::Paginator.
174
+ #
175
+ # params:
176
+ # - <tt>current_page</tt> (Integer)
177
+ # - <tt>total_pages</tt> (Integer)
178
+ # - <tt>options</tt> (Hash):
179
+ # - <tt>per_page</tt> (Integer) - defaults to self.per_page.
180
+ # - <tt>base_url</tt> (String) - defaults to self.base_url (joins
181
+ # JsonApiServer::Configuration#base_url with request.path.).
182
+ def paginator(current_page, total_pages, options = {})
183
+ per_page = options[:per_page] || self.per_page
184
+ base_url = options[:base_url] || self.base_url
185
+ JsonApiServer.paginator(current_page, total_pages, per_page,
186
+ base_url, params)
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,134 @@
1
+ module JsonApiServer # :nodoc:
2
+ # Creates JSON API pagination entries per http://jsonapi.org/examples/#pagination.
3
+ #
4
+ # JsonApiServer::Paginator#as_json generates a hash like the following which can be
5
+ # added to JsonApiServer::BaseSerializer#links section.
6
+ #
7
+ # - 'next' is nil when self is the last page.
8
+ # - 'prev' is nil when self is the first page.
9
+ #
10
+ # ===== Example:
11
+ # "links": {
12
+ # "self": "http://example.com/articles?page[number]=3&page[limit]=5",
13
+ # "first": "http://example.com/articles?page[number]=1&page[limit]=5",
14
+ # "prev": "http://example.com/articles?page[number]=2&page[limit]=5",
15
+ # "next": "http://example.com/articles?page[number]=4&page[limit]=5",
16
+ # "last": "http://example.com/articles?page[number]=13&page[limit]=5"
17
+ # }
18
+ class Paginator
19
+ @attrs = %w[first last self next prev]
20
+
21
+ # Params:
22
+ # - <tt>current_page</tt> (Integer)
23
+ # - <tt>total_pages</tt> (Integer)
24
+ # - <tt>per_page</tt> (Integer)
25
+ # - <tt>base_url</tt> (String) - Base url for resource, i.e., <tt>http://example.com/articles</tt>.
26
+ # - <tt>params</tt> (Hash) - Request parameters. Pagination params are merged into these.
27
+ def initialize(current_page, total_pages, per_page, base_url, params = {})
28
+ @current_page = current_page
29
+ @total_pages = total_pages
30
+ @per_page = per_page
31
+ @base_url = base_url
32
+ @params = params
33
+ end
34
+
35
+ class << self
36
+ attr_accessor :attrs
37
+ end
38
+
39
+ # First page url.
40
+ def first
41
+ @first ||= build_url(merge_params(1))
42
+ end
43
+
44
+ # Last page url.
45
+ def last
46
+ @last ||= build_url(merge_params(@total_pages))
47
+ end
48
+
49
+ # Current page url.
50
+ def self
51
+ @self ||= build_url(merge_params(@current_page))
52
+ end
53
+
54
+ # Next page url.
55
+ def next
56
+ @next ||= begin
57
+ n = calculate_next
58
+ n.nil? ? nil : build_url(merge_params(n))
59
+ end
60
+ end
61
+
62
+ # Previous page url.
63
+ def prev
64
+ @prev ||= begin
65
+ p = calculate_prev
66
+ p.nil? ? nil : build_url(merge_params(p))
67
+ end
68
+ end
69
+
70
+ # Returns hash:
71
+ # # i.e.,
72
+ # {
73
+ # self: "http://example.com/articles?page[number]=3&page[limit]=5",
74
+ # first: "http://example.com/articles?page[number]=1&page[limit]=5",
75
+ # prev: "http://example.com/articles?page[number]=2&page[limit]=5",
76
+ # next: "http://example.com/articles?page[number]=4&page[limit]=5",
77
+ # last: "http://example.com/articles?page[number]=13&page[limit]=5"
78
+ # }
79
+ def as_json
80
+ self.class.attrs.each_with_object({}) { |attr, acc| acc[attr] = send(attr); }
81
+ end
82
+
83
+ alias to_h as_json
84
+
85
+ # Hash with pagination meta information. Useful for user interfaces, i.e.,
86
+ # 'page #{current_page} of #{total_pages}'.
87
+ #
88
+ # #i.e.,
89
+ # {
90
+ # links: {
91
+ # current_page: 2,
92
+ # total_pages: 13,
93
+ # per_page: 5
94
+ # }
95
+ # }
96
+ def meta_info
97
+ {
98
+ 'links' => {
99
+ 'current_page' => @current_page,
100
+ 'total_pages' => @total_pages,
101
+ 'per_page' => @per_page
102
+ }
103
+ }
104
+ end
105
+
106
+ protected
107
+
108
+ # Merges pagination params with request params. Pagination params look like
109
+ # this to page[number]=x&page[limit]=y.
110
+ def merge_params(number)
111
+ @params.merge(page: { number: number, limit: @per_page })
112
+ end
113
+
114
+ # Merges base_url with modified params. Params are Url encoded, i.e.,
115
+ # page%5Blimit%5D=5&page%5Bnumber%5D=1
116
+ def build_url(params)
117
+ "#{@base_url}?#{params.to_query}"
118
+ end
119
+
120
+ # Calculates next page. Returns nil when value is invalid, i.e.,
121
+ # exceeds total_pages.
122
+ def calculate_next
123
+ n = @current_page + 1
124
+ n > @total_pages || n <= 0 ? nil : n
125
+ end
126
+
127
+ # Calculates previous page. Returns nil if value is invalid, i.e.,
128
+ # less than or equal to 0.
129
+ def calculate_prev
130
+ p = @current_page - 1
131
+ p <= 0 || p > @total_pages ? nil : p
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,215 @@
1
+ module JsonApiServer # :nodoc:
2
+ # ==== Description
3
+ #
4
+ # Part of http://jsonapi.org/format/#fetching-includes:
5
+ # "An endpoint may support an include request parameter to allow the client to
6
+ # customize which related resources should be returned.""
7
+ #
8
+ # ie., GET /articles/1?include=comments,comment.author,tags HTTP/1.1
9
+ #
10
+ # Use this class to build <tt>relationships</tt> and <tt>included</tt> sections
11
+ # in serializers.
12
+ #
13
+ # ==== Examples
14
+ #
15
+ # Relate, relate conditionally, or relate a collection. In this example,
16
+ # Publisher is added only if a condition is met (current user is an admin).
17
+ #
18
+ # JsonApiServer::RelationshipsBuilder.new
19
+ # .relate('author', AuthorSerializer.new(@object.author))
20
+ # .relate_each('comments', @object.comments) {|c| CommentSerializer.new(c)}
21
+ # .relationships
22
+ #
23
+ # # produces something like this if current user is an admin:
24
+ # # {
25
+ # # "author"=>{
26
+ # # {data: {type: "authors", id: 6, attributes: {first_name: "john", last_name: "Doe"}}}
27
+ # # },
28
+ # # "publisher"=>{
29
+ # # {data: {type: "publishers", id: 1, attributes: {name: "abc"}}}
30
+ # # },
31
+ # # "comments"=>[
32
+ # # {data: {type: "comments", id: 1, attributes: {title: "a", comment: "b"}}},
33
+ # # {data: {type: "comments", id: 2, attributes: {title: "c", comment: "d"}}}
34
+ # # ]
35
+ # # }
36
+ #
37
+ # <tt>relationships</tt> can include all relationship data or it can reference data in
38
+ # <tt>included</tt>. To include and relate in one go, #include with the <tt>:relate</tt> option
39
+ # which takes a BaseSerializer#as_json_options hash.
40
+ #
41
+ # builder = JsonApiServer::RelationshipsBuilder.new
42
+ # .include('author', AuthorSerializer.new(@object.author),
43
+ # relate: { include: [:relationship_data] })
44
+ # .include_each('comments', @object.comments) {|c| CommentSerializer.new(c) }
45
+ #
46
+ # builder.relationships
47
+ # # produces something like this if current user is an admin:
48
+ # # {
49
+ # # "author" => {
50
+ # # {data: {id: 6, type: "authors"}}
51
+ # # },
52
+ # # "publisher" => {
53
+ # # {links: {self: 'http://.../publishers/1'}}
54
+ # # }
55
+ # # }
56
+ #
57
+ # builder.included
58
+ # # produces something like this:
59
+ # # [
60
+ # # {type: "author", id: 6, attributes: {first_name: "john", last_name: "Doe"}},
61
+ # # {type: "publisher", id: 1, attributes: {name: "abc"}},
62
+ # # {type: "comments", id: 1, attributes: {title: "a", comment: "b"}},
63
+ # # {type: "comments", id: 2, attributes: {title: "c", comment: "d"}}
64
+ # # ]
65
+ #
66
+ class RelationshipsBuilder
67
+ def initialize
68
+ @relationships = {}
69
+ @included = []
70
+ end
71
+
72
+ # Returns <tt>relationships</tt> object. Relationships added with
73
+ # #relate, #relate_if, #relate_each and #include (with <tt>:relate</tt> option).
74
+ def relationships
75
+ @relationships.each do |k, v|
76
+ if v.respond_to?(:uniq!)
77
+ v.uniq!
78
+ @relationships[k] = v.first if v.length == 1
79
+ end
80
+ end
81
+ @relationships
82
+ end
83
+
84
+ # Returns <tt>included</tt> object. Includes added with
85
+ # #include, #include_if, #include_each.
86
+ def included
87
+ @included.uniq! if @included.respond_to?(:uniq!)
88
+ @included
89
+ end
90
+
91
+ # Add relationships with this method.
92
+ #
93
+ # Arguments:
94
+ #
95
+ # - <tt>type</tt> - (String) Relationship type/name.
96
+ # - <tt>serializer</tt> - (instance of serializer or something that responds to :as_json) Content.
97
+ #
98
+ # i.e.,
99
+ # JsonApiServer::RelationshipsBuilder.new(['comment.author'])
100
+ # .relate('author', author_serializer)
101
+ #
102
+ # # outputs something like...
103
+ # # { 'author' => {
104
+ # # :data => {
105
+ # # :type => "people",
106
+ # # :id => 6,
107
+ # # :attributes => {:first_name=>"John", :last_name=>"Steinbeck"}
108
+ # # }
109
+ # # }
110
+ # # }
111
+ #
112
+ # JsonApiServer::RelationshipsBuilder.new(['comment.author'])
113
+ # .relate('author', author_serializer)
114
+ #
115
+ # # outputs something like...
116
+ # # { 'author' => {
117
+ # # :data => {
118
+ # # :type => "people",
119
+ # # :id => 6,
120
+ # # :attributes => {:first_name=>"John", :last_name=>"Steinbeck"}
121
+ # # }
122
+ # # }
123
+ # # }
124
+ def relate(type, serializer)
125
+ merge_relationship(type, serializer)
126
+ self
127
+ end
128
+
129
+ # Add a collection to <tt>relationships</tt> with this method.
130
+ #
131
+ # Arguments:
132
+ #
133
+ # - <tt>type</tt> - (String) Relationship type/name.
134
+ # - <tt>collection</tt> - Collection of objects to pass to serializer.
135
+ # - <tt>block</tt> - Block that returns a serializer or something that repsonds_to as_json.
136
+ #
137
+ # i.e.,
138
+ # JsonApiServer::RelationshipsBuilder.new(['comments'])
139
+ # .relate_each('comments', @comments) { |c| CommentSerializer.new(c) }
140
+ def relate_each(type, collection)
141
+ collection.each { |item| relate(type, yield(item)) }
142
+ self
143
+ end
144
+
145
+ # Add to <tt>included</tt> with this method.
146
+ #
147
+ # Arguments:
148
+ #
149
+ # - <tt>type</tt> - (String) Relationship type/name.
150
+ # - <tt>serializer</tt> - (instance of serializer or something that responds to :as_json) - relationship content.
151
+ # - <tt>options</tt> - (Hash) -
152
+ # - :relate (Hash) - Optional. Add to relationships. BaseSerializer#as_json_options hash, i.e, <tt>{ include: [:relationship_data] }</tt>.
153
+ #
154
+ # i.e.,
155
+ # # include and relate
156
+ # JsonApiServer::RelationshipsBuilder.new(['comment.author'])
157
+ # .include('author', author_serializer,
158
+ # relate: {include: [:relationship_data]})
159
+ #
160
+ # # or just include
161
+ # JsonApiServer::RelationshipsBuilder.new(['author'])
162
+ # .include('author', author_serializer)
163
+ def include(type, serializer, **options)
164
+ merge_included(serializer)
165
+ if options[:relate]
166
+ serializer.as_json_options = options[:relate]
167
+ relate(type, serializer)
168
+ end
169
+ self
170
+ end
171
+
172
+ # Add a collection to <tt>included</tt> with this method.
173
+ #
174
+ # Arguments:
175
+ #
176
+ # - <tt>type</tt> - (String) Relationship type/name.
177
+ # - <tt>collection</tt> - Collection of objects to pass to block.
178
+ # - <tt>options</tt> - (Hash) -
179
+ # - :relate (Hash) - Optional. Add to relationships. BaseSerializer#as_json_options hash, i.e, <tt>{ include: [:relationship_data] }</tt>.
180
+ # - <tt>block</tt> - Block that returns a serializer or something that repsonds_to as_json.
181
+ #
182
+ # i.e.,
183
+ #
184
+ # JsonApiServer::RelationshipsBuilder.new(['comments'])
185
+ # .include_each('comments', @comments) {|c| CommentSerializer.new(c)}
186
+ #
187
+ def include_each(type, collection, **options)
188
+ collection.each { |item| include(type, yield(item), options) }
189
+ self
190
+ end
191
+
192
+ protected
193
+
194
+ def merge_relationship(type, value)
195
+ content = value.as_json if value.respond_to?(:as_json)
196
+ return if content.blank?
197
+
198
+ if @relationships.key?(type)
199
+ unless @relationships[type].is_a?(Array)
200
+ @relationships[type] = [@relationships[type]]
201
+ end
202
+ @relationships[type].push(content)
203
+ else
204
+ @relationships.merge!(type => content)
205
+ end
206
+ end
207
+
208
+ def merge_included(value)
209
+ if value.respond_to?(:as_json)
210
+ content = value.respond_to?(:as_json_options) ? value.as_json(include: [:data]) : value.as_json
211
+ @included.push(content[:data]) if content.present?
212
+ end
213
+ end
214
+ end
215
+ end