power_api 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 +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +9 -0
- data/.hound.yml +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +479 -0
- data/.ruby-version +1 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +310 -0
- data/Guardfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +904 -0
- data/Rakefile +10 -0
- data/app/assets/config/power_api_manifest.js +2 -0
- data/app/assets/images/power_api/.keep +0 -0
- data/app/assets/javascripts/power_api/application.js +13 -0
- data/app/assets/stylesheets/power_api/application.css +15 -0
- data/app/controllers/concerns/api/deprecated.rb +23 -0
- data/app/controllers/concerns/api/error.rb +37 -0
- data/app/controllers/concerns/api/filtered.rb +15 -0
- data/app/controllers/concerns/api/versioned.rb +36 -0
- data/app/controllers/power_api/base_controller.rb +14 -0
- data/app/helpers/power_api/application_helper.rb +4 -0
- data/app/jobs/power_api/application_job.rb +4 -0
- data/app/mailers/power_api/application_mailer.rb +6 -0
- data/app/models/power_api/application_record.rb +5 -0
- data/app/responders/api_responder.rb +13 -0
- data/app/views/layouts/power_api/application.html.erb +14 -0
- data/bin/rails +14 -0
- data/config/routes.rb +2 -0
- data/lib/generators/power_api/controller/USAGE +5 -0
- data/lib/generators/power_api/controller/controller_generator.rb +152 -0
- data/lib/generators/power_api/install/USAGE +5 -0
- data/lib/generators/power_api/install/install_generator.rb +67 -0
- data/lib/generators/power_api/version/USAGE +5 -0
- data/lib/generators/power_api/version/version_generator.rb +54 -0
- data/lib/power_api.rb +35 -0
- data/lib/power_api/engine.rb +28 -0
- data/lib/power_api/errors.rb +4 -0
- data/lib/power_api/generator_helper/active_record_resource.rb +192 -0
- data/lib/power_api/generator_helper/ams_helper.rb +43 -0
- data/lib/power_api/generator_helper/controller_helper.rb +184 -0
- data/lib/power_api/generator_helper/pagination_helper.rb +48 -0
- data/lib/power_api/generator_helper/resource_helper.rb +33 -0
- data/lib/power_api/generator_helper/routes_helper.rb +91 -0
- data/lib/power_api/generator_helper/rubocop_helper.rb +11 -0
- data/lib/power_api/generator_helper/simple_token_auth_helper.rb +124 -0
- data/lib/power_api/generator_helper/swagger_helper.rb +463 -0
- data/lib/power_api/generator_helper/template_builder_helper.rb +23 -0
- data/lib/power_api/generator_helper/version_helper.rb +16 -0
- data/lib/power_api/generator_helpers.rb +25 -0
- data/lib/power_api/version.rb +3 -0
- data/lib/tasks/power_api_tasks.rake +4 -0
- data/power_api.gemspec +44 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +5 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/concerns/api/deprecated_spec.rb +37 -0
- data/spec/dummy/app/controllers/concerns/api/error_spec.rb +97 -0
- data/spec/dummy/app/controllers/concerns/api/filtered_spec.rb +42 -0
- data/spec/dummy/app/controllers/concerns/api/versioned_spec.rb +64 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/blog.rb +5 -0
- data/spec/dummy/app/models/portfolio.rb +5 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -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 +38 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/bin/yarn +11 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +54 -0
- data/spec/dummy/config/environments/production.rb +91 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +14 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -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/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +56 -0
- data/spec/dummy/config/routes.rb +12 -0
- data/spec/dummy/config/secrets.yml +32 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/db/migrate/20190322205209_create_blogs.rb +10 -0
- data/spec/dummy/db/migrate/20200215225917_create_users.rb +9 -0
- data/spec/dummy/db/migrate/20200227150449_create_portfolios.rb +10 -0
- data/spec/dummy/db/migrate/20200227150548_add_portfolio_id_blogs.rb +5 -0
- data/spec/dummy/db/schema.rb +38 -0
- data/spec/dummy/package.json +5 -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/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/spec/assets/image.png +0 -0
- data/spec/dummy/spec/assets/video.mp4 +0 -0
- data/spec/dummy/spec/factories/blogs.rb +7 -0
- data/spec/dummy/spec/factories/portfolios.rb +6 -0
- data/spec/dummy/spec/factories/users.rb +5 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/ams_helper_spec.rb +87 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/controller_helper_spec.rb +361 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/pagination_helper_spec.rb +56 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/resource_helper_spec.rb +31 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/routes_helper_spec.rb +179 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/simple_token_auth_helper_spec.rb +164 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/swagger_helper_spec.rb +451 -0
- data/spec/dummy/spec/lib/power_api/generator_helper/version_helper_spec.rb +55 -0
- data/spec/dummy/spec/support/shared_examples/active_record_resource.rb +101 -0
- data/spec/dummy/spec/support/shared_examples/active_record_resource_atrributes.rb +164 -0
- data/spec/dummy/spec/support/test_generator_helpers.rb +29 -0
- data/spec/dummy/spec/support/test_helpers.rb +11 -0
- data/spec/rails_helper.rb +49 -0
- data/spec/spec_helper.rb +9 -0
- metadata +602 -0
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.5
|
data/.travis.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
sudo: false
|
|
2
|
+
language: ruby
|
|
3
|
+
rvm:
|
|
4
|
+
- 2.3.1
|
|
5
|
+
script:
|
|
6
|
+
- RAILS_ENV=test bundle exec rake db:create db:migrate
|
|
7
|
+
- bundle exec rspec spec
|
|
8
|
+
deploy:
|
|
9
|
+
provider: rubygems
|
|
10
|
+
api_key:
|
|
11
|
+
secure: your_secure_secret
|
|
12
|
+
gem: power_api
|
|
13
|
+
on:
|
|
14
|
+
tags: true
|
|
15
|
+
repo: platanus/power_api
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
# Declare your gem's dependencies in power_api.gemspec.
|
|
4
|
+
# Bundler will treat runtime dependencies like base dependencies, and
|
|
5
|
+
# development dependencies will be added by default to the :development group.
|
|
6
|
+
gemspec
|
|
7
|
+
|
|
8
|
+
# Declare any dependencies that are still in development here instead of in
|
|
9
|
+
# your gemspec. These might include edge Rails or gems from your path or
|
|
10
|
+
# Git. Remember to move these dependencies to your gemspec before releasing
|
|
11
|
+
# your gem to rubygems.org.
|
|
12
|
+
|
|
13
|
+
# To use a debugger
|
|
14
|
+
# gem 'byebug', group: [:development, :test]
|
|
15
|
+
|
|
16
|
+
group :development, :test do
|
|
17
|
+
gem 'rswag-specs'
|
|
18
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
power_api (0.1.0)
|
|
5
|
+
active_model_serializers (~> 0.10.0)
|
|
6
|
+
api-pagination
|
|
7
|
+
kaminari
|
|
8
|
+
rails (>= 4.2.0)
|
|
9
|
+
ransack
|
|
10
|
+
responders
|
|
11
|
+
rswag-api
|
|
12
|
+
rswag-ui
|
|
13
|
+
simple_token_authentication (~> 1.0)
|
|
14
|
+
versionist (~> 1.0)
|
|
15
|
+
|
|
16
|
+
GEM
|
|
17
|
+
remote: https://rubygems.org/
|
|
18
|
+
specs:
|
|
19
|
+
actioncable (5.2.2.1)
|
|
20
|
+
actionpack (= 5.2.2.1)
|
|
21
|
+
nio4r (~> 2.0)
|
|
22
|
+
websocket-driver (>= 0.6.1)
|
|
23
|
+
actionmailer (5.2.2.1)
|
|
24
|
+
actionpack (= 5.2.2.1)
|
|
25
|
+
actionview (= 5.2.2.1)
|
|
26
|
+
activejob (= 5.2.2.1)
|
|
27
|
+
mail (~> 2.5, >= 2.5.4)
|
|
28
|
+
rails-dom-testing (~> 2.0)
|
|
29
|
+
actionpack (5.2.2.1)
|
|
30
|
+
actionview (= 5.2.2.1)
|
|
31
|
+
activesupport (= 5.2.2.1)
|
|
32
|
+
rack (~> 2.0)
|
|
33
|
+
rack-test (>= 0.6.3)
|
|
34
|
+
rails-dom-testing (~> 2.0)
|
|
35
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
|
36
|
+
actionview (5.2.2.1)
|
|
37
|
+
activesupport (= 5.2.2.1)
|
|
38
|
+
builder (~> 3.1)
|
|
39
|
+
erubi (~> 1.4)
|
|
40
|
+
rails-dom-testing (~> 2.0)
|
|
41
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
|
42
|
+
active_model_serializers (0.10.10)
|
|
43
|
+
actionpack (>= 4.1, < 6.1)
|
|
44
|
+
activemodel (>= 4.1, < 6.1)
|
|
45
|
+
case_transform (>= 0.2)
|
|
46
|
+
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
|
47
|
+
activejob (5.2.2.1)
|
|
48
|
+
activesupport (= 5.2.2.1)
|
|
49
|
+
globalid (>= 0.3.6)
|
|
50
|
+
activemodel (5.2.2.1)
|
|
51
|
+
activesupport (= 5.2.2.1)
|
|
52
|
+
activerecord (5.2.2.1)
|
|
53
|
+
activemodel (= 5.2.2.1)
|
|
54
|
+
activesupport (= 5.2.2.1)
|
|
55
|
+
arel (>= 9.0)
|
|
56
|
+
activestorage (5.2.2.1)
|
|
57
|
+
actionpack (= 5.2.2.1)
|
|
58
|
+
activerecord (= 5.2.2.1)
|
|
59
|
+
marcel (~> 0.3.1)
|
|
60
|
+
activesupport (5.2.2.1)
|
|
61
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
62
|
+
i18n (>= 0.7, < 2)
|
|
63
|
+
minitest (~> 5.1)
|
|
64
|
+
tzinfo (~> 1.1)
|
|
65
|
+
addressable (2.7.0)
|
|
66
|
+
public_suffix (>= 2.0.2, < 5.0)
|
|
67
|
+
api-pagination (4.8.2)
|
|
68
|
+
arel (9.0.0)
|
|
69
|
+
ast (2.4.0)
|
|
70
|
+
bcrypt (3.1.13)
|
|
71
|
+
builder (3.2.3)
|
|
72
|
+
case_transform (0.2)
|
|
73
|
+
activesupport
|
|
74
|
+
coderay (1.1.2)
|
|
75
|
+
concurrent-ruby (1.1.5)
|
|
76
|
+
coveralls (0.8.22)
|
|
77
|
+
json (>= 1.8, < 3)
|
|
78
|
+
simplecov (~> 0.16.1)
|
|
79
|
+
term-ansicolor (~> 1.3)
|
|
80
|
+
thor (~> 0.19.4)
|
|
81
|
+
tins (~> 1.6)
|
|
82
|
+
crass (1.0.4)
|
|
83
|
+
devise (4.7.1)
|
|
84
|
+
bcrypt (~> 3.0)
|
|
85
|
+
orm_adapter (~> 0.1)
|
|
86
|
+
railties (>= 4.1.0)
|
|
87
|
+
responders
|
|
88
|
+
warden (~> 1.2.3)
|
|
89
|
+
diff-lcs (1.3)
|
|
90
|
+
docile (1.3.1)
|
|
91
|
+
erubi (1.8.0)
|
|
92
|
+
factory_bot (5.0.2)
|
|
93
|
+
activesupport (>= 4.2.0)
|
|
94
|
+
factory_bot_rails (5.0.1)
|
|
95
|
+
factory_bot (~> 5.0.0)
|
|
96
|
+
railties (>= 4.2.0)
|
|
97
|
+
ffi (1.10.0)
|
|
98
|
+
formatador (0.2.5)
|
|
99
|
+
globalid (0.4.2)
|
|
100
|
+
activesupport (>= 4.2.0)
|
|
101
|
+
guard (2.15.0)
|
|
102
|
+
formatador (>= 0.2.4)
|
|
103
|
+
listen (>= 2.7, < 4.0)
|
|
104
|
+
lumberjack (>= 1.0.12, < 2.0)
|
|
105
|
+
nenv (~> 0.1)
|
|
106
|
+
notiffany (~> 0.0)
|
|
107
|
+
pry (>= 0.9.12)
|
|
108
|
+
shellany (~> 0.0)
|
|
109
|
+
thor (>= 0.18.1)
|
|
110
|
+
guard-compat (1.2.1)
|
|
111
|
+
guard-rspec (4.7.3)
|
|
112
|
+
guard (~> 2.1)
|
|
113
|
+
guard-compat (~> 1.1)
|
|
114
|
+
rspec (>= 2.99.0, < 4.0)
|
|
115
|
+
i18n (1.6.0)
|
|
116
|
+
concurrent-ruby (~> 1.0)
|
|
117
|
+
jaro_winkler (1.5.4)
|
|
118
|
+
json (2.2.0)
|
|
119
|
+
json-schema (2.8.1)
|
|
120
|
+
addressable (>= 2.4)
|
|
121
|
+
jsonapi-renderer (0.2.2)
|
|
122
|
+
kaminari (1.1.1)
|
|
123
|
+
activesupport (>= 4.1.0)
|
|
124
|
+
kaminari-actionview (= 1.1.1)
|
|
125
|
+
kaminari-activerecord (= 1.1.1)
|
|
126
|
+
kaminari-core (= 1.1.1)
|
|
127
|
+
kaminari-actionview (1.1.1)
|
|
128
|
+
actionview
|
|
129
|
+
kaminari-core (= 1.1.1)
|
|
130
|
+
kaminari-activerecord (1.1.1)
|
|
131
|
+
activerecord
|
|
132
|
+
kaminari-core (= 1.1.1)
|
|
133
|
+
kaminari-core (1.1.1)
|
|
134
|
+
listen (3.1.5)
|
|
135
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
|
136
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
|
137
|
+
ruby_dep (~> 1.2)
|
|
138
|
+
loofah (2.2.3)
|
|
139
|
+
crass (~> 1.0.2)
|
|
140
|
+
nokogiri (>= 1.5.9)
|
|
141
|
+
lumberjack (1.0.13)
|
|
142
|
+
mail (2.7.1)
|
|
143
|
+
mini_mime (>= 0.1.1)
|
|
144
|
+
marcel (0.3.3)
|
|
145
|
+
mimemagic (~> 0.3.2)
|
|
146
|
+
method_source (0.9.2)
|
|
147
|
+
mimemagic (0.3.3)
|
|
148
|
+
mini_mime (1.0.2)
|
|
149
|
+
mini_portile2 (2.4.0)
|
|
150
|
+
minitest (5.11.3)
|
|
151
|
+
nenv (0.3.0)
|
|
152
|
+
nio4r (2.5.2)
|
|
153
|
+
nokogiri (1.10.1)
|
|
154
|
+
mini_portile2 (~> 2.4.0)
|
|
155
|
+
notiffany (0.1.1)
|
|
156
|
+
nenv (~> 0.1)
|
|
157
|
+
shellany (~> 0.0)
|
|
158
|
+
orm_adapter (0.5.0)
|
|
159
|
+
parallel (1.19.1)
|
|
160
|
+
parser (2.7.0.2)
|
|
161
|
+
ast (~> 2.4.0)
|
|
162
|
+
polyamorous (2.3.0)
|
|
163
|
+
activerecord (>= 5.0)
|
|
164
|
+
powerpack (0.1.2)
|
|
165
|
+
pry (0.12.2)
|
|
166
|
+
coderay (~> 1.1.0)
|
|
167
|
+
method_source (~> 0.9.0)
|
|
168
|
+
pry-rails (0.3.9)
|
|
169
|
+
pry (>= 0.10.4)
|
|
170
|
+
psych (3.1.0)
|
|
171
|
+
public_suffix (4.0.1)
|
|
172
|
+
rack (2.0.6)
|
|
173
|
+
rack-test (1.1.0)
|
|
174
|
+
rack (>= 1.0, < 3)
|
|
175
|
+
rails (5.2.2.1)
|
|
176
|
+
actioncable (= 5.2.2.1)
|
|
177
|
+
actionmailer (= 5.2.2.1)
|
|
178
|
+
actionpack (= 5.2.2.1)
|
|
179
|
+
actionview (= 5.2.2.1)
|
|
180
|
+
activejob (= 5.2.2.1)
|
|
181
|
+
activemodel (= 5.2.2.1)
|
|
182
|
+
activerecord (= 5.2.2.1)
|
|
183
|
+
activestorage (= 5.2.2.1)
|
|
184
|
+
activesupport (= 5.2.2.1)
|
|
185
|
+
bundler (>= 1.3.0)
|
|
186
|
+
railties (= 5.2.2.1)
|
|
187
|
+
sprockets-rails (>= 2.0.0)
|
|
188
|
+
rails-dom-testing (2.0.3)
|
|
189
|
+
activesupport (>= 4.2.0)
|
|
190
|
+
nokogiri (>= 1.6)
|
|
191
|
+
rails-html-sanitizer (1.0.4)
|
|
192
|
+
loofah (~> 2.2, >= 2.2.2)
|
|
193
|
+
railties (5.2.2.1)
|
|
194
|
+
actionpack (= 5.2.2.1)
|
|
195
|
+
activesupport (= 5.2.2.1)
|
|
196
|
+
method_source
|
|
197
|
+
rake (>= 0.8.7)
|
|
198
|
+
thor (>= 0.19.0, < 2.0)
|
|
199
|
+
rainbow (3.0.0)
|
|
200
|
+
rake (12.3.2)
|
|
201
|
+
ransack (2.3.0)
|
|
202
|
+
actionpack (>= 5.0)
|
|
203
|
+
activerecord (>= 5.0)
|
|
204
|
+
activesupport (>= 5.0)
|
|
205
|
+
i18n
|
|
206
|
+
polyamorous (= 2.3.0)
|
|
207
|
+
rb-fsevent (0.10.3)
|
|
208
|
+
rb-inotify (0.10.0)
|
|
209
|
+
ffi (~> 1.0)
|
|
210
|
+
responders (3.0.0)
|
|
211
|
+
actionpack (>= 5.0)
|
|
212
|
+
railties (>= 5.0)
|
|
213
|
+
rspec (3.8.0)
|
|
214
|
+
rspec-core (~> 3.8.0)
|
|
215
|
+
rspec-expectations (~> 3.8.0)
|
|
216
|
+
rspec-mocks (~> 3.8.0)
|
|
217
|
+
rspec-core (3.8.0)
|
|
218
|
+
rspec-support (~> 3.8.0)
|
|
219
|
+
rspec-expectations (3.8.2)
|
|
220
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
221
|
+
rspec-support (~> 3.8.0)
|
|
222
|
+
rspec-mocks (3.8.0)
|
|
223
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
224
|
+
rspec-support (~> 3.8.0)
|
|
225
|
+
rspec-rails (3.8.2)
|
|
226
|
+
actionpack (>= 3.0)
|
|
227
|
+
activesupport (>= 3.0)
|
|
228
|
+
railties (>= 3.0)
|
|
229
|
+
rspec-core (~> 3.8.0)
|
|
230
|
+
rspec-expectations (~> 3.8.0)
|
|
231
|
+
rspec-mocks (~> 3.8.0)
|
|
232
|
+
rspec-support (~> 3.8.0)
|
|
233
|
+
rspec-support (3.8.0)
|
|
234
|
+
rswag-api (2.2.0)
|
|
235
|
+
railties (>= 3.1, < 6.1)
|
|
236
|
+
rswag-specs (2.2.0)
|
|
237
|
+
activesupport (>= 3.1, < 6.1)
|
|
238
|
+
json-schema (~> 2.2)
|
|
239
|
+
railties (>= 3.1, < 6.1)
|
|
240
|
+
rswag-ui (2.2.0)
|
|
241
|
+
actionpack (>= 3.1, < 6.1)
|
|
242
|
+
railties (>= 3.1, < 6.1)
|
|
243
|
+
rubocop (0.65.0)
|
|
244
|
+
jaro_winkler (~> 1.5.1)
|
|
245
|
+
parallel (~> 1.10)
|
|
246
|
+
parser (>= 2.5, != 2.5.1.1)
|
|
247
|
+
powerpack (~> 0.1)
|
|
248
|
+
psych (>= 3.1.0)
|
|
249
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
250
|
+
ruby-progressbar (~> 1.7)
|
|
251
|
+
unicode-display_width (~> 1.4.0)
|
|
252
|
+
rubocop-rspec (1.35.0)
|
|
253
|
+
rubocop (>= 0.60.0)
|
|
254
|
+
ruby-progressbar (1.10.1)
|
|
255
|
+
ruby_dep (1.5.0)
|
|
256
|
+
shellany (0.0.1)
|
|
257
|
+
simple_token_authentication (1.17.0)
|
|
258
|
+
actionmailer (>= 3.2.6, < 7)
|
|
259
|
+
actionpack (>= 3.2.6, < 7)
|
|
260
|
+
devise (>= 3.2, < 6)
|
|
261
|
+
simplecov (0.16.1)
|
|
262
|
+
docile (~> 1.1)
|
|
263
|
+
json (>= 1.8, < 3)
|
|
264
|
+
simplecov-html (~> 0.10.0)
|
|
265
|
+
simplecov-html (0.10.2)
|
|
266
|
+
sprockets (4.0.0)
|
|
267
|
+
concurrent-ruby (~> 1.0)
|
|
268
|
+
rack (> 1, < 3)
|
|
269
|
+
sprockets-rails (3.2.1)
|
|
270
|
+
actionpack (>= 4.0)
|
|
271
|
+
activesupport (>= 4.0)
|
|
272
|
+
sprockets (>= 3.0.0)
|
|
273
|
+
sqlite3 (1.3.13)
|
|
274
|
+
term-ansicolor (1.7.1)
|
|
275
|
+
tins (~> 1.0)
|
|
276
|
+
thor (0.19.4)
|
|
277
|
+
thread_safe (0.3.6)
|
|
278
|
+
tins (1.20.2)
|
|
279
|
+
tzinfo (1.2.5)
|
|
280
|
+
thread_safe (~> 0.1)
|
|
281
|
+
unicode-display_width (1.4.1)
|
|
282
|
+
versionist (1.7.0)
|
|
283
|
+
activesupport (>= 3)
|
|
284
|
+
railties (>= 3)
|
|
285
|
+
yard (~> 0.9.11)
|
|
286
|
+
warden (1.2.8)
|
|
287
|
+
rack (>= 2.0.6)
|
|
288
|
+
websocket-driver (0.7.1)
|
|
289
|
+
websocket-extensions (>= 0.1.0)
|
|
290
|
+
websocket-extensions (0.1.4)
|
|
291
|
+
yard (0.9.20)
|
|
292
|
+
|
|
293
|
+
PLATFORMS
|
|
294
|
+
ruby
|
|
295
|
+
|
|
296
|
+
DEPENDENCIES
|
|
297
|
+
coveralls
|
|
298
|
+
factory_bot_rails
|
|
299
|
+
guard-rspec
|
|
300
|
+
power_api!
|
|
301
|
+
pry
|
|
302
|
+
pry-rails
|
|
303
|
+
rspec-rails
|
|
304
|
+
rswag-specs
|
|
305
|
+
rubocop (= 0.65.0)
|
|
306
|
+
rubocop-rspec
|
|
307
|
+
sqlite3 (~> 1.3.0)
|
|
308
|
+
|
|
309
|
+
BUNDLED WITH
|
|
310
|
+
1.17.3
|
data/Guardfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
|
2
|
+
spec_dic = "spec/dummy/spec"
|
|
3
|
+
# RSpec files
|
|
4
|
+
watch("spec/spec_helper.rb") { spec_dic }
|
|
5
|
+
watch("spec/rails_helper.rb") { spec_dic }
|
|
6
|
+
watch(%r{^spec\/dummy\/spec\/support\/(.+)\.rb$}) { spec_dic }
|
|
7
|
+
watch(%r{^spec\/dummy\/spec\/.+_spec\.rb$})
|
|
8
|
+
# Engine files
|
|
9
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/dummy/spec/lib/#{m[1]}_spec.rb" }
|
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/dummy/spec/#{m[1]}_spec.rb" }
|
|
11
|
+
watch(%r{^app/(.*)(\.erb)$}) { |m| "spec/dummy/spec/#{m[1]}#{m[2]}_spec.rb" }
|
|
12
|
+
# Dummy app files
|
|
13
|
+
watch(%r{^spec\/dummy\/app/(.+)\.rb$}) { |m| "spec/dummy/spec/#{m[1]}_spec.rb" }
|
|
14
|
+
watch(%r{^spec\/dummy\/app/(.*)(\.erb)$}) { |m| "spec/dummy/spec/#{m[1]}#{m[2]}_spec.rb" }
|
|
15
|
+
end
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright 2019 Platanus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
# Power API
|
|
2
|
+
|
|
3
|
+
It's a Rails engine that gathers a set of gems and configurations designed to build incredible REST APIs.
|
|
4
|
+
|
|
5
|
+
These gems are:
|
|
6
|
+
|
|
7
|
+
- [API Pagination](https://github.com/davidcelis/api-pagination): to handle issues related to pagination.
|
|
8
|
+
- [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers): to handle API response format.
|
|
9
|
+
- [Ransack](https://github.com/activerecord-hackery/ransack): to handle filters.
|
|
10
|
+
- [Responders](https://github.com/heartcombo/responders): to dry up your API.
|
|
11
|
+
- [Rswag](https://github.com/rswag/rswag): to test and document the API.
|
|
12
|
+
- [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication): to authenticate your resources.
|
|
13
|
+
- [Versionist](https://github.com/bploetz/versionist): to handle the API versioning.
|
|
14
|
+
|
|
15
|
+
> To understand what this gem does, it is recommended to read first about those mentioned above.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Content
|
|
19
|
+
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Usage](#usage)
|
|
22
|
+
- [Initial Setup](#initial-setup)
|
|
23
|
+
- [Command Options](#command-options)
|
|
24
|
+
- [--authenticated-resources](#--authenticated-resources)
|
|
25
|
+
- [Version Creation](#version-creation)
|
|
26
|
+
- [Controller Generation](#controller-generation)
|
|
27
|
+
- [Command Options](#command-options-1)
|
|
28
|
+
- [--attributes](#--attributes)
|
|
29
|
+
- [--version-number](#--version-number)
|
|
30
|
+
- [--use-paginator](#--use-paginator)
|
|
31
|
+
- [--allow-filters](#--allow-filters)
|
|
32
|
+
- [--authenticate-with](#--authenticate-with)
|
|
33
|
+
- [--owned-by-authenticated-resource](#--owned-by-authenticated-resource)
|
|
34
|
+
- [--parent-resource](#--parent-resource)
|
|
35
|
+
- [Inside the gem](#inside-the-gem)
|
|
36
|
+
- [The Api::Error Concern](#the-apierror-concern)
|
|
37
|
+
- [The Api::Deprecated Concern](#the-apideprecated-concern)
|
|
38
|
+
- [The Api::Versioned Concern](#the-apiversioned-concern)
|
|
39
|
+
- [The ApiResponder](#the-apiresponder)
|
|
40
|
+
- [Testing](#testing)
|
|
41
|
+
- [Contributing](#contributing)
|
|
42
|
+
- [Credits](#credits)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Add to your Gemfile:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
gem 'power_api'
|
|
51
|
+
|
|
52
|
+
group :development, :test do
|
|
53
|
+
gem 'factory_bot_rails'
|
|
54
|
+
gem 'rspec-rails'
|
|
55
|
+
gem 'rswag-specs'
|
|
56
|
+
gem 'rubocop'
|
|
57
|
+
gem 'rubocop-rspec'
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then,
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Initial Setup
|
|
70
|
+
|
|
71
|
+
You must run the following command to have the initial configuration:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rails generate power_api:install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After doing this you will get:
|
|
78
|
+
|
|
79
|
+
- A base controller for your API under `/your_app/app/controllers/api/base_controller.rb`
|
|
80
|
+
```ruby
|
|
81
|
+
class Api::BaseController < PowerApi::BaseController
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
Here you should include everything common to all your API versions. It is usually empty because most of the configuration comes in the `PowerApi::BaseController` that es inside the gem.
|
|
85
|
+
|
|
86
|
+
- A base controller for the first version of your API under `/your_api/app/controllers/api/v1/base_controller.rb`
|
|
87
|
+
```ruby
|
|
88
|
+
class Api::V1::BaseController < Api::BaseController
|
|
89
|
+
before_action do
|
|
90
|
+
self.namespace_for_serializer = ::Api::V1
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
Everything related to version 1 of your API must be included here.
|
|
95
|
+
|
|
96
|
+
- Some initializers:
|
|
97
|
+
- `/your_api/config/initializers/active_model_serializers.rb`:
|
|
98
|
+
```ruby
|
|
99
|
+
class ActiveModelSerializers::Adapter::JsonApi
|
|
100
|
+
def self.default_key_transform
|
|
101
|
+
:unaltered
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
ActiveModelSerializers.config.adapter = :json_api
|
|
106
|
+
```
|
|
107
|
+
Here we tell AMS that we will use the [json api](https://jsonapi.org/) format.
|
|
108
|
+
|
|
109
|
+
- `/your_api/config/initializers/api_pagination.rb`:
|
|
110
|
+
```ruby
|
|
111
|
+
ApiPagination.configure do |config|
|
|
112
|
+
config.paginator = :kaminari
|
|
113
|
+
|
|
114
|
+
# more options...
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
We use what comes by default and kaminari as pager.
|
|
118
|
+
|
|
119
|
+
- `/your_api/config/initializers/rswag-api.rb`:
|
|
120
|
+
```ruby
|
|
121
|
+
Rswag::Api.configure do |c|
|
|
122
|
+
c.swagger_root = Rails.root.to_s + '/swagger'
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
We use the default options but setting the `your_api/swagger` directory as container for the generated Swagger JSON files.
|
|
126
|
+
|
|
127
|
+
- `/your_api/config/initializers/rswag-ui.rb`:
|
|
128
|
+
```ruby
|
|
129
|
+
Rswag::Ui.configure do |c|
|
|
130
|
+
c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs'
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
We configure the first version to be seen in the documentation view.
|
|
134
|
+
|
|
135
|
+
- `/your_api/config/initializers/simple_token_authentication.rb`:
|
|
136
|
+
```ruby
|
|
137
|
+
SimpleTokenAuthentication.configure do |config|
|
|
138
|
+
# options...
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
We use the default options.
|
|
142
|
+
- A modified `/your_api/config/routes.rb` file:
|
|
143
|
+
```ruby
|
|
144
|
+
Rails.application.routes.draw do
|
|
145
|
+
scope path: '/api' do
|
|
146
|
+
api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
mount Rswag::Api::Engine => '/api-docs'
|
|
150
|
+
mount Rswag::Ui::Engine => '/api-docs'
|
|
151
|
+
# ...
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
Here we create the first version with [Versionist](https://github.com/bploetz/versionist) and mount Rswag.
|
|
155
|
+
- A file with the swagger definition for the first version under `/your_api/spec/swagger/v1/definition.rb`
|
|
156
|
+
```ruby
|
|
157
|
+
API_V1 = {
|
|
158
|
+
swagger: '2.0',
|
|
159
|
+
info: {
|
|
160
|
+
title: 'API V1',
|
|
161
|
+
version: 'v1'
|
|
162
|
+
},
|
|
163
|
+
basePath: '/api/v1',
|
|
164
|
+
definitions: {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
- The `/your_api/spec/swagger_helper.rb` (similar to rails_helper.rb file):
|
|
169
|
+
```ruby
|
|
170
|
+
require 'rails_helper'
|
|
171
|
+
|
|
172
|
+
Dir[::Rails.root.join("spec/swagger/**/schemas/*.rb")].each { |f| require f }
|
|
173
|
+
Dir[::Rails.root.join("spec/swagger/**/definition.rb")].each { |f| require f }
|
|
174
|
+
|
|
175
|
+
RSpec.configure do |config|
|
|
176
|
+
# Specify a root folder where Swagger JSON files are generated
|
|
177
|
+
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
|
178
|
+
# to ensure that it's confiugred to serve Swagger from the same folder
|
|
179
|
+
config.swagger_root = Rails.root.to_s + '/swagger'
|
|
180
|
+
|
|
181
|
+
# Define one or more Swagger documents and provide global metadata for each one
|
|
182
|
+
# When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will
|
|
183
|
+
# be generated at the provided relative path under swagger_root
|
|
184
|
+
# By default, the operations defined in spec files are added to the first
|
|
185
|
+
# document below. You can override this behavior by adding a swagger_doc tag to the
|
|
186
|
+
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
|
|
187
|
+
config.swagger_docs = {
|
|
188
|
+
'v1/swagger.json' => API_V1
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
- An empty directory indicating where you should put your serializers for the first version: `/your_api/app/serializers/api/v1/.gitkeep`
|
|
193
|
+
- An empty directory indicating where you should put your API tests: `/your_api/spec/integration/.gitkeep`
|
|
194
|
+
- An empty directory indicating where you should put your swagger schemas `/your_api/spec/swagger/v1/schemas/.gitkeep`
|
|
195
|
+
|
|
196
|
+
#### Command options:
|
|
197
|
+
|
|
198
|
+
##### `--authenticated-resources`
|
|
199
|
+
|
|
200
|
+
Use this option if you want to configure [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication) for one or more models.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
rails g power_api:install --authenticated-resources=user
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Running the above code will generate, in addition to everything described in the initial setup, the following:
|
|
207
|
+
|
|
208
|
+
- The [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication) initializer `/your_api/config/initializers/simple_token_authentication.rb`
|
|
209
|
+
|
|
210
|
+
- An edited version of the User model with the configuration needed for Simple Token Authentication.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class User < ApplicationRecord
|
|
214
|
+
acts_as_token_authenticatable
|
|
215
|
+
|
|
216
|
+
# more code...
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
- The migration `/your_api/db/migrate/20200228173608_add_authentication_token_to_users.rb` to add the `authentication_token` to your users table.
|
|
220
|
+
|
|
221
|
+
### Version Creation
|
|
222
|
+
|
|
223
|
+
To add a new version you must run the following command:
|
|
224
|
+
```bash
|
|
225
|
+
rails g power_api:version VERSION_NUMBER
|
|
226
|
+
```
|
|
227
|
+
Example:
|
|
228
|
+
```bash
|
|
229
|
+
rails g power_api:version 2
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Doing this will add the same thing that was added for version one in the initial setup but this time for the number version provided as parameter.
|
|
233
|
+
|
|
234
|
+
### Controller Generation
|
|
235
|
+
|
|
236
|
+
To add a controller you must run the following command:
|
|
237
|
+
```bash
|
|
238
|
+
rails g power_api:controller MODEL_NAME [options]
|
|
239
|
+
```
|
|
240
|
+
Example:
|
|
241
|
+
```bash
|
|
242
|
+
rails g power_api:controller blog
|
|
243
|
+
```
|
|
244
|
+
Assuming we have the following model,
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class Blog < ApplicationRecord
|
|
248
|
+
# == Schema Information
|
|
249
|
+
#
|
|
250
|
+
# Table name: blogs
|
|
251
|
+
#
|
|
252
|
+
# id :bigint(8) not null, primary key
|
|
253
|
+
# title :string(255)
|
|
254
|
+
# body :text(65535)
|
|
255
|
+
# created_at :datetime not null
|
|
256
|
+
# updated_at :datetime not null
|
|
257
|
+
#
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
after doing this you will get:
|
|
262
|
+
|
|
263
|
+
- A modified `/your_api/config/routes.rb` file with the new resource:
|
|
264
|
+
```ruby
|
|
265
|
+
Rails.application.routes.draw do
|
|
266
|
+
scope path: '/api' do
|
|
267
|
+
api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
|
|
268
|
+
resources :blogs
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
- A controller under `/your_api/app/controllers/api/v1/blogs_controller.rb`
|
|
274
|
+
```ruby
|
|
275
|
+
class Api::V1::BlogsController < Api::V1::BaseController
|
|
276
|
+
def index
|
|
277
|
+
respond_with Blog.all
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def show
|
|
281
|
+
respond_with blog
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def create
|
|
285
|
+
respond_with Blog.create!(blog_params)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def update
|
|
289
|
+
respond_with blog.update!(blog_params)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def destroy
|
|
293
|
+
respond_with blog.destroy!
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def blog
|
|
299
|
+
@blog ||= Blog.find_by!(id: params[:id])
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def blog_params
|
|
303
|
+
params.require(:blog).permit(
|
|
304
|
+
:title,
|
|
305
|
+
:body,
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
```
|
|
310
|
+
- A serializer under `/your_api/app/serializers/api/v1/blog_serializer.rb`
|
|
311
|
+
```ruby
|
|
312
|
+
class Api::V1::BlogSerializer < ActiveModel::Serializer
|
|
313
|
+
type :blog
|
|
314
|
+
|
|
315
|
+
attributes(
|
|
316
|
+
:title,
|
|
317
|
+
:body,
|
|
318
|
+
:created_at,
|
|
319
|
+
:updated_at
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
- A spec file under `/your_api/spec/integration/api/v1/blogs_spec.rb`
|
|
324
|
+
```ruby
|
|
325
|
+
require 'swagger_helper'
|
|
326
|
+
|
|
327
|
+
describe 'API V1 Blogs', swagger_doc: 'v1/swagger.json' do
|
|
328
|
+
path '/blogs' do
|
|
329
|
+
get 'Retrieves Blogs' do
|
|
330
|
+
description 'Retrieves all the blogs'
|
|
331
|
+
produces 'application/json'
|
|
332
|
+
|
|
333
|
+
let(:collection_count) { 5 }
|
|
334
|
+
let(:expected_collection_count) { collection_count }
|
|
335
|
+
|
|
336
|
+
before { create_list(:blog, collection_count) }
|
|
337
|
+
|
|
338
|
+
response '200', 'Blogs retrieved' do
|
|
339
|
+
schema('$ref' => '#/definitions/blogs_collection')
|
|
340
|
+
|
|
341
|
+
run_test! do |response|
|
|
342
|
+
expect(JSON.parse(response.body)['data'].count).to eq(expected_collection_count)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
post 'Creates Blog' do
|
|
348
|
+
description 'Creates Blog'
|
|
349
|
+
consumes 'application/json'
|
|
350
|
+
produces 'application/json'
|
|
351
|
+
parameter(name: :blog, in: :body)
|
|
352
|
+
|
|
353
|
+
response '201', 'blog creaed' do
|
|
354
|
+
let(:blog) do
|
|
355
|
+
{
|
|
356
|
+
title: 'Some title',
|
|
357
|
+
body: 'Some body'
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
run_test!
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
path '/blogs/{id}' do
|
|
367
|
+
parameter name: :id, in: :path, type: :integer
|
|
368
|
+
|
|
369
|
+
let(:existent_blog) { create(:blog) }
|
|
370
|
+
let(:id) { existent_blog.id }
|
|
371
|
+
|
|
372
|
+
get 'Retrieves Blog' do
|
|
373
|
+
produces 'application/json'
|
|
374
|
+
|
|
375
|
+
response '200', 'blog retrieved' do
|
|
376
|
+
schema('$ref' => '#/definitions/blog_resource')
|
|
377
|
+
|
|
378
|
+
run_test!
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
response '404', 'invalid blog id' do
|
|
382
|
+
let(:id) { 'invalid' }
|
|
383
|
+
run_test!
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
put 'Updates Blog' do
|
|
388
|
+
description 'Updates Blog'
|
|
389
|
+
consumes 'application/json'
|
|
390
|
+
produces 'application/json'
|
|
391
|
+
parameter(name: :blog, in: :body)
|
|
392
|
+
|
|
393
|
+
response '200', 'blog updated' do
|
|
394
|
+
let(:blog) do
|
|
395
|
+
{
|
|
396
|
+
title: 'Some title',
|
|
397
|
+
body: 'Some body'
|
|
398
|
+
}
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
run_test!
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
delete 'Deletes Blog' do
|
|
406
|
+
produces 'application/json'
|
|
407
|
+
description 'Deletes specific blog'
|
|
408
|
+
|
|
409
|
+
response '204', 'blog deleted' do
|
|
410
|
+
run_test!
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
response '404', 'blog not found' do
|
|
414
|
+
let(:id) { 'invalid' }
|
|
415
|
+
|
|
416
|
+
run_test!
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
- A swagger schema definition under `/your_api/spec/swagger/v1/schemas/blog_schema.rb`
|
|
423
|
+
```ruby
|
|
424
|
+
BLOG_SCHEMA = {
|
|
425
|
+
type: :object,
|
|
426
|
+
properties: {
|
|
427
|
+
id: { type: :string, example: '1' },
|
|
428
|
+
type: { type: :string, example: 'blog' },
|
|
429
|
+
attributes: {
|
|
430
|
+
type: :object,
|
|
431
|
+
properties: {
|
|
432
|
+
title: { type: :string, example: 'Some title', 'x-nullable': true },
|
|
433
|
+
body: { type: :string, example: 'Some body', 'x-nullable': true },
|
|
434
|
+
created_at: { type: :string, example: '1984-06-04 09:00', 'x-nullable': true },
|
|
435
|
+
updated_at: { type: :string, example: '1984-06-04 09:00', 'x-nullable': true }
|
|
436
|
+
},
|
|
437
|
+
required: [
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
required: [
|
|
442
|
+
:id,
|
|
443
|
+
:type,
|
|
444
|
+
:attributes
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
BLOGS_COLLECTION_SCHEMA = {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
data: {
|
|
452
|
+
type: "array",
|
|
453
|
+
items: { "$ref" => "#/definitions/blog" }
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
required: [
|
|
457
|
+
:data
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
BLOG_RESOURCE_SCHEMA = {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
data: { "$ref" => "#/definitions/blog" }
|
|
465
|
+
},
|
|
466
|
+
required: [
|
|
467
|
+
:data
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
- An edited version of `your_api/api_example/spec/swagger/v1/definition.rb` with the schema definitions for the `Blog` resource.
|
|
472
|
+
```ruby
|
|
473
|
+
API_V1 = {
|
|
474
|
+
swagger: '2.0',
|
|
475
|
+
info: {
|
|
476
|
+
title: 'API V1',
|
|
477
|
+
version: 'v1'
|
|
478
|
+
},
|
|
479
|
+
basePath: '/api/v1',
|
|
480
|
+
definitions: {
|
|
481
|
+
blog: BLOG_SCHEMA,
|
|
482
|
+
blogs_collection: BLOGS_COLLECTION_SCHEMA,
|
|
483
|
+
blog_resource: BLOG_RESOURCE_SCHEMA,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Command options:
|
|
489
|
+
|
|
490
|
+
##### `--attributes`
|
|
491
|
+
|
|
492
|
+
Use this option if you want to choose which attributes of your model to add to the API response.
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
rails g power_api:controller blog --attributes=title
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
When you do this, you will see permited_params, serializers, swagger definitions, etc. showing only the selected attributes
|
|
499
|
+
|
|
500
|
+
For example, the serializer under `/your_api/app/serializers/api/v1/blog_serializer.rb` will show:
|
|
501
|
+
```ruby
|
|
502
|
+
class Api::V1::BlogSerializer < ActiveModel::Serializer
|
|
503
|
+
type :blog
|
|
504
|
+
|
|
505
|
+
attributes(
|
|
506
|
+
:title,
|
|
507
|
+
)
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
##### `--version-number`
|
|
512
|
+
|
|
513
|
+
Use this option if you want to decide which version the new controller will belong to.
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
rails g power_api:controller blog --version-number=2
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
##### `--use-paginator`
|
|
520
|
+
|
|
521
|
+
Use this option if you want to paginate the index endpoint collection.
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
rails g power_api:controller blog --use-paginator
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
The controller under `/your_api/app/controllers/api/v1/blogs_controller.rb` will be modified to use the paginator like this:
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
class Api::V1::BlogsController < Api::V1::BaseController
|
|
531
|
+
def index
|
|
532
|
+
respond_with paginate(Blog.all)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# more code...
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Due to the API Pagination gem the `X-Total`, `X-Per-Page` and `X-Page` headers will be added to the answer. The parameters `params[:page][:number]` and `params[:page][:size]` can also be passed through the query string to access the different pages.
|
|
540
|
+
|
|
541
|
+
Because the AMS gem is set with "json api" format, links related to pagination will be added to the API response.
|
|
542
|
+
|
|
543
|
+
##### `--allow-filters`
|
|
544
|
+
|
|
545
|
+
Use this option if you want to filter your index endpoint collection with [Ransack](https://github.com/activerecord-hackery/ransack)
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
rails g power_api:controller blog --allow-filters
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
The controller under `/your_api/app/controllers/api/v1/blogs_controller.rb` will be modified like this:
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
class Api::V1::BlogsController < Api::V1::BaseController
|
|
555
|
+
def index
|
|
556
|
+
respond_with filtered_collection(Blog.all)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# more code...
|
|
560
|
+
end
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The `filtered_collection` method is defined inside the gem and uses ransack below.
|
|
564
|
+
You will be able to filter the results according to this: https://github.com/activerecord-hackery/ransack#search-matchers
|
|
565
|
+
|
|
566
|
+
For example:
|
|
567
|
+
|
|
568
|
+
`http://localhost:3000/api/v1/blogs?q[id_gt]=22`
|
|
569
|
+
|
|
570
|
+
to search blogs with id greater than 22
|
|
571
|
+
|
|
572
|
+
##### `--authenticate-with`
|
|
573
|
+
|
|
574
|
+
Use this option if you want to have authorized resources.
|
|
575
|
+
|
|
576
|
+
> To learn more about the authentication method used please read more about [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication) gem.
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
rails g power_api:controller MODEL_NAME --authenticate-with=ANOTHER_MODEL_NAME
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Example:
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
rails g power_api:controller blog --authenticate-with=user
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
When you do this your controller will have the following line:
|
|
589
|
+
|
|
590
|
+
```ruby
|
|
591
|
+
class Api::V1::BlogsController < Api::V1::BaseController
|
|
592
|
+
acts_as_token_authentication_handler_for User, fallback: :exception
|
|
593
|
+
|
|
594
|
+
# mode code...
|
|
595
|
+
end
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
In addition, the specs under `/your_api/spec/integration/api/v1/blogs_spec.rb` will add tests related with authorization.
|
|
599
|
+
|
|
600
|
+
```ruby
|
|
601
|
+
response '401', 'user unauthorized' do
|
|
602
|
+
let(:user_token) { 'invalid' }
|
|
603
|
+
|
|
604
|
+
run_test!
|
|
605
|
+
end
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
##### `--owned-by-authenticated-resource`
|
|
609
|
+
|
|
610
|
+
If you have an authenticated resource you can choose your new resource be owned by the authenticated one.
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
rails g power_api:controller blog --authenticate-with=user --owned-by-authenticated-resource
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
The controller will look like this:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
class Api::V1::BlogsController < Api::V1::BaseController
|
|
620
|
+
acts_as_token_authentication_handler_for User, fallback: :exception
|
|
621
|
+
|
|
622
|
+
def index
|
|
623
|
+
respond_with blogs
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def show
|
|
627
|
+
respond_with blog
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def create
|
|
631
|
+
respond_with blogs.create!(blog_params)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def update
|
|
635
|
+
respond_with blog.update!(blog_params)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def destroy
|
|
639
|
+
respond_with blog.destroy!
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
private
|
|
643
|
+
|
|
644
|
+
def blog
|
|
645
|
+
@blog ||= blogs.find_by!(id: params[:id])
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def blogs
|
|
649
|
+
@blogs ||= current_user.blogs
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def blog_params
|
|
653
|
+
params.require(:blog).permit(
|
|
654
|
+
:title,
|
|
655
|
+
:body
|
|
656
|
+
)
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
As you can see the resource (`blog`) will always come from the authorized one (`current_user.blogs`)
|
|
662
|
+
|
|
663
|
+
To make this possible, the models should be related as follows:
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
class Blog < ApplicationRecord
|
|
667
|
+
belongs_to :user
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
class User < ApplicationRecord
|
|
671
|
+
has_many :blogs
|
|
672
|
+
end
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
##### `--parent-resource`
|
|
676
|
+
|
|
677
|
+
Assuming we have the following models,
|
|
678
|
+
|
|
679
|
+
```ruby
|
|
680
|
+
class Blog < ApplicationRecord
|
|
681
|
+
has_many :comments
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
class Comment < ApplicationRecord
|
|
685
|
+
belongs_to :blog
|
|
686
|
+
end
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
we can run the following code to handle nested resources:
|
|
690
|
+
|
|
691
|
+
```ruby
|
|
692
|
+
rails g power_api:controller comment --attributes=body --parent-resource=blog
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Running the previous code we will get:
|
|
696
|
+
|
|
697
|
+
- The controller under `/your_api/app/controllers/api/v1/comments_controller.rb`:
|
|
698
|
+
```ruby
|
|
699
|
+
class Api::V1::CommentsController < Api::V1::BaseController
|
|
700
|
+
def index
|
|
701
|
+
respond_with comments
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def show
|
|
705
|
+
respond_with comment
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def create
|
|
709
|
+
respond_with comments.create!(comment_params)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def update
|
|
713
|
+
respond_with comment.update!(comment_params)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def destroy
|
|
717
|
+
respond_with comment.destroy!
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
private
|
|
721
|
+
|
|
722
|
+
def comment
|
|
723
|
+
@comment ||= Comment.find_by!(id: params[:id])
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def comments
|
|
727
|
+
@comments ||= blog.comments
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def blog
|
|
731
|
+
@blog ||= Blog.find_by!(id: params[:blog_id])
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def comment_params
|
|
735
|
+
params.require(:comment).permit(
|
|
736
|
+
:body
|
|
737
|
+
)
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
```
|
|
741
|
+
As you can see the `comments` used on `index` and `create` will always come from `blog` (the parent resource)
|
|
742
|
+
|
|
743
|
+
- A modified `/your_api/config/routes.rb` file with the nested resource:
|
|
744
|
+
```ruby
|
|
745
|
+
Rails.application.routes.draw do
|
|
746
|
+
scope path: '/api' do
|
|
747
|
+
api_version(module: 'Api::V1', path: { value: 'v1' }, defaults: { format: 'json' }) do
|
|
748
|
+
resources :comments, only: [:show, :update, :destroy]
|
|
749
|
+
resources :blogs do
|
|
750
|
+
resources :comments, only: [:index, :create]
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
```
|
|
756
|
+
- A spec file under `/your_api/spec/integration/api/v1/blogs_spec.rb` reflecting the nested resources:
|
|
757
|
+
```ruby
|
|
758
|
+
require 'swagger_helper'
|
|
759
|
+
|
|
760
|
+
describe 'API V1 Comments', swagger_doc: 'v1/swagger.json' do
|
|
761
|
+
let(:blog) { create(:blog) }
|
|
762
|
+
let(:blog_id) { blog.id }
|
|
763
|
+
|
|
764
|
+
path '/blogs/{blog_id}/comments' do
|
|
765
|
+
parameter name: :blog_id, in: :path, type: :integer
|
|
766
|
+
get 'Retrieves Comments' do
|
|
767
|
+
description 'Retrieves all the comments'
|
|
768
|
+
produces 'application/json'
|
|
769
|
+
|
|
770
|
+
let(:collection_count) { 5 }
|
|
771
|
+
let(:expected_collection_count) { collection_count }
|
|
772
|
+
|
|
773
|
+
before { create_list(:comment, collection_count, blog: blog) }
|
|
774
|
+
|
|
775
|
+
response '200', 'Comments retrieved' do
|
|
776
|
+
schema('$ref' => '#/definitions/comments_collection')
|
|
777
|
+
|
|
778
|
+
run_test! do |response|
|
|
779
|
+
expect(JSON.parse(response.body)['data'].count).to eq(expected_collection_count)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# more code...
|
|
786
|
+
end
|
|
787
|
+
```
|
|
788
|
+
> Note that the options: `--parent-resource` and `--owned-by-authenticated-resource` cannot be used together.
|
|
789
|
+
|
|
790
|
+
## Inside the gem
|
|
791
|
+
|
|
792
|
+
```ruby
|
|
793
|
+
module PowerApi
|
|
794
|
+
class BaseController < ApplicationController
|
|
795
|
+
include Api::Error
|
|
796
|
+
include Api::Deprecated
|
|
797
|
+
include Api::Versioned
|
|
798
|
+
|
|
799
|
+
self.responder = ApiResponder
|
|
800
|
+
|
|
801
|
+
respond_to :json
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
The `PowerApi::BaseController` class that exists inside this gem and is inherited by the base class of your API (`/your_app/app/controllers/api/base_controller.rb`) includes functionality that I will describe bellow:
|
|
807
|
+
|
|
808
|
+
### The `Api::Error` concern
|
|
809
|
+
|
|
810
|
+
This module handles common exceptions like:
|
|
811
|
+
|
|
812
|
+
- `ActiveRecord::RecordNotFound`
|
|
813
|
+
- `ActiveModel::ForbiddenAttributesError`
|
|
814
|
+
- `ActiveRecord::RecordInvalid`
|
|
815
|
+
- `PowerApi::InvalidVersion`
|
|
816
|
+
- `Exception`
|
|
817
|
+
|
|
818
|
+
If you want to handle new errors, this can be done by calling the `respond_api_error` method in the base class of your API like this:
|
|
819
|
+
|
|
820
|
+
```ruby
|
|
821
|
+
class Api::BaseController < PowerApi::BaseController
|
|
822
|
+
rescue_from "MyCustomErrorClass" do |exception|
|
|
823
|
+
respond_api_error(:bad_request, message: "some error message", detail: exception.message)
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### The `Api::Deprecated` concern
|
|
829
|
+
|
|
830
|
+
This module is useful when you want to mark endpoints as deprecated.
|
|
831
|
+
|
|
832
|
+
For example, if you have the following controller:
|
|
833
|
+
|
|
834
|
+
```ruby
|
|
835
|
+
class Api::V1::CommentsController < Api::V1::BaseController
|
|
836
|
+
deprecate :index
|
|
837
|
+
|
|
838
|
+
def index
|
|
839
|
+
respond_with comments
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# more code...
|
|
843
|
+
end
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
And then in your browser you execute: `GET /api/v1/comments`, you will get a `Deprecated: true` response header.
|
|
847
|
+
|
|
848
|
+
This is useful to notify your customers that an endpoint will not be available in the next version of the API.
|
|
849
|
+
|
|
850
|
+
### The `Api::Versioned` concern
|
|
851
|
+
|
|
852
|
+
This module includes to your API responses the version of the API in a header. For example: `Content-Type: application/json; charset=utf-8; version=1`
|
|
853
|
+
|
|
854
|
+
### The `ApiResponder`
|
|
855
|
+
|
|
856
|
+
It look like this:
|
|
857
|
+
|
|
858
|
+
```ruby
|
|
859
|
+
class ApiResponder < ActionController::Responder
|
|
860
|
+
def api_behavior
|
|
861
|
+
raise MissingRenderer.new(format) unless has_renderer?
|
|
862
|
+
|
|
863
|
+
if delete?
|
|
864
|
+
head :no_content
|
|
865
|
+
elsif post?
|
|
866
|
+
display resource, status: :created
|
|
867
|
+
else
|
|
868
|
+
display resource
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
As you can see, this simple [Responder](https://github.com/heartcombo/responders) handles the API response based on the HTTP verbs.
|
|
875
|
+
|
|
876
|
+
## Testing
|
|
877
|
+
|
|
878
|
+
To run the specs you need to execute, **in the root path of the gem**, the following command:
|
|
879
|
+
|
|
880
|
+
```bash
|
|
881
|
+
bundle exec guard
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
You need to put **all your tests** in the `/power_api/spec/dummy/spec/` directory.
|
|
885
|
+
|
|
886
|
+
## Contributing
|
|
887
|
+
|
|
888
|
+
1. Fork it
|
|
889
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
890
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
891
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
892
|
+
5. Create new Pull Request
|
|
893
|
+
|
|
894
|
+
## Credits
|
|
895
|
+
|
|
896
|
+
Thank you [contributors](https://github.com/platanus/power_api/graphs/contributors)!
|
|
897
|
+
|
|
898
|
+
<img src="http://platan.us/gravatar_with_text.png" alt="Platanus" width="250"/>
|
|
899
|
+
|
|
900
|
+
Power API is maintained by [platanus](http://platan.us).
|
|
901
|
+
|
|
902
|
+
## License
|
|
903
|
+
|
|
904
|
+
Power API is © 2019 platanus, spa. It is free software and may be redistributed under the terms specified in the LICENSE file.
|