|
1 | | -# Architecture |
| 1 | +# cdd-python-client Architecture |
2 | 2 |
|
3 | | -The `cdd-python-client` is designed around a strictly modular, bidirectional Code-to-Spec and Spec-to-Code architecture, heavily utilizing Abstract Syntax Trees (AST). |
| 3 | +<!-- BADGES_START --> |
| 4 | +<!-- Replace these placeholders with your repository-specific badges --> |
| 5 | +[](https://opensource.org/licenses/Apache-2.0) |
| 6 | +[](https://github.com/offscale/cdd-python-client/actions) |
| 7 | +[](https://codecov.io/gh/offscale/cdd-python-client) |
| 8 | +<!-- BADGES_END --> |
4 | 9 |
|
5 | | -## Core Concepts |
| 10 | +The **cdd-python-client** tool acts as a dedicated compiler and transpiler. Its fundamental architecture follows standard compiler design principles, divided into three distinct phases: **Frontend (Parsing)**, **Intermediate Representation (IR)**, and **Backend (Emitting)**. |
6 | 11 |
|
7 | | -1. **Contract-Driven Development (CDD)**: The source of truth can be dynamically negotiated. You can start with an OpenAPI specification (`openapi.json`) or you can start with Python code (FastAPI mocks, Pydantic models, or pytest tests). |
8 | | -2. **Abstract Syntax Trees (AST)**: Instead of regex or simple templating, this tool utilizes `libcst` (Concrete Syntax Tree) to read and write Python code. This preserves comments, whitespace, and structural integrity. |
9 | | -3. **Pydantic Models**: The OpenAPI 3.2.0 specification is modeled heavily using Pydantic (`models.py`). This guarantees in-memory type safety and validation when parsing or emitting specifications. |
| 12 | +This decoupled design ensures that any format capable of being parsed into the IR can subsequently be emitted into any supported output format, whether that is a server-side route, a client-side SDK, a database ORM, or an OpenAPI specification. |
10 | 13 |
|
11 | | -## Directory Structure |
| 14 | +## 🏗 High-Level Overview |
12 | 15 |
|
13 | | -The repository is modularized by domain, ensuring that parsing and emitting logic for specific Python/OpenAPI concepts remain isolated: |
| 16 | +```mermaid |
| 17 | +graph TD |
| 18 | + %% Styling Definitions |
| 19 | + classDef frontend fill:#57caff,stroke:#4285f4,stroke-width:2px,color:#20344b,font-family:Roboto Mono |
| 20 | + classDef core fill:#ffd427,stroke:#f9ab00,stroke-width:3px,color:#20344b,font-family:Google Sans,font-weight:bold |
| 21 | + classDef backend fill:#5cdb6d,stroke:#34a853,stroke-width:2px,color:#20344b,font-family:Roboto Mono |
| 22 | + classDef endpoint fill:#ffffff,stroke:#20344b,stroke-width:1px,color:#20344b,font-family:Google Sans |
14 | 23 |
|
| 24 | + subgraph Frontend [Parsers] |
| 25 | + A[OpenAPI .yaml/.json]:::endpoint --> P1(OpenAPI Parser):::frontend |
| 26 | + B[`Python` Models / Source]:::endpoint --> P2(`Python` Parser):::frontend |
| 27 | + C[Server Routes / Frameworks]:::endpoint --> P3(Framework Parser):::frontend |
| 28 | + D[Client SDKs / ORMs]:::endpoint --> P4(Ext Parser):::frontend |
| 29 | + end |
| 30 | +
|
| 31 | + subgraph Core [Intermediate Representation] |
| 32 | + IR((CDD IR)):::core |
| 33 | + end |
| 34 | +
|
| 35 | + subgraph Backend [Emitters] |
| 36 | + E1(OpenAPI Emitter):::backend --> X[OpenAPI .yaml/.json]:::endpoint |
| 37 | + E2(`Python` Emitter):::backend --> Y[`Python` Models / Structs]:::endpoint |
| 38 | + E3(Server Emitter):::backend --> Z[Server Routes / Controllers]:::endpoint |
| 39 | + E4(Client Emitter):::backend --> W[Client SDKs / API Calls]:::endpoint |
| 40 | + E5(Data Emitter):::backend --> V[ORM Models / CLI Parsers]:::endpoint |
| 41 | + end |
| 42 | +
|
| 43 | + P1 --> IR |
| 44 | + P2 --> IR |
| 45 | + P3 --> IR |
| 46 | + P4 --> IR |
| 47 | +
|
| 48 | + IR --> E1 |
| 49 | + IR --> E2 |
| 50 | + IR --> E3 |
| 51 | + IR --> E4 |
| 52 | + IR --> E5 |
15 | 53 | ``` |
16 | | -src/openapi_client/ |
17 | | -├── models.py # Single Source of Truth for OpenAPI 3.2.0 specs |
18 | | -├── cli.py # CLI entrypoints and unified sync workflows |
19 | | -├── openapi/ # JSON/Dict Serialization |
20 | | -│ ├── parse.py |
21 | | -│ └── emit.py |
22 | | -├── classes/ # Pydantic Models <-> OpenAPI Components/Schemas |
23 | | -│ ├── parse.py # Extracts JSON schemas from Python classes |
24 | | -│ └── emit.py # Emits Pydantic models from JSON schemas |
25 | | -├── functions/ # Python Methods <-> OpenAPI Operations |
26 | | -│ ├── parse.py |
27 | | -│ └── emit.py |
28 | | -├── docstrings/ # Python Docstrings <-> OpenAPI Summary/Description |
29 | | -│ ├── parse.py |
30 | | -│ └── emit.py |
31 | | -├── routes/ # Client generation (combining classes + functions) |
32 | | -│ ├── parse.py |
33 | | -│ └── emit.py |
34 | | -├── mocks/ # FastAPI Mocks <-> OpenAPI Paths/Operations |
35 | | -│ ├── parse.py |
36 | | -│ └── emit.py |
37 | | -└── tests/ # Pytest functions <-> OpenAPI Examples/Validation |
38 | | - ├── parse.py |
39 | | - └── emit.py |
40 | | -``` |
41 | 54 |
|
42 | | -## The Sync Lifecycle (`cdd sync --dir`) |
| 55 | +## 🧩 Core Components |
| 56 | + |
| 57 | +### 1. The Frontend (Parsers) |
| 58 | + |
| 59 | +The Frontend's responsibility is to read an input source and translate it into the universal CDD Intermediate Representation (IR). |
| 60 | + |
| 61 | +* **Static Analysis (AST-Driven)**: For `Python` source code, the tool **does not** use dynamic reflection or execute the code. Instead, it reads the source files, generates an Abstract Syntax Tree (AST), and navigates the tree to extract classes, structs, functions, type signatures, API client definitions, server routes (like FastAPI mocks), and docstrings. |
| 62 | +* **OpenAPI Parsing**: For OpenAPI and JSON Schema inputs, the parser normalizes the structure, resolving internal `$ref`s and extracting properties, endpoints (client or server perspectives), and metadata into the IR. |
| 63 | + |
| 64 | +### 2. Intermediate Representation (IR) |
| 65 | + |
| 66 | +The Intermediate Representation is the crucial "glue" of the architecture. It is a normalized, language-agnostic data structure that represents concepts like: |
| 67 | +* **Models**: Entities containing typed properties, required fields, defaults, and descriptions. |
| 68 | +* **Endpoints / Operations**: HTTP verbs, paths, path/query/body parameters, and responses. In the IR, an operation is an abstract concept that can represent *either* a Server Route receiving a request *or* an API Client dispatching a request. |
| 69 | +* **Metadata**: Tooling hints, docstrings, and validations. |
| 70 | + |
| 71 | +By standardizing on a single IR (heavily inspired by OpenAPI / JSON Schema primitives), the system guarantees that parsing logic and emitting logic remain completely decoupled. |
| 72 | + |
| 73 | +### 3. The Backend (Emitters) |
| 74 | + |
| 75 | +The Backend's responsibility is to take the universal IR and generate valid target output. Emitters can be written to support various environments (e.g., Client vs Server, Web vs CLI). |
| 76 | + |
| 77 | +* **Code Generation**: Emitters iterate over the IR and generate idiomatic `Python` source code. |
| 78 | + * A **Server Emitter** creates routing controllers and request-validation logic (for example, generating local test-mock servers). |
| 79 | + * A **Client Emitter** creates API wrappers, fetch functions, and response-parsing logic. |
| 80 | +* **Database & CLI Generation**: Emitters can also target ORM models or command-line parsers by mapping IR properties to database columns or CLI arguments. |
| 81 | +* **Specification Generation**: Emitters translating back to OpenAPI serialize the IR into standard OpenAPI 3.x JSON or YAML, rigorously formatting descriptions, type constraints, and endpoint schemas based on what was parsed from the source code. |
| 82 | + |
| 83 | +## 🔄 Extensibility |
| 84 | + |
| 85 | +Because of the IR-centric design, adding support for a new `Python` framework (e.g., a new Client library, Web framework, or ORM) requires minimal effort: |
| 86 | +1. **To support parsing a new framework**: Write a parser that converts the framework's AST/DSL into the CDD IR. Once written, the framework can automatically be exported to OpenAPI, Client SDKs, CLI parsers, or any other existing output target. |
| 87 | +2. **To support emitting a new framework**: Write an emitter that converts the CDD IR into the framework's DSL/AST. Once written, the framework can automatically be generated from OpenAPI or any other supported input. |
| 88 | + |
| 89 | +## 🛡 Design Principles |
43 | 90 |
|
44 | | -When using the unified sync workflow, the system performs a bidirectional merge: |
45 | | -1. **Parse**: `libcst` Visitors traverse `client.py`, `mock_server.py`, and `test_client.py`. Any discovered routes, Pydantic models, and docstrings are extracted and merged into the in-memory OpenAPI Pydantic model. |
46 | | -2. **Merge**: If an `openapi.json` exists, it is parsed and used as the baseline. Code-extracted definitions overlay or expand the JSON definitions. |
47 | | -3. **Emit**: The fully reconciled OpenAPI model is serialized back to `openapi.json`. Then, `libcst` generators rebuild `client.py`, `mock_server.py`, and `test_client.py`, ensuring 100% synchronization across the client, backend mocks, tests, and documentation. |
| 91 | +1. **A Single Source of Truth**: Developers should be able to maintain their definitions in whichever format is most ergonomic for their team (OpenAPI files, Native Code, Client libraries, ORM models) and generate the rest. |
| 92 | +2. **Zero-Execution Parsing**: Ensure security and resilience by strictly statically analyzing inputs. The compiler must never need to run the target code to understand its structure. |
| 93 | +3. **Lossless Conversion**: Maximize the retention of metadata (e.g., type annotations, docstrings, default values, validators) during the transition `Source -> IR -> Target`. |
| 94 | +4. **Symmetric Operations**: An Endpoint in the IR holds all the information necessary to generate both the Server-side controller that fulfills it, and the Client-side SDK method that calls it. |
0 commit comments