jsonapi_for_rails 0.1.0

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.
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: []