restpack_serializer 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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