Skip to content

Commit b5d9f0f

Browse files
[feature] CLI support (#7)
* 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
1 parent 82027ea commit b5d9f0f

File tree

13 files changed

+284
-37
lines changed

13 files changed

+284
-37
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
- "3.0"
3030
- "3.1"
3131
- "3.2"
32+
- "3.3"
3233
- ruby-head
3334

3435
runs-on: ${{ matrix.os }}

README.md

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# APICraft Rails
22
[![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)
3-
[![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)
3+
[![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)
44

55
🚀 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.
66

@@ -14,8 +14,6 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
1414

1515
- [APICraft Rails](#apicraft-rails)
1616
- [✨ Features](#-features)
17-
- [🔜 Upcoming Features](#-upcoming-features)
18-
- [🪄 Works Like Magic](#-works-like-magic)
1917
- [🕊 API Design First Philosophy](#-api-design-first-philosophy)
2018
- [🏗 Installation](#-installation)
2119
- [⚙️ Usage](#️-usage)
@@ -24,6 +22,7 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
2422
- [🎮 Behaviour Mocking](#-behaviour-mocking)
2523
- [🧐 Introspection](#-introspection)
2624
- [📖 Documentation (Swagger docs and RapiDoc)](#-documentation-swagger-docs-and-rapidoc)
25+
- [📖 CLI Support](#-cli-support)
2726
- [🔧 Configuration](#-configuration)
2827
- [🤝 Contributing](#-contributing)
2928
- [📝 License](#-license)
@@ -42,13 +41,7 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
4241

4342
- 🗂 **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
4443

45-
## 🔜 Upcoming Features
46-
- 💎 **Clean & Custom Ruby DSL** - Support for a Ruby DSL alongwith the current `.json` and `.yaml` formats.
47-
48-
49-
## 🪄 Works Like Magic
50-
51-
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!
44+
- 🗂 **CLI Support** - Specification validations can be triggered from the CLI allowing integrations into your CI/CD pipelines.
5245

5346
## 🕊 API Design First Philosophy
5447

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

7265
## 🏗 Installation
7366

74-
Add this line to your application's Gemfile:
67+
1. Add this line to your application's Gemfile:
7568

7669
```ruby
77-
gem 'apicraft-rails', '~> 1.0.1'
70+
gem 'apicraft-rails', '~> 1.0.2'
7871
```
7972

80-
And then execute:
73+
2. And then execute:
74+
```bash
75+
$ bundle install
76+
$ rails apicraft:init
77+
```
8178

82-
$ bundle install
79+
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`.
8380

84-
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.
8581

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

8884
```ruby
89-
# config/application.rb
90-
module App
91-
class Application < Rails::Application
92-
# Rest of the configuration...
93-
94-
[
95-
Apicraft::Middlewares::Mocker,
96-
Apicraft::Middlewares::Introspector,
97-
Apicraft::Middlewares::RequestValidator
98-
].each { |mw| config.middleware.use mw }
99-
100-
Apicraft.configure do |config|
101-
config.contracts_path = Rails.root.join("app/contracts")
102-
end
103-
end
85+
Rails.application.routes.draw do
86+
# other routes
87+
mount Apicraft::Web::App, at: "/apicraft"
10488
end
10589
```
10690

10791
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.
10892

93+
4. Generate a sample spec file
94+
```
95+
rails apicraft:generate file=v2/openapi
96+
```
97+
98+
This will generate a sample file called `app/contracts/v2/openapi.yaml`
10999
## ⚙️ Usage
110100

111101
Add your specification files to the `app/contracts` directory in your Rails project. You can also configure this directory to be something else.
@@ -199,9 +189,7 @@ Example: `https://yoursite.com/api/orders`
199189
}
200190
}
201191
],
202-
"responses": {
203-
...
204-
}
192+
"responses": {}
205193
}
206194
```
207195
### 📖 Documentation (Swagger docs and RapiDoc)
@@ -238,6 +226,17 @@ RapiDoc | SwaggerDoc
238226
:-------------------------:|:-------------------------:
239227
![](assets/rapidoc.png) | ![](assets/swaggerdoc.png)
240228

229+
### 📖 CLI Support
230+
231+
To check if all the specification are valid
232+
```
233+
$ rails apicraft:validate
234+
```
235+
236+
To generate a new spec file in the contracts directory
237+
```
238+
$ rails apicraft:generate file=openapi
239+
```
241240
## 🔧 Configuration
242241

243242
List of available configurations.

lib/apicraft-rails.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require_relative "apicraft/mocker"
2020
require_relative "apicraft/openapi"
2121

22+
require_relative "apicraft/validator"
2223
require_relative "apicraft/loader"
2324
require_relative "apicraft/railtie"
2425

lib/apicraft/config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def contracts_path
4343
@opts[:contracts_path]
4444
end
4545

46+
def default_contracts_path
47+
Rails.root.join("app", "contracts")
48+
end
49+
4650
def mocks
4751
@opts[:mocks]
4852
end

lib/apicraft/loader.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def self.load_file!(file)
3838
OpenAPIParser.parse(
3939
parsed,
4040
{
41-
strict_reference_validation: config.strict_reference_validation
41+
strict_reference_validation: config.strict_reference_validation,
42+
expand_reference: true
4243
}
4344
)
4445

lib/apicraft/railtie.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,22 @@ module Apicraft
44
# Hooks into the application boot process
55
# using Rails::Railtie
66
class Railtie < Rails::Railtie
7-
initializer "apicraft.load_api_contracts" do
7+
initializer "apicraft.use_middlewares" do
8+
[
9+
Apicraft::Middlewares::Mocker,
10+
Apicraft::Middlewares::Introspector,
11+
Apicraft::Middlewares::RequestValidator
12+
].each { |mw| Rails.application.config.middleware.use mw }
13+
end
14+
15+
config.after_initialize do
816
Apicraft::Loader.load!
917
end
18+
19+
rake_tasks do
20+
load "apicraft/tasks/validate.rake"
21+
load "apicraft/tasks/init.rake"
22+
load "apicraft/tasks/generate.rake"
23+
end
1024
end
1125
end

lib/apicraft/tasks/generate.rake

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
namespace :apicraft do
4+
desc "Generate an example spec file"
5+
6+
task generate: :environment do |_t, _args|
7+
arguments = ARGV.reduce({}) do |final, current|
8+
key, val = current.split("=").map(&:strip)
9+
final.merge!({
10+
key => val
11+
})
12+
end
13+
14+
filepath = arguments["file"]
15+
template = File.expand_path("../templates/openapi.example.yaml", __dir__)
16+
17+
# root path of all contracts
18+
contracts_path = Apicraft.config.contracts_path
19+
20+
# Split the filepath into parts to extract the directory structure and file name
21+
path_parts = filepath.split("/")
22+
dir_path = File.join(contracts_path, *path_parts[0..-2])
23+
file_name = "#{path_parts[-1]}.yaml"
24+
25+
# Create the directory if it doesn't exist
26+
FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
27+
28+
File.write(File.join(dir_path, file_name), File.read(template))
29+
end
30+
end

lib/apicraft/tasks/init.rake

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
namespace :apicraft do
4+
desc "Initialize apicraft"
5+
task init: :environment do
6+
# Setup the apicraft initializer
7+
destination = Rails.root.join("config", "initializers", "apicraft.rb")
8+
if File.exist?(destination)
9+
puts "File already exists: #{destination}"
10+
else
11+
template = File.expand_path("../templates/initializer.rb", __dir__)
12+
FileUtils.cp(template, destination)
13+
puts "Apicraft initializer created at config/initializers/apicraft.rb"
14+
end
15+
16+
# Create the default contracts directory
17+
contracts_path = Apicraft.config.default_contracts_path
18+
FileUtils.mkdir_p(contracts_path) unless Dir.exist?(contracts_path)
19+
end
20+
end

lib/apicraft/tasks/validate.rake

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
namespace :apicraft do
4+
desc "Validate all contracts"
5+
task validate: :environment do
6+
Apicraft::Validator.validate!
7+
end
8+
end

lib/apicraft/templates/initializer.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
Apicraft::Web::App.use do |user, password|
4+
[user, password] == %w[admin password]
5+
end
6+
7+
Apicraft.configure do |config|
8+
config.contracts_path = Rails.root.join("app", "contracts")
9+
10+
# Enables or disables the mocking features
11+
# Defaults to true
12+
config.mocks = true
13+
14+
# Enables or disables the introspection features
15+
# Defaults to true
16+
config.introspection = true
17+
18+
# allows you to enforce stricter validation of $ref
19+
# references in your OpenAPI specifications.
20+
# When this option is enabled, the parser will raise
21+
# an error if any $ref references in your OpenAPI
22+
# document are invalid, ensuring that all references
23+
# are correctly defined and resolved.
24+
# Defaults to true
25+
config.strict_reference_validation = true
26+
27+
# When simulating delay using the mocks, the max
28+
# delay in seconds that can be simulated
29+
config.max_allowed_delay = 0
30+
31+
config.headers = {
32+
# The name of the header used to control
33+
# the response code of the mock
34+
# Defaults to Apicraft-Response-Code
35+
response_code: "Apicraft-Response-Code",
36+
37+
# The name of the header to introspect the API.
38+
# Defaults to Apicraft-Introspect
39+
introspect: "Apicraft-Introspect",
40+
41+
# The name of the header to mock the API.
42+
# Defaults to Apicraft-Mock
43+
mock: "Apicraft-Mock",
44+
45+
# Delay simulation header name
46+
delay: "Apicraft-Delay"
47+
}
48+
49+
config.request_validation = {
50+
enabled: true,
51+
52+
# Return the http code for validation errors, defaults to 400
53+
http_code: 400,
54+
55+
# Return a custom response body, defaults to `{ message: "..." }`
56+
response_body: proc do |ex|
57+
{
58+
message: ex.message
59+
}
60+
end
61+
}
62+
end

0 commit comments

Comments
 (0)