jsonapi_for_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f9cfc12b29fbd2b7976e2f8afdd25efd52b8f1e3
4
+ data.tar.gz: 4a677ebb54f964c8e339b56dfe2c03d886f669b9
5
+ SHA512:
6
+ metadata.gz: 9ded9261047e1a2f118251b7003ee0113722c3ef9086d9228d8d1a4943c75633ea509e91519f987c6ea93e664e86beadc55c53c8f86e61bf1b9af2546f6f9628
7
+ data.tar.gz: b5b734f5214dbde8371c756974a335e92deba05bf18888687b8a7701eac471b16e99190c863151022fe7b7f46ebbf5634b6d8ed056824b2469cf2c84c4b5b06a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Doga Armangil
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # JsonapiForRails
2
+ A [Rails](http://rubyonrails.org/) 5+ plugin for providing a [JSONAPI v1.0](http://jsonapi.org/format/1.0/) API from your application with very little coding.
3
+
4
+ ## Usage
5
+
6
+ ### 1. Set up one API controller per model
7
+
8
+ Generate a controller for each model that will be accessible from your API. Controller names need to be the plural version of your model names.
9
+
10
+ ```bash
11
+ $ # Go to the root directory of your existing Rails application
12
+ $ cd path/to/railsapp
13
+ $
14
+ $ # Generate your models
15
+ $ bin/rails generate model author
16
+ $ bin/rails generate model article
17
+ $
18
+ $ # Generate your API controllers
19
+ $ bin/rails generate controller authors
20
+ $ bin/rails generate controller articles
21
+ ```
22
+
23
+ Then enable JSONAPI in a parent class of your API controllers.
24
+
25
+ ```ruby
26
+ # app/controllers/application_controller.rb
27
+ class ApplicationController < ActionController::Base # or ActionController::API
28
+
29
+ # Enable JSONAPI
30
+ acts_as_jsonapi_resources
31
+
32
+ # ...
33
+ end
34
+ ```
35
+
36
+ If only some of your controllers are JSONAPI controllers, then create a parent controller for only those controllers, and enable JSONAPI inside that controller rather than `ApplicationController`.
37
+
38
+ ```bash
39
+ $ cat > app/controllers/jsonapi_resources_controller.rb
40
+
41
+ class JsonapiResourcesController < ApplicationController
42
+ acts_as_jsonapi_resources
43
+
44
+ # ...
45
+ end
46
+ ```
47
+
48
+ ```ruby
49
+ # app/controllers/authors_controller.rb
50
+
51
+ # Change the API controller's parent class
52
+ class AuthorsController < JsonapiResourcesController
53
+ # ...
54
+ end
55
+
56
+ # Do the same with ArticlesController
57
+ ```
58
+
59
+ ### 2. Configure your API controller routes
60
+ Update your application routes as follows:
61
+
62
+ ```ruby
63
+ # config/routes.rb
64
+ Rails.application.routes.draw do
65
+ # ...
66
+
67
+ scope '/api/v1' do # Optional scoping
68
+
69
+ [ # List your API controllers here
70
+ :authors, :articles
71
+ ].each do |resources_name|
72
+ resources resources_name do
73
+ controller resources_name do
74
+ get 'relationships/:relationship', action: "relationship_show"
75
+ patch 'relationships/:relationship', action: "relationship_update"
76
+ post 'relationships/:relationship', action: "relationship_add"
77
+ delete 'relationships/:relationship', action: "relationship_remove"
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ # ...
85
+ end
86
+
87
+ ```
88
+
89
+ ### 3. Verify your setup
90
+ After populating your database and launching the built-in Rails server with the `bin/rails server` shell command, you can issue some HTTP requests to your API and verify the correctness of the responses.
91
+
92
+ ```bash
93
+ $ # Get the list of articles
94
+ $ curl 'http://localhost:3000/api/v1/articles'
95
+ {"data":[{"type":"articles","id":184578894},{"type":"articles","id":388548390},{"type":"articles","id":618037523},{"type":"articles","id":994552601}]}
96
+ $
97
+ $ # Get an article
98
+ $ curl 'http://localhost:3000/api/v1/articles/184578894'
99
+ {"data":{"type":"articles","id":618037523,"attributes":{"title":"UK bank pay and bonuses in the spotlight as results season starts","content":"The pay deals handed to the bosses of Britain’s biggest banks will be in focus ...","created_at":"2016-02-22T16:57:43.401Z","updated_at":"2016-02-22T16:57:43.401Z"},"relationships":{"author":{"data":{"type":"authors","id":1023487079}}}}}
100
+ $
101
+ $ # Get only the title of an article, include the author name
102
+ $ curl 'http://localhost:3000/api/v1/articles/184578894?filter%5Barticles%5D=title,author;include=author;filter%5Bauthors%5D=name'
103
+ {"data":{"type":"articles","id":618037523,"attributes":{"title":"UK bank pay and bonuses in the spotlight as results season starts"},"relationships":{"author":{"data":{"type":"authors","id":1023487079}}}},"include":[{"data":{"type":"authors","id":1023487079,"attributes":{"name":"..."},"relationships":{}}}]}
104
+
105
+ ```
106
+
107
+ ## Modifying the default API behaviour
108
+ By default, all API end-points are accessible to all clients, and all end-points behave the same way for all clients. In a real-world setting, you may want to restrict access to an end-point and/or change the behaviour of an end-point depending on the client.
109
+
110
+ ### Client authentication
111
+ Clients can be authenticated with a `before_action` method in your API controller. Inside controllers, instance variable names starting with the `@jsonapi_` prefix and method names starting with the `jsonapi_` prefix are reserved by *jsonapi_for_rails*, so try to avoid those.
112
+
113
+ ```ruby
114
+ # app/controllers/jsonapi_resources_controller.rb
115
+ class JsonapiResourcesController < ApplicationController
116
+ acts_as_jsonapi_resources
117
+
118
+ before_action :authenticate
119
+
120
+ private
121
+ def authenticate
122
+ # ...
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### Access control
128
+ Access control for authenticated and unauthenticated clients can be implemented in `before_action` methods in your API controllers.
129
+
130
+ ```ruby
131
+ # app/controllers/jsonapi_resources_controller.rb
132
+ class JsonapiResourcesController < ApplicationController
133
+ acts_as_jsonapi_resources
134
+
135
+ before_action :permit_read, only: [
136
+ :index,
137
+ :show,
138
+ :relationship_show
139
+ ]
140
+
141
+ before_action :permit_write, only: [
142
+ :create,
143
+ :update,
144
+ :destroy,
145
+ :relationship_update,
146
+ :relationship_add,
147
+ :relationship_remove
148
+ ]
149
+
150
+ private
151
+ def permit_read
152
+ # ...
153
+ end
154
+
155
+ def permit_write
156
+ # ...
157
+ end
158
+ end
159
+ ```
160
+
161
+ ### Overriding an API end-point
162
+ The `bin/rails routes` shell command shows you the end-points that *jsonapi_for_rails* defines. In order to change the behaviour of an action, you can define an action with the same name inside an API controller. *jsonapi_for_rails* provides utility methods and instance variables that can help you.
163
+
164
+ ```ruby
165
+ # app/controllers/articles_controller.rb
166
+ class ArticlesController < JsonapiResourcesController
167
+
168
+ def index
169
+ jsonapi_model_class # => Article
170
+ jsonapi_model_class_name # => "Article"
171
+ jsonapi_model_type # => :articles
172
+
173
+ # ...
174
+ end
175
+
176
+ def show
177
+ @jsonapi_record.to_jsonapi_hash # => {data: {...}}
178
+
179
+ # ...
180
+ end
181
+
182
+ def relationship_show
183
+ @jsonapi_relationship # => {:definition=>{:name=>:author, :type=>:to_one, :receiver=>{:type=>:authors, :class=>Author}}
184
+
185
+ # ...
186
+ end
187
+
188
+ end
189
+ ```
190
+
191
+ ## Implementation status
192
+ * [Inclusion of related resources](http://jsonapi.org/format/1.0/#fetching-includes) is currently only implemented for requests that return a single resource, and relationship paths are not supported.
193
+ * [Sparse fieldsets](http://jsonapi.org/format/1.0/#fetching-sparse-fieldsets) is currently only implemented for requests that return a single resource.
194
+ * [Sorting](http://jsonapi.org/format/1.0/#fetching-sorting) is currently not implemented.
195
+ * [Pagination](http://jsonapi.org/format/1.0/#fetching-pagination) is currently not implemented.
196
+ * [Deleting resources](http://jsonapi.org/format/1.0/#crud-deleting) is currently not implemented.
197
+ * Test coverage is sparse.
198
+
199
+ ## Installation
200
+
201
+ ### Edge version
202
+
203
+ ```bash
204
+ $ # Clone this repository
205
+ $ git clone 'https://github.com/doga/jsonapi_for_rails.git'
206
+ $
207
+ $ # Update your Rails application's gem file
208
+ $ cat >> path/to/Gemfile
209
+
210
+ group :development do
211
+ gem 'jsonapi_for_rails', path: 'path/to/jsonapi_for_rails'
212
+ #gem 'jsonapi_for_rails', git: 'https://github.com/doga/jsonapi_for_rails.git'
213
+ end
214
+ ```
215
+
216
+ ### Latest stable version
217
+
218
+ ```bash
219
+ $ # Update your Rails application's gem file
220
+ $ cat >> path/to/Gemfile
221
+
222
+ gem 'jsonapi_for_rails'
223
+ ```
224
+
225
+ ## Contributing
226
+ If you find a bug in this project, have trouble following the documentation or have a question about the project – create an [issue](https://guides.github.com/activities/contributing-to-open-source/#contributing).
227
+
228
+ ## License
229
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'JsonapiForRails'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,293 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module Actions
4
+ module Object
5
+ def self.included receiver
6
+ receiver.send :include, InstanceMethods
7
+ run_macros receiver
8
+ end
9
+
10
+ module InstanceMethods
11
+ # TODO: pagination
12
+ def index
13
+ @json = {data: []}
14
+ jsonapi_model_class.all.each do |record|
15
+ @json[:data] << {
16
+ type: record.class.to_s.underscore.pluralize, # TODO: factor out type generation from class
17
+ id: record.id
18
+ }
19
+ end
20
+ render_json @json
21
+ end
22
+
23
+ # implements Create and Update operations
24
+ def create
25
+ begin
26
+ # attributes
27
+ attrs = received_attributes
28
+ if attrs
29
+ if @jsonapi_record
30
+ # update
31
+ @jsonapi_record.update! attrs
32
+ else
33
+ # create
34
+ @jsonapi_record = jsonapi_model_class.create! attrs
35
+ end
36
+ end
37
+
38
+ # relationships
39
+ received_relationships.each do |relationship|
40
+ # to-one
41
+ if relationship[:definition][:type] == :to_one
42
+ @jsonapi_record.send :"#{relationship[:definition][:name]}=", relationship[:params][:data]
43
+ next
44
+ end
45
+
46
+ # to-many
47
+ @jsonapi_record.send(relationship[:definition][:name]).send :clear # initialize the relation
48
+
49
+ relationship[:params][:data].each do |item|
50
+ object = relationship[:receiver][:class].find_by_id item[:id]
51
+ @jsonapi_record.send(relationship[:definition][:name]).send :<<, object
52
+ end
53
+ end
54
+ rescue NameError => e
55
+
56
+ # error when creating record
57
+ render_error 500, "Model class not found."
58
+ return
59
+ rescue
60
+ # error when creating relationship?
61
+ @jsonapi_record.destroy if @jsonapi_record
62
+
63
+ render_error 500, "Record could not be created."
64
+ return
65
+ end
66
+ show
67
+ end
68
+
69
+ def show
70
+ @json = @jsonapi_record.to_jsonapi_hash(
71
+ @jsonapi_sparse_fieldsets[jsonapi_model_type]
72
+ )
73
+ #$stderr.puts "#{@json}"
74
+
75
+ # Include resources
76
+ # TODO: relationship paths when including resources (http://jsonapi.org/format/1.0/#fetching-includes)
77
+ if @jsonapi_include and @json[:data][:relationships]
78
+ @json[:include] = []
79
+ @jsonapi_include.each do |rel_name|
80
+ rel = @json[:data][:relationships][rel_name]
81
+ next unless rel
82
+ rel = rel[:data]
83
+ next unless rel
84
+ rel = [rel] if rel.kind_of?(Hash)
85
+ rel.each do |r|
86
+ type = r[:type].to_sym
87
+ klass = nil
88
+ begin
89
+ klass = r[:type].singularize.camelize.constantize
90
+ rescue NameError => e
91
+ next
92
+ end
93
+ r = klass.find_by_id r[:id]
94
+ next unless r
95
+
96
+ @json[:include] << r.to_jsonapi_hash(
97
+ @jsonapi_sparse_fieldsets[type]
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ render_json @json
104
+ end
105
+
106
+ def update
107
+ create
108
+ end
109
+
110
+ def destroy
111
+ render_error 500, "Not implemented."
112
+ end
113
+
114
+ # private
115
+
116
+ # Extracts record attributes from received params.
117
+ # Use this for creating/updating a database record.
118
+ # Note that relationships (has_one associations etc) are filtered out
119
+ # but are still available in the original params.
120
+ def received_attributes
121
+ begin
122
+ params.require(
123
+ :data
124
+ ).require(
125
+ :attributes
126
+ ).permit(
127
+ *jsonapi_model_class.attribute_names
128
+ ).reject do |key, value|
129
+ # ignore automatically generated attributes
130
+ %w(
131
+ id
132
+ created_at created_on
133
+ updated_at updated_on
134
+ ).include?(
135
+ key.to_s
136
+ ) or
137
+
138
+ # ignore reference attributes
139
+ key.to_s =~ /_id$/
140
+ end
141
+ rescue ActionController::ParameterMissing => e
142
+ nil
143
+ end
144
+ end
145
+
146
+ def relationships
147
+ jsonapi_model_class.reflect_on_all_associations.collect do |association|
148
+ type = nil
149
+
150
+ type = :to_one if [
151
+ ActiveRecord::Reflection::HasOneReflection,
152
+ ActiveRecord::Reflection::BelongsToReflection
153
+ ].include? association.class
154
+
155
+ type = :to_many if [
156
+ ActiveRecord::Reflection::HasManyReflection,
157
+ ActiveRecord::Reflection::HasAndBelongsToManyReflection
158
+ ].include? association.class
159
+
160
+ next unless type
161
+
162
+ {
163
+ name: association.name,
164
+ type: type,
165
+ receiver: {
166
+ type: association.klass.to_s.underscore.pluralize.to_sym,
167
+ class: association.klass.to_s.constantize
168
+ }
169
+ }
170
+ end.compact
171
+ end
172
+
173
+ def received_relationships
174
+ rels = relationships
175
+ if params[:relationship] # only one relationship received for relationship action
176
+ rels.select! do |rel|
177
+ rel[:name].to_sym == params[:relationship].to_sym
178
+ end
179
+ if request.method == "GET"
180
+ # no relationship received, return definition only
181
+ return rels.collect do |rel|
182
+ {definition: rel}
183
+ end
184
+ end
185
+ end
186
+ rels.collect do |relationship|
187
+ begin
188
+ received_params = nil
189
+ if params[:relationship]
190
+ received_params = params.permit({
191
+ data: [
192
+ :type, :id
193
+ ]
194
+ })
195
+ else
196
+ received_params = params.require(
197
+ :data
198
+ ).require(
199
+ :relationships
200
+ ).require(
201
+ relationship[:name]
202
+ ).permit({
203
+ data: [
204
+ :type, :id
205
+ ]
206
+ })
207
+ end
208
+ # => {"data"=>{"type"=>"users", "id"=>1}} # sample value for a to-one association
209
+ # => {"data"=>[{"type"=>"properties", "id"=>1}, {"type"=>"properties", "id"=>2}]} # sample value for a to-many association
210
+
211
+ # is received data conformant to the database schema?
212
+ conformant = true
213
+ loop do
214
+ # to-many
215
+ if received_params[:data].kind_of? Array
216
+ if relationship[:type] != :to_many
217
+ conformant = false
218
+ break
219
+ end
220
+ received_params[:data].each do |item|
221
+ next if item[:type] == relationship[:receiver][:type]
222
+ conformant = false
223
+ break
224
+ end
225
+ break
226
+ end
227
+
228
+ # to-one
229
+ if relationship[:type] != :to_one
230
+ conformant = false
231
+ break
232
+ end
233
+ conformant = false unless received_params[:data][:type] == relationship[:receiver][:type]
234
+
235
+ break
236
+ end
237
+ next unless conformant
238
+
239
+ {
240
+ definition: relationship,
241
+ params: received_params
242
+ }
243
+ rescue ActionController::ParameterMissing => e
244
+
245
+ # nil assignment to to-one relationship?
246
+ if relationship[:type] == :to_one
247
+ begin
248
+ if params[:relationship] # relationship action
249
+ received_params = params.permit(
250
+ :data
251
+ )
252
+ else
253
+ received_params = params.require(
254
+ :data
255
+ ).require(
256
+ :relationships
257
+ ).require(
258
+ relationship[:name]
259
+ ).permit(
260
+ :data
261
+ )
262
+ end
263
+
264
+ # received nil?
265
+ next if received_params[:data] # TODO: should return error to client?
266
+
267
+ next {
268
+ definition: relationship,
269
+ params: received_params
270
+ }
271
+ rescue ActionController::ParameterMissing => e
272
+ end
273
+ end
274
+
275
+ nil
276
+ end
277
+ end.compact
278
+ end
279
+
280
+ end
281
+
282
+ def self.run_macros receiver
283
+ receiver.instance_exec do
284
+ private :received_attributes
285
+ private :relationships
286
+ private :received_relationships
287
+ end
288
+ end
289
+
290
+ end
291
+ end
292
+
293
+ end
@@ -0,0 +1,111 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module Actions
4
+ module Relationship
5
+ def self.included receiver
6
+ receiver.send :include, InstanceMethods
7
+ run_macros receiver
8
+ end
9
+
10
+ module InstanceMethods
11
+ # GET
12
+ def relationship_show
13
+ #$stderr.puts "JsonapiForRails::Controller::Actions::Relationship#relationship_show called"
14
+ rel = @jsonapi_record.send @jsonapi_relationship[:definition][:name]
15
+
16
+ @json = nil
17
+ if @jsonapi_relationship[:definition][:type] == :to_one
18
+ @json = {
19
+ type: @jsonapi_relationship[:definition][:receiver][:type],
20
+ id: rel.id
21
+ }
22
+ elsif @jsonapi_relationship[:definition][:type] == :to_many
23
+ @json = rel.collect do |r|
24
+ {
25
+ type: @jsonapi_relationship[:definition][:receiver][:type],
26
+ id: r.id
27
+ }
28
+ end
29
+ end
30
+ @json = {data: @json}
31
+
32
+ render_json @json
33
+ end
34
+
35
+ # PATCH
36
+ def relationship_update
37
+ if @jsonapi_relationship[:definition][:type] == :to_many
38
+ render_error 403, 'Replacing all members of a to-many relationship is forbidden.'
39
+ return
40
+ end
41
+
42
+ related = nil
43
+ if @jsonapi_relationship[:params][:data]
44
+ related = @jsonapi_relationship[:definition][:receiver][:class].find_by_id(
45
+ @jsonapi_relationship[:params][:data][:id]
46
+ )
47
+ unless related
48
+ render_error 403, 'Record not found.'
49
+ return
50
+ end
51
+ end
52
+
53
+ @jsonapi_record.send :"#{@jsonapi_relationship[:definition][:name]}=", related
54
+ @jsonapi_record.save
55
+ end
56
+
57
+ # POST for to-many relations only
58
+ def relationship_add
59
+ unless @jsonapi_relationship[:definition][:type] == :to_many
60
+ render_error 403, 'Operation allowed for to-many relationships only.'
61
+ return
62
+ end
63
+
64
+ records = @jsonapi_relationship[:params][:data].collect do |record|
65
+ record = @jsonapi_relationship[:definition][:receiver][:class].find_by_id(
66
+ record[:id]
67
+ )
68
+ unless record
69
+ render_error 403, "Non-existing record #{record.inspect}."
70
+ return
71
+ end
72
+ record
73
+ end
74
+
75
+ records.each do |record|
76
+ @jsonapi_record.send(@jsonapi_relationship[:definition][:name]) << record
77
+ end
78
+ end
79
+
80
+ # DELETE for to-many relations only
81
+ def relationship_remove
82
+ unless @jsonapi_relationship[:definition][:type] == :to_many
83
+ render_error 403, 'Operation allowed for to-many relationships only.'
84
+ return
85
+ end
86
+
87
+ records = @jsonapi_relationship[:params][:data].collect do |record|
88
+ record = @jsonapi_relationship[:definition][:receiver][:class].find_by_id(
89
+ record[:id]
90
+ )
91
+ unless record
92
+ render_error 403, "Non-existing record #{record.inspect}."
93
+ return
94
+ end
95
+ record
96
+ end
97
+
98
+ records.each do |record|
99
+ @jsonapi_record.send(@jsonapi_relationship[:definition][:name]).delete record
100
+ end
101
+ end
102
+ end
103
+
104
+ def self.run_macros receiver
105
+ receiver.instance_exec do
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ end
@@ -0,0 +1,30 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module BeforeActions
4
+ module Include
5
+
6
+ def self.included receiver
7
+ receiver.send :include, InstanceMethods
8
+ run_macros receiver
9
+ end
10
+
11
+ def self.run_macros receiver
12
+ receiver.instance_exec do
13
+ before_action :jsonapi_include
14
+ private :jsonapi_include
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ def jsonapi_include
20
+ #@jsonapi_include = nil
21
+ return unless params[:include]
22
+
23
+ @jsonapi_include = params[:include].split(',').map{|rel| rel.strip.to_sym }
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,51 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module BeforeActions
4
+ module Record
5
+
6
+ def self.included receiver
7
+ #$stderr.puts "JsonapiForRails::Controller::RecordFromRequest included into #{receiver}"
8
+ receiver.send :include, InstanceMethods
9
+ run_macros receiver
10
+ end
11
+
12
+ def self.run_macros receiver
13
+ receiver.instance_exec do
14
+ before_action :jsonapi_require_record, except: [
15
+ :index,
16
+ :create
17
+ ]
18
+ private :jsonapi_require_record
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def jsonapi_require_record
24
+ #$stderr.puts "@jsonapi_sparse_fieldsets: #{@jsonapi_sparse_fieldsets.inspect}"
25
+ #$stderr.puts "JsonapiForRails::Controller::RecordFromRequest#jsonapi_require_record called"
26
+ if params[:relationship]
27
+ # relationship action
28
+ @jsonapi_record = jsonapi_model_class.find_by_id params["#{jsonapi_model_class_name.underscore}_id"].to_i
29
+ else
30
+ # CRUD action
31
+ @jsonapi_record = jsonapi_model_class
32
+ =begin
33
+ if false and @jsonapi_sparse_fieldsets[jsonapi_model_type]
34
+ @jsonapi_record = @jsonapi_record.select(
35
+ @jsonapi_sparse_fieldsets[jsonapi_model_type]
36
+ )
37
+ end
38
+ =end
39
+ @jsonapi_record = @jsonapi_record.find_by_id params[:id].to_i
40
+ end
41
+ #$stderr.puts "@jsonapi_record: #{@jsonapi_record.inspect}"
42
+ return if @jsonapi_record
43
+
44
+ render_error 401, "Bad request."
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,34 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module BeforeActions
4
+ module Relationship
5
+
6
+ def self.included receiver
7
+ receiver.send :include, InstanceMethods
8
+ run_macros receiver
9
+ end
10
+
11
+ def self.run_macros receiver
12
+ receiver.instance_exec do
13
+ before_action :jsonapi_require_relationship, only: [
14
+ :relationship_show,
15
+ :relationship_update,
16
+ :relationship_add,
17
+ :relationship_remove
18
+ ]
19
+ private :jsonapi_require_relationship
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ def jsonapi_require_relationship
25
+ #$stderr.puts "JsonapiForRails::Controller::RelationshipFromRequest#jsonapi_require_relationship called"
26
+ @jsonapi_relationship = received_relationships.first
27
+ return if @jsonapi_relationship
28
+
29
+ render_error 401, "Bad request."
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module BeforeActions
4
+ module SparseFieldsets
5
+
6
+ def self.included receiver
7
+ receiver.send :include, InstanceMethods
8
+ run_macros receiver
9
+ end
10
+
11
+ def self.run_macros receiver
12
+ receiver.instance_exec do
13
+ before_action :jsonapi_sparse_fieldsets
14
+ private :jsonapi_sparse_fieldsets
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ def jsonapi_sparse_fieldsets
20
+ @jsonapi_sparse_fieldsets = {}
21
+ return unless params[:fields]
22
+
23
+ params[:fields].each do |resources_name, fields|
24
+ resources_name = resources_name.to_sym
25
+ fields =
26
+ fields.split(',').
27
+ map{|field| field.strip.to_sym }.
28
+ select{|e| e =~ /^[A-Za-z1-9_]+$/} # BUG: selector too restrictive
29
+ next if fields.size.zero?
30
+ @jsonapi_sparse_fieldsets[resources_name] = fields#.join(',')
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,36 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module Utils
4
+ module Model
5
+ def self.included receiver
6
+ #$stderr.puts "JsonapiForRails::Controller::ModelUtils included into #{receiver}"
7
+ receiver.send :include, InstanceMethods
8
+ run_macros receiver
9
+ end
10
+
11
+ module InstanceMethods
12
+ def jsonapi_model_class_name
13
+ controller_class_name = "#{self.class}"
14
+ controller_class_name.underscore.split('_')[0..-2].join('_').camelize.singularize
15
+ end
16
+
17
+ def jsonapi_model_class
18
+ jsonapi_model_class_name.constantize # Object.const_get jsonapi_model_class_name
19
+ end
20
+
21
+ # used in returned JSON API data
22
+ def jsonapi_model_type
23
+ jsonapi_model_class_name.underscore.pluralize.to_sym
24
+ end
25
+ end
26
+
27
+ def self.run_macros receiver
28
+ receiver.instance_exec do
29
+ private :jsonapi_model_class_name, :jsonapi_model_class, :jsonapi_model_type
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,59 @@
1
+ module JsonapiForRails::Controller
2
+
3
+ module Utils
4
+ module Render
5
+ def self.included receiver
6
+ receiver.send :include, InstanceMethods
7
+ run_macros receiver
8
+ end
9
+
10
+ JSONAPI = {
11
+ specification: 'http://jsonapi.org/format/',
12
+ content_type: 'application/vnd.api+json'
13
+ }.freeze
14
+
15
+ def self.run_macros receiver
16
+ receiver.instance_exec do
17
+ private :render_json, :render_error
18
+ end
19
+ end
20
+
21
+ module InstanceMethods
22
+ def render_json object
23
+ unless object
24
+ render_error 500, 'No message specified.'
25
+ return
26
+ end
27
+
28
+ @status = 200
29
+ @json = object
30
+ @content_type = JSONAPI[:content_type]
31
+
32
+ render(
33
+ json: @json,
34
+ status: @status,
35
+ content_type: @content_type
36
+ )
37
+ end
38
+
39
+ def render_error status, title
40
+ @status = status
41
+ @json = {
42
+ errors: [
43
+ {title: title}
44
+ ]
45
+ }
46
+ @content_type = JSONAPI[:content_type]
47
+
48
+ render(
49
+ json: @json,
50
+ status: @status,
51
+ content_type: @content_type
52
+ )
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,33 @@
1
+ require "jsonapi_for_rails/controller/utils/model"
2
+ require "jsonapi_for_rails/controller/utils/render"
3
+ require "jsonapi_for_rails/controller/before_actions/sparse_fieldsets"
4
+ require "jsonapi_for_rails/controller/before_actions/include"
5
+ require "jsonapi_for_rails/controller/before_actions/record"
6
+ require "jsonapi_for_rails/controller/before_actions/relationship"
7
+ require "jsonapi_for_rails/controller/actions/object"
8
+ require "jsonapi_for_rails/controller/actions/relationship"
9
+
10
+
11
+ module JsonapiForRails::Controller
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ #$stderr.puts "JsonapiForRails::Controller included into #{self}"
16
+ end
17
+
18
+ class_methods do
19
+ def acts_as_jsonapi_resources model: nil
20
+ #$stderr.puts "JsonapiForRails::Controller macro called from #{self}:\n acts_as_jsonapi_resources(model: #{model or 'nil'})"
21
+
22
+ include JsonapiForRails::Controller::Utils::Model
23
+ include JsonapiForRails::Controller::Utils::Render
24
+ include JsonapiForRails::Controller::BeforeActions::SparseFieldsets
25
+ include JsonapiForRails::Controller::BeforeActions::Include
26
+ include JsonapiForRails::Controller::BeforeActions::Record
27
+ include JsonapiForRails::Controller::BeforeActions::Relationship
28
+ include JsonapiForRails::Controller::Actions::Object
29
+ include JsonapiForRails::Controller::Actions::Relationship
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,94 @@
1
+ module JsonapiForRails::Model
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ #$stderr.puts "JsonapiForRails::Model included into #{self}"
6
+
7
+ # Define instance methods
8
+ class_exec do
9
+ def to_jsonapi_hash sparse_fieldset=nil
10
+ #$stderr.puts "JsonapiForRails::Controller::Actions::Object#show called"
11
+
12
+ # attributes
13
+ attrs = attributes.reject do |key, value|
14
+ key =~ /^id$|_id$/
15
+ end
16
+ if sparse_fieldset
17
+ attrs.reject! do |key, value|
18
+ not sparse_fieldset.find{|f| key.to_sym == f}
19
+ end
20
+ end
21
+
22
+ # relationships
23
+ relationships = {}
24
+ self.class.reflect_on_all_associations.each do |association|
25
+ if sparse_fieldset
26
+ next unless sparse_fieldset.find{|f| association.name == f}
27
+ end
28
+ relationship = {}
29
+ relationships[association.name] = relationship
30
+
31
+ # to-many relationship
32
+ if [
33
+ ActiveRecord::Reflection::HasManyReflection,
34
+ ActiveRecord::Reflection::HasAndBelongsToManyReflection
35
+ ].include? association.class
36
+
37
+ relationship[:data] = []
38
+ #$stderr.puts "\nreading relationship '#{association.name}' of class '#{association.class}'"
39
+ #$stderr.puts "#{@record.send(association.name).inspect}"
40
+ self.send(association.name).each do |record|
41
+ #$stderr.puts "self.#{association.name}: #{record.class}"
42
+ relationship[:data] << {
43
+ type: record.class.to_s.underscore.pluralize, # TODO: factor out type generation from class
44
+ id: record.id
45
+ }
46
+ end
47
+
48
+ # to-one relationship
49
+ elsif [
50
+
51
+ ActiveRecord::Reflection::HasOneReflection,
52
+ ActiveRecord::Reflection::BelongsToReflection
53
+ ].include? association.class
54
+
55
+ relationship[:data] = nil
56
+ #$stderr.puts "\nreading relationship '#{association.name}' of class '#{association.class}'"
57
+ #$stderr.puts "#{self.send(association.name).inspect}"
58
+ if record = self.send(association.name)
59
+ relationship[:data] = {
60
+ type: record.class.to_s.underscore.pluralize, # TODO: factor out type generation from class
61
+ id: record.id
62
+ }
63
+ end
64
+ end
65
+ end
66
+
67
+ # message
68
+ {
69
+ =begin
70
+ meta: {
71
+ generated_by_class: "#{self.class}"
72
+ },
73
+ =end
74
+ data: {
75
+ type: jsonapi_model_type,
76
+ id: self.id,
77
+
78
+ attributes: attrs,
79
+
80
+ relationships: relationships
81
+ }
82
+ }
83
+
84
+ end
85
+
86
+ def jsonapi_model_type
87
+ "#{self.class}".underscore.pluralize.to_sym
88
+ end
89
+
90
+ private :jsonapi_model_type
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module JsonapiForRails
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,15 @@
1
+
2
+ # TODO: send pull request to add jsonapi_for_rails to JSON API implementations list (http://jsonapi.org/implementations/)
3
+ # TODO: double-check the installation instructions in README.md
4
+ # TODO: 'Contributing' section in README.md
5
+
6
+ require "jsonapi_for_rails/version"
7
+ require "jsonapi_for_rails/controller"
8
+ require "jsonapi_for_rails/model"
9
+
10
+ # Add 'acts_as_jsonapi_resources' class method to controllers
11
+ ActionController::Metal.send :include, JsonapiForRails::Controller
12
+
13
+ # Add 'to_jsonapi_hash' instance method to models
14
+ ActiveRecord::Base.send :include, JsonapiForRails::Model
15
+
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :jsonapi_for_rails do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsonapi_for_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Doga Armangil
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0.beta2
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 5.0.0.beta2
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.1'
33
+ - !ruby/object:Gem::Dependency
34
+ name: sqlite3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Jsonapi for Rails empowers your JSON API (http://jsonapi.org/format/)
48
+ compliant Rails APIs. Implement your REST API with very little coding.
49
+ email:
50
+ - doga.armangil@alumni.epfl.ch
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - MIT-LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - lib/jsonapi_for_rails.rb
59
+ - lib/jsonapi_for_rails/controller.rb
60
+ - lib/jsonapi_for_rails/controller/actions/object.rb
61
+ - lib/jsonapi_for_rails/controller/actions/relationship.rb
62
+ - lib/jsonapi_for_rails/controller/before_actions/include.rb
63
+ - lib/jsonapi_for_rails/controller/before_actions/record.rb
64
+ - lib/jsonapi_for_rails/controller/before_actions/relationship.rb
65
+ - lib/jsonapi_for_rails/controller/before_actions/sparse_fieldsets.rb
66
+ - lib/jsonapi_for_rails/controller/utils/model.rb
67
+ - lib/jsonapi_for_rails/controller/utils/render.rb
68
+ - lib/jsonapi_for_rails/model.rb
69
+ - lib/jsonapi_for_rails/version.rb
70
+ - lib/tasks/jsonapi_for_rails_tasks.rake
71
+ homepage: https://github.com/doga/jsonapi_for_rails
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.5.1
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A Rails plugin for providing JSON API compliant APIs with very little coding.
95
+ test_files: []