apicasso 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +84 -0
- data/Rakefile +32 -0
- data/app/assets/config/apicasso_manifest.js +2 -0
- data/app/assets/javascripts/apicasso/application.js +15 -0
- data/app/assets/stylesheets/apicasso/application.css +15 -0
- data/app/controllers/apicasso/apidocs_controller.rb +472 -0
- data/app/controllers/apicasso/application_controller.rb +106 -0
- data/app/controllers/apicasso/crud_controller.rb +148 -0
- data/app/controllers/concerns/orderable.rb +44 -0
- data/app/helpers/apicasso/application_helper.rb +4 -0
- data/app/jobs/apicasso/application_job.rb +4 -0
- data/app/mailers/apicasso/application_mailer.rb +6 -0
- data/app/models/apicasso/ability.rb +40 -0
- data/app/models/apicasso/application_record.rb +6 -0
- data/app/models/apicasso/key.rb +25 -0
- data/app/models/apicasso/request.rb +8 -0
- data/app/views/layouts/apicasso/application.html.erb +16 -0
- data/config/routes.rb +13 -0
- data/lib/apicasso/active_record_extension.rb +80 -0
- data/lib/apicasso/engine.rb +6 -0
- data/lib/apicasso/version.rb +3 -0
- data/lib/apicasso.rb +9 -0
- data/lib/generators/apicasso/install/install_generator.rb +25 -0
- data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +17 -0
- data/lib/tasks/apicasso_tasks.rake +4 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 918cf0c81232e0e147d5bf174b7759e189cc82933d8d7341b64e6765bcd5af57
|
4
|
+
data.tar.gz: 929566ab3bfde9f9980263a2304072be2a6d21e5e82198e89e43ecfecfc7c588
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bdfae676999589bce579a57c07adb0d58ecf2e1a18c94a4f754cd8428b3708d92d745a521ebc419c739ae1e08e30c36c93c6013c84afac4056252d770ebc0671
|
7
|
+
data.tar.gz: 6a7fb2af0119e03ae6ba657c713d7dc03b0e4331475b582d826f28486f40c33f4a22d17584ce7a5ccfef37e90fbabdd6db0c9f42cb4e42e631765cb53de5b911
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
<img src="https://raw.githubusercontent.com/ErvalhouS/APIcasso/master/APIcasso.png" width="300" />
|
2
|
+
|
3
|
+
JSON API development can get boring and time consuming. If you think it through, every time you make one you use almost the same route structure, pointing to the same controller actions, with the same ordering, filtering and pagination features.
|
4
|
+
|
5
|
+
**APIcasso** is intended to be used as a full-fledged CRUD JSON API or as a base controller to speed-up development.
|
6
|
+
It is a route-based resource abstraction using API key scoping. This makes it possible to make CRUD-only applications just by creating functional Rails' models. It is a perfect candidate for legacy Rails projects that do not have an API. Access to your application's resources is managed by a `.scope` JSON object per API key. It uses that permission scope to restrict and extend access.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
Add this line to your application's `Gemfile`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'apicasso'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute this to generate the required migrations:
|
16
|
+
```bash
|
17
|
+
$ rails g apicasso:install
|
18
|
+
```
|
19
|
+
You will need to use a database with JSON fields support to use this gem.
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
After installing APIcasso into your application you can mount a full-fledged CRUD JSON API just by attaching into some route. Usually you will have it under a scoped route like `/api/v1` or a subdomain. You can do that by adding this into your `config/routes.rb`:
|
23
|
+
```ruby
|
24
|
+
# To mount your APIcasso routes under the path scope `/api/v1`
|
25
|
+
mount Apicasso::Engine, at: "/api/v1"
|
26
|
+
# or, if you prefer subdomain scope isolation
|
27
|
+
constraints subdomain: 'apiv1' do
|
28
|
+
mount Apicasso::Engine, at: "/"
|
29
|
+
end
|
30
|
+
```
|
31
|
+
Your API will reflect very similarly a `resources :resource` statement with the following routes:
|
32
|
+
```ruby
|
33
|
+
get '/:resource/' # Index action, listing a `:resource` collection from your application
|
34
|
+
post '/:resource/' # Create action for one `:resource` from your application
|
35
|
+
get '/:resource/:id' # Show action for one `:resource` from your application
|
36
|
+
patch '/:resource/:id' # Update action for one `:resource` from your application
|
37
|
+
delete '/:resource/:id' # Destroy action for one `:resource` from your application
|
38
|
+
get '/:resource/:id/:nested/' # Index action, listing a collection of a `:nested` relation from one of your application's `:resource`
|
39
|
+
options '/:resource/' # A schema dump for the required `:resource`
|
40
|
+
options '/:resource/:id/:nested/' # A schema dump for the required `:nested` relation from one of your application's `:resource`
|
41
|
+
```
|
42
|
+
This means all your application's models will be exposed as `:resource` and it's relations will be exposed as `:nested`. It will enable you to CRUD and get schema metadata from your records.
|
43
|
+
|
44
|
+
> But this is permissive as hell! I do not want to expose my entire application like this, haven't you thought about security?
|
45
|
+
|
46
|
+
*Sure!* The API is being exposed using authentication through `Authorization: Token` [HTTP header authentication](http://tools.ietf.org/html/draft-hammer-http-token-auth-01). The API key objects are manageable through the `Apicasso::Key` model, which gets setup at install. When a new key is created a `.token` is generated using an [Universally Unique Identifier(RFC 4122)](https://tools.ietf.org/html/rfc4122).
|
47
|
+
|
48
|
+
Your API is then exposed based on each `Apicasso::Key.scope` definition
|
49
|
+
```ruby
|
50
|
+
Apicasso::Key.create(scope:
|
51
|
+
{ manage:
|
52
|
+
[{ order: true }, { user: { account_id: 1 } }],
|
53
|
+
read:
|
54
|
+
[{ account: { id: 1 } }]
|
55
|
+
})
|
56
|
+
```
|
57
|
+
This translates directly into which parts of your application is exposed to each APIcasso keys.
|
58
|
+
|
59
|
+
The key from this example will have full access to all orders and to users with `account_id == 1`. It will have also read-only access to accounts with `id == 1`.
|
60
|
+
|
61
|
+
This saves you the trouble of having to setup each and every controller for each model. And even if your application really need it, just make your controllers inherit from `Apicasso::CrudController` and extend it's functionalities. This authorization feature is why one of the dependencies for this gem is [CanCanCan](https://github.com/CanCanCommunity/cancancan), that abstracts the scope field into authorization for your application's resources.
|
62
|
+
|
63
|
+
The `crud#index` and `crud#nested_index` actions are already equipped with pagination, ordering and filtering.
|
64
|
+
|
65
|
+
- You can pass `params[:sort]` with field names preffixed with `+` or `-` to configure custom ordering per request. I.E.: `?sort=+updated_at,-name`
|
66
|
+
- You can pass `params[:q]` using [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) to build a search query. I.E.: `?q[full_name_start]=Picasso`
|
67
|
+
- You can pass `params[:page]` and `params[:per_page]` to build pagination options. I.E.: `?page=2&per_page=12`
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ErvalhouS/APIcasso. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant code of conduct](http://contributor-covenant.org/).
|
71
|
+
|
72
|
+
## License
|
73
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
74
|
+
|
75
|
+
## Code of conduct
|
76
|
+
Everyone interacting in the APIcasso project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ErvalhouS/APIcasso/blob/master/CODE_OF_CONDUCT.md).
|
77
|
+
|
78
|
+
## TODO
|
79
|
+
|
80
|
+
- Add gem options like: Token rotation, Alternative authentication methods
|
81
|
+
- Response fields selecting
|
82
|
+
- Rate limiting
|
83
|
+
- Testing suite
|
84
|
+
- Travis CI
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
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 = 'Apicasso'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,472 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Controller used to generate an application Swagger JSON, used by
|
5
|
+
# SwaggerUI to generate beautiful API documentation
|
6
|
+
class ApidocsController < ActionController::API
|
7
|
+
include Swagger::Blocks
|
8
|
+
|
9
|
+
swagger_root do
|
10
|
+
key :swagger, '2.0'
|
11
|
+
info do
|
12
|
+
key :version, ENV.fetch('VERSION', I18n.t('application.version'))
|
13
|
+
key :title, ENV.fetch('APP_NAME', I18n.t('application.name'))
|
14
|
+
key :description, ENV.fetch('APP_DESCRIPTION', I18n.t('application.description'))
|
15
|
+
key :termsOfService, I18n.t('application.terms_of_service')
|
16
|
+
contact do
|
17
|
+
key :name, I18n.t('application.contact_name')
|
18
|
+
end
|
19
|
+
license do
|
20
|
+
key :name, I18n.t('application.license')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
ActiveRecord::Base.descendants.each do |model|
|
24
|
+
tag do
|
25
|
+
key :name, I18n.t("activerecord.models.#{model.name.underscore}", default: model.name.underscore)
|
26
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.description", default: model.name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
key :host, I18n.t('application.apicasso_host', default: ENV.fetch('ROOT', 'localhost:3000'))
|
30
|
+
key :basePath, I18n.t('application.apicasso_path', default: '/')
|
31
|
+
key :consumes, ['application/json']
|
32
|
+
key :produces, ['application/json']
|
33
|
+
end
|
34
|
+
|
35
|
+
# Eager load application to be able to list all models
|
36
|
+
Rails.application.eager_load! unless Rails.configuration.cache_classes
|
37
|
+
# A list of all classes that have swagger_* declarations, which gets injected
|
38
|
+
# by this gem in all `ActiveRecord::Base` classes
|
39
|
+
SWAGGERED_CLASSES = [
|
40
|
+
*ActiveRecord::Base.descendants,
|
41
|
+
self
|
42
|
+
].freeze
|
43
|
+
|
44
|
+
swagger_schema :ErrorModel do
|
45
|
+
key :required, [:code, :message]
|
46
|
+
property :code do
|
47
|
+
key :type, :integer
|
48
|
+
key :format, :int32
|
49
|
+
end
|
50
|
+
property :message do
|
51
|
+
key :type, :string
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ActiveRecord::Base.descendants do |model|
|
56
|
+
swagger_path "/#{model.name.underscore}" do
|
57
|
+
operation :get do
|
58
|
+
key :summary, I18n.t("activerecord.models.#{model.name.underscore}.index.summary", default: model.name)
|
59
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.index.description", default: model.name)
|
60
|
+
key :operationId, "find#{model.name.pluralize}"
|
61
|
+
key :produces, ['application/json']
|
62
|
+
key :tags, [model.name.underscore]
|
63
|
+
parameter do
|
64
|
+
key :name, :sort
|
65
|
+
key :in, :query
|
66
|
+
key :description, I18n.t('apicasso.sort.description',
|
67
|
+
default: 'Parameters sorting splitted by `,` preffixed by `+` or `-` which translates into ascending or descending order')
|
68
|
+
key :required, false
|
69
|
+
key :type, :string
|
70
|
+
key :collectionFormat, :json
|
71
|
+
end
|
72
|
+
parameter do
|
73
|
+
key :name, :q
|
74
|
+
key :in, :query
|
75
|
+
key :description, I18n.t('apicasso.q.description',
|
76
|
+
default: 'Records filtering by attribute and search query as affix. Usage: `?q[{attribute}{search_affix}]={matcher}`. All available search affixes are listed on: https://github.com/activerecord-hackery/ransack#search-matchers')
|
77
|
+
key :required, false
|
78
|
+
key :type, :json
|
79
|
+
end
|
80
|
+
parameter do
|
81
|
+
key :name, :page
|
82
|
+
key :in, :query
|
83
|
+
key :description, I18n.t('apicasso.page.description',
|
84
|
+
default: 'Records pagination paging, which offsets collection based on `params[:per_page]`')
|
85
|
+
key :required, false
|
86
|
+
key :type, :integer
|
87
|
+
end
|
88
|
+
parameter do
|
89
|
+
key :name, :per_page
|
90
|
+
key :in, :query
|
91
|
+
key :description, I18n.t('apicasso.per_page.description',
|
92
|
+
default: 'Records pagination size, which sets how many records will be rendered per request')
|
93
|
+
key :required, false
|
94
|
+
key :type, :integer
|
95
|
+
end
|
96
|
+
response 200 do
|
97
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.index.response",
|
98
|
+
default: "#{model.name} response, which include records matching current query and pagination metadata")
|
99
|
+
parameter do
|
100
|
+
key :name, :total
|
101
|
+
key :description, I18n.t('apicasso.total.description',
|
102
|
+
default: 'Total records contained in current collection, as if there was no pagination.')
|
103
|
+
key :required, true
|
104
|
+
key :type, :integer
|
105
|
+
end
|
106
|
+
parameter do
|
107
|
+
key :name, :total_pages
|
108
|
+
key :description, I18n.t('apicasso.total_pages.description',
|
109
|
+
default: 'How many pages of data the current collection has.')
|
110
|
+
key :required, true
|
111
|
+
key :type, :integer
|
112
|
+
end
|
113
|
+
parameter do
|
114
|
+
key :name, :last_page
|
115
|
+
key :description, I18n.t('apicasso.last_page.description',
|
116
|
+
default: 'An indication if current request is the last to paginate in the current collection')
|
117
|
+
key :required, true
|
118
|
+
key :type, :boolean
|
119
|
+
end
|
120
|
+
parameter do
|
121
|
+
key :name, :previous_page
|
122
|
+
key :description, I18n.t('apicasso.previous_page.description',
|
123
|
+
default: "The link of the previous page for the current collection. It can be null if there isn't any")
|
124
|
+
key :required, false
|
125
|
+
key :type, :string
|
126
|
+
end
|
127
|
+
parameter do
|
128
|
+
key :name, :next_page
|
129
|
+
key :description, I18n.t('apicasso.next_page.description',
|
130
|
+
default: "The link of the next page for the current collection. It can be null if there isn't any")
|
131
|
+
key :required, false
|
132
|
+
key :type, :string
|
133
|
+
end
|
134
|
+
parameter do
|
135
|
+
key :name, :out_of_bounds
|
136
|
+
key :description, I18n.t('apicasso.out_of_bounds.description',
|
137
|
+
default: 'An indication if current request is out of pagination bounds for the current collection')
|
138
|
+
key :required, true
|
139
|
+
key :type, :boolean
|
140
|
+
end
|
141
|
+
parameter do
|
142
|
+
key :name, :offset
|
143
|
+
key :description, I18n.t('apicasso.offset.description',
|
144
|
+
default: 'How many records were offsetted from the collection to render the current page')
|
145
|
+
key :required, true
|
146
|
+
key :type, :integer
|
147
|
+
end
|
148
|
+
parameter do
|
149
|
+
key :name, :entries
|
150
|
+
key :description, I18n.t('apicasso.entries.description',
|
151
|
+
default: 'The records collection in the current pagination scope.')
|
152
|
+
key :required, true
|
153
|
+
key :type, :array
|
154
|
+
items do
|
155
|
+
key :'$ref', model.name.to_sym
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
response :default do
|
160
|
+
key :description, I18n.t("activerecord.errors.models.#{model.name.underscore}",
|
161
|
+
default: "Unexpected error in #{model.name}")
|
162
|
+
schema do
|
163
|
+
key :'$ref', :ErrorModel
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
operation :options do
|
168
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.schema.description",
|
169
|
+
default: "#{model.name} metadata information.")
|
170
|
+
key :operationId, "schema#{model.name.pluralize}"
|
171
|
+
key :produces, ['application/json']
|
172
|
+
key :tags, [model.name.underscore]
|
173
|
+
response 200 do
|
174
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.schema.response",
|
175
|
+
default: "#{model.name} metadata as a json with field names as keys and field types as values.")
|
176
|
+
schema do
|
177
|
+
key :'$ref', "#{model.name}Metadata".to_sym
|
178
|
+
end
|
179
|
+
end
|
180
|
+
response :default do
|
181
|
+
key :description, I18n.t("activerecord.errors.models.#{model.name.underscore}",
|
182
|
+
default: "Unexpected error in #{model.name}")
|
183
|
+
schema do
|
184
|
+
key :'$ref', :ErrorModel
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
operation :post do
|
189
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.create.response",
|
190
|
+
default: "Creates a #{model.name}")
|
191
|
+
key :operationId, "add#{model.name}"
|
192
|
+
key :produces, ['application/json']
|
193
|
+
key :tags, [model.name.underscore]
|
194
|
+
parameter do
|
195
|
+
key :name, model.name.underscore.to_sym
|
196
|
+
key :in, :body
|
197
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.create.description",
|
198
|
+
default: "#{model.name} to add into application")
|
199
|
+
key :required, true
|
200
|
+
schema do
|
201
|
+
key :'$ref', "#{model.name}Input".to_sym
|
202
|
+
end
|
203
|
+
end
|
204
|
+
response 201 do
|
205
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.show.description",
|
206
|
+
default: "#{model.name} response")
|
207
|
+
schema do
|
208
|
+
key :'$ref', model.name.to_sym
|
209
|
+
end
|
210
|
+
end
|
211
|
+
response :default do
|
212
|
+
key :description, I18n.t("activerecord.errors.models.#{model.name.underscore}",
|
213
|
+
default: "Unexpected error in #{model.name}")
|
214
|
+
schema do
|
215
|
+
key :'$ref', :ErrorModel
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
swagger_path "/#{model.name.underscore}/{id}" do
|
221
|
+
operation :patch do
|
222
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.update.response",
|
223
|
+
default: "Updates a #{model.name}")
|
224
|
+
key :operationId, "edit#{model.name}"
|
225
|
+
key :produces, ['application/json']
|
226
|
+
key :tags, [model.name.underscore]
|
227
|
+
parameter do
|
228
|
+
key :name, :id
|
229
|
+
key :in, :path
|
230
|
+
key :description, I18n.t("activerecord.models.attributes.#{model.name.underscore}.id",
|
231
|
+
default: "ID of #{model.name} to update on the application")
|
232
|
+
key :required, true
|
233
|
+
schema do
|
234
|
+
key :'$ref', "#{model.name}Input".to_sym
|
235
|
+
end
|
236
|
+
end
|
237
|
+
parameter do
|
238
|
+
key :name, model.name.underscore.to_sym
|
239
|
+
key :in, :body
|
240
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.update.description",
|
241
|
+
default: "Existing #{model.name} to update on the application")
|
242
|
+
key :required, true
|
243
|
+
schema do
|
244
|
+
key :'$ref', "#{model.name}Input".to_sym
|
245
|
+
end
|
246
|
+
end
|
247
|
+
response 200 do
|
248
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.show.description",
|
249
|
+
default: "#{model.name} response")
|
250
|
+
schema do
|
251
|
+
key :'$ref', model.name.to_sym
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
operation :get do
|
256
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.show.response",
|
257
|
+
default: "Creates a #{model.name}")
|
258
|
+
key :operationId, "show#{model.name}"
|
259
|
+
key :produces, ['application/json']
|
260
|
+
key :tags, [model.name.underscore]
|
261
|
+
parameter do
|
262
|
+
key :name, :id
|
263
|
+
key :in, :path
|
264
|
+
key :description, I18n.t("activerecord.models.attributes.#{model.name.underscore}.id",
|
265
|
+
default: "ID of #{model.name} to fetch on the application")
|
266
|
+
key :required, true
|
267
|
+
schema do
|
268
|
+
key :'$ref', "#{model.name}Input".to_sym
|
269
|
+
end
|
270
|
+
end
|
271
|
+
response 200 do
|
272
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.show.description",
|
273
|
+
default: "#{model.name} response")
|
274
|
+
schema do
|
275
|
+
key :'$ref', model.name.to_sym
|
276
|
+
end
|
277
|
+
end
|
278
|
+
response :default do
|
279
|
+
key :description, I18n.t("activerecord.errors.models.#{model.name.underscore}",
|
280
|
+
default: "Unexpected error in #{model.name}")
|
281
|
+
schema do
|
282
|
+
key :'$ref', :ErrorModel
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
operation :delete do
|
287
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.destroy.response",
|
288
|
+
default: "Deletes a #{model.name}")
|
289
|
+
key :operationId, "destroy#{model.name}"
|
290
|
+
key :produces, ['application/json']
|
291
|
+
key :tags, [model.name.underscore]
|
292
|
+
parameter do
|
293
|
+
key :name, :id
|
294
|
+
key :in, :path
|
295
|
+
key :description, I18n.t("activerecord.models.attributes.#{model.name.underscore}.id",
|
296
|
+
default: "ID of #{model.name} to delete on the application")
|
297
|
+
key :required, true
|
298
|
+
schema do
|
299
|
+
key :'$ref', "#{model.name}Input".to_sym
|
300
|
+
end
|
301
|
+
end
|
302
|
+
response 200 do
|
303
|
+
key :description, I18n.t("activerecord.models.#{model.name.underscore}.destroy.description",
|
304
|
+
default: "#{model.name} response")
|
305
|
+
end
|
306
|
+
response :default do
|
307
|
+
key :description, I18n.t("activerecord.errors.models.#{model.name.underscore}",
|
308
|
+
default: "Unexpected error in #{model.name}")
|
309
|
+
schema do
|
310
|
+
key :'$ref', :ErrorModel
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
model.reflect_on_all_associations.map(&:name) do |association|
|
317
|
+
inner_name = extract_klass_name(model: model, association: association)
|
318
|
+
inner_klass = extract_klass(model: model, association: association)
|
319
|
+
swagger_path "/#{model.name.underscore}/{id}/#{association}" do
|
320
|
+
operation :get do
|
321
|
+
key :summary, I18n.t("activerecord.models.#{inner_name.underscore}.index.summary", default: inner_name)
|
322
|
+
key :description, I18n.t("activerecord.models.#{inner_name.underscore}.index.description", default: inner_name)
|
323
|
+
key :operationId, "find#{inner_name.pluralize}"
|
324
|
+
key :produces, ['application/json']
|
325
|
+
key :tags, [inner_name.underscore]
|
326
|
+
parameter do
|
327
|
+
key :name, :sort
|
328
|
+
key :in, :query
|
329
|
+
key :description, I18n.t('apicasso.sort.description',
|
330
|
+
default: 'Parameters sorting splitted by `,` preffixed by `+` or `-` which translates into ascending or descending order')
|
331
|
+
key :required, false
|
332
|
+
key :type, :string
|
333
|
+
key :collectionFormat, :json
|
334
|
+
end
|
335
|
+
parameter do
|
336
|
+
key :name, :q
|
337
|
+
key :in, :query
|
338
|
+
key :description, I18n.t('apicasso.q.description',
|
339
|
+
default: 'Records filtering by attribute and search query as affix. Usage: `?q[{attribute}{search_affix}]={matcher}`. All available search affixes are listed on: https://github.com/activerecord-hackery/ransack#search-matchers')
|
340
|
+
key :required, false
|
341
|
+
key :type, :json
|
342
|
+
end
|
343
|
+
parameter do
|
344
|
+
key :name, :page
|
345
|
+
key :in, :query
|
346
|
+
key :description, I18n.t('apicasso.page.description',
|
347
|
+
default: 'Records pagination paging, which offsets collection based on `params[:per_page]`')
|
348
|
+
key :required, false
|
349
|
+
key :type, :integer
|
350
|
+
end
|
351
|
+
parameter do
|
352
|
+
key :name, :per_page
|
353
|
+
key :in, :query
|
354
|
+
key :description, I18n.t('apicasso.per_page.description',
|
355
|
+
default: 'Records pagination size, which sets how many records will be rendered per request')
|
356
|
+
key :required, false
|
357
|
+
key :type, :integer
|
358
|
+
end
|
359
|
+
response 200 do
|
360
|
+
key :description, I18n.t("activerecord.models.#{inner_name.underscore}.index.response",
|
361
|
+
default: "#{inner_name} response, which include records matching current query and pagination metadata")
|
362
|
+
parameter do
|
363
|
+
key :name, :total
|
364
|
+
key :description, I18n.t('apicasso.total.description',
|
365
|
+
default: 'Total records contained in current collection, as if there was no pagination.')
|
366
|
+
key :required, true
|
367
|
+
key :type, :integer
|
368
|
+
end
|
369
|
+
parameter do
|
370
|
+
key :name, :total_pages
|
371
|
+
key :description, I18n.t('apicasso.total_pages.description',
|
372
|
+
default: 'How many pages of data the current collection has.')
|
373
|
+
key :required, true
|
374
|
+
key :type, :integer
|
375
|
+
end
|
376
|
+
parameter do
|
377
|
+
key :name, :last_page
|
378
|
+
key :description, I18n.t('apicasso.last_page.description',
|
379
|
+
default: 'An indication if current request is the last to paginate in the current collection')
|
380
|
+
key :required, true
|
381
|
+
key :type, :boolean
|
382
|
+
end
|
383
|
+
parameter do
|
384
|
+
key :name, :previous_page
|
385
|
+
key :description, I18n.t('apicasso.previous_page.description',
|
386
|
+
default: "The link of the previous page for the current collection. It can be null if there isn't any")
|
387
|
+
key :required, false
|
388
|
+
key :type, :string
|
389
|
+
end
|
390
|
+
parameter do
|
391
|
+
key :name, :next_page
|
392
|
+
key :description, I18n.t('apicasso.next_page.description',
|
393
|
+
default: "The link of the next page for the current collection. It can be null if there isn't any")
|
394
|
+
key :required, false
|
395
|
+
key :type, :string
|
396
|
+
end
|
397
|
+
parameter do
|
398
|
+
key :name, :out_of_bounds
|
399
|
+
key :description, I18n.t('apicasso.out_of_bounds.description',
|
400
|
+
default: 'An indication if current request is out of pagination bounds for the current collection')
|
401
|
+
key :required, true
|
402
|
+
key :type, :boolean
|
403
|
+
end
|
404
|
+
parameter do
|
405
|
+
key :name, :offset
|
406
|
+
key :description, I18n.t('apicasso.offset.description',
|
407
|
+
default: 'How many records were offsetted from the collection to render the current page')
|
408
|
+
key :required, true
|
409
|
+
key :type, :integer
|
410
|
+
end
|
411
|
+
parameter do
|
412
|
+
key :name, :entries
|
413
|
+
key :description, I18n.t('apicasso.entries.description',
|
414
|
+
default: 'The records collection in the current pagination scope.')
|
415
|
+
key :required, true
|
416
|
+
key :type, :array
|
417
|
+
items do
|
418
|
+
key :'$ref', inner_name.to_sym
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
response :default do
|
423
|
+
key :description, I18n.t("activerecord.errors.models.#{inner_name.underscore}",
|
424
|
+
default: "Unexpected error in #{inner_name}")
|
425
|
+
schema do
|
426
|
+
key :'$ref', :ErrorModel
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
operation :options do
|
431
|
+
key :description, I18n.t("activerecord.models.#{inner_name.underscore}.schema.description",
|
432
|
+
default: "#{inner_name} metadata information.")
|
433
|
+
key :operationId, "schema#{inner_name.pluralize}"
|
434
|
+
key :produces, ['application/json']
|
435
|
+
key :tags, [inner_name.underscore]
|
436
|
+
response 200 do
|
437
|
+
key :description, I18n.t("activerecord.models.#{inner_name.underscore}.schema.response",
|
438
|
+
default: "#{inner_name} metadata as a json with field names as keys and field types as values.")
|
439
|
+
schema do
|
440
|
+
key :'$ref', "#{inner_name}Metadata".to_sym
|
441
|
+
end
|
442
|
+
end
|
443
|
+
response :default do
|
444
|
+
key :description, I18n.t("activerecord.errors.models.#{inner_name.underscore}",
|
445
|
+
default: "Unexpected error in #{inner_name}")
|
446
|
+
schema do
|
447
|
+
key :'$ref', :ErrorModel
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def index
|
456
|
+
render json: Swagger::Blocks.build_root_json(SWAGGERED_CLASSES)
|
457
|
+
end
|
458
|
+
|
459
|
+
private
|
460
|
+
|
461
|
+
def extract_klass_name(opts = {})
|
462
|
+
association = opts[:model].reflect_on_all_associations.select{ |ass| ass.name == opts[:association] }.first
|
463
|
+
association.class_name
|
464
|
+
end
|
465
|
+
|
466
|
+
def extract_klass(opts = {})
|
467
|
+
extract_klass_name(opts).constantize
|
468
|
+
rescue NameError
|
469
|
+
return false
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|