Skip to content

Commit

Permalink
[feature] CLI support (#7)
Browse files Browse the repository at this point in the history
* added cli command to validate all spec files

* improved init and setup via CLI

* added file generation from cli

* rubocop offences

* added ruby 3.3 into the matrix

* README.md update

* README.md update

* README.md update

* README.md update

* README.md update
  • Loading branch information
abhisheksarka authored Dec 22, 2024
1 parent 82027ea commit b5d9f0f
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 37 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
- "3.0"
- "3.1"
- "3.2"
- "3.3"
- ruby-head

runs-on: ${{ matrix.os }}
Expand Down
67 changes: 33 additions & 34 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://d25lcipzij17d.cloudfront.net/badge.png?id=rb&r=r&ts=1683906897&type=3e&v=1.0.1&x2=0)](https://badge.fury.io/rb/apicraft-rails)
[![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.png?id=rb&r=r&ts=1683906897&type=3e&v=1.0.2&x2=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 @@ -14,8 +14,6 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g

- [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)
Expand All @@ -24,6 +22,7 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
- [🎮 Behaviour Mocking](#-behaviour-mocking)
- [🧐 Introspection](#-introspection)
- [📖 Documentation (Swagger docs and RapiDoc)](#-documentation-swagger-docs-and-rapidoc)
- [📖 CLI Support](#-cli-support)
- [🔧 Configuration](#-configuration)
- [🤝 Contributing](#-contributing)
- [📝 License](#-license)
Expand All @@ -42,13 +41,7 @@ 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
- 💎 **Clean & Custom Ruby DSL** - Support for a Ruby DSL alongwith the current `.json` and `.yaml` formats.


## 🪄 Works Like Magic

Once you’ve installed the gem, getting started is a breeze. Simply create your OpenAPI contracts within the `app/contracts` directory of your Rails application. You’re free to organize this directory in a way that aligns with your project's standards and preferences. That’s it—your APIs will be up and running with mock responses, ready for development without any additional setup. It's as effortless as it sounds!
- 🗂 **CLI Support** - Specification validations can be triggered from the CLI allowing integrations into your CI/CD pipelines.

## 🕊 API Design First Philosophy

Expand All @@ -71,41 +64,38 @@ By adopting an API Design First approach with APICraft Rails, you can accelerate

## 🏗 Installation

Add this line to your application's Gemfile:
1. Add this line to your application's Gemfile:

```ruby
gem 'apicraft-rails', '~> 1.0.1'
gem 'apicraft-rails', '~> 1.0.2'
```

And then execute:
2. And then execute:
```bash
$ bundle install
$ rails apicraft:init
```

$ bundle install
This will create a file called `config/initializers/apicraft.rb` with all the necessary configurations. It will also create the default contracts directory called `app/contracts`.

After the installation in your rails project, you can start adding contracts in the `app/contracts` directory. This can have any internal directory structure based on your API versions, standards, etc.

Add the following into your Rails application, via the `config/application.rb`
3. Add the `apicraft` route to your route file (for documentation):

```ruby
# config/application.rb
module App
class Application < Rails::Application
# Rest of the configuration...

[
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")
end
end
Rails.application.routes.draw do
# other routes
mount Apicraft::Web::App, at: "/apicraft"
end
```

Now every API in the specification has a functional version. For any path (from the contracts), APICraft serves a mock response when `Apicraft-Mock: true` is passed in the headers otherwise, it forwards the request to your application as usual.

4. Generate a sample spec file
```
rails apicraft:generate file=v2/openapi
```

This will generate a sample file called `app/contracts/v2/openapi.yaml`
## ⚙️ Usage

Add your specification files to the `app/contracts` directory in your Rails project. You can also configure this directory to be something else.
Expand Down Expand Up @@ -199,9 +189,7 @@ Example: `https://yoursite.com/api/orders`
}
}
],
"responses": {
...
}
"responses": {}
}
```
### 📖 Documentation (Swagger docs and RapiDoc)
Expand Down Expand Up @@ -238,6 +226,17 @@ RapiDoc | SwaggerDoc
:-------------------------:|:-------------------------:
![](assets/rapidoc.png) | ![](assets/swaggerdoc.png)

### 📖 CLI Support

To check if all the specification are valid
```
$ rails apicraft:validate
```

To generate a new spec file in the contracts directory
```
$ rails apicraft:generate file=openapi
```
## 🔧 Configuration

List of available configurations.
Expand Down
1 change: 1 addition & 0 deletions lib/apicraft-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require_relative "apicraft/mocker"
require_relative "apicraft/openapi"

require_relative "apicraft/validator"
require_relative "apicraft/loader"
require_relative "apicraft/railtie"

Expand Down
4 changes: 4 additions & 0 deletions lib/apicraft/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def contracts_path
@opts[:contracts_path]
end

def default_contracts_path
Rails.root.join("app", "contracts")
end

def mocks
@opts[:mocks]
end
Expand Down
3 changes: 2 additions & 1 deletion lib/apicraft/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def self.load_file!(file)
OpenAPIParser.parse(
parsed,
{
strict_reference_validation: config.strict_reference_validation
strict_reference_validation: config.strict_reference_validation,
expand_reference: true
}
)

Expand Down
16 changes: 15 additions & 1 deletion lib/apicraft/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ module Apicraft
# Hooks into the application boot process
# using Rails::Railtie
class Railtie < Rails::Railtie
initializer "apicraft.load_api_contracts" do
initializer "apicraft.use_middlewares" do
[
Apicraft::Middlewares::Mocker,
Apicraft::Middlewares::Introspector,
Apicraft::Middlewares::RequestValidator
].each { |mw| Rails.application.config.middleware.use mw }
end

config.after_initialize do
Apicraft::Loader.load!
end

rake_tasks do
load "apicraft/tasks/validate.rake"
load "apicraft/tasks/init.rake"
load "apicraft/tasks/generate.rake"
end
end
end
30 changes: 30 additions & 0 deletions lib/apicraft/tasks/generate.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

namespace :apicraft do
desc "Generate an example spec file"

task generate: :environment do |_t, _args|
arguments = ARGV.reduce({}) do |final, current|
key, val = current.split("=").map(&:strip)
final.merge!({
key => val
})
end

filepath = arguments["file"]
template = File.expand_path("../templates/openapi.example.yaml", __dir__)

# root path of all contracts
contracts_path = Apicraft.config.contracts_path

# Split the filepath into parts to extract the directory structure and file name
path_parts = filepath.split("/")
dir_path = File.join(contracts_path, *path_parts[0..-2])
file_name = "#{path_parts[-1]}.yaml"

# Create the directory if it doesn't exist
FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)

File.write(File.join(dir_path, file_name), File.read(template))
end
end
20 changes: 20 additions & 0 deletions lib/apicraft/tasks/init.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

namespace :apicraft do
desc "Initialize apicraft"
task init: :environment do
# Setup the apicraft initializer
destination = Rails.root.join("config", "initializers", "apicraft.rb")
if File.exist?(destination)
puts "File already exists: #{destination}"
else
template = File.expand_path("../templates/initializer.rb", __dir__)
FileUtils.cp(template, destination)
puts "Apicraft initializer created at config/initializers/apicraft.rb"
end

# Create the default contracts directory
contracts_path = Apicraft.config.default_contracts_path
FileUtils.mkdir_p(contracts_path) unless Dir.exist?(contracts_path)
end
end
8 changes: 8 additions & 0 deletions lib/apicraft/tasks/validate.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

namespace :apicraft do
desc "Validate all contracts"
task validate: :environment do
Apicraft::Validator.validate!
end
end
62 changes: 62 additions & 0 deletions lib/apicraft/templates/initializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

Apicraft::Web::App.use do |user, password|
[user, password] == %w[admin password]
end

Apicraft.configure do |config|
config.contracts_path = Rails.root.join("app", "contracts")

# Enables or disables the mocking features
# Defaults to true
config.mocks = true

# Enables or disables the introspection features
# Defaults to true
config.introspection = true

# allows you to enforce stricter validation of $ref
# references in your OpenAPI specifications.
# When this option is enabled, the parser will raise
# an error if any $ref references in your OpenAPI
# document are invalid, ensuring that all references
# are correctly defined and resolved.
# Defaults to true
config.strict_reference_validation = true

# When simulating delay using the mocks, the max
# delay in seconds that can be simulated
config.max_allowed_delay = 0

config.headers = {
# The name of the header used to control
# the response code of the mock
# Defaults to Apicraft-Response-Code
response_code: "Apicraft-Response-Code",

# The name of the header to introspect the API.
# Defaults to Apicraft-Introspect
introspect: "Apicraft-Introspect",

# The name of the header to mock the API.
# Defaults to Apicraft-Mock
mock: "Apicraft-Mock",

# 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
66 changes: 66 additions & 0 deletions lib/apicraft/templates/openapi.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
openapi: 3.0.0
info:
title: Sample API
version: 1.0.0
description: API template to manage users.

servers:
- url: https://api.example.com

paths:
/users:
get:
summary: Get a list of users
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: User created successfully

/users/{id}:
get:
summary: Get a user by id
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: A user object
content:
application/json:
schema:
$ref: '#/components/schemas/User'

components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
required:
- id
- name
- email
Loading

0 comments on commit b5d9f0f

Please sign in to comment.