restpack_serializer 0.2.3

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/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ *.db
3
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ script: bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,87 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ restpack_serializer (0.2.3)
5
+ activerecord (>= 3.0)
6
+ activesupport (>= 3.0)
7
+ will_paginate (~> 3.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (3.2.13)
13
+ activesupport (= 3.2.13)
14
+ builder (~> 3.0.0)
15
+ activerecord (3.2.13)
16
+ activemodel (= 3.2.13)
17
+ activesupport (= 3.2.13)
18
+ arel (~> 3.0.2)
19
+ tzinfo (~> 0.3.29)
20
+ activesupport (3.2.13)
21
+ i18n (= 0.6.1)
22
+ multi_json (~> 1.0)
23
+ arel (3.0.2)
24
+ builder (3.0.4)
25
+ bump (0.4.1)
26
+ coderay (1.0.9)
27
+ database_cleaner (0.9.1)
28
+ diff-lcs (1.2.3)
29
+ factory_girl (4.2.0)
30
+ activesupport (>= 3.0.0)
31
+ ffi (1.7.0)
32
+ formatador (0.2.4)
33
+ growl (1.0.3)
34
+ guard (1.8.0)
35
+ formatador (>= 0.2.4)
36
+ listen (>= 1.0.0)
37
+ lumberjack (>= 1.0.2)
38
+ pry (>= 0.9.10)
39
+ thor (>= 0.14.6)
40
+ guard-rspec (2.5.4)
41
+ guard (>= 1.1)
42
+ rspec (~> 2.11)
43
+ i18n (0.6.1)
44
+ listen (1.0.0)
45
+ rb-fsevent (>= 0.9.3)
46
+ rb-inotify (>= 0.9)
47
+ rb-kqueue (>= 0.2)
48
+ lumberjack (1.0.3)
49
+ method_source (0.8.1)
50
+ multi_json (1.7.2)
51
+ pry (0.9.12)
52
+ coderay (~> 1.0.5)
53
+ method_source (~> 0.8)
54
+ slop (~> 3.4)
55
+ rake (10.0.4)
56
+ rb-fsevent (0.9.3)
57
+ rb-inotify (0.9.0)
58
+ ffi (>= 0.5.0)
59
+ rb-kqueue (0.2.0)
60
+ ffi (>= 0.5.0)
61
+ rspec (2.13.0)
62
+ rspec-core (~> 2.13.0)
63
+ rspec-expectations (~> 2.13.0)
64
+ rspec-mocks (~> 2.13.0)
65
+ rspec-core (2.13.1)
66
+ rspec-expectations (2.13.0)
67
+ diff-lcs (>= 1.1.3, < 2.0)
68
+ rspec-mocks (2.13.1)
69
+ slop (3.4.4)
70
+ sqlite3 (1.3.7)
71
+ thor (0.18.1)
72
+ tzinfo (0.3.37)
73
+ will_paginate (3.0.4)
74
+
75
+ PLATFORMS
76
+ ruby
77
+
78
+ DEPENDENCIES
79
+ bump
80
+ database_cleaner (~> 0.9.1)
81
+ factory_girl (~> 4.2.0)
82
+ growl (~> 1.0.3)
83
+ guard-rspec (~> 2.5.4)
84
+ rake (~> 10.0.3)
85
+ restpack_serializer!
86
+ rspec (~> 2.12)
87
+ sqlite3 (~> 1.3.7)
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :cli => "-c -f doc" do
2
+ watch(%r{^spec/.+_spec\.rb$}) { "spec" }
3
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Gavin Joyce and RestPack contributors.
2
+ Copyright (c) 2011-2012 José Valim & Yehuda Katz (https://github.com/rails-api/active_model_serializers/blob/master/MIT-LICENSE.txt)
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ this software and associated documentation files (the "Software"), to deal in
6
+ the Software without restriction, including without limitation the rights to
7
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8
+ of the Software, and to permit persons to whom the Software is furnished to do
9
+ so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # restpack_serializer [![Build Status](https://travis-ci.org/RestPack/restpack_serializer.png?branch=master)](https://travis-ci.org/RestPack/restpack_serializer) [![Code Climate](https://codeclimate.com/github/RestPack/restpack_serializer.png)](https://codeclimate.com/github/RestPack/restpack_serializer) [![Dependency Status](https://gemnasium.com/RestPack/restpack_serializer.png)](https://gemnasium.com/RestPack/restpack_serializer) [![Gem Version](https://badge.fury.io/rb/restpack_serializer.png)](http://badge.fury.io/rb/restpack_serializer)
2
+
3
+ **Model serialization, paging, side-loading and filtering**
4
+
5
+ restpack_serializer allows you to quickly provide a set of RESTful endpoints for your application. It is an implementation of the emerging [JSON API](http://jsonapi.org/) standard.
6
+
7
+ > [Live Demo of RestPack Serializer](http://restpack-serializer-sample.herokuapp.com/)
8
+
9
+ ---
10
+
11
+ * [An overview of RestPack](http://goo.gl/rGoIQ)
12
+ * [JSON API](http://jsonapi.org/)
13
+
14
+ ## Serialization
15
+
16
+ Let's say we have an `Album` model:
17
+
18
+ ```ruby
19
+ class Album < ActiveRecord::Base
20
+ attr_accessible :title, :year, :artist
21
+
22
+ belongs_to :artist
23
+ has_many :songs
24
+ end
25
+ ```
26
+
27
+ restpack_serializer allows us to define a corresponding serializer:
28
+
29
+ ```ruby
30
+ class AlbumSerializer
31
+ include RestPack::Serializer
32
+ attributes :id, :title, :year, :artist_id, :href
33
+
34
+ def href
35
+ "/albums/#{id}.json"
36
+ end
37
+ end
38
+ ```
39
+
40
+ `AlbumSerializer.as_json(album)` produces:
41
+
42
+ ```javascript
43
+ {
44
+ "id": "1",
45
+ "title": "Kid A",
46
+ "year": 2000,
47
+ "artist_id": 1,
48
+ "href": "/albums/1.json"
49
+ }
50
+ ```
51
+
52
+ ## Exposing an API
53
+
54
+ The `AlbumSerializer` provides `page` and `resource` method which provide paged collection and singular resource GET endpoints.
55
+
56
+ ```ruby
57
+ class AlbumsController < ApplicationController
58
+ def index
59
+ render json: AlbumSerializer.page(params)
60
+ end
61
+
62
+ def show
63
+ render json: AlbumSerializer.resource(params)
64
+ end
65
+ end
66
+ ```
67
+
68
+ These endpoint will live at URLs such as `/albums.json` and `/albums/142857.json`:
69
+
70
+ * http://restpack-serializer-sample.herokuapp.com/albums.json
71
+ * http://restpack-serializer-sample.herokuapp.com/albums/4.json
72
+
73
+ Both `page` and `resource` methods can have an optional scope allowing us to enforce arbitrary constraints:
74
+
75
+ ```ruby
76
+ AlbumSerializer.page(params, Albums.where("year < 1950"))
77
+ ```
78
+
79
+ ## Paging
80
+
81
+ Collections are paged by default. `page` and `page_size` parameters are available:
82
+
83
+ * http://restpack-serializer-sample.herokuapp.com/songs.json?page=2
84
+ * http://restpack-serializer-sample.herokuapp.com/songs.json?page=2&page_size=3
85
+
86
+ Paging details are included in a `meta` attribute:
87
+
88
+ http://restpack-serializer-sample.herokuapp.com/songs.json?page=2&page_size=3 yields:
89
+
90
+ ```javascript
91
+ {
92
+ "songs": [
93
+ {
94
+ "id": "4",
95
+ "title": "How to Dissapear Completely",
96
+ "href": "/songs/4.json",
97
+ "links": {
98
+ "artist": "1",
99
+ "album": "1"
100
+ }
101
+ },
102
+ {
103
+ "id": "5",
104
+ "title": "Treedfingers",
105
+ "href": "/songs/5.json",
106
+ "links": {
107
+ "artist": "1",
108
+ "album": "1"
109
+ }
110
+ },
111
+ {
112
+ "id": "6",
113
+ "title": "Optimistic",
114
+ "href": "/songs/6.json",
115
+ "links": {
116
+ "artist": "1",
117
+ "album": "1"
118
+ }
119
+ }
120
+ ],
121
+ "meta": {
122
+ "songs": {
123
+ "page": 2,
124
+ "page_size": 3,
125
+ "count": 42,
126
+ "includes": [],
127
+ "page_count": 14,
128
+ "previous_page": 1,
129
+ "next_page": 3
130
+ }
131
+ },
132
+ "links": {
133
+ "songs.artists": {
134
+ "href": "/artists/{songs.artist}.json",
135
+ "type": "artists"
136
+ },
137
+ "songs.albums": {
138
+ "href": "/albums/{songs.album}.json",
139
+ "type": "albums"
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ URL Templates to related data are included in the `links` element. These can be used to construct URLs such as:
146
+
147
+ * /artists/1.json
148
+ * /albums/1.json
149
+
150
+ ## Side-loading
151
+
152
+ Side-loading allows related resources to be optionally included in a single API response. Valid side-loads can be defined in Serializers by using ```can_include``` as follows:
153
+
154
+ ```ruby
155
+ class AlbumSerializer
156
+ include RestPack::Serializer
157
+ attributes :id, :title, :year, :artist_id, :href
158
+ can_include :songs, :artists
159
+
160
+ def href
161
+ "/albums/#{id}.json"
162
+ end
163
+ end
164
+ ```
165
+
166
+ In this example, we are allowing related `songs` and `artists` to be included in API responses. Side-loads can be specifed by using the `includes` parameter:
167
+
168
+ #### No side-loads
169
+
170
+ * http://restpack-serializer-sample.herokuapp.com/albums.json
171
+
172
+ #### Side-load related Artists
173
+
174
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?includes=artists
175
+
176
+ which yields:
177
+
178
+ ```javascript
179
+ {
180
+ "albums": [
181
+ {
182
+ "id": "1",
183
+ "title": "Kid A",
184
+ "year": 2000,
185
+ "href": "/albums/1.json",
186
+ "links": {
187
+ "artist": "1"
188
+ }
189
+ },
190
+ {
191
+ "id": "2",
192
+ "title": "Amnesiac",
193
+ "year": 2001,
194
+ "href": "/albums/2.json",
195
+ "links": {
196
+ "artist": "1"
197
+ }
198
+ },
199
+ {
200
+ "id": "3",
201
+ "title": "Murder Ballads",
202
+ "year": 1996,
203
+ "href": "/albums/3.json",
204
+ "links": {
205
+ "artist": "2"
206
+ }
207
+ },
208
+ {
209
+ "id": "4",
210
+ "title": "Curtains",
211
+ "year": 2005,
212
+ "href": "/albums/4.json",
213
+ "links": {
214
+ "artist": "3"
215
+ }
216
+ }
217
+ ],
218
+ "meta": {
219
+ "albums": {
220
+ "page": 1,
221
+ "page_size": 10,
222
+ "count": 4,
223
+ "includes": [
224
+ "artists"
225
+ ],
226
+ "page_count": 1,
227
+ "previous_page": null,
228
+ "next_page": null
229
+ }
230
+ },
231
+ "links": {
232
+ "albums.songs": {
233
+ "href": "/songs.json?album_id={albums.id}",
234
+ "type": "songs"
235
+ },
236
+ "albums.artists": {
237
+ "href": "/artists/{albums.artist}.json",
238
+ "type": "artists"
239
+ },
240
+ "artists.albums": {
241
+ "href": "/albums.json?artist_id={artists.id}",
242
+ "type": "albums"
243
+ },
244
+ "artists.songs": {
245
+ "href": "/songs.json?artist_id={artists.id}",
246
+ "type": "songs"
247
+ }
248
+ },
249
+ "artists": [
250
+ {
251
+ "id": "1",
252
+ "name": "Radiohead",
253
+ "website": "http://radiohead.com/",
254
+ "href": "/artists/1.json"
255
+ },
256
+ {
257
+ "id": "2",
258
+ "name": "Nick Cave & The Bad Seeds",
259
+ "website": "http://www.nickcave.com/",
260
+ "href": "/artists/2.json"
261
+ },
262
+ {
263
+ "id": "3",
264
+ "name": "John Frusciante",
265
+ "website": "http://johnfrusciante.com/",
266
+ "href": "/artists/3.json"
267
+ }
268
+ ]
269
+ }
270
+ ```
271
+
272
+ #### Side-load related Songs
273
+
274
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?includes=songs
275
+
276
+ An album `:has_many` songs, so the side-loads are paged. We'll be soon adding URLs to the response meta data which will point to the next page of side-loaded data. These URLs will be something like:
277
+
278
+ * http://restpack-serializer-sample.herokuapp.com/songs.json?album_ids=1,2,3,4&page=2
279
+
280
+ #### Side-load related Artists and Songs
281
+
282
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?includes=artists,songs
283
+
284
+ ## Filtering
285
+
286
+ Simple filtering based on primary and foreign keys is possible:
287
+
288
+ #### By primary key:
289
+
290
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?id=1
291
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?ids=1,2,4
292
+
293
+ #### By foreign key:
294
+
295
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?artist_id=1
296
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?artist_ids=2,3
297
+
298
+ Side-loading is available when filtering:
299
+
300
+ * http://restpack-serializer-sample.herokuapp.com/albums.json?artist_ids=2,3&includes=artists,songs
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'bump/tasks'
2
+ require_relative 'lib/restpack_serializer/version'
3
+
4
+ task :default => :test
5
+ task :test => :spec
6
+
7
+ begin
8
+ require "rspec/core/rake_task"
9
+
10
+ desc "Run all specs"
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.rspec_opts = ['-cfs']
13
+ end
14
+ rescue LoadError
15
+ end
16
+
17
+ namespace :test do
18
+ task :irb do
19
+ exec "irb -r ./spec/spec_helper.rb"
20
+ end
21
+ end
22
+
23
+ task :gem do
24
+ Rake::Task["gem:bump"].invoke
25
+ Rake::Task["gem:tag"].invoke
26
+ Rake::Task["gem:build"].invoke
27
+ Rake::Task["gem:push"].invoke
28
+ end
29
+
30
+ namespace :gem do
31
+ task :build do
32
+ sh "gem build restpack_serializer.gemspec"
33
+ end
34
+
35
+ task :push do
36
+ require 'bump'
37
+ sh "gem push restpack_serializer-#{Bump::Bump.current}.gem"
38
+ end
39
+
40
+ task :tag do
41
+ require 'bump'
42
+ version = Bump::Bump.current
43
+ puts "tagging v#{version}"
44
+ `git push && git tag v#{version} && git push --tags`
45
+ end
46
+
47
+ task :bump do
48
+ Rake::Task["bump:patch"].invoke
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ class RestPack::Serializer::Factory
2
+ def self.create(*identifiers)
3
+ serializers = identifiers.map { |identifier| self.classify(identifier) }
4
+ serializers.count == 1 ? serializers.first : serializers
5
+ end
6
+
7
+ private
8
+
9
+ def self.classify(identifier)
10
+ begin
11
+ "#{identifier}Serializer".classify.constantize.new
12
+ rescue
13
+ "#{identifier.to_s.singularize.to_sym}Serializer".classify.constantize.new
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,48 @@
1
+ module RestPack::Serializer
2
+ class Options
3
+ attr_accessor :page, :page_size, :includes, :filters, :model_class, :scope, :include_links
4
+
5
+ def initialize(model_class, params = {}, scope = nil)
6
+ params.symbolize_keys! if params.respond_to?(:symbolize_keys!)
7
+
8
+ @page = 1
9
+ @page_size = 10
10
+ @includes = []
11
+ @filters = filters_from_params(params, model_class)
12
+ @model_class = model_class
13
+ @scope = scope || model_class.send(:scoped)
14
+ @include_links = true
15
+
16
+ @page = params[:page].to_i if params[:page]
17
+ @page_size = params[:page_size].to_i if params[:page_size]
18
+ @includes = params[:includes].split(',').map(&:to_sym) if params[:includes]
19
+ end
20
+
21
+ def scope_with_filters
22
+ scope_filter = {}
23
+ @filters.keys.each do |filter|
24
+ value = @filters[filter]
25
+ if value.is_a?(String)
26
+ value = value.split(',')
27
+ end
28
+ scope_filter[filter] = value
29
+ end
30
+
31
+ @scope.where(scope_filter)
32
+ end
33
+
34
+ private
35
+
36
+ def filters_from_params(params, model_class)
37
+ serializer = RestPack::Serializer::Factory.create(model_class)
38
+ filters = {}
39
+ serializer.class.filterable_by.each do |filter|
40
+ [filter, "#{filter}s".to_sym].each do |key|
41
+ filters[filter] = params[key].split(',') if params[key]
42
+ end
43
+ end
44
+ filters
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ module RestPack::Serializer::Attributes
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def serializable_attributes
6
+ @serializable_attributes
7
+ end
8
+
9
+ def attributes(*attrs)
10
+ attrs.each { |attr| attribute attr }
11
+ end
12
+
13
+ def attribute(name, options={})
14
+ options[:key] ||= name.to_sym
15
+
16
+ @serializable_attributes ||= {}
17
+ @serializable_attributes[options[:key]] = name
18
+
19
+ define_attribute_method name
20
+ define_include_method name
21
+ end
22
+
23
+ def define_attribute_method(name)
24
+ unless method_defined?(name)
25
+ define_method name do
26
+ value = @model.send(name)
27
+ value = value.to_s if name == :id
28
+ value
29
+ end
30
+ end
31
+ end
32
+
33
+ def define_include_method(name)
34
+ method = "include_#{name}?".to_sym
35
+
36
+ unless method_defined?(method)
37
+ define_method method do
38
+ @options[method].nil? || @options[method]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ module RestPack::Serializer::Paging
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def page(params = {}, scope = nil)
6
+ page_with_options RestPack::Serializer::Options.new(self.model_class, params, scope)
7
+ end
8
+
9
+ def page_with_options(options)
10
+ page = options.scope_with_filters.paginate(
11
+ page: options.page,
12
+ per_page: options.page_size
13
+ )
14
+
15
+ result = {
16
+ self.key => serialize_page(page),
17
+ :meta => {
18
+ self.key => serialize_meta(page, options)
19
+ }
20
+ }
21
+
22
+ if options.include_links
23
+ result[:links] = self.links
24
+ Array(RestPack::Serializer::Factory.create(*options.includes)).each do |serializer|
25
+ result[:links].merge! serializer.class.links
26
+ end
27
+ end
28
+
29
+ side_load_data = side_loads(page, options)
30
+ result[:meta].merge!(side_load_data[:meta] || {})
31
+ result.merge side_load_data.except(:meta)
32
+ end
33
+
34
+ private
35
+
36
+ def serialize_page(page)
37
+ page.map { |model| self.as_json(model) }
38
+ end
39
+
40
+ def serialize_meta(page, options)
41
+ meta = {
42
+ page: options.page,
43
+ page_size: options.page_size,
44
+ count: page.total_entries,
45
+ includes: options.includes
46
+ }
47
+
48
+ meta[:page_count] = ((page.total_entries - 1) / options.page_size) + 1
49
+ meta[:previous_page] = meta[:page] > 1 ? meta[:page] - 1 : nil
50
+ meta[:next_page] = meta[:page] < meta[:page_count] ? meta[:page] + 1 : nil
51
+ meta
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,9 @@
1
+ module RestPack::Serializer::Resource
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def resource(params = {}, scope = nil)
6
+ page_with_options RestPack::Serializer::Options.new(self.model_class, params, scope)
7
+ end
8
+ end
9
+ end