Skip to content

Commit

Permalink
Expose error responses in the spec output (#25)
Browse files Browse the repository at this point in the history
Apia defines errors responses like this:

```ruby
potential_error 'DiskNotFound' do
  code :disk_not_found
  description 'No disk was found matching any of the criteria provided in the arguments'
  http_status 404
end
```

So now we declare these in the responses for each endpoint.

https://swagger.io/docs/specification/describing-responses/

closes: #2
  • Loading branch information
paulsturgess authored Nov 21, 2023
1 parent 17f9194 commit f8e1266
Show file tree
Hide file tree
Showing 18 changed files with 691 additions and 134 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ gem "rspec", "~> 3.0"
gem "rubocop", "~> 1.21"

group :test do
gem "pry"
gem "simplecov", require: false
end
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@ GEM
json
rack
ast (2.4.2)
coderay (1.1.3)
concurrent-ruby (1.2.2)
diff-lcs (1.5.0)
docile (1.4.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
method_source (1.0.0)
minitest (5.20.0)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.7.1)
rack (3.0.8)
rainbow (3.1.1)
Expand Down Expand Up @@ -75,6 +80,7 @@ PLATFORMS
DEPENDENCIES
apia (~> 3.5)
apia-open_api!
pry
rake (~> 13.0)
rspec (~> 3.0)
rubocop (~> 1.21)
Expand Down
57 changes: 57 additions & 0 deletions examples/core_api/authenticators/main_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module CoreAPI
module Authenticators
class MainAuthenticator < Apia::Authenticator

BEARER_TOKEN = "example"

type :bearer

potential_error "InvalidToken" do
code :invalid_token
description "The token provided is invalid. In this example, you should provide '#{BEARER_TOKEN}'."
http_status 403

field :given_token, type: :string
end

potential_error "UnauthorizedNetworkForAPIToken" do
code :unauthorized_network_for_api_token
description "Network is not allowed to access the API with this API token"
http_status 403

field :ip_address, :string do
description "The IP address the request was received from"
end
end

def call
configure_cors_response
return if request.options?

given_token = request.headers["authorization"]&.sub(/\ABearer /, "")
if given_token == BEARER_TOKEN
request.identity = { name: "Example User", id: 1234 }
else
raise_error "CoreAPI/MainAuthenticator/InvalidToken", given_token: given_token.to_s
end
end

private

# These are not strictly required, but it allows the app to work with swagger-ui.
def configure_cors_response
# Define a list of cors methods that are permitted for the request.
cors.methods = %w[GET POST PUT PATCH DELETE OPTIONS]

# Define a list of cors headers that are permitted for the request.
cors.headers = %w[Authorization Content-Type] # or allow all with '*'

# Define a the hostname to allow for CORS requests.
cors.origin = "*" # or 'example.com'
end

end
end
end
13 changes: 13 additions & 0 deletions examples/core_api/authenticators/time_controller_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require "core_api/errors/rate_limit_reached"

module CoreAPI
module Authenticators
class TimeControllerAuthenticator < Apia::Authenticator

potential_error CoreAPI::Errors::RateLimitReached

end
end
end
17 changes: 17 additions & 0 deletions examples/core_api/authenticators/time_now_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "core_api/errors/rate_limit_reached"

module CoreAPI
module Authenticators
class TimeNowAuthenticator < Apia::Authenticator

potential_error "WrongDayOfWeek" do
code :wrong_day_of_week
description "You called this API on the wrong day of the week, try again tomorrow"
http_status 503
end

end
end
end
4 changes: 2 additions & 2 deletions examples/core_api/base.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# frozen_string_literal: true

require "core_api/main_authenticator"
require "core_api/authenticators/main_authenticator"
require "core_api/controllers/time_controller"
require "core_api/endpoints/test_endpoint"

module CoreAPI
class Base < Apia::API

authenticator MainAuthenticator
authenticator Authenticators::MainAuthenticator

scopes do
add "time", "Allows time telling functions"
Expand Down
3 changes: 3 additions & 0 deletions examples/core_api/controllers/time_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "core_api/objects/time"
require "core_api/authenticators/time_controller_authenticator"
require "core_api/argument_sets/time_lookup_argument_set"
require "core_api/endpoints/time_now_endpoint"

Expand All @@ -11,6 +12,8 @@ class TimeController < Apia::Controller
name "Time API"
description "Returns the time in varying ways"

authenticator Authenticators::TimeControllerAuthenticator

endpoint :now, Endpoints::TimeNowEndpoint

# TODO: add example of multiple objects using the same objects, to ensure
Expand Down
13 changes: 13 additions & 0 deletions examples/core_api/endpoints/test_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ module Endpoints
class TestEndpoint < Apia::Endpoint

description "Returns the current time"

argument :object, type: ArgumentSets::ObjectLookup, required: true
argument :scalar, type: :string, required: true

field :time, type: Objects::Time, include: "unix,day_of_week,year[as_string]", null: true do
condition do |o|
o[:time].year.to_s == "2023"
Expand All @@ -17,8 +19,19 @@ class TestEndpoint < Apia::Endpoint
field :object_id, type: :string do
backend { |o| o[:object_id][:id] }
end

scope "time"

potential_error "InvalidTest" do
code :invalid_test
http_status 400
end

potential_error "AnotherInvalidTest" do
code :another_invalid_test
http_status 400
end

def call
object = request.arguments[:object].resolve
response.add_field :time, get_time_now
Expand Down
6 changes: 6 additions & 0 deletions examples/core_api/endpoints/time_now_endpoint.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# frozen_string_literal: true

require "core_api/authenticators/time_now_authenticator"
require "core_api/objects/time_zone"

module CoreAPI
module Endpoints
class TimeNowEndpoint < Apia::Endpoint

description "Returns the current time"

authenticator Authenticators::TimeNowAuthenticator

argument :timezone, type: Objects::TimeZone
argument :time_zones, [Objects::TimeZone], required: true
argument :filters, [:string]

field :time, type: Objects::Time
field :time_zones, type: [Objects::TimeZone]
field :filters, [:string], null: true
field :my_polymorph, type: Objects::MonthPolymorph
field :my_partial_polymorph, type: Objects::MonthPolymorph, include: "number", null: true

scope "time"

def call
Expand Down
17 changes: 17 additions & 0 deletions examples/core_api/errors/rate_limit_reached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module CoreAPI
module Errors
class RateLimitReached < Apia::Error

code :rate_limit_reached
http_status 429
description "You have reached the rate limit for this type of request"

field :total_permitted, type: :integer do
description "The total number of requests per minute that are permitted"
end

end
end
end
45 changes: 0 additions & 45 deletions examples/core_api/main_authenticator.rb

This file was deleted.

18 changes: 14 additions & 4 deletions lib/apia/open_api/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Helpers
# A component schema is a re-usable schema that can be referenced by other parts of the spec
# e.g. { "$ref": "#/components/schemas/PaginationObject" }
def add_to_components_schemas(definition, id, **schema_opts)
return unless @spec.dig(:components, :schemas, id).nil?
return true unless @spec.dig(:components, :schemas, id).nil?

component_schema = {}
@spec[:components][:schemas][id] = component_schema
Expand All @@ -19,6 +19,11 @@ def add_to_components_schemas(definition, id, **schema_opts)
id: id,
**schema_opts
).add_to_spec

