jko_api 0.1.3 → 0.2.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3711a73e0bd2f3bb5186bf24f11d4530b9bbeb6c
4
- data.tar.gz: e3a28ae1fc62c36cf5a3b77ee0f6820873fb9a75
2
+ SHA256:
3
+ metadata.gz: 88aaa8b5e8d2dbda590b75a03aa6a860f982fe2be6bab9b2cd85ccc2a7e1b579
4
+ data.tar.gz: d3a107e9504ab41724cee758bb4f987f0c47de5b8f04a1a29943a4c1b79f1dd5
5
5
  SHA512:
6
- metadata.gz: c6adaaad9e11e56f1971934b7522dae6cc7376e18ddae3b3358202ee73dc85452327d5eab3a62c594e05f525ced2320ff1df5b1acbcf03b8e8ce0faedcbadc6a
7
- data.tar.gz: 26a75d36b762ffa83d8f74f93374b5a843f73fdc9d1c80e3f969afdd68722d2055e9d527e239b722f70477304680f2de002490f19c041bace08d668cd4816bc3
6
+ metadata.gz: fd91bf624a79ba36727c6ff46964b785aa250d2704958a54946e2a777a70f2317a24f4730a2de0f87df2cdd68f2633db5a0208488a7c0996c959b4e43e0fbdfd
7
+ data.tar.gz: f2426c6e85c5febd3abcc5317f324f97b790ad8bed546d57bec5051ebef4e286968334821365732788c0cdd16401e95a4c2df1af8d862a4c9d93bb3d1c07b00b
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'warden-oauth2-strategies', github: 'airservice/warden-oauth2', require: false
3
4
  # Specify your gem's dependencies in jko_api.gemspec
4
5
  gemspec
data/README.md CHANGED
@@ -6,7 +6,11 @@ This gem lets you make a single change and reversion an API without copying over
6
6
 
7
7
  ## Props
8
8
 
