rubst_api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +17 -0
- data/LICENSES/FASTAPI-MIT.txt +21 -0
- data/README.md +618 -0
- data/SECURITY.md +29 -0
- data/THIRD_PARTY_NOTICES.md +20 -0
- data/exe/rubst_api +29 -0
- data/lib/rubst_api/app.rb +253 -0
- data/lib/rubst_api/background_tasks.rb +18 -0
- data/lib/rubst_api/dependencies.rb +57 -0
- data/lib/rubst_api/errors.rb +46 -0
- data/lib/rubst_api/http.rb +52 -0
- data/lib/rubst_api/middleware.rb +76 -0
- data/lib/rubst_api/model.rb +195 -0
- data/lib/rubst_api/openapi.rb +118 -0
- data/lib/rubst_api/params.rb +43 -0
- data/lib/rubst_api/responses.rb +60 -0
- data/lib/rubst_api/routing.rb +89 -0
- data/lib/rubst_api/security.rb +85 -0
- data/lib/rubst_api/sse.rb +28 -0
- data/lib/rubst_api/static_files.rb +30 -0
- data/lib/rubst_api/status.rb +28 -0
- data/lib/rubst_api/test_client.rb +51 -0
- data/lib/rubst_api/version.rb +5 -0
- data/lib/rubst_api/websocket.rb +21 -0
- data/lib/rubst_api.rb +40 -0
- metadata +182 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0421cfdff576c394312d131b716b781f0811ca24d8572386ac9a94a307759da6
|
|
4
|
+
data.tar.gz: 706a40d9e4be0c54e888047cbd1f46d6131cf705d1457d6b1a52ee29b4a1c85d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1d97840b9d9ef0bf1ef8a20abe93b3ceaeded0863d0e0f33056b373446c85d405fd3ea4a9e9f3cc76ec5277b58ce9a75f06e7767f0791e35ba1fdfcd6d368c79
|
|
7
|
+
data.tar.gz: e79e8e83813610bf9c1e45de62e029ebfc85072e5ae2116c3e2431be4b1116c55d8e2f6cb739c4f7ddae06cfbceebec1cc98cb644ba731c1f3b99a00fabd979f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to RubstAPI will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Professional RubyGems package metadata and canonical repository links.
|
|
13
|
+
- FastAPI license attribution and third-party notices.
|
|
14
|
+
|
|
15
|
+
## [0.1.0] - 2026-07-29
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Rack-compatible API application and router.
|
|
20
|
+
- Typed request parameter extraction and model validation.
|
|
21
|
+
- Dependency injection with caching, sub-dependencies, and test overrides.
|
|
22
|
+
- OpenAPI 3.1, Swagger UI, and ReDoc generation.
|
|
23
|
+
- Basic, bearer, OAuth2 password-bearer, and API-key security helpers.
|
|
24
|
+
- CORS, GZip, trusted-host, and HTTPS-redirect middleware.
|
|
25
|
+
- JSON, HTML, text, redirect, file, streaming, and event-stream responses.
|
|
26
|
+
- Background tasks, static files, WebSocket wrapper, and test client.
|
|
27
|
+
- `rubst_api run` and `rubst_api dev` commands.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/joryleech/RubstApi/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/joryleech/RubstApi/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastAPI Ruby contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Sebastián Ramírez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# RubstAPI
|
|
2
|
+
|
|
3
|
+
### A FastAPI-inspired framework for building typed Ruby APIs
|
|
4
|
+
|
|
5
|
+
RubstAPI is a Ruby REST API framework that brings the developer experience of
|
|
6
|
+
[Python FastAPI](https://fastapi.tiangolo.com/) to Rack applications. Define
|
|
7
|
+
typed request parameters and data models once, then receive validation,
|
|
8
|
+
serialization, OpenAPI 3.1 documentation, Swagger UI, ReDoc, dependency
|
|
9
|
+
injection, and security integration automatically.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "rubst_api"
|
|
13
|
+
|
|
14
|
+
class Item < RubstApi::Model
|
|
15
|
+
field :name, String, min_length: 2
|
|
16
|
+
field :price, Float, gt: 0
|
|
17
|
+
field :in_stock, :boolean, default: true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
APP = RubstApi::App.new(title: "Store API", version: "1.0.0")
|
|
21
|
+
|
|
22
|
+
APP.post("/items", params: {
|
|
23
|
+
item: RubstApi.Body(Item)
|
|
24
|
+
}, response_model: Item, status_code: 201) do |item:|
|
|
25
|
+
item
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Start the application and visit `/docs` for an interactive Swagger UI:
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
rubst_api run app.rb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> [!IMPORTANT]
|
|
36
|
+
> RubstAPI is an independent Ruby project inspired by FastAPI. It is not
|
|
37
|
+
> affiliated with, endorsed by, or maintained by the Python FastAPI project.
|
|
38
|
+
> It is an idiomatic Ruby implementation—not a Python source-compatibility
|
|
39
|
+
> layer.
|
|
40
|
+
|
|
41
|
+
## Why RubstAPI?
|
|
42
|
+
|
|
43
|
+
Ruby has excellent web frameworks, but API projects often assemble request
|
|
44
|
+
validation, serialization, dependency injection, OpenAPI generation, security,
|
|
45
|
+
and documentation from separate libraries. RubstAPI provides those concerns
|
|
46
|
+
through one cohesive, Rack-compatible API.
|
|
47
|
+
|
|
48
|
+
- **Typed inputs:** Coerce and validate path, query, header, cookie, form, file,
|
|
49
|
+
and JSON body parameters.
|
|
50
|
+
- **Ruby data models:** Define nested request and response schemas with
|
|
51
|
+
`RubstApi::Model`.
|
|
52
|
+
- **Standards first:** Generate OpenAPI 3.1 and JSON Schema from application
|
|
53
|
+
declarations.
|
|
54
|
+
- **Documentation included:** Serve Swagger UI at `/docs`, ReDoc at `/redoc`,
|
|
55
|
+
and the raw schema at `/openapi.json`.
|
|
56
|
+
- **Dependency injection:** Compose cached dependencies and sub-dependencies,
|
|
57
|
+
with per-application overrides for testing.
|
|
58
|
+
- **Integrated security:** Describe HTTP Basic, Bearer, OAuth2 password bearer,
|
|
59
|
+
and API keys in headers, queries, or cookies.
|
|
60
|
+
- **Rack compatible:** Mount RubstAPI beside existing Ruby and Rack
|
|
61
|
+
applications.
|
|
62
|
+
- **Focused operational footprint:** Runtime dependencies are limited to Rack,
|
|
63
|
+
Rackup, and WEBrick for the framework's HTTP interface and bundled CLI.
|
|
64
|
+
|
|
65
|
+
## Contents
|
|
66
|
+
|
|
67
|
+
- [Requirements](#requirements)
|
|
68
|
+
- [Installation](#installation)
|
|
69
|
+
- [Quick start](#quick-start)
|
|
70
|
+
- [Parameters and validation](#parameters-and-validation)
|
|
71
|
+
- [Models](#models)
|
|
72
|
+
- [Dependency injection](#dependency-injection)
|
|
73
|
+
- [Security](#security)
|
|
74
|
+
- [Routers](#routers)
|
|
75
|
+
- [Responses and background tasks](#responses-and-background-tasks)
|
|
76
|
+
- [Middleware](#middleware)
|
|
77
|
+
- [OpenAPI and API documentation](#openapi-and-api-documentation)
|
|
78
|
+
- [Testing](#testing)
|
|
79
|
+
- [Running with Docker](#running-with-docker)
|
|
80
|
+
- [FastAPI and RubstAPI](#fastapi-and-rubstapi)
|
|
81
|
+
- [Project status](#project-status)
|
|
82
|
+
- [Development](#development)
|
|
83
|
+
- [Contributing](#contributing)
|
|
84
|
+
- [Security](#security-policy)
|
|
85
|
+
- [License](#license)
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Ruby 3.2 or newer
|
|
90
|
+
- A Rack-compatible server for serving HTTP
|
|
91
|
+
|
|
92
|
+
The built-in `rubst_api run` and `rubst_api dev` commands use Rackup and
|
|
93
|
+
WEBrick. Add these server dependencies to your application:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
gem "rubst_api"
|
|
97
|
+
gem "rack", ">= 3.0"
|
|
98
|
+
gem "rackup", ">= 2.0"
|
|
99
|
+
gem "webrick", ">= 1.8"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Installation
|
|
103
|
+
|
|
104
|
+
Add RubstAPI to your bundle:
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
bundle add rubst_api
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or add it manually to your `Gemfile`:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
gem "rubst_api"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then run:
|
|
117
|
+
|
|
118
|
+
```console
|
|
119
|
+
bundle install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For local development before a RubyGems release, reference the checkout:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
gem "rubst_api", path: "../RubstApi"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Quick start
|
|
129
|
+
|
|
130
|
+
Create `app.rb`:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
require "rubst_api"
|
|
134
|
+
|
|
135
|
+
class Product < RubstApi::Model
|
|
136
|
+
field :name, String, min_length: 2, max_length: 100
|
|
137
|
+
field :price, Float, gt: 0
|
|
138
|
+
field :tags, { array: String }, default: []
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
APP = RubstApi::App.new(
|
|
142
|
+
title: "Products API",
|
|
143
|
+
description: "A typed REST API built with Ruby and RubstAPI.",
|
|
144
|
+
version: "1.0.0"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
APP.get("/") do
|
|
148
|
+
{ name: "Products API", docs: "/docs" }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
APP.get("/products/{product_id}", params: {
|
|
152
|
+
product_id: RubstApi.Path(Integer, ge: 1),
|
|
153
|
+
search: RubstApi.Query(String, required: false, max_length: 50)
|
|
154
|
+
}) do |product_id:, search:|
|
|
155
|
+
{ product_id:, search: }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
APP.post("/products", params: {
|
|
159
|
+
product: RubstApi.Body(Product)
|
|
160
|
+
}, response_model: Product, status_code: 201) do |product:|
|
|
161
|
+
product
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Run it:
|
|
166
|
+
|
|
167
|
+
```console
|
|
168
|
+
bundle exec rubst_api run app.rb --host 0.0.0.0 --port 8000
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Open:
|
|
172
|
+
|
|
173
|
+
| URL | Purpose |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `http://localhost:8000/docs` | Interactive Swagger UI |
|
|
176
|
+
| `http://localhost:8000/redoc` | ReDoc API reference |
|
|
177
|
+
| `http://localhost:8000/openapi.json` | OpenAPI 3.1 document |
|
|
178
|
+
|
|
179
|
+
Test the endpoint:
|
|
180
|
+
|
|
181
|
+
```console
|
|
182
|
+
curl -X POST http://localhost:8000/products \
|
|
183
|
+
-H "Content-Type: application/json" \
|
|
184
|
+
-d '{"name":"Mechanical Keyboard","price":129.0,"tags":["hardware"]}'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Invalid input receives a structured `422 Unprocessable Entity` response:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"detail": [
|
|
192
|
+
{
|
|
193
|
+
"type": "greater_than",
|
|
194
|
+
"loc": ["body", "product", "price"],
|
|
195
|
+
"msg": "Value must be greater than 0",
|
|
196
|
+
"input": -1
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Parameters and validation
|
|
203
|
+
|
|
204
|
+
Declare where every input comes from:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
APP.put("/accounts/{account_id}", params: {
|
|
208
|
+
account_id: RubstApi.Path(Integer, ge: 1),
|
|
209
|
+
verbose: RubstApi.Query(:boolean, default: false),
|
|
210
|
+
request_id: RubstApi.Header(String, alias: "x-request-id"),
|
|
211
|
+
session: RubstApi.Cookie(String, required: false),
|
|
212
|
+
account: RubstApi.Body(Account)
|
|
213
|
+
}) do |account_id:, verbose:, request_id:, session:, account:|
|
|
214
|
+
# All values are extracted, coerced, and validated before this block runs.
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Declaration | Request source |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `RubstApi.Path` | URL path segment |
|
|
221
|
+
| `RubstApi.Query` | Query string |
|
|
222
|
+
| `RubstApi.Header` | HTTP header |
|
|
223
|
+
| `RubstApi.Cookie` | Cookie |
|
|
224
|
+
| `RubstApi.Body` | JSON request body |
|
|
225
|
+
| `RubstApi.Form` | URL-encoded form |
|
|
226
|
+
| `RubstApi.File` | Multipart upload |
|
|
227
|
+
|
|
228
|
+
Supported constraints include:
|
|
229
|
+
|
|
230
|
+
- `min_length` and `max_length`
|
|
231
|
+
- `gt`, `ge`, `lt`, and `le`
|
|
232
|
+
- `pattern` or `regex`
|
|
233
|
+
- `enum`
|
|
234
|
+
- `nullable`
|
|
235
|
+
- `required`
|
|
236
|
+
- `default`
|
|
237
|
+
- `alias`
|
|
238
|
+
|
|
239
|
+
Common Ruby types such as `String`, `Integer`, `Float`, `Numeric`, `Hash`,
|
|
240
|
+
`Array`, `Date`, `Time`, and boolean values are coerced automatically.
|
|
241
|
+
|
|
242
|
+
## Models
|
|
243
|
+
|
|
244
|
+
Models validate nested input and generate JSON Schema:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class Address < RubstApi::Model
|
|
248
|
+
field :city, String, min_length: 1
|
|
249
|
+
field :postal_code, String, pattern: /\A[0-9A-Z -]+\z/i
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class Customer < RubstApi::Model
|
|
253
|
+
field :name, String
|
|
254
|
+
field :address, Address
|
|
255
|
+
field :email, [String, NilClass], default: nil
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Type notation:
|
|
260
|
+
|
|
261
|
+
| Shape | Declaration |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| Array of strings | `{ array: String }` |
|
|
264
|
+
| Array of models | `{ array: Customer }` |
|
|
265
|
+
| Union | `[String, Integer]` |
|
|
266
|
+
| Nullable string | `[String, NilClass]` |
|
|
267
|
+
|
|
268
|
+
Serialize a model with `model_dump`:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
customer.model_dump(exclude_none: true)
|
|
272
|
+
customer.model_dump(by_alias: true)
|
|
273
|
+
customer.to_json
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Dependency injection
|
|
277
|
+
|
|
278
|
+
Dependencies are ordinary Ruby callables:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
def database
|
|
282
|
+
Database.connect
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
database_dependency = RubstApi.Depends(method(:database))
|
|
286
|
+
|
|
287
|
+
APP.get("/reports", params: {
|
|
288
|
+
db: database_dependency
|
|
289
|
+
}) do |db:|
|
|
290
|
+
db.reports
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Dependencies are cached once per request by default. They can also depend on
|
|
295
|
+
other dependencies:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
database_dependency = RubstApi.Depends(method(:database))
|
|
299
|
+
|
|
300
|
+
current_user_dependency = RubstApi.Depends(
|
|
301
|
+
method(:current_user),
|
|
302
|
+
dependencies: { db: database_dependency }
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Override dependencies during tests:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
APP.dependency_overrides[method(:database)] = -> { FakeDatabase.new }
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Enumerator-based dependencies can yield a value and perform cleanup after the
|
|
313
|
+
request, which is useful for database sessions and other scoped resources.
|
|
314
|
+
|
|
315
|
+
## Security
|
|
316
|
+
|
|
317
|
+
Security helpers both authenticate requests and populate the OpenAPI security
|
|
318
|
+
schema.
|
|
319
|
+
|
|
320
|
+
### Bearer authentication
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
bearer = RubstApi::Security::HTTPBearer.new
|
|
324
|
+
|
|
325
|
+
APP.get("/me", params: {
|
|
326
|
+
credentials: RubstApi.Security(bearer)
|
|
327
|
+
}) do |credentials:|
|
|
328
|
+
{ token: credentials.credentials }
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### OAuth2 password bearer
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
oauth2 = RubstApi::Security::OAuth2PasswordBearer.new(
|
|
336
|
+
token_url: "/token",
|
|
337
|
+
scopes: { "items:read" => "Read items" }
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
APP.get("/items", params: {
|
|
341
|
+
token: RubstApi.Security(oauth2, scopes: ["items:read"])
|
|
342
|
+
}) do |token:|
|
|
343
|
+
{ token: token }
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Available helpers:
|
|
348
|
+
|
|
349
|
+
- `HTTPBasic`
|
|
350
|
+
- `HTTPBearer`
|
|
351
|
+
- `OAuth2PasswordBearer`
|
|
352
|
+
- `APIKeyHeader`
|
|
353
|
+
- `APIKeyQuery`
|
|
354
|
+
- `APIKeyCookie`
|
|
355
|
+
|
|
356
|
+
Authentication policy, token issuance, password hashing, and database access
|
|
357
|
+
remain application concerns.
|
|
358
|
+
|
|
359
|
+
## Routers
|
|
360
|
+
|
|
361
|
+
Split larger APIs into focused routers:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
users = RubstApi::APIRouter.new(prefix: "/users", tags: ["users"])
|
|
365
|
+
|
|
366
|
+
users.get("/{user_id}", params: {
|
|
367
|
+
user_id: RubstApi.Path(Integer)
|
|
368
|
+
}) do |user_id:|
|
|
369
|
+
{ user_id: user_id }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
APP.include_router(users, prefix: "/v1")
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
RubstAPI supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD`,
|
|
376
|
+
and `TRACE` routes.
|
|
377
|
+
|
|
378
|
+
## Responses and background tasks
|
|
379
|
+
|
|
380
|
+
Return ordinary Ruby objects for automatic JSON serialization, or return an
|
|
381
|
+
explicit response:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
APP.get("/redirect") do
|
|
385
|
+
RubstApi::RedirectResponse.new("/docs")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
APP.get("/download") do
|
|
389
|
+
RubstApi::FileResponse.new("report.csv", filename: "report.csv")
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Available response classes:
|
|
394
|
+
|
|
395
|
+
- `JSONResponse`
|
|
396
|
+
- `HTMLResponse`
|
|
397
|
+
- `PlainTextResponse`
|
|
398
|
+
- `RedirectResponse`
|
|
399
|
+
- `StreamingResponse`
|
|
400
|
+
- `FileResponse`
|
|
401
|
+
- `EventSourceResponse` for server-sent events
|
|
402
|
+
|
|
403
|
+
Schedule in-process work after a response:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
APP.post("/notifications") do |background_tasks:|
|
|
407
|
+
background_tasks.add_task(method(:deliver_notification), "user-123")
|
|
408
|
+
{ queued: true }
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
For durable, retryable, or distributed jobs, use a dedicated Ruby job system.
|
|
413
|
+
|
|
414
|
+
## Middleware
|
|
415
|
+
|
|
416
|
+
Add middleware with explicit options:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
APP.add_middleware(
|
|
420
|
+
RubstApi::Middleware::CORSMiddleware,
|
|
421
|
+
allow_origins: ["https://example.com"],
|
|
422
|
+
allow_methods: %w[GET POST],
|
|
423
|
+
allow_headers: ["authorization", "content-type"]
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
APP.add_middleware(
|
|
427
|
+
RubstApi::Middleware::GZipMiddleware,
|
|
428
|
+
minimum_size: 500
|
|
429
|
+
)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Included middleware:
|
|
433
|
+
|
|
434
|
+
- CORS
|
|
435
|
+
- GZip
|
|
436
|
+
- Trusted Host
|
|
437
|
+
- HTTPS Redirect
|
|
438
|
+
|
|
439
|
+
Any compatible Rack middleware can wrap a RubstAPI application.
|
|
440
|
+
|
|
441
|
+
## OpenAPI and API documentation
|
|
442
|
+
|
|
443
|
+
RubstAPI follows the same standards-first philosophy popularized by Python
|
|
444
|
+
FastAPI: route declarations, request parameters, bodies, response models, and
|
|
445
|
+
security requirements produce an OpenAPI document automatically.
|
|
446
|
+
|
|
447
|
+
Configure documentation endpoints when creating the app:
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
APP = RubstApi::App.new(
|
|
451
|
+
title: "Billing API",
|
|
452
|
+
version: "2.1.0",
|
|
453
|
+
openapi_url: "/openapi.json",
|
|
454
|
+
docs_url: "/docs",
|
|
455
|
+
redoc_url: "/redoc",
|
|
456
|
+
openapi_tags: [
|
|
457
|
+
{ name: "invoices", description: "Invoice operations" }
|
|
458
|
+
]
|
|
459
|
+
)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Set any documentation URL to `nil` to disable that endpoint.
|
|
463
|
+
|
|
464
|
+
## Testing
|
|
465
|
+
|
|
466
|
+
The in-process test client does not require a running web server:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
require "minitest/autorun"
|
|
470
|
+
require "rubst_api"
|
|
471
|
+
|
|
472
|
+
class ProductsApiTest < Minitest::Test
|
|
473
|
+
def test_get_product
|
|
474
|
+
client = RubstApi::TestClient.new(APP)
|
|
475
|
+
response = client.get("/products/42")
|
|
476
|
+
|
|
477
|
+
assert_equal 200, response.status
|
|
478
|
+
assert_equal 42, response.json.fetch("product_id")
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def test_invalid_product
|
|
482
|
+
client = RubstApi::TestClient.new(APP)
|
|
483
|
+
response = client.post(
|
|
484
|
+
"/products",
|
|
485
|
+
json: { name: "x", price: -10 }
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
assert_equal 422, response.status
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Supported test requests include `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
|
|
494
|
+
`OPTIONS`, and `HEAD`, with query parameters, JSON, form data, headers, and
|
|
495
|
+
cookies.
|
|
496
|
+
|
|
497
|
+
## Running with Docker
|
|
498
|
+
|
|
499
|
+
```dockerfile
|
|
500
|
+
FROM ruby:4.0-slim
|
|
501
|
+
|
|
502
|
+
WORKDIR /app
|
|
503
|
+
|
|
504
|
+
COPY Gemfile Gemfile.lock ./
|
|
505
|
+
RUN bundle install
|
|
506
|
+
|
|
507
|
+
COPY . .
|
|
508
|
+
|
|
509
|
+
EXPOSE 8000
|
|
510
|
+
CMD ["bundle", "exec", "rubst_api", "run", "app.rb", "--host", "0.0.0.0", "--port", "8000"]
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Example Compose service:
|
|
514
|
+
|
|
515
|
+
```yaml
|
|
516
|
+
services:
|
|
517
|
+
api:
|
|
518
|
+
build: .
|
|
519
|
+
ports:
|
|
520
|
+
- "8000:8000"
|
|
521
|
+
healthcheck:
|
|
522
|
+
test:
|
|
523
|
+
- CMD
|
|
524
|
+
- ruby
|
|
525
|
+
- -rnet/http
|
|
526
|
+
- -e
|
|
527
|
+
- 'exit(Net::HTTP.get_response(URI("http://127.0.0.1:8000/health")).is_a?(Net::HTTPSuccess) ? 0 : 1)'
|
|
528
|
+
interval: 10s
|
|
529
|
+
timeout: 3s
|
|
530
|
+
retries: 5
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## FastAPI and RubstAPI
|
|
534
|
+
|
|
535
|
+
[FastAPI](https://fastapi.tiangolo.com/) is a high-performance Python API
|
|
536
|
+
framework built on Starlette and Pydantic. RubstAPI adapts several of its core
|
|
537
|
+
ideas to Ruby:
|
|
538
|
+
|
|
539
|
+
| Python FastAPI concept | RubstAPI equivalent |
|
|
540
|
+
| --- | --- |
|
|
541
|
+
| ASGI application | Rack-compatible callable |
|
|
542
|
+
| Python type annotations | Explicit `params:` declarations |
|
|
543
|
+
| Pydantic `BaseModel` | `RubstApi::Model` |
|
|
544
|
+
| `Depends()` | `RubstApi.Depends` |
|
|
545
|
+
| `Security()` | `RubstApi.Security` |
|
|
546
|
+
| `APIRouter` | `RubstApi::APIRouter` |
|
|
547
|
+
| Starlette responses | `RubstApi::*Response` |
|
|
548
|
+
| HTTPX test client | `RubstApi::TestClient` |
|
|
549
|
+
| Uvicorn command | `rubst_api run` |
|
|
550
|
+
|
|
551
|
+
RubstAPI does not include or emulate Python, ASGI, Starlette, Pydantic, Uvicorn,
|
|
552
|
+
or FastAPI Cloud. Existing FastAPI Python applications must be translated to
|
|
553
|
+
Ruby declarations and Rack integrations.
|
|
554
|
+
|
|
555
|
+
The FastAPI name and trademark belong to their respective owner. See the
|
|
556
|
+
[official FastAPI documentation](https://fastapi.tiangolo.com/) and
|
|
557
|
+
[FastAPI source repository](https://github.com/fastapi/fastapi) for the Python
|
|
558
|
+
project.
|
|
559
|
+
|
|
560
|
+
## Project status
|
|
561
|
+
|
|
562
|
+
RubstAPI is an early-stage framework. Its public API covers the central request,
|
|
563
|
+
validation, routing, dependency, security, response, middleware, documentation,
|
|
564
|
+
and testing workflows described above. Before adopting it for critical
|
|
565
|
+
production systems, evaluate the current release against your performance,
|
|
566
|
+
security, concurrency, observability, and deployment requirements.
|
|
567
|
+
|
|
568
|
+
RubstAPI uses the `RubstApi` namespace and the `require "rubst_api"` entry
|
|
569
|
+
point exclusively. It does not define a `FastAPI` Ruby constant or provide a
|
|
570
|
+
`require "fast_api"` compatibility loader.
|
|
571
|
+
|
|
572
|
+
## Development
|
|
573
|
+
|
|
574
|
+
Install dependencies and run the complete test suite:
|
|
575
|
+
|
|
576
|
+
```console
|
|
577
|
+
bundle install
|
|
578
|
+
bundle exec rake test
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Build the gem:
|
|
582
|
+
|
|
583
|
+
```console
|
|
584
|
+
gem build rubst_api.gemspec
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
The project uses Minitest. New functionality should include success, validation,
|
|
588
|
+
error, and OpenAPI coverage where applicable.
|
|
589
|
+
|
|
590
|
+
## Contributing
|
|
591
|
+
|
|
592
|
+
Bug reports, documentation improvements, tests, and focused pull requests are
|
|
593
|
+
welcome.
|
|
594
|
+
|
|
595
|
+
1. Fork the repository.
|
|
596
|
+
2. Create a descriptive branch.
|
|
597
|
+
3. Add tests for behavioral changes.
|
|
598
|
+
4. Run `bundle exec rake test`.
|
|
599
|
+
5. Update user-facing documentation.
|
|
600
|
+
6. Open a pull request explaining the problem and solution.
|
|
601
|
+
|
|
602
|
+
Please keep changes focused, preserve backward compatibility when practical,
|
|
603
|
+
and avoid claiming compatibility that is not covered by tests.
|
|
604
|
+
|
|
605
|
+
## Security policy
|
|
606
|
+
|
|
607
|
+
Do not disclose suspected vulnerabilities in a public issue. Follow the private
|
|
608
|
+
reporting instructions in [SECURITY.md](SECURITY.md).
|
|
609
|
+
|
|
610
|
+
## License
|
|
611
|
+
|
|
612
|
+
RubstAPI is available under the [MIT License](LICENSE).
|
|
613
|
+
|
|
614
|
+
RubstAPI is inspired by the Python FastAPI project. FastAPI's original MIT
|
|
615
|
+
license and copyright notice are preserved verbatim in
|
|
616
|
+
[LICENSES/FASTAPI-MIT.txt](LICENSES/FASTAPI-MIT.txt) and summarized in
|
|
617
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). These files are included in
|
|
618
|
+
the published gem.
|