Skip to content

Commit

Permalink
Add Faraday::Openapi.register
Browse files Browse the repository at this point in the history
to easily load, cache and reference OADs
  • Loading branch information
ahx committed Feb 26, 2025
1 parent 78e8dbb commit a000b26
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add Faraday::Openapi.register to easily load, cache and reference OADs

## 0.1.1

Fix URL to homepage, changelog
Expand Down
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ You can use this to test your client code or to make sure your mocks do match th

Note that the middleware currently deliberately ignores **unknown** responses with status codes 401 or higher because those usually don't come with a useful response body.

## TL;DR

```ruby
conn = Faraday.new do |f|
f.use :openapi
end
```

## Installation

Add this line to your application's Gemfile:
Expand All @@ -21,33 +29,41 @@ And then execute:
bundle install
```

Or install it yourself as:
## Usage

```shell
gem install faraday-openapi
```
In order to avoid loading YAML files at inappropriate times you should
register your API description (OAD) globally and reference it via a Symbol in your client code

## Usage
```ruby
# initializer.rb

require 'faraday/openapi'
Faraday::Openapi.register 'dice-openapi.yaml', as: :dice_api

# Only activate in test env
Faraday::Openapi::Middleware.enabled = ENV['RACK_ENV'] == 'test'
```

```ruby
# some_client.rb
require 'faraday/openapi'

conn = Faraday.new do |f|
f.use :openapi, 'openapi/openapi.yaml'
f.use :openapi, :dice_api
end

# Or validate only requests
conn = Faraday.new do |f|
f.request :openapi, 'openapi/openapi.yaml'
f.request :openapi, :dice_api
end

# Or validate only responses
conn = Faraday.new do |f|
f.response :openapi, 'openapi/openapi.yaml'
f.response :openapi, :dice_api
end
```

You can disable the whole middleware globally:
You can disable the whole middleware globally via Faraday's conventional default_options as well:

```ruby
Faraday::Openapi::Middleware.default_options[:enabled] = false
Expand Down
34 changes: 25 additions & 9 deletions lib/faraday/openapi.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'openapi_first'
require_relative 'openapi/errors'
require_relative 'openapi/middleware'
require_relative 'openapi/version'
Expand All @@ -14,18 +15,33 @@ module Openapi
# After calling this line, the following are both valid ways to set the middleware in a connection:
# * conn.use Faraday::Openapi::Middleware
# * conn.use :openapi
# Without this line, only the former method is valid.
Faraday::Middleware.register_middleware(openapi: Faraday::Openapi::Middleware)
Faraday::Request.register_middleware(openapi: Faraday::Openapi::Middleware)
Faraday::Response.register_middleware(openapi: Faraday::Openapi::Middleware)

# Alternatively, you can register your middleware under Faraday::Request or Faraday::Response.
# This will allow to load your middleware using the `request` or `response` methods respectively.
#
# Load middleware with conn.request :openapi
# Faraday::Request.register_middleware(openapi: Faraday::Openapi::Middleware)
#
# Load middleware with conn.response :openapi
# Faraday::Response.register_middleware(openapi: Faraday::Openapi::Middleware)
@registry = {}

class << self
attr_reader :registry
end

def self.register(filepath, as: :default)
raise AlreadyRegisteredError, "API description #{as} is already registered" if registry.key?(as)

registry[as] = OpenapiFirst.load(filepath)
end

def self.[](key)
registry.fetch(key) do
message = if registry.empty?
'No API descriptions have been registered. Please register your API description via ' \
"Faraday::Openapi.register('myopenapi.yaml')"
else
"API description #{key.inspect} was not found. Please register your API description via " \
"Faraday::Openapi.register('myopenapi.yaml'#{key == :default ? '' : ", as: #{key.inspect}"})"
end
raise NotRegisteredError, message
end
end
end
end
3 changes: 3 additions & 0 deletions lib/faraday/openapi/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ class RequestInvalidError < Error; end

# Raised if response does not match API description or is unknown
class ResponseInvalidError < Error; end

class AlreadyRegisteredError < Error; end
class NotRegisteredError < Error; end
end
end
11 changes: 8 additions & 3 deletions lib/faraday/openapi/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ module Openapi
class Middleware < Faraday::Middleware
DEFAULT_OPTIONS = { enabled: true }.freeze

def initialize(app, filepath)
def self.enabled=(bool)
Faraday::Openapi::Middleware.default_options[:enabled] = bool
end

def initialize(app, path = :default)
super(app)
@filepath = filepath
@enabled = options.fetch(:enabled, true)
@oad = OpenapiFirst.load(filepath) if @enabled
return unless @enabled

@oad = path.is_a?(Symbol) ? Faraday::Openapi[path] : OpenapiFirst.load(path)
end

def call(env)
Expand Down
59 changes: 59 additions & 0 deletions spec/faraday/openapi/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,63 @@
end
end
end

context 'when named API was not found' do
it 'raises an error' do
expect do
connection = Faraday.new(url: 'http://dice.local') do |f|
f.use :openapi, :unknown
end
connection.post('roll')
end.to raise_error Faraday::Openapi::NotRegisteredError
end
end

context 'without a specified name (:default)' do
subject(:connection) do
Faraday.new(url: 'http://dice.local') do |f|
f.response :json
f.use :openapi
end
end

before do
Faraday::Openapi.register('spec/data/dice.yaml')
end

it 'validates against that API' do
stub_request(:post, 'http://dice.local/roll')
.to_return(
headers: { 'content-type' => 'application/json' },
body: JSON.generate({ bar: 'baz' }),
status: 200
)

expect { connection.post('/roll') }.to raise_error Faraday::Openapi::ResponseInvalidError
end
end

context 'with a named API' do
subject(:connection) do
Faraday.new(url: 'http://dice.local') do |f|
f.response :json
f.use :openapi, :dice_api
end
end

before do
Faraday::Openapi.register('spec/data/dice.yaml', as: :dice_api)
end

it 'validates against that API' do
stub_request(:post, 'http://dice.local/roll')
.to_return(
headers: { 'content-type' => 'application/json' },
body: JSON.generate({ bar: 'baz' }),
status: 200
)

expect { connection.post('/roll') }.to raise_error Faraday::Openapi::ResponseInvalidError
end
end
end
31 changes: 31 additions & 0 deletions spec/faraday/openapi_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

RSpec.describe Faraday::Openapi do
describe '.register' do
it 'adds an OAD' do
described_class.register('spec/data/dice.yaml')
expect(described_class[:default]['info']['title']).to eq 'Dice API'
end

it 'raises an error if :default was already registered' do
described_class.register('spec/data/dice.yaml')
expect { described_class.register('spec/data/dice.yaml') }.to raise_error described_class::AlreadyRegisteredError
end

it 'adds an OAD under a custom name' do
described_class.register('spec/data/dice.yaml', as: :dice)
expect(described_class[:dice]['info']['title']).to eq 'Dice API'
end
end

describe '.[]' do
it 'returns an error if nothing was registered' do
expect { described_class[:default] }.to raise_error described_class::NotRegisteredError
end

it 'returns the registered OAD' do
described_class.register('spec/data/dice.yaml')
expect(described_class[:default]['info']['title']).to eq 'Dice API'
end
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
c.syntax = :expect
end

config.after do
Faraday::Openapi.registry.clear
end

config.order = :random
end

0 comments on commit a000b26

Please sign in to comment.