9
- 99% of this gem was written by [Justin Ko](https://github.com/justinko). Since he's lazy and won't make a gem from it, I'm doing it for him :stuck_out_tongue: That's why the gem is named after him.
9
+ 90% of the code was originally written by [Justin Ko](https://github.com/justinko). Since he's lazy and won't make a gem from it, I'm doing it for him :stuck_out_tongue: That's why the gem is named after him. I've since modified some of the stuff, and will continue to make it easier to use and more configurable.
10
+
11
+ ## Requirements
12
+
13
+ Rails 5+ and Ruby 2.3
10
14
 
11
15
  ## Installation
12
16
 
@@ -31,9 +35,19 @@ In your `config/initializers/jko_api.rb` add
31
35
  ```ruby
32
36
  JkoApi.configure do |c|
33
37
  # This is the default. You can override this if you need a different controller
34
- # c.base_controller = Api::ApplicationController
38
+ # c.base_controller = ApiApplicationController
39
+
35
40
  # This is the folder name where all the api controllers would go
36
41
  # c.api_namespace = 'api'
42
+
43
+ # If you want to use your own authentication stuff, leave this as default
44
+ # c.use_authentication = false
45
+
46
+ # The built in authentication is Warden::OAuth2 with a Bearer strategy. Currently no other option exists
47
+ # c.strategy = :bearer
48
+
49
+ # If you use the authentication, then you will need to make some model, and set this value. More notes below.
50
+ # c.token_model = SomeModelYouCreated
37
51
  end
38
52
  ```
39
53
 
@@ -53,10 +67,10 @@ JkoApi.routes self do
53
67
  end
54
68
  ```
55
69
 
56
- Place your version 1 controller code in `app/controllers/api/v1`. Controllers should all inherit from `Api::ApplicationController`, or something that inherits from that.
70
+ Place your version 1 controller code in `app/controllers/api/v1`. Controllers should all inherit from `ApiApplicationController`, or something that inherits from that.
57
71
 
58
72
  ```ruby
59
- class Api::V1::BarsController < Api::ApplicationController
73
+ class Api::V1::BarsController < ApiApplicationController
60
74
  def index
61
75
  # do stuff
62
76
  end
@@ -80,7 +94,7 @@ end
80
94
  We can still do this though
81
95
 
82
96
  ```ruby
83
- class Api::V3::BarsController < Api::ApplicationController
97
+ class Api::V3::BarsController < ApiApplicationController
84
98
  def index
85
99
  # do different stuff
86
100
  end
@@ -89,6 +103,63 @@ end
89
103
 
90
104
  You can test this all by booting up a simple rails app, then do `curl -H "Accept: application/vnd.api.v1+json" http://localhost:3000/bars`
91
105
 
106
+ The `Accept` header is required to make the calls.
107
+
108
+ ## Authentication
109
+ If your API calls need to be authenticated by some API key, then you will need to configure a few more things.
110
+
111
+ ```ruby
112
+ JkoApi.configure do |c|
113
+ c.use_authentication = true
114
+ c.token_model = MyCustomTokenVerifier
115
+ end
116
+ ```
117
+
118
+ Your `MyCustomTokenVerifier` must be a ruby object that responds to a `locate` class method, and take a single string argument for the api token that needs to be authenticated. The method should return the token string if it's valid, and nil if it's not. You can [read more here](https://github.com/opperator/warden-oauth2#access-token).
119
+
120
+ This `MyCustomTokenVerifier` could look a few different ways. One way would be to make this a rails model like
121
+
122
+ ```ruby
123
+ class MyCustomTokenVerifier < ApplicationModel
124
+
125
+ def self.locate(token_string)
126
+ find_by(api_token: token_string)
127
+ end
128
+
129
+ end
130
+ ```
131
+
132
+ Another way would be a simple class that verifies against some configuration option like
133
+
134
+ ```ruby
135
+ class MyCustomTokenVerifier
136
+
137
+ def self.locate(token_string)
138
+ if token_string == Rails.configuration.x.my_custom_api_token
139
+ token_string
140
+ else
141
+ nil
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## Testing
148
+ If you're trying to test controllers, keep in mind that Rails doesn't process the middleware stack for controller tests. This means that the `JkoApi::Middleware` as well as the `JkoApi::Strategies` modules won't exist, and neither will `warden`. You can [read up more here](https://github.com/hassox/warden/issues/117).
149
+
150
+ My recommendation for testing api endpoints is doing an integration test. With RSpec, just create your `spec/requests` folder, and add your spec for the endpoint you want to test.
151
+
152
+ If you need access to a mock warden object, you can add
153
+
154
+ ```ruby
155
+ require 'jko_api/test_helpers'
156
+ include JkoApi::TestHelpers
157
+ ```
158
+
159
+ ## Debugging notes
160
+
161
+ If you have a route that matches one of the API routes, and it's listed first, then Rails will match that route first. This might not be what you expected, so put your `JkoApi.routes` near the top to ensure that gets hit first.
162
+
92
163
 
93
164
  ## Contributing
94
165
 
@@ -0,0 +1,3 @@
1
+ class ApiApplicationController < ActionController::Base
2
+ include JkoApi::Controller
3
+ end
@@ -0,0 +1,7 @@
1
+ JkoApi.auth_setup do
2
+ Warden::OAuth2.configure do |config|
3
+ if JkoApi.configuration.strategy.bearer?
4
+ config.token_model = JkoApi.configuration.token_model
5
+ end
6
+ end
7
+ end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["jeremywoertink@gmail.com"]
11
11
  spec.summary = %q{A Rails API gem}
12
12
  spec.description = %q{Some Rails API code written by JustinKo and ported to a badly written gem}
13
- spec.homepage = ""
13
+ spec.homepage = "https://github.com/jwoertink/jko_api"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -18,8 +18,9 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.7"
22
- spec.add_development_dependency "rake", "~> 10.0"
23
- spec.add_dependency "responders", "2.1.0"
24
- spec.add_dependency "rails", ">= 4.2.0", "< 5"
21
+ spec.add_development_dependency "bundler"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_dependency "responders", ">= 2.4.0"
24
+ spec.add_dependency "rails", ">= 4.2.7"
25
+ spec.add_dependency "warden-oauth2"
25
26
  end
@@ -2,7 +2,9 @@ require "active_support/all"
2
2
  require "active_model/railtie"
3
3
  require "action_controller/railtie"
4
4
  require "responders"
5
+ require "warden-oauth2"
5
6
  require "jko_api/version"
7
+ require "jko_api/strategies"
6
8
  require "jko_api/class_descendants_builder"
7
9
  require "jko_api/constraints"
8
10
  require "jko_api/controller"
@@ -20,17 +22,22 @@ module JkoApi
20
22
  ACCEPT_HEADER_REGEX = /\Aapplication\/vnd\.api(\.v([0-9]))?\+json\z/
21
23
 
22
24
  mattr_accessor :configuration, instance_accessor: false
25
+ mattr_accessor :auth_initializer, instance_accessor: false
23
26
  mattr_reader :current_version_number, instance_reader: false
27
+ @@versioning = nil
24
28
 
25
29
  def self.configure
26
30
  self.configuration ||= Configuration.new
27
31
  yield(configuration)
28
- setup(configuration.base_controller)
32
+ self.configuration
29
33
  end
30
34
 
31
- def self.setup(base_controller)
32
- Util.stupid_hack!
33
- ClassDescendantsBuilder.build base_controller, level: max_version_number
35
+ def self.complete_setup(base_controller)
36
+ if Rails.version.to_i < 5
37
+ Util.stupid_hack!
38
+ end
39
+ ClassDescendantsBuilder.build(base_controller, upto: max_version_number)
40
+ @@auth_initializer.call
34
41
  end
35
42
 
36
43
  def self.activated?
@@ -65,5 +72,11 @@ module JkoApi
65
72
  context.scope(module: JkoApi.configuration.api_namespace, constraints: JkoApi::Constraints, defaults: {format: :json}) do
66
73
  JkoApi.versions(context, &block)
67
74
  end
75
+ complete_setup(configuration.base_controller)
76
+ context
77
+ end
78
+
79
+ def self.auth_setup(&block)
80
+ self.auth_initializer = block
68
81
  end
69
82
  end
@@ -2,45 +2,79 @@ module JkoApi
2
2
  class ClassDescendantsBuilder
3
3
  LEVEL_REGEX = /\d+/
4
4
 
5
- def self.build(base_class, level:)
5
+ def self.build(base_class, upto:)
6
6
  base_class.descendants.each do |descendant|
7
- new(descendant, level).build
7
+ new(descendant, upto).build
8
8
  end
9
9
  end
10
10
 
11
- def initialize(descendant, level)
12
- @descendant, @level = descendant, level
11
+ attr_reader :descendant, :upto
12
+
13
+ def initialize(descendant, upto)
14
+ @descendant, @upto = descendant, upto
13
15
  end
14
16
 
15
17
  def build
16
- initial_level.upto(@level - 1) do |level|
17
- build_descendant(level) unless descendant_defined?(level)
18
+ initial_level.upto(upto) do |level|
19
+ unless Module.const_defined?(swap_level(level))
20
+ build_descendant level.pred
21
+ end
18
22
  end
19
23
  end
20
24
 
21
25
  private
22
26
 
23
- def descendant_defined?(level)
24
- !!swap_level(level.next).safe_constantize
25
- end
26
-
27
27
  def build_descendant(level)
28
- namespace(level.next).constantize.const_set(
29
- swap_level(level.next).demodulize,
30
- Class.new(swap_level(level).constantize)
28
+ namespace(level).const_set(
29
+ swap_level(level).demodulize,
30
+ Class.new(swap_level(level.next).constantize)
31
31
  )
32
32
  end
33
33
 
34
34
  def namespace(level)
35
- swap_level(level).deconstantize.presence || 'Object'
35
+ deconstantized = swap_level(level).deconstantize
36
+ unless qualified_const_defined?(deconstantized)
37
+ qualified_const_set(deconstantized, Module.new)
38
+ end
39
+ deconstantized.constantize
36
40
  end
37
41
 
38
42
  def swap_level(level)
39
- @descendant.name.sub LEVEL_REGEX, level.to_s
43
+ descendant.name.sub LEVEL_REGEX, level.to_s
40
44
  end
41
45
 
42
46
  def initial_level
43
- @descendant.name[LEVEL_REGEX].to_i
47
+ descendant.name[LEVEL_REGEX].to_i
48
+ end
49
+
50
+ # Pulled from Rails 4.2 source
51
+ def qualified_const_defined?(path)
52
+ raise NameError, "#{path.inspect} is not a valid constant name!" unless /^(::)?([A-Z]\w*)(::[A-Z]\w*)*$/ =~ path
53
+
54
+ names = path.to_s.split('::')
55
+ names.shift if names.first.empty?
56
+
57
+ # We can't use defined? because it will invoke const_missing for the parent
58
+ # of the name we are checking.
59
+ names.inject(Module) do |mod, name|
60
+ return false unless mod.const_defined?(name, false)
61
+ mod.const_get name
62
+ end
63
+ return true
64
+ end
65
+
66
+ def qualified_const_set(path, value)
67
+ raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/
68
+ const_name = path.demodulize
69
+ mod_name = path.deconstantize
70
+ mod = mod_name.empty? ? self : qualified_const_get(mod_name)
71
+ mod.const_set(const_name, value) unless mod.const_defined?(const_name)
72
+ end
73
+
74
+ def qualified_const_get(path)
75
+ path.split('::').inject(Module) do |mod, name|
76
+ mod.const_get(name)
77
+ end
44
78
  end
45
79
  end
46
80
  end
@@ -1,11 +1,21 @@
1
1
  module JkoApi
2
2
  class Configuration
3
3
 
4
- attr_accessor :base_controller, :api_namespace
4
+ attr_accessor :base_controller, :api_namespace, :use_authentication, :token_model
5
5
 
6
6
  def initialize
7
- @base_controller = Api::ApplicationController
7
+ @base_controller = ApiApplicationController
8
8
  @api_namespace = 'api'
9
+ @use_authentication = false
10
+ @strategy = :bearer
11
+ end
12
+
13
+ def authenticate?
14
+ use_authentication
15
+ end
16
+
17
+ def strategy
18
+ @strategy.to_s.inquiry
9
19
  end
10
20
 
11
21
  end
@@ -4,7 +4,12 @@ module JkoApi
4
4
 
5
5
  included do
6
6
  include JkoApi::ControllerHelpers
7
- skip_authentication # TODO: make this configurable
7
+
8
+ if JkoApi.configuration&.use_authentication
9
+ prepend_before_action :authenticate!
10
+ else
11
+ skip_authentication
12
+ end
8
13
 
9
14
  self.responder = JkoApi::Responder
10
15
  respond_to :json
@@ -15,5 +20,14 @@ module JkoApi
15
20
  def render_json_errors(status, message = status)
16
21
  render json: JkoApi::RequestError[status, message], status: status
17
22
  end
23
+
24
+ def warden
25
+ request.env['warden']
26
+ end
27
+
28
+ def authenticate!
29
+ token = warden.authenticate!
30
+ self.authenticated = !!token
31
+ end
18
32
  end
19
33
  end
@@ -5,7 +5,11 @@ module JkoApi
5
5
  included do
6
6
  class_attribute :authenticated
7
7
  self.authenticated = false
8
- append_before_action { raise 'setup authentication' unless authenticated }
8
+ append_before_action do
9
+ unless authenticated
10
+ render(json: {error: 'Authentication Failed'}, status: 401)
11
+ end
12
+ end
9
13
  end
10
14
 
11
15
  class_methods do
@@ -3,7 +3,12 @@ require 'rails/railtie'
3
3
  module JkoApi
4
4
  class Engine < ::Rails::Engine
5
5
  initializer "jko_api.configure_rails_initialization" do |app|
6
- app.middleware.use JkoApi::Middleware
6
+ app.middleware.use JkoApi::Middleware, only: proc { |env|
7
+ env['HTTP_ACCEPT'] && env['HTTP_ACCEPT'].include?('application/vnd.api') }
8
+
9
+ if JkoApi.configuration&.authenticate?
10
+ app.middleware.use "JkoApi::Strategies::#{JkoApi.configuration.strategy.to_s.capitalize}".constantize, only: proc { |env| env['HTTP_ACCEPT'] && env['HTTP_ACCEPT'].include?('application/vnd.api') }
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -1,14 +1,17 @@
1
1
  module JkoApi
2
2
  class Middleware
3
- def initialize(app)
3
+ def initialize(app, options={})
4
4
  @app = app
5
+ @only = options[:only]
5
6
  end
6
7
 
7
8
  def call(env)
8
- if version_number = extract_version_number(env)
9
- ::JkoApi.current_version_number = version_number
10
- else
11
- ::JkoApi.reset
9
+ if @only && @only.call(env)
10
+ if version_number = extract_version_number(env)
11
+ ::JkoApi.current_version_number = version_number
12
+ else
13
+ ::JkoApi.reset
14
+ end
12
15
  end
13
16
  @app.call env
14
17
  end
@@ -0,0 +1 @@
1
+ require 'jko_api/strategies/bearer'
@@ -0,0 +1,26 @@
1
+ module JkoApi
2
+ module Strategies
3
+ class Bearer
4
+
5
+ def initialize(app, options ={})
6
+ @app = app
7
+ @only = options[:only]
8
+ @mgr = Warden::Manager.new(@app, options) do |config|
9
+ config.strategies.add :bearer, Warden::OAuth2::Strategies::Bearer
10
+ config.default_strategies :bearer
11
+ config.failure_app = Warden::OAuth2::FailureApp
12
+ end
13
+
14
+ @mgr
15
+ end
16
+
17
+ def call(env)
18
+ if @only && @only.call(env)
19
+ @mgr.call(env)
20
+ else
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ module JkoApi
2
+ module TestHelpers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Warden::Test::Mock
7
+ end
8
+ end
9
+ end
@@ -1,6 +1,8 @@
1
1
  module JkoApi
2
2
  class Util
3
3
 
4
+ # This was a hack needed in rails 4.2
5
+ # I'm keeping the code for backwards compatibility (for now)
4
6
  def self.stupid_hack!
5
7
  Rails.application.reload_routes!
6
8
  eager_load_api_controllers
@@ -1,3 +1,3 @@
1
1
  module JkoApi
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.9"
3
3
  end
metadata CHANGED
@@ -1,77 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jko_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Woertink
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-17 00:00:00.000000000 Z
11
+ date: 2020-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: responders
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 2.1.0
47
+ version: 2.4.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 2.1.0
54
+ version: 2.4.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rails
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 4.2.0
62
- - - "<"
63
- - !ruby/object:Gem::Version
64
- version: '5'
61
+ version: 4.2.7
65
62
  type: :runtime
66
63
  prerelease: false
67
64
  version_requirements: !ruby/object:Gem::Requirement
68
65
  requirements:
69
66
  - - ">="
70
67
  - !ruby/object:Gem::Version
71
- version: 4.2.0
72
- - - "<"
68
+ version: 4.2.7
69
+ - !ruby/object:Gem::Dependency
70
+ name: warden-oauth2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
73
81
  - !ruby/object:Gem::Version
74
- version: '5'
82
+ version: '0'
75
83
  description: Some Rails API code written by JustinKo and ported to a badly written
76
84
  gem
77
85
  email:
@@ -85,7 +93,8 @@ files:
85
93
  - LICENSE.txt
86
94
  - README.md
87
95
  - Rakefile
88
- - app/controllers/api/application_controller.rb
96
+ - app/controllers/api_application_controller.rb
97
+ - config/initializers/warden.rb
89
98
  - jko_api.gemspec
90
99
  - lib/jko_api.rb
91
100
  - lib/jko_api/class_descendants_builder.rb
@@ -97,10 +106,13 @@ files:
97
106
  - lib/jko_api/middleware.rb
98
107
  - lib/jko_api/request_error.rb
99
108
  - lib/jko_api/responder.rb
109
+ - lib/jko_api/strategies.rb
110
+ - lib/jko_api/strategies/bearer.rb
111
+ - lib/jko_api/test_helpers.rb
100
112
  - lib/jko_api/util.rb
101
113
  - lib/jko_api/version.rb
102
114
  - lib/jko_api/versioning.rb
103
- homepage: ''
115
+ homepage: https://github.com/jwoertink/jko_api
104
116
  licenses:
105
117
  - MIT
106
118
  metadata: {}
@@ -119,8 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
131
  - !ruby/object:Gem::Version
120
132
  version: '0'
121
133
  requirements: []
122
- rubyforge_project:
123
- rubygems_version: 2.4.3
134
+ rubygems_version: 3.1.2
124
135
  signing_key:
125
136
  specification_version: 4
126
137
  summary: A Rails API gem
@@ -1,3 +0,0 @@
1
- class Api::ApplicationController < ActionController::Base
2
- include JkoApi::Controller
3
- end