introspective_grape 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +35 -0
- data/Gemfile +15 -0
- data/MIT-LICENSE +20 -0
- data/README.md +103 -0
- data/Rakefile +26 -0
- data/app/assets/images/introspective_grape/.keep +0 -0
- data/app/assets/stylesheets/introspective_grape/.keep +0 -0
- data/app/controllers/.keep +0 -0
- data/app/helpers/.keep +0 -0
- data/app/mailers/.keep +0 -0
- data/app/models/.keep +0 -0
- data/app/views/.keep +0 -0
- data/bin/rails +12 -0
- data/introspective_grape.gemspec +49 -0
- data/lib/introspective_grape/api.rb +445 -0
- data/lib/introspective_grape/camel_snake.rb +71 -0
- data/lib/introspective_grape/version.rb +3 -0
- data/lib/introspective_grape.rb +4 -0
- data/lib/tasks/introspective_grape_tasks.rake +4 -0
- data/spec/dummy/Gemfile +4 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/api/active_record_helpers.rb +17 -0
- data/spec/dummy/app/api/api_helpers.rb +36 -0
- data/spec/dummy/app/api/dummy/chat_api.rb +108 -0
- data/spec/dummy/app/api/dummy/company_api.rb +8 -0
- data/spec/dummy/app/api/dummy/entities.rb +25 -0
- data/spec/dummy/app/api/dummy/location_api.rb +37 -0
- data/spec/dummy/app/api/dummy/project_api.rb +51 -0
- data/spec/dummy/app/api/dummy/role_api.rb +7 -0
- data/spec/dummy/app/api/dummy/sessions.rb +55 -0
- data/spec/dummy/app/api/dummy/user_api.rb +32 -0
- data/spec/dummy/app/api/dummy_api.rb +57 -0
- data/spec/dummy/app/api/error_handlers.rb +28 -0
- data/spec/dummy/app/api/permissions_helper.rb +7 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/abstract_adapter.rb +13 -0
- data/spec/dummy/app/models/admin_user.rb +6 -0
- data/spec/dummy/app/models/chat.rb +18 -0
- data/spec/dummy/app/models/chat_message.rb +34 -0
- data/spec/dummy/app/models/chat_message_user.rb +17 -0
- data/spec/dummy/app/models/chat_user.rb +16 -0
- data/spec/dummy/app/models/company.rb +14 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/image.rb +21 -0
- data/spec/dummy/app/models/job.rb +10 -0
- data/spec/dummy/app/models/locatable.rb +6 -0
- data/spec/dummy/app/models/location.rb +26 -0
- data/spec/dummy/app/models/location_beacon.rb +16 -0
- data/spec/dummy/app/models/location_gps.rb +14 -0
- data/spec/dummy/app/models/project.rb +20 -0
- data/spec/dummy/app/models/project_job.rb +7 -0
- data/spec/dummy/app/models/role.rb +30 -0
- data/spec/dummy/app/models/super_user.rb +11 -0
- data/spec/dummy/app/models/team.rb +9 -0
- data/spec/dummy/app/models/team_user.rb +13 -0
- data/spec/dummy/app/models/user/chatter.rb +79 -0
- data/spec/dummy/app/models/user.rb +84 -0
- data/spec/dummy/app/models/user_location.rb +28 -0
- data/spec/dummy/app/models/user_project_job.rb +16 -0
- data/spec/dummy/app/policies/application_policy.rb +47 -0
- data/spec/dummy/app/policies/chat_policy.rb +22 -0
- data/spec/dummy/app/policies/company_policy.rb +32 -0
- data/spec/dummy/app/policies/location_policy.rb +29 -0
- data/spec/dummy/app/policies/project_policy.rb +42 -0
- data/spec/dummy/app/policies/role_policy.rb +33 -0
- data/spec/dummy/app/policies/user_location_policy.rb +12 -0
- data/spec/dummy/app/policies/user_policy.rb +8 -0
- data/spec/dummy/app/views/layouts/application.html.erb +13 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +38 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml +23 -0
- data/spec/dummy/config/environment.rb +11 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +43 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/devise.rb +262 -0
- data/spec/dummy/config/initializers/devise_async.rb +2 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/paperclip.rb +13 -0
- data/spec/dummy/config/initializers/paperclip_adapter.rb +13 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/devise.en.yml +60 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +8 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20141002205024_devise_create_users.rb +42 -0
- data/spec/dummy/db/migrate/20141002211055_devise_create_admin_users.rb +48 -0
- data/spec/dummy/db/migrate/20141002211057_create_active_admin_comments.rb +19 -0
- data/spec/dummy/db/migrate/20141002220722_add_lockable_to_users.rb +8 -0
- data/spec/dummy/db/migrate/20150406213646_create_companies.rb +11 -0
- data/spec/dummy/db/migrate/20150414213154_add_user_authentication_token.rb +11 -0
- data/spec/dummy/db/migrate/20150415222005_create_roles.rb +12 -0
- data/spec/dummy/db/migrate/20150505181635_create_chats.rb +9 -0
- data/spec/dummy/db/migrate/20150505181636_create_chat_users.rb +11 -0
- data/spec/dummy/db/migrate/20150505181640_create_chat_messages.rb +11 -0
- data/spec/dummy/db/migrate/20150507191529_create_chat_message_users.rb +11 -0
- data/spec/dummy/db/migrate/20150601200526_create_locations.rb +12 -0
- data/spec/dummy/db/migrate/20150601200533_create_locatables.rb +10 -0
- data/spec/dummy/db/migrate/20150601212924_create_location_beacons.rb +15 -0
- data/spec/dummy/db/migrate/20150601213542_create_location_gps.rb +12 -0
- data/spec/dummy/db/migrate/20150609201823_create_user_locations.rb +14 -0
- data/spec/dummy/db/migrate/20150616205336_add_role_user_constraint.rb +9 -0
- data/spec/dummy/db/migrate/20150617232519_create_projects.rb +10 -0
- data/spec/dummy/db/migrate/20150617232521_create_jobs.rb +9 -0
- data/spec/dummy/db/migrate/20150617232522_create_project_jobs.rb +11 -0
- data/spec/dummy/db/migrate/20150623170133_create_user_project_jobs.rb +12 -0
- data/spec/dummy/db/migrate/20150701234929_create_teams.rb +11 -0
- data/spec/dummy/db/migrate/20150701234930_create_team_users.rb +11 -0
- data/spec/dummy/db/migrate/20150727214950_add_confirmable_to_devise.rb +11 -0
- data/spec/dummy/db/migrate/20150820190524_add_user_names.rb +6 -0
- data/spec/dummy/db/migrate/20150824215701_create_images.rb +15 -0
- data/spec/dummy/db/migrate/20150909225019_add_password_to_project.rb +5 -0
- data/spec/dummy/db/schema.rb +278 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/fixtures/images/avatar.jpeg +0 -0
- data/spec/fixtures/images/exif.jpeg +0 -0
- data/spec/models/chat_spec.rb +32 -0
- data/spec/models/image_spec.rb +14 -0
- data/spec/models/locatable_spec.rb +10 -0
- data/spec/models/project_spec.rb +17 -0
- data/spec/models/role_spec.rb +63 -0
- data/spec/models/team_spec.rb +17 -0
- data/spec/models/team_user_spec.rb +20 -0
- data/spec/models/user_location_spec.rb +35 -0
- data/spec/models/user_project_job_spec.rb +30 -0
- data/spec/models/user_spec.rb +125 -0
- data/spec/rails_helper.rb +23 -0
- data/spec/requests/chat_api_spec.rb +174 -0
- data/spec/requests/company_api_spec.rb +61 -0
- data/spec/requests/location_api_spec.rb +96 -0
- data/spec/requests/project_api_spec.rb +151 -0
- data/spec/requests/role_api_spec.rb +37 -0
- data/spec/requests/sessions_api_spec.rb +55 -0
- data/spec/requests/user_api_spec.rb +191 -0
- data/spec/support/blueprints.rb +103 -0
- data/spec/support/location_helper.rb +56 -0
- data/spec/support/pundit_helpers.rb +13 -0
- data/spec/support/request_helpers.rb +22 -0
- metadata +562 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
require 'action_controller'
|
|
2
|
+
|
|
3
|
+
module IntrospectiveGrape
|
|
4
|
+
# Allow files to be uploaded through ActionController:
|
|
5
|
+
ActionController::Parameters::PERMITTED_SCALAR_TYPES.push Rack::Multipart::UploadedFile, ActionController::Parameters
|
|
6
|
+
|
|
7
|
+
class API < Grape::API
|
|
8
|
+
# Generate uniform RESTful APIs for an ActiveRecord Model:
|
|
9
|
+
#
|
|
10
|
+
# class <Some API Controller> < IntrospectiveGrape::API
|
|
11
|
+
# exclude_actions Model, :index,:show,:create,:update,:destroy
|
|
12
|
+
# default_includes Model, <associations for eager loading>
|
|
13
|
+
# restful <Model Class>, [<strong, param, fields>]
|
|
14
|
+
#
|
|
15
|
+
# class <Model>Entity < Grape::Entity
|
|
16
|
+
# expose :id, :attribute
|
|
17
|
+
# expose :association, using: <Association>Entity>
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# To define a Grape param type for a virtual attribute or override the defaut param
|
|
22
|
+
# type from model introspection, define a class method in the model with the param
|
|
23
|
+
# types for the attributes specified in a hash:
|
|
24
|
+
#
|
|
25
|
+
# def self.attribute_param_types
|
|
26
|
+
# { "<attribute name>" => Virtus::Attribute::Boolean }
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# For nested models declared in Rails' strong params both the Grape params for the
|
|
30
|
+
# nested params as well as nested routes will be declared, allowing for
|
|
31
|
+
# a good deal of flexibility for API consumers out of the box, nested params for
|
|
32
|
+
# bulk updates and nested routes for interacting with single records.
|
|
33
|
+
#
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
PLURAL_REFLECTIONS = [ ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasManyReflection]
|
|
37
|
+
|
|
38
|
+
Pg2Ruby = {
|
|
39
|
+
# mapping of activerecord/postgres 'type's to ruby data classes, where they differ
|
|
40
|
+
datetime: DateTime
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def exclude_actions(model, *args)
|
|
44
|
+
@exclude_actions ||= {}
|
|
45
|
+
@@api_actions ||= [:index,:show,:create,:update,:destroy,nil]
|
|
46
|
+
raise "#{model.name} defines invalid exclude_actions: #{args-@@api_actions}" if (args.flatten-@@api_actions).present?
|
|
47
|
+
@exclude_actions[model.name] = args.present? ? args.flatten : @exclude_actions[model.name] || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def default_includes(model, *args)
|
|
51
|
+
@default_includes ||= {}
|
|
52
|
+
@default_includes[model.name] = args.present? ? args.flatten : @default_includes[model.name] || []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def inherited(child)
|
|
56
|
+
super(child)
|
|
57
|
+
child.before do
|
|
58
|
+
# Convert incoming camel case params to snake case: grape will totally blow this
|
|
59
|
+
# if the params hash is not a Hashie::Mash, so make it one of those:
|
|
60
|
+
@params = Hashie::Mash.new(snake_keys(params))
|
|
61
|
+
# ensure that a user is logged in
|
|
62
|
+
authorize!
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# We will probably need before and after hooks eventually, but haven't yet...
|
|
67
|
+
#api_actions.each do |a|
|
|
68
|
+
# define_method "before_#{a}_hook" do |model,params| ; end
|
|
69
|
+
# define_method "after_#{a}_hook" do |model,params| ; end
|
|
70
|
+
#end
|
|
71
|
+
|
|
72
|
+
def restful(model, strong_params=[], routes=[])
|
|
73
|
+
# Recursively define endpoints for the model and any nested models.
|
|
74
|
+
#
|
|
75
|
+
# model: the model class for the API
|
|
76
|
+
# whitelist: a list of fields in Rail's strong params structure, also used to
|
|
77
|
+
# generate grape's permitted params.
|
|
78
|
+
# routes: An array of OpenStruct representations of a nested route's ancestors
|
|
79
|
+
#
|
|
80
|
+
|
|
81
|
+
# Defining the api will break pending migrations during db:migrate, so bail:
|
|
82
|
+
begin ActiveRecord::Migration.check_pending! rescue return end
|
|
83
|
+
|
|
84
|
+
# normalize the whitelist to symbols
|
|
85
|
+
strong_params.map!{|f| f.kind_of?(String) ? f.to_sym : f }
|
|
86
|
+
# default to a flat representation of the model's attributes if left unspecified
|
|
87
|
+
strong_params = strong_params.blank? ? model.attribute_names.map(&:to_sym)-[:id, :updated_at, :created_at] : strong_params
|
|
88
|
+
|
|
89
|
+
# The strong params will be the same for all routes, differing from the Grape params
|
|
90
|
+
# when routes are nested
|
|
91
|
+
whitelist = whitelist( strong_params )
|
|
92
|
+
|
|
93
|
+
# As routes are nested keep track of the routes, we are preventing siblings from
|
|
94
|
+
# appending to the routes array here:
|
|
95
|
+
routes = build_routes(routes, model)
|
|
96
|
+
|
|
97
|
+
define_routes(routes,whitelist)
|
|
98
|
+
|
|
99
|
+
resource routes.first.name.pluralize do
|
|
100
|
+
# yield to append additional routes under the root namespace
|
|
101
|
+
yield if block_given?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def define_routes(routes, whitelist)
|
|
106
|
+
define_endpoint(routes, whitelist)
|
|
107
|
+
|
|
108
|
+
# recursively define endpoints
|
|
109
|
+
model = routes.last.model || return
|
|
110
|
+
|
|
111
|
+
whitelist.select{|a| a.kind_of?(Hash) }.each do |nested|
|
|
112
|
+
# Recursively add RESTful nested routes for every nested model:
|
|
113
|
+
nested.each do |r,fields|
|
|
114
|
+
# Look at model.reflections to find the association's class name:
|
|
115
|
+
reflection_name = r.to_s.sub(/_attributes$/,'')
|
|
116
|
+
begin
|
|
117
|
+
relation = model.reflections[reflection_name].class_name.constantize
|
|
118
|
+
rescue
|
|
119
|
+
Rails.logger.fatal "Can't find associated model for #{r} on #{model}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
next_routes = build_routes(routes, relation, reflection_name)
|
|
123
|
+
|
|
124
|
+
define_routes(next_routes, fields)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_routes(routes, model, reflection_name=nil)
|
|
130
|
+
routes = routes.clone
|
|
131
|
+
# routes: the existing routes array passed from the parent
|
|
132
|
+
# model: the model being manipulated in this leaf
|
|
133
|
+
# reflection_name: the association name from the original strong_params declaration
|
|
134
|
+
#
|
|
135
|
+
# Constructs an array representation of the route's models and their associations,
|
|
136
|
+
# a /root/:rootId/branch/:branchId/leaf/:leafId path would have flat array like
|
|
137
|
+
# [root,branch,leaf] representing the path structure and its models, used to
|
|
138
|
+
# manipulate ActiveRecord relationships and params hashes and so on.
|
|
139
|
+
parent_model = routes.last.try(:model)
|
|
140
|
+
return routes if model == parent_model
|
|
141
|
+
|
|
142
|
+
name = reflection_name || model.name.underscore
|
|
143
|
+
reflection = parent_model && parent_model.reflections[reflection_name]
|
|
144
|
+
many = parent_model && PLURAL_REFLECTIONS.include?( reflection.class ) ? true : false
|
|
145
|
+
routes.push OpenStruct.new(name: name, param: "#{name}_attributes", model: model, many?: many, key: "#{name.singularize}_id".to_sym, swaggerKey: "#{name.singularize.camelize(:lower)}Id", reflection: reflection)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def define_endpoint(routes,api_params)
|
|
149
|
+
# Inside the Grape DSL "self" will derefernece to its Endpoint classes,
|
|
150
|
+
# so save a reference to our API class:
|
|
151
|
+
klass = self
|
|
152
|
+
# De-reference these as local variables from their class scope, or when we make
|
|
153
|
+
# calls to the API they will be whatever they were last set to by the recursive
|
|
154
|
+
# calls to "nest_routes".
|
|
155
|
+
routes = routes.clone
|
|
156
|
+
api_params = api_params.clone
|
|
157
|
+
|
|
158
|
+
root = routes.first
|
|
159
|
+
model = routes.last.model || return
|
|
160
|
+
name = routes.last.name.singularize
|
|
161
|
+
# We define the param keys for ID fields in camelcase for swagger's URL substitution,
|
|
162
|
+
# they'll come back in snake case in the params hash, the API as a whole is agnostic:
|
|
163
|
+
swaggerKey = routes.last.swaggerKey
|
|
164
|
+
|
|
165
|
+
namespace = routes[0..-2].map{|p| "#{p.name.pluralize}/:#{p.swaggerKey}/" }.join + name.pluralize
|
|
166
|
+
|
|
167
|
+
resource namespace do
|
|
168
|
+
|
|
169
|
+
after_validation do
|
|
170
|
+
# After Grape validates its parameters:
|
|
171
|
+
# 1) Find the root model instance for the API if its passed (implicitly either
|
|
172
|
+
# an update/destroy on the root node or it's a nested route
|
|
173
|
+
# 2) For nested endpoints convert the params hash into Rails-compliant nested
|
|
174
|
+
# attributes, to be passed to the root instance for update. This keeps
|
|
175
|
+
# behavior consistent between bulk and single record updates.
|
|
176
|
+
if params[root.key]
|
|
177
|
+
default_includes = routes.size > 1 ? [] : root.model.default_includes
|
|
178
|
+
@model = root.model.includes( default_includes ).find(params[root.key])
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
if routes.size > 1
|
|
182
|
+
nested_attributes = klass.build_nested_attributes(routes[1..-1], params.except(root.key,:api_key) )
|
|
183
|
+
|
|
184
|
+
@params.merge!( nested_attributes ) if nested_attributes.kind_of?(Hash)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
unless model.exclude_actions.include?(:index)
|
|
190
|
+
desc "list #{name.pluralize}" do
|
|
191
|
+
detail "returns list of all #{name.pluralize}"
|
|
192
|
+
end
|
|
193
|
+
get '/' do
|
|
194
|
+
# Invoke the policy for the action, defined in the policy classes for the model:
|
|
195
|
+
authorize root.model.new, :index?
|
|
196
|
+
|
|
197
|
+
# Nested route indexes need to be scoped by the API's top level policy class:
|
|
198
|
+
records = policy_scope( root.model.includes(klass.default_includes(root.model)) )
|
|
199
|
+
|
|
200
|
+
records = records.map{|r| klass.find_leaves( routes, r, params ) }.flatten.compact.uniq
|
|
201
|
+
|
|
202
|
+
present records, with: "#{klass}::#{model}Entity".constantize
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
unless model.exclude_actions.include?(:show)
|
|
208
|
+
desc "retrieve a #{name}" do
|
|
209
|
+
detail "returns details on a #{name}"
|
|
210
|
+
end
|
|
211
|
+
get ":#{swaggerKey}" do
|
|
212
|
+
authorize @model, :show?
|
|
213
|
+
present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
unless model.exclude_actions.include?(:create)
|
|
219
|
+
desc "create a #{name}" do
|
|
220
|
+
detail "creates a new #{name} record"
|
|
221
|
+
end
|
|
222
|
+
params do
|
|
223
|
+
klass.generate_params(self, klass, :create, model, api_params)
|
|
224
|
+
end
|
|
225
|
+
post do
|
|
226
|
+
if (@model)
|
|
227
|
+
@model.update_attributes( safe_params(params).permit(klass.whitelist) )
|
|
228
|
+
else
|
|
229
|
+
@model = root.model.new( safe_params(params).permit(klass.whitelist) )
|
|
230
|
+
end
|
|
231
|
+
authorize @model, :create?
|
|
232
|
+
@model.save!
|
|
233
|
+
present klass.find_leaves(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
unless model.exclude_actions.include?(:update)
|
|
239
|
+
desc "update a #{name}" do
|
|
240
|
+
detail "updates the details of a #{name}"
|
|
241
|
+
end
|
|
242
|
+
params do
|
|
243
|
+
klass.generate_params(self, klass, :update, model, api_params)
|
|
244
|
+
end
|
|
245
|
+
put ":#{swaggerKey}" do
|
|
246
|
+
authorize @model, :update?
|
|
247
|
+
|
|
248
|
+
@model.update_attributes!( safe_params(params).permit(klass.whitelist) )
|
|
249
|
+
|
|
250
|
+
present klass.find_leaf(routes, @model, params), with: "#{klass}::#{model}Entity".constantize
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
unless model.exclude_actions.include?(:destroy)
|
|
256
|
+
desc "destroy a #{name}" do
|
|
257
|
+
detail "destroys the details of a #{name}"
|
|
258
|
+
end
|
|
259
|
+
delete ":#{swaggerKey}" do
|
|
260
|
+
authorize @model, :destroy?
|
|
261
|
+
present status: (klass.find_leaf(routes, @model, params).destroy ? true : false)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def whitelist(whitelist=nil)
|
|
270
|
+
return @whitelist if !whitelist
|
|
271
|
+
@whitelist = whitelist
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def skip_presence_validations(fields=nil)
|
|
275
|
+
return @skip_presence_fields||[] if !fields
|
|
276
|
+
@skip_presence_fields = [fields].flatten
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build_nested_attributes(routes,hash)
|
|
280
|
+
# Recursively re-express the flat attributes hash from nested routes as nested
|
|
281
|
+
# attributes that can be used to perform an update on the root model.
|
|
282
|
+
|
|
283
|
+
# do nothing if the params are already nested.
|
|
284
|
+
return {} if routes.blank? || hash[routes.first.param]
|
|
285
|
+
|
|
286
|
+
route = routes.shift
|
|
287
|
+
# change 'x_id' to 'x_attributes': { id: id, y_attributes: {} }
|
|
288
|
+
id = hash.delete route.key
|
|
289
|
+
attributes = id ? { id: id } : {}
|
|
290
|
+
|
|
291
|
+
attributes.merge!( hash ) if routes.blank? # assign param values to the last reference
|
|
292
|
+
|
|
293
|
+
if route.many? # nest it in an array if it is a has_many association
|
|
294
|
+
{ route.param => [attributes.merge( build_nested_attributes(routes, hash) )] }
|
|
295
|
+
else
|
|
296
|
+
{ route.param => attributes.merge( build_nested_attributes(routes, hash) ) }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def find_leaves(routes, record, params)
|
|
302
|
+
# Traverse down our route and find the leaf's siblings from its parent, e.g.
|
|
303
|
+
# project/#/teams/#/team_users ~> project.find.teams.find.team_users.
|
|
304
|
+
# (the traversal of the intermediate nodes occurs in find_leaf())
|
|
305
|
+
return record if routes.size < 2 # the leaf is the root
|
|
306
|
+
if record = find_leaf(routes, record, params)
|
|
307
|
+
assoc = routes.last
|
|
308
|
+
if assoc.many? && leaves = record.send( assoc.reflection.name ).includes( default_includes(assoc.model) )
|
|
309
|
+
if (leaves.map(&:class) - [routes.last.model]).size > 0
|
|
310
|
+
raise ActiveRecord::RecordNotFound.new("Records contain the wrong models, they should all be #{routes.last.model.name}, found #{records.map(&:class).map(&:name).join(',')}")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
leaves
|
|
314
|
+
else
|
|
315
|
+
# has_one associations don't return a CollectionProxy and so don't support
|
|
316
|
+
# eager loading.
|
|
317
|
+
record.send( assoc.reflection.name )
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def find_leaf(routes, record, params)
|
|
323
|
+
return record unless routes.size > 1
|
|
324
|
+
# For deeply nested routes we need to search from the root of the API to the leaf
|
|
325
|
+
# of its nested associations in order to guarantee the validity of the relationship,
|
|
326
|
+
# the authorization on the parent model, and the sanity of passed parameters.
|
|
327
|
+
routes[1..-1].each_with_index do |r|
|
|
328
|
+
if record && params[r.key]
|
|
329
|
+
if ref = r.reflection
|
|
330
|
+
record = record.send(ref.name).where( id: params[r.key] ).first
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
if params[routes.last.key] && record.class != routes.last.model
|
|
336
|
+
raise ActiveRecord::RecordNotFound.new("No #{routes.last.model.name} with ID '#{params[routes.last.key]}'")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
record
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def generate_params(dsl, klass, action, model, fields)
|
|
344
|
+
# We'll be doing a recursive walk (to handle nested attributes) down the
|
|
345
|
+
# whitelisted params, generating the Grape param definitions by introspecting
|
|
346
|
+
# on the model and its associations.
|
|
347
|
+
raise "Invalid action: #{action}" unless [:update, :create].include?(action)
|
|
348
|
+
# dsl : The Grape::Validations::ParamsScope object
|
|
349
|
+
# klass : A reference back to the original descendant of IntrospectiveGrape::API.
|
|
350
|
+
# You have to pass this around to remember who you were before the DSL
|
|
351
|
+
# scope hijacked your identity.
|
|
352
|
+
# action: create or update
|
|
353
|
+
# model : The ActiveRecord model class
|
|
354
|
+
# fields: The whitelisted data structure for Rails' strong params, from which we
|
|
355
|
+
# infer Grape's parameters
|
|
356
|
+
|
|
357
|
+
(fields-[:id]).each do |field|
|
|
358
|
+
if ( field.kind_of?(Hash) )
|
|
359
|
+
generate_nested_params(dsl,klass,action,model,field)
|
|
360
|
+
else
|
|
361
|
+
if (action==:create && klass.param_required?(model,field) )
|
|
362
|
+
# All params are optional on an update, only require them during creation.
|
|
363
|
+
# Updating a record with new child models will have to rely on ActiveRecord
|
|
364
|
+
# validations:
|
|
365
|
+
dsl.requires field, type: klass.param_type(model,field)
|
|
366
|
+
else
|
|
367
|
+
dsl.optional field, type: klass.param_type(model,field)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def generate_nested_params(dsl,klass,action,model,fields)
|
|
374
|
+
fields.each do |r,v|
|
|
375
|
+
# Look at model.reflections to find the association's class name:
|
|
376
|
+
reflection = r.to_s.sub(/_attributes$/,'') # the reflection name
|
|
377
|
+
relation = begin
|
|
378
|
+
model.reflections[reflection].class_name.constantize # its class
|
|
379
|
+
rescue
|
|
380
|
+
model
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if is_file_attachment?(model,r)
|
|
384
|
+
# Handle Carrierwave file upload fields
|
|
385
|
+
if s = [:filename, :type, :name, :tempfile, :head]-v && s.present?
|
|
386
|
+
Rails.logger.warn "Missing required file upload parameters #{s} for uploader field #{r}"
|
|
387
|
+
end
|
|
388
|
+
elsif PLURAL_REFLECTIONS.include?( model.reflections[reflection].class )
|
|
389
|
+
# In case you need a refresher on how these work:
|
|
390
|
+
# http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
|
|
391
|
+
dsl.optional r, type: Array do |dsl|
|
|
392
|
+
klass.generate_params(dsl,klass,action,relation,v)
|
|
393
|
+
klass.add_destroy_param(dsl,model,reflection) unless action==:create
|
|
394
|
+
end
|
|
395
|
+
else
|
|
396
|
+
# TODO: handle any remaining correctly. Presently defaults to a Hash
|
|
397
|
+
# http://www.rubydoc.info/github/rails/rails/ActiveRecord/Reflection
|
|
398
|
+
# ThroughReflection, HasOneReflection,
|
|
399
|
+
# HasAndBelongsToManyReflection, BelongsToReflection
|
|
400
|
+
dsl.optional r, type: Hash do |dsl|
|
|
401
|
+
klass.generate_params(dsl,klass,action,relation,v)
|
|
402
|
+
klass.add_destroy_param(dsl,model,reflection) unless action==:create
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def is_file_attachment?(model,field)
|
|
409
|
+
model.try(:uploaders) && model.uploaders[field.to_sym] || # carrierwave
|
|
410
|
+
model.try(:paperclip_definitions) && model.paperclip_definitions[field.to_sym] || # paperclip
|
|
411
|
+
model.send(:new).try(field).kind_of?(Paperclip::Attachment)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def param_type(model,f)
|
|
415
|
+
# Translate from the AR type to the GrapeParam types
|
|
416
|
+
f = f.to_s
|
|
417
|
+
db_type = (model.try(:columns_hash)||{})[f].try(:type)
|
|
418
|
+
|
|
419
|
+
# Look for an override class from the model, check Pg2Ruby, use the database type,
|
|
420
|
+
# or fail over to a String:
|
|
421
|
+
( is_file_attachment?(model,f) && Rack::Multipart::UploadedFile ) ||
|
|
422
|
+
(model.try(:attribute_param_types)||{})[f] ||
|
|
423
|
+
Pg2Ruby[db_type] ||
|
|
424
|
+
begin db_type.to_s.camelize.constantize rescue nil end ||
|
|
425
|
+
String
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def param_required?(model,f)
|
|
429
|
+
return false if skip_presence_validations.include? f
|
|
430
|
+
# Detect if the field is a required field for the create action
|
|
431
|
+
model.validators_on(f.to_sym).any?{|v| v.kind_of? ActiveRecord::Validations::PresenceValidator }
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def add_destroy_param(dsl,model,reflection)
|
|
435
|
+
raise "#{model} does not accept nested attributes for #{reflection}" if !model.nested_attributes_options[reflection.to_sym]
|
|
436
|
+
# If destruction is allowed append the _destroy field
|
|
437
|
+
if model.nested_attributes_options[reflection.to_sym][:allow_destroy]
|
|
438
|
+
dsl.optional '_destroy', type: Integer
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'grape-swagger'
|
|
2
|
+
require 'active_support' #/core_ext/module/aliasing'
|
|
3
|
+
module IntrospectiveGrape::CamelSnake
|
|
4
|
+
def snake_keys(data)
|
|
5
|
+
if data.kind_of? Array
|
|
6
|
+
data.map { |v| snake_keys(v) }
|
|
7
|
+
elsif data.kind_of? Hash
|
|
8
|
+
Hash[data.map {|k, v| [k.to_s.underscore, snake_keys(v)] }]
|
|
9
|
+
else
|
|
10
|
+
data
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def camel_keys(data)
|
|
15
|
+
if data.kind_of? Array
|
|
16
|
+
data.map { |v| camel_keys(v) }
|
|
17
|
+
elsif data.kind_of?(Hash)
|
|
18
|
+
Hash[data.map {|k, v| [k.to_s.camelize(:lower), camel_keys(v)] }]
|
|
19
|
+
else
|
|
20
|
+
data
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Duck type Grape's built in JSON formatter to convert all snake case hash keys
|
|
26
|
+
# to camel case.
|
|
27
|
+
module Grape
|
|
28
|
+
module Formatter
|
|
29
|
+
module Json
|
|
30
|
+
class << self
|
|
31
|
+
include IntrospectiveGrape::CamelSnake
|
|
32
|
+
def call(object, env)
|
|
33
|
+
if object.respond_to?(:to_json)
|
|
34
|
+
camel_keys(JSON.parse(object.to_json)).to_json
|
|
35
|
+
else
|
|
36
|
+
camel_keys(MultiJson.dump(object)).to_json
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Camelize the parameters in the swagger documentation.
|
|
45
|
+
module Grape
|
|
46
|
+
class API
|
|
47
|
+
class << self
|
|
48
|
+
private
|
|
49
|
+
def create_documentation_class_with_camelized
|
|
50
|
+
doc = create_documentation_class_without_camelized
|
|
51
|
+
doc.class_eval do
|
|
52
|
+
class << self
|
|
53
|
+
def parse_params_with_camelized(params, path, method)
|
|
54
|
+
parsed_params = parse_params_without_camelized(params, path, method)
|
|
55
|
+
parsed_params.each_with_index do |param|
|
|
56
|
+
param[:name] = param[:name]
|
|
57
|
+
.camelize(:lower)
|
|
58
|
+
.gsub(/\[Destroy\]/,'[_destroy]')
|
|
59
|
+
end
|
|
60
|
+
parsed_params
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
alias_method_chain :parse_params, :camelized
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
doc
|
|
67
|
+
end
|
|
68
|
+
alias_method_chain :create_documentation_class, :camelized
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/spec/dummy/Gemfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
== README
|
|
2
|
+
|
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
|
4
|
+
application up and running.
|
|
5
|
+
|
|
6
|
+
Things you may want to cover:
|
|
7
|
+
|
|
8
|
+
* Ruby version
|
|
9
|
+
|
|
10
|
+
* System dependencies
|
|
11
|
+
|
|
12
|
+
* Configuration
|
|
13
|
+
|
|
14
|
+
* Database creation
|
|
15
|
+
|
|
16
|
+
* Database initialization
|
|
17
|
+
|
|
18
|
+
* How to run the test suite
|
|
19
|
+
|
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
|
21
|
+
|
|
22
|
+
* Deployment instructions
|
|
23
|
+
|
|
24
|
+
* ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
|
28
|
+
<tt>rake doc:app</tt>.
|
data/spec/dummy/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Duck-type some helper class methods into our ActiveRecord models to
|
|
2
|
+
# allow us to configure API behaviors granularly, at the model level.
|
|
3
|
+
class ActiveRecord::Base
|
|
4
|
+
class << self
|
|
5
|
+
@@api_actions ||= [:index,:show,:create,:update,:destroy,nil]
|
|
6
|
+
def api_actions; @@api_actions; end
|
|
7
|
+
|
|
8
|
+
def exclude_actions(*args) # Do not define endpoints for these actions
|
|
9
|
+
raise "#{self.name} defines invalid exclude_actions: #{args-@@api_actions}" if (args.flatten-@@api_actions).present?
|
|
10
|
+
@exclude_actions = args.present? ? args.flatten : @exclude_actions || []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def default_includes(*args) # Eager load these associations.
|
|
14
|
+
@default_includes = args.present? ? args.flatten : @default_includes || []
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ApiHelpers
|
|
2
|
+
include IntrospectiveGrape::CamelSnake
|
|
3
|
+
def warden
|
|
4
|
+
env['warden']
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def current_user
|
|
8
|
+
warden.user || params[:api_key].present? && @user = User.find_by_authentication_token(params[:api_key])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def authorize!
|
|
12
|
+
unauthorized! unless current_user
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# returns an 'unauthorized' response
|
|
16
|
+
def unauthorized!(error_type = nil)
|
|
17
|
+
respond_error!('unauthorized', error_type, 401)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# returns a error response with given type, message_key and status
|
|
21
|
+
def respond_error!(type, message_key, status = 500, other = {})
|
|
22
|
+
e = {
|
|
23
|
+
type: type,
|
|
24
|
+
status: status
|
|
25
|
+
}
|
|
26
|
+
e['message_key'] = message_key if message_key
|
|
27
|
+
e.merge!(other)
|
|
28
|
+
error!({ error: e }, status)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def safe_params(params)
|
|
34
|
+
ActionController::Parameters.new(params)
|
|
35
|
+
end
|
|
36
|
+
end
|