return true if component_schema.present?

@spec[:components][:schemas].delete(id)
false
end

def convert_type_to_open_api_data_type(type)
Expand All @@ -35,13 +40,18 @@ def convert_type_to_open_api_data_type(type)

def generate_schema_ref(definition, id: nil, **schema_opts)
id ||= generate_id_from_definition(definition.type.klass.definition)
add_to_components_schemas(definition, id, **schema_opts)
{ "$ref": "#/components/schemas/#{id}" }
success = add_to_components_schemas(definition, id, **schema_opts)

if success
{ "$ref": "#/components/schemas/#{id}" }
else # no properties were defined, so just declare an object with unknown properties
{ type: "object" }
end
end

# forward slashes do not work in ids (e.g. schema ids)
def generate_id_from_definition(definition)
definition.id.gsub(/\//, "_")
definition.id.gsub(/\//, "")
end

def formatted_description(description)
Expand Down
2 changes: 1 addition & 1 deletion lib/apia/open_api/objects.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

Dir.glob(File.join(File.dirname(__FILE__), "objects", "*.rb")).each do |file|
Dir.glob(File.join(File.dirname(__FILE__), "objects", "*.rb")).sort.each do |file|
require_relative file
end

Expand Down
11 changes: 9 additions & 2 deletions lib/apia/open_api/objects/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ class Path

include Apia::OpenApi::Helpers

def initialize(spec:, path_ids:, route:, name:)
def initialize(spec:, path_ids:, route:, name:, api_authenticator:)
@spec = spec
@path_ids = path_ids
@route = route
@api_authenticator = api_authenticator
@route_spec = {
operationId: convert_route_to_id,
tags: [name]
Expand Down Expand Up @@ -69,7 +70,13 @@ def add_request_body
end

def add_responses
Response.new(spec: @spec, path_ids: @path_ids, route: @route, route_spec: @route_spec).add_to_spec
Response.new(
spec: @spec,
path_ids: @path_ids,
route: @route,
route_spec: @route_spec,
api_authenticator: @api_authenticator
).add_to_spec
end

# It's worth creating a 'nice' operationId for each route, as this is used as the
Expand Down
Loading

0 comments on commit f8e1266

Please sign in to comment.