json_api_server 0.0.1

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