Skip to content

Commit 9f49b09

Browse files
committed
Decouple actix and diesel
1 parent 6a35ac9 commit 9f49b09

File tree

12 files changed

+672
-426
lines changed

12 files changed

+672
-426
lines changed

README.md

Lines changed: 129 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,218 @@
11
cdd-rust: OpenAPI ↔ Rust
22
========================
3-
[![Rust: nightly](https://img.shields.io/badge/Rust-nightly-blue.svg)](https://www.rust-lang.org)
4-
[![License: (Apache-2.0 OR MIT)](https://img.shields.io/badge/LICENSE-Apache--2.0%20OR%20MIT-orange)](LICENSE-APACHE)
5-
[![CI](https://github.com/offscale/cdd-rust/actions/workflows/ci-cargo.yml/badge.svg)](https://github.com/offscale/cdd-rust/actions/workflows/ci-cargo.yml)
3+
[![Rust: nightly](https://img.shields.io/badge/Rust-nightly-blue.svg)](https://www.rust-lang.org)
4+
[![License: (Apache-2.0 OR MIT)](https://img.shields.io/badge/LICENSE-Apache--2.0%20OR%20MIT-orange)](LICENSE-APACHE)
5+
[![CI](https://github.com/offscale/cdd-rust/actions/workflows/ci-cargo.yml/badge.svg)](https://github.com/offscale/cdd-rust/actions/workflows/ci-cargo.yml)
66

7-
**Compiler Driven Development (CDD)** for Rust.
7+
**Compiler Driven Development (CDD)** for Rust.
88

9-
**cdd-rust** creates a symbiotic link between your **Database**, **Rust Code**, and **OpenAPI Specifications**. Unlike traditional generators that overwrite files or hide code in "generated" directories, `cdd-rust` uses advanced AST parsing (`ra_ap_syntax`) to surgically patch your *existing* source files, strictly typed handlers, and integration tests.
9+
**cdd-rust** creates a symbiotic link between your **Database**, **Rust Code**, and **OpenAPI Specifications**. Unlike traditional generators that overwrite files or hide code in "generated" directories, `cdd-rust` uses advanced AST parsing (`ra_ap_syntax`) to surgically patch your *existing* source files, strictly typed handlers, and integration tests.
1010

11-
It supports two distinct workflows:
12-
1. **Scaffold | Patch (OpenAPI ➔ Rust):** Generate/update Actix handlers, routes, and Diesel models from OpenAPI.
13-
2. **Reflect & Sync (Rust ➔ OpenAPI):** Generate OpenAPI specifications from your actual source code.
11+
Uniquely, `cdd-rust` is built on a **strategy-based architecture**. While the default implementation provides a robust **Actix Web + Diesel** workflow, the core logic is decoupled into strategies and mappers, making it extensible to other ecosystems (e.g., Axum, SQLx) in the future.
12+
13+
It supports two distinct workflows:
14+
1. **Scaffold | Patch (OpenAPI ➔ Rust):** Generate/update handlers, routes, and models via `BackendStrategy`.
15+
2. **Reflect & Sync (Rust ➔ OpenAPI):** Generate OpenAPI specifications from your source code and DB via `ModelMapper`.
1416

1517
## ⚡️ The CDD Loop
1618

1719
```mermaid
18-
%%{init: {
19-
'theme': 'base',
20-
'themeVariables': {
21-
'primaryColor': '#ffffff',
22-
'primaryTextColor': '#20344b',
23-
'primaryBorderColor': '#20344b',
24-
'lineColor': '#20344b',
25-
'fontFamily': 'Google Sans, sans-serif'
26-
}
27-
}}%%
20+
%%{init: {
21+
'theme': 'base',
22+
'themeVariables': {
23+
'primaryColor': '#ffffff',
24+
'primaryTextColor': '#20344b',
25+
'primaryBorderColor': '#20344b',
26+
'lineColor': '#20344b',
27+
'fontFamily': 'Google Sans, sans-serif'
28+
}
29+
}}%%
2830
2931
graph TD
30-
%% --- Section 1: Route Logic ---
32+
%% --- Section 1: Route Logic ---
3133
32-
%% Node: OAS Path
34+
%% Node: OpenAPI Path
3335
OasPath("<strong>OpenAPI Path (YAML)</strong><br/>/users/{id}:<br/>&nbsp;&nbsp;get:<br/>&nbsp;&nbsp;&nbsp;&nbsp;parameters:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- name: id<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;in: path<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;schema: {format: uuid}"):::yellow
3436
3537
%% Node: Actix Handler
36-
Actix("<strong>Actix Handler (Rust)</strong><br/>async fn get_user(<br/>&nbsp;&nbsp;id: web::Path&lt;Uuid&gt;<br/>) -> impl Responder {<br/>&nbsp;&nbsp;/* Logic */<br/>}"):::blue
38+
Actix("<strong>Handler (Rust)</strong><br/>async fn get_user(<br/>&nbsp;&nbsp;id: web::Path&lt;Uuid&gt;<br/>) -> impl Responder {<br/>&nbsp;&nbsp;/* Logic */<br/>}"):::blue
3739
38-
%% Flow: Down (Scaffold) & Up (Reflect)
40+
%% Flow: Down (Scaffold) & Up (Reflect)
3941
OasPath ==>|"1. SCAFFOLD / PATCH<br/>(Injects Handler & Route Signature)"| Actix
4042
Actix -.->|"2. REFLECT / GENERATE<br/>(Extracts Paths via AST)"| OasPath
4143
42-
%% --- Spacer to force vertical layout ---
44+
%% --- Spacer to force vertical layout ---
4345
Actix ~~~ OasSchema
4446
45-
%% --- Section 2: Data Models ---
47+
%% --- Section 2: Data Models ---
4648
47-
%% Node: OAS Schema
49+
%% Node: OpenAPI Schema
4850
OasSchema("<strong>OpenAPI Schema (YAML)</strong><br/>components:<br/>&nbsp;&nbsp;schemas:<br/>&nbsp;&nbsp;&nbsp;&nbsp;User:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;type: object<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;properties:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;email: {type: string}"):::yellow
4951
5052
%% Node: Diesel Model
51-
Diesel("<strong>Diesel Model (Rust)</strong><br/>#[derive(ToSchema)]<br/>struct User {<br/>&nbsp;&nbsp;id: Uuid,<br/>&nbsp;&nbsp;email: String<br/>}"):::green
53+
Diesel("<strong>Data Model (Rust)</strong><br/>#[derive(ToSchema)]<br/>struct User {<br/>&nbsp;&nbsp;id: Uuid,<br/>&nbsp;&nbsp;email: String<br/>}"):::green
5254
53-
%% Flow: Down (Scaffold) & Up (Reflect)
55+
%% Flow: Down (Scaffold) & Up (Reflect)
5456
OasSchema ==>|"1. SCAFFOLD / PATCH<br/>(Injects Fields & Types)"| Diesel
5557
Diesel -.->|"2. REFLECT / GENERATE<br/>(Derives Attributes)"| OasSchema
5658
57-
%% --- Styles ---
58-
classDef yellow fill:#f9ab00,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
59-
classDef blue fill:#4285f4,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
60-
classDef green fill:#34a853,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
59+
%% --- Styles ---
60+
classDef yellow fill:#f9ab00,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
61+
classDef blue fill:#4285f4,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
62+
classDef green fill:#34a853,stroke:#20344b,stroke-width:2px,color:#ffffff,font-family:'Consolas',font-size:14px,text-align:left;
6163
62-
linkStyle default stroke:#20344b,stroke-width:2px;
64+
linkStyle default stroke:#20344b,stroke-width:2px;
6365
```
6466

65-
---
67+
## 🏗 Architecture
68+
69+
The internal architecture separates the core AST/OpenAPI parsing logic from the target code generation. This allows the tool to support multiple web frameworks and ORMs through the `BackendStrategy` and `ModelMapper` traits.
70+
71+
```mermaid
72+
graph LR
73+
%% --- NODES ---
74+
InputOAS(<strong>OpenAPI Spec</strong><br/><em>YAML</em>)
75+
InputSrc(<strong>Rust Source</strong><br/><em>Files / Schema</em>)
76+
77+
subgraph Core [Layer 1: Core]
78+
direction TB
79+
P_OAS(<strong>OAS Parser</strong><br/><em>serde_yaml</em>)
80+
P_AST(<strong>AST Parser</strong><br/><em>ra_ap_syntax</em>)
81+
end
82+
83+
subgraph Analysis [Layer 2: Analysis]
84+
IR(<strong>Intermediate Representation</strong><br/><em>ParsedRoute / ParsedStruct</em>)
85+
end
86+
87+
subgraph Gen [Layer 3: Generation]
88+
Base(<strong>Generator Engine</strong><br/><em>Traits: BackendStrategy & ModelMapper</em>)
89+
90+
%% The Fork
91+
subgraph Targets [Targets]
92+
direction TB
93+
T_Actix(<strong>Actix</strong><br/><em>ActixStrategy</em>)
94+
T_Diesel(<strong>Diesel</strong><br/><em>DieselMapper</em>)
95+
T_OutputOAS(<strong>OpenAPI</strong><br/><em>Spec Generation</em>)
96+
T_Future(<strong>Axum / SQLx</strong><br/><em>Future Strategies</em>)
97+
end
98+
end
99+
100+
%% --- EDGES ---
101+
InputOAS --> P_OAS
102+
InputSrc --> P_AST
103+
104+
P_OAS --> IR
105+
P_AST --> IR
106+
107+
IR --> Base
108+
109+
Base -- "Scaffold / Test" --> T_Actix
110+
Base -- "Sync Models" --> T_Diesel
111+
Base -- "Reflect" --> T_OutputOAS
112+
Base -. "Extension" .-> T_Future
113+
114+
%% --- STYLING ---
115+
classDef blue fill:#4285f4,stroke:#ffffff,color:#ffffff,stroke-width:0px
116+
classDef yellow fill:#f9ab00,stroke:#ffffff,color:#20344b,stroke-width:0px
117+
classDef green fill:#34a853,stroke:#ffffff,color:#ffffff,stroke-width:0px
118+
classDef white fill:#ffffff,stroke:#20344b,color:#20344b,stroke-width:2px
119+
classDef future fill:#f1f3f4,stroke:#20344b,color:#20344b,stroke-width:2px,stroke-dasharray: 5 5
120+
121+
class InputOAS,InputSrc white
122+
class P_OAS,P_AST blue
123+
class IR yellow
124+
class Base green
125+
class T_Actix,T_Diesel,T_OutputOAS white
126+
class T_Future future
127+
```
128+
129+
---
66130

67131
## 🚀 Features
68132

69-
### 1. Zero-Boilerplate Scaffolding (OpenAPI ➔ Rust)
70-
Stop manually writing repetitive handler signatures. `cdd-rust` reads your spec and generates strictly typed code.
71-
* **Handler Scaffolding:** Transforms OpenAPI paths into `async fn` signatures with correct extractors:
133+
### 1. Framework-Agnostic Scaffolding (OpenAPI ➔ Rust)
134+
Stop manually writing repetitive handler signatures. `cdd-rust` reads your spec and generates strictly typed code based on the active `BackendStrategy`.
135+
* **Handler Scaffolding:** Transforms OpenAPI paths into `async fn` signatures with correct extractors (currently `ActixStrategy` defaults):
72136
* Path variables ➔ `web::Path<Uuid>`
73137
* Query strings ➔ `web::Query<Value>`
74138
* Request bodies ➔ `web::Json<T>`
75-
* **Route Registration:** Surgically injects `cfg.service(...)` calls into your `routes.rs` configuration using AST analysis, preserving existing logic.
76-
* **Non-Destructive Patching:** Uses [`ra_ap_syntax`](https://docs.rs/ra_ap_syntax/) (part of official [rust-lang/rust-analyzer](https://github.com/rust-lang/rust-analyzer)) to edit files safely. It respects your manual comments and formatting.
139+
* **Route Registration:** Surgically injects route configuration strings (e.g., `cfg.service(...)`) using AST analysis, preserving existing logic.
140+
* **Non-Destructive Patching:** Uses [`ra_ap_syntax`](https://docs.rs/ra_ap_syntax/) (official [rust-analyzer](https://github.com/rust-lang/rust-analyzer) parser) to edit files safely.
77141

78142
### 2. Source-of-Truth Reflection (Rust ➔ OpenAPI)
79143
Keep your documentation alive. Your Rust code *is* the spec.
80-
* **Database Sync:** Wraps `dsync` to generate Rust structs directly from your Postgres `schema.rs`.
81-
* **Attribute Injection:** Automatically parses structs and injects `#[derive(ToSchema)]` and `#[serde(...)]` attributes.
82-
* **Type Mapping:** Maps Rust types (`Uuid`, `chrono::NaiveDateTime`) back to OpenAPI formats automatically.
144+
* **Model Mapper Trait:** Extracts DB Schemas via `ModelMapper` (currently `DieselMapper` wrapping `dsync`) to generate/update Rust structs.
145+
* **Attribute Injection:** Automatically parses structs and injects `#[derive(ToSchema)]` and `#[serde(...)]` attributes to make models OpenAPI-compatible.
146+
* **Type Mapping:** Maps Rust types (`Uuid`, `chrono::NaiveDateTime`, `Decimal`) back to OpenAPI formats automatically using the `TypeMapper` module.
83147

84148
### 3. Contract Safety (`test-gen`)
85-
Ensure your implementation actually matches the spec.
149+
Ensure your implementation actually matches the spec using the same strategies used for code generation.
86150
* **Test Generation:** Generates `tests/api_contracts.rs` based on your `openapi.yaml`.
87-
* **Smart Mocking:** Automatically fills request parameters with valid dummy data (e.g., UUIDs for ID fields, ISO strings for Dates).
88-
* **Validation:** Verifies that your API responses align with the JSON Schema defined in your spec.
151+
* **Smart Mocking:** Automatically fills request parameters with valid dummy data based on type signatures.
152+
* **Validation:** Verifies that API responses align with the JSON Schema defined in your spec.
89153

90-
---
154+
---
91155

92156
## 📦 Command Usage
93157

94158
### 1. The Sync Pipeline
95159
**DB ➔ Rust Models ➔ OpenAPI Attributes**
96-
Synchronizes your database schema to your Rust structs and ensures they are ready for OpenAPI generation.
160+
Synchronizes your database schema to your Rust structs using the configured `ModelMapper`.
97161

98162
```bash
99-
cargo run -p cdd-cli -- sync \
100-
--schema-path web/src/schema.rs \
163+
cargo run -p cdd-cli -- sync \
164+
--schema-path web/src/schema.rs \
101165
--model-dir web/src/models
102166
```
103167

104168
### 2. The Test Pipeline
105169
**OpenAPI ➔ Integration Tests**
106-
Generates a test suite that treats your app as a black box.
170+
Generates a test suite via `BackendStrategy` (default: Actix) that treats your app as a black box.
107171

108172
```bash
109-
cargo run -p cdd-cli -- test-gen \
110-
--openapi-path docs/openapi.yaml \
111-
--output-path web/tests/api_contracts.rs \
173+
cargo run -p cdd-cli -- test-gen \
174+
--openapi-path docs/openapi.yaml \
175+
--output-path web/tests/api_contracts.rs \
112176
--app-factory crate::create_app
113177
```
114178

115-
---
179+
---
116180

117181
## 🛠 Project Structure
118182

119-
* **`core/`**: The engine. Contains AST parsers, OpenAPI parsers, and the diff/patch logic.
183+
* **`core/`**: The engine. Contains AST parsers, strategies, and the diff/patch logic.
184+
* `strategies.rs`: Defines `BackendStrategy` trait and `ActixStrategy`.
120185
* `patcher.rs`: Surgical code editing.
121-
* `handler_generator.rs`: Scaffolds Actix handlers.
122-
* `oas.rs`: Parses OpenAPI YAML.
123-
* **`cli/`**: The workflow runner. Use this to run `sync` or `test-gen`.
186+
* `oas.rs`: Parses OpenAPI YAML into Intermediate Representations.
187+
* `handlers/routes/contract_test`: Functional modules utilizing strategies to generate code.
188+
* **`cli/`**: The workflow runner. Wires up specific strategies to commands.
189+
* `generator.rs`: Defines `ModelMapper` and `DieselMapper`.
190+
* `main.rs`: Dependency injection root.
124191
* **`web/`**: Reference implementation. An Actix Web + Diesel project demonstrating the generated code in action.
125192

126193
## 🎨 Design Principles
127194

128195
* **No Magic Folders:** We generate code you can read, debug, and commit.
129196
* **Lossless Patching:** We edit your source files without breaking your style.
197+
* **Pluggable Backend:** Core logic is decoupled from specific frameworks (Actix, Axum, etc.).
130198
* **Type Safety:** `Uuid`, `chrono`, and `rust_decimal` are first-class citizens.
131-
* **100% Coverage:** The toolchain itself enforces strict documentation and test coverage.
132199

133-
---
200+
---
134201

135202
## Developer guide
136203

137204
Install the latest version of [Rust](https://www.rust-lang.org). We tend to use nightly versions. [CLI tool for installing Rust](https://rustup.rs).
138205

139206
We use [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy) linters to improve code quality.
140207

141-
There are plenty of [IDEs](https://areweideyet.com) and other [Rust development tools to consider](https://github.com/rust-unofficial/awesome-rust#development-tools).
142-
143208
### Step-by-step guide
144209

145210
```bash
146-
# Install Rust (nightly)
211+
# Install Rust (nightly)
147212
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly
148-
# Install cargo-make (cross-platform feature-rich reimplementation of Make)
213+
# Install cargo-make (cross-platform feature-rich reimplementation of Make)
149214
$ cargo install --force cargo-make
150-
# Install rustfmt (Rust formatter)
215+
# Install rustfmt (Rust formatter)
151216
$ rustup component add rustfmt
152217
# Clone this repo
153218
$ git clone https://github.com/offscale/cdd-rust && cd cdd-rust
@@ -165,9 +230,3 @@ Licensed under either of
165230
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
166231

167232
at your option.
168-
169-
### Contribution
170-
171-
Unless you explicitly state otherwise, any contribution intentionally submitted
172-
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
173-
dual licensed as above, without any additional terms or conditions.

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ cdd-core = { path = "../core" }
88
clap = { version = "4.4", features = ["derive"] }
99
dsync = "0.1.0"
1010
walkdir = "2.5.0"
11+
derive_more = { version = "^2.1", features = ["from", "display"] }
1112

1213
[dev-dependencies]
1314
tempfile = "3.8"

cli/src/error.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@
44
//!
55
//! Error types for the CLI crate.
66
7-
use derive_more::{Display, Error, From};
7+
use derive_more::{Display, From};
88

99
/// Main error enum for CLI operations.
10-
#[derive(Debug, Display, From, Error)]
10+
#[derive(Debug, Display, From)]
1111
pub enum CliError {
1212
/// IO Error wrapper.
13-
#[display(fmt = "IO Error: {}", _0)]
13+
#[display("IO Error: {}", _0)]
1414
Io(std::io::Error),
1515

1616
/// General failure message.
17-
#[display(fmt = "Operation failed: {}", _0)]
17+
#[display("Operation failed: {}", _0)]
1818
General(String),
1919
}
2020

21+
/// Manual implementation of the standard Error trait.
22+
///
23+
/// We implement this manually (instead of `derive(Error)`) because the `General(String)`
24+
/// variant contains a `String`, which does not implement `std::error::Error`, causing
25+
/// auto-derived `source()` implementations to fail compilation.
26+
impl std::error::Error for CliError {}
27+
2128
/// Result type alias.
2229
pub type CliResult<T> = Result<T, CliError>;

0 commit comments

Comments
 (0)