Skip to content

Commit

Permalink
Request Validations (#4)
Browse files Browse the repository at this point in the history
* Added request validations feature

* README.md update

* Rubocop fixes

* Missing newline

* Updated version to 1.0.0

* README and introspection bug fix

* README cleaned up
  • Loading branch information
abhisheksarka authored Oct 2, 2024
1 parent 1418265 commit 221d657
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 66 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ Gemfile.lock

/demo
vendor/ruby
/vendor
6 changes: 5 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ Layout/LineLength:
Exclude:
- 'apicraft-rails.gemspec'

Naming/MethodParameterName:
MinNameLength: 2

Metrics/CyclomaticComplexity:
Exclude:
- 'lib/apicraft/middlewares/mocker.rb'
Metrics/AbcSize:
Exclude:
- 'lib/apicraft/mocker/any_of.rb'
- 'lib/apicraft/middlewares/mocker.rb'
- 'lib/apicraft/middlewares/mocker.rb'
- 'lib/apicraft/middlewares/request_validator.rb'
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# APICraft Rails
[![Build](https://github.com/apicraft-dev/apicraft-rails/actions/workflows/build.yml/badge.svg)](https://github.com/apicraft-dev/apicraft-rails/actions/workflows/build.yml)
[![Gem Version](https://badge.fury.io/rb/apicraft-rails.svg?v=0.5.2.beta1)](https://badge.fury.io/rb/apicraft-rails)
[![Gem Version](https://badge.fury.io/rb/apicraft-rails.svg?v=1.0.0)](https://badge.fury.io/rb/apicraft-rails)

🚀 Accelerates your development by 2-3x with an API Design First approach. Seamlessly integrates with your Rails application server — no fancy tooling or expenses required.

Expand All @@ -12,23 +12,26 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g

![APICraft Rails Logo](assets/apicraft_rails.png)

- [APICraft Rails (Beta)](#apicraft-rails-beta)
- [APICraft Rails](#apicraft-rails)
- [✨ Features](#-features)
- [🔜 Upcoming Features](#-upcoming-features)
- [🪄 Works Like Magic](#-works-like-magic)
- [🕊 API Design First Philosophy](#-api-design-first-philosophy)
- [🏗 Installation](#-installation)
- [⚙️ Usage](#️-usage)
- [🎭 API Mocking](#-api-mocking)
- [🎮 API Mocking (Behaviours)](#-api-mocking-behaviours)
- [🧐 API Introspection](#-api-introspection)
- [📖 API Documentation (Swagger docs and RapiDoc)](#-api-documentation-swagger-docs-and-rapidoc)
- [🛡️ Request Validations](#️-request-validations)
- [🎭 Mocking](#-mocking)
- [🎮 Behaviour Mocking](#-behaviour-mocking)
- [🧐 Introspection](#-introspection)
- [📖 Documentation (Swagger docs and RapiDoc)](#-documentation-swagger-docs-and-rapidoc)
- [🔧 Configuration](#-configuration)
- [🤝 Contributing](#-contributing)
- [📝 License](#-license)
- [📘 Code of Conduct](#-code-of-conduct)

## ✨ Features
- 🛡️ **Automatic Request Validations** - Validates the request based on the openapi specs so that you don't need to add params validations everywhere in your controllers.

- 🧑‍💻️ **Dynamic Mock Data Generation** - Detects the specifications and instantly mounts working routes with mock responses. No extra configuration required.

- ⚙️ **Customizable Mock Responses** - Tailor mock responses to simulate different scenarios and edge cases, helping your team prepare for real-world conditions right from the start.
Expand All @@ -40,7 +43,6 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
- 🗂 **Easy Contracts Management** - Management of `openapi` specifications from within `app/contracts` directory. No new syntax, just plain old `openapi` standard with `.json` or `.yaml` formats

## 🔜 Upcoming Features
- 💢 **Request Validations** - Automatic request validations.
- 💎 **Clean & Custom Ruby DSL** - Support for a Ruby DSL alongwith the current `.json` and `.yaml` formats.


Expand Down Expand Up @@ -72,7 +74,7 @@ By adopting an API Design First approach with APICraft Rails, you can accelerate
Add this line to your application's Gemfile:

```ruby
gem 'apicraft-rails', '~> 0.5.2.beta1'
gem 'apicraft-rails', '~> 1.0.0'
```

And then execute:
Expand All @@ -89,8 +91,11 @@ module App
class Application < Rails::Application
# Rest of the configuration...

config.middleware.use Apicraft::Middlewares::Mocker
config.middleware.use Apicraft::Middlewares::Introspector
[
Apicraft::Middlewares::Mocker,
Apicraft::Middlewares::Introspector,
Apicraft::Middlewares::RequestValidator
].each { |mw| config.middleware.use mw }

Apicraft.configure do |config|
config.contracts_path = Rails.root.join("app/contracts")
Expand All @@ -117,7 +122,11 @@ my_rails_app/
│ │ ├── user.rb
│ │ └── order.rb
```
### 🎭 API Mocking

### 🛡️ Request Validations
All incoming requests will be validated against the defined schema. This ensures that by the time the params reach the controller they are adhering to all the schema requirements. It's enabled by default. You can customize the response of a failed validation. Check the [configuration section](#-configuration) section for a full list of options for this.

### 🎭 Mocking
**APICraft** dynamically generates mock APIs by interpreting contract specifications on the fly. You can request the mock response by passing `Apicraft-Mock: true` in the headers.

`https://yoursite.com/api/orders`
Expand All @@ -141,7 +150,7 @@ headers: {
]
```

### 🎮 API Mocking (Behaviours)
### 🎮 Behaviour Mocking
The above is an example of a 200 response. If you have more responses documented you can force that behaviour using `Apicraft-Response-Code` header in the mock request.
You can find a list of all the supported headers in the [configuration section](#-configuration) that would allow you to manipulate the API Behaviour.

Expand All @@ -160,12 +169,12 @@ headers: {
}
```

### 🧐 API Introspection
All APIs are can be introspected. You can do so by passing the `Apicraft-Introspection` header.
### 🧐 Introspection
All APIs are can be introspected. You can do so by passing the `Apicraft-Introspect` header.

```
headers: {
Apicraft-Introspection: true
Apicraft-Introspect: true
}
```

Expand Down Expand Up @@ -195,7 +204,7 @@ Example: `https://yoursite.com/api/orders`
}
}
```
### 📖 API Documentation (Swagger docs and RapiDoc)
### 📖 Documentation (Swagger docs and RapiDoc)

Mount the documentation views in your route file.

Expand Down Expand Up @@ -275,6 +284,20 @@ Apicraft.configure do |config|
# Delay simulation header name
delay: "Apicraft-Delay"
}

config.request_validation = {
enabled: true,

# Return the http code for validation errors, defaults to 400
http_code: 400,

# Return a custom response body, defaults to `{ message: "..." }`
response_body: proc do |ex|
{
message: ex.message
}
end
}
end

Apicraft::Web::App.use do |user, password|
Expand Down
1 change: 1 addition & 0 deletions lib/apicraft/concerns.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require_relative "concerns/cacheable"
require_relative "concerns/middleware_util"

module Apicraft
# Namespace module for Concerns
Expand Down
18 changes: 18 additions & 0 deletions lib/apicraft/concerns/middleware_util.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Apicraft
module Concerns
# Helper class with shared methods
module MiddlewareUtil
def config
@config ||= Apicraft.config
end

def convertor(format)
return if format.blank?

Apicraft::Constants::MIME_TYPE_CONVERTORS[format]
end
end
end
end
36 changes: 28 additions & 8 deletions lib/apicraft/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ class Config
mocks: true,
introspection: true,
strict_reference_validation: true,
request_validations: true,
max_allowed_delay: 30
max_allowed_delay: 30,
request_validation: {
enabled: true,
http_code: 400,
response_body: proc { |ex| { message: ex.message } }
}
}.with_indifferent_access

def initialize(opts = {})
@opts = DEFAULTS.merge(
opts
).with_indifferent_access
def initialize
@opts = DEFAULTS
end

def headers
Expand All @@ -53,6 +55,22 @@ def max_allowed_delay
@opts[:max_allowed_delay]
end

def request_validation
@opts[:request_validation]
end

def request_validation_enabled?
@opts[:request_validation][:enabled]
end

def request_validation_http_code
@opts[:request_validation][:http_code] || DEFAULTS[:request_validation][:http_code]
end

def request_validation_response_body
@opts[:request_validation][:response_body]
end

def contracts_path=(contracts_path)
@opts[:contracts_path] = contracts_path
end
Expand All @@ -69,8 +87,10 @@ def strict_reference_validation=(enabled)
@opts[:strict_reference_validation] = enabled
end

def request_validations=(enabled)
@opts[:request_validations] = enabled
def request_validation=(request_validation_opts)
@opts[:request_validation] = @opts[:request_validation].merge(
request_validation_opts.with_indifferent_access
)
end

def max_allowed_delay=(enabled)
Expand Down
1 change: 1 addition & 0 deletions lib/apicraft/middlewares.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "middlewares/mocker"
require_relative "middlewares/introspector"
require_relative "middlewares/request_validator"

module Apicraft
# Namespace module for Concerns
Expand Down
6 changes: 2 additions & 4 deletions lib/apicraft/middlewares/introspector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Apicraft
module Middlewares
# Apicraft Middleware to handle API Introspection.
class Introspector
include Concerns::MiddlewareUtil

def initialize(app)
@app = app
end
Expand All @@ -30,10 +32,6 @@ def call(env)

private

def config
@config ||= Apicraft.config
end

def introspect?(request)
request.headers[config.headers[:introspect]].present?
end
Expand Down
21 changes: 3 additions & 18 deletions lib/apicraft/middlewares/mocker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Middlewares
# Apicraft Middleware to handle routing
# and make mock calls available.
class Mocker
include Concerns::MiddlewareUtil

def initialize(app)
@app = app
end
Expand All @@ -23,14 +25,7 @@ def call(env)
response = operation.response_for(code.to_s)
raise Errors::InvalidResponse if response.blank?

# Determine the format passed in the request.
# If passed we use it and the response format.
# If not we use the first format from the specs.
request.format.to_s
# indicates that not format was specified.
format = nil

content, content_type = response.mock(format)
content, content_type = response.mock(request.content_type)

[
code.to_i,
Expand All @@ -45,16 +40,6 @@ def call(env)

private

def config
@config ||= Apicraft.config
end

def convertor(format)
return if format.blank?

Apicraft::Constants::MIME_TYPE_CONVERTORS[format]
end

def mock?(request)
request.headers[config.headers[:mock]].present?
end
Expand Down
52 changes: 52 additions & 0 deletions lib/apicraft/middlewares/request_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Apicraft
module Middlewares
# Middleware to handle Request Validation.
class RequestValidator
include Concerns::MiddlewareUtil

def initialize(app)
@app = app
end

def call(env)
return @app.call(env) unless validate?

request = ActionDispatch::Request.new(env)
content_type = request.content_type

operation = Apicraft::Openapi::Contract.find_by_operation(
request.method, request.path_info
)&.operation(
request.method, request.path_info
)
return @app.call(env) if operation.blank?

operation.validate_request_body(
content_type,
request.params
)
@app.call(env)
rescue OpenAPIParser::OpenAPIError => e
[
config.request_validation_http_code,
{ 'Content-Type': content_type },
[
response_body(e)&.send(convertor(content_type))
].compact
]
end

private

def validate?
config.request_validation_enabled?
end

def response_body(ex)
config.request_validation_response_body.call(ex)
end
end
end
end
8 changes: 4 additions & 4 deletions lib/apicraft/openapi/contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ def initialize(document)
end

def operation(method, path)
with_cache("operation-#{method.downcase}-#{path}") do
op = document.request_operation(method.downcase, path)
Operation.new(op) if op.present?
end
# with_cache("operation-#{method.downcase}-#{path}") do
op = document.request_operation(method.downcase, path)
Operation.new(op) if op.present?
# end
end

class << self
Expand Down
Loading

0 comments on commit 221d657

Please sign in to comment.