domainstack-cli 1.0.1

CLI to generate TypeScript/Zod, JSON Schema, and GraphQL from Rust validation rules. Single source of truth for frontend + backend.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# domainstack-cli

[![Blackwell Systems™](https://raw.githubusercontent.com/blackwell-systems/blackwell-docs-theme/main/badge-trademark.svg)](https://github.com/blackwell-systems)
[![Crates.io](https://img.shields.io/crates/v/domainstack-cli.svg)](https://crates.io/crates/domainstack-cli)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg)](https://github.com/blackwell-systems/domainstack/blob/main/LICENSE-MIT)

Code generation CLI for the [domainstack](https://crates.io/crates/domainstack) full-stack validation ecosystem. Generate TypeScript/Zod schemas, JSON Schema, and OpenAPI specs from your Rust `#[validate(...)]` attributes.

## Overview

`domainstack-cli` is a command-line tool that transforms Rust types annotated with domainstack validation rules into equivalent schemas for other languages and frameworks. This ensures your validation logic stays synchronized across your entire stack.

```bash
# Single source of truth in Rust
#[derive(Validate)]
struct User {
    #[validate(email)]
    #[validate(max_len = 255)]
    email: String,

    #[validate(range(min = 18, max = 120))]
    age: u8,
}

# Generate TypeScript/Zod schemas
domainstack zod --input src --output frontend/schemas.ts

# Generate JSON Schema (Draft 2020-12)
domainstack json-schema --input src --output schemas/types.json

# Generate OpenAPI 3.0/3.1 specification
domainstack openapi --input src --output api/openapi.json
```

## Installation

### From crates.io (when published)

```bash
cargo install domainstack-cli
```

### From source

```bash
# Clone the repository
git clone https://github.com/blackwell-systems/domainstack
cd domainstack/domainstack/domainstack-cli

# Build and install
cargo install --path .
```

### Verify installation

```bash
domainstack --version
```

## Quick Start

### 1. Define your Rust types with validation rules

```rust
// src/models.rs
use domainstack::Validate;

#[derive(Validate)]
struct User {
    #[validate(email)]
    #[validate(max_len = 255)]
    email: String,

    #[validate(length(min = 3, max = 50))]
    #[validate(alphanumeric)]
    username: String,

    #[validate(range(min = 18, max = 120))]
    age: u8,

    #[validate(url)]
    profile_url: Option<String>,
}
```

### 2. Generate Zod schemas

```bash
domainstack zod --input src --output frontend/src/schemas.ts
```

### 3. Use the generated schemas in TypeScript

```typescript
// frontend/src/schemas.ts (auto-generated)
import { z } from "zod";

export const UserSchema = z.object({
  email: z.string().email().max(255),
  username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9]*$/),
  age: z.number().min(18).max(120),
  profile_url: z.string().url().optional(),
});

export type User = z.infer<typeof UserSchema>;

// Use it in your application
const result = UserSchema.safeParse(formData);
if (result.success) {
  // Type-safe validated data
  const user: User = result.data;
}
```

## Commands

### `domainstack zod`

Generate Zod validation schemas from Rust types.

```bash
domainstack zod [OPTIONS]
```

**Options:**

- `-i, --input <PATH>` - Input directory containing Rust source files (default: `src`)
- `-o, --output <PATH>` - Output TypeScript file (required)
- `-w, --watch` - Watch for changes and regenerate automatically
- `-v, --verbose` - Enable verbose output
- `-h, --help` - Print help information

**Examples:**

```bash
# Basic usage
domainstack zod --output schemas.ts

# Specify input directory
domainstack zod --input backend/src --output frontend/schemas.ts

# Verbose output
domainstack zod -i src -o schemas.ts -v

# Watch mode for development
domainstack zod -i src -o schemas.ts --watch
```

### `domainstack json-schema`

Generate JSON Schema (Draft 2020-12) from Rust types.

```bash
domainstack json-schema [OPTIONS]
```

**Options:**

- `-i, --input <PATH>` - Input directory containing Rust source files (default: `src`)
- `-o, --output <PATH>` - Output JSON file (required)
- `-w, --watch` - Watch for changes and regenerate automatically
- `-v, --verbose` - Enable verbose output
- `-h, --help` - Print help information

**Examples:**

```bash
# Basic usage
domainstack json-schema --output schema.json

# Specify input directory
domainstack json-schema --input backend/src --output api/schemas.json

# Verbose with watch mode
domainstack json-schema -i src -o schema.json -v --watch
```

**Generated output:**

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "User": {
      "type": "object",
      "properties": {
        "email": { "type": "string", "format": "email", "maxLength": 255 },
        "age": { "type": "integer", "minimum": 18, "maximum": 120 }
      },
      "required": ["email", "age"],
      "additionalProperties": false
    }
  }
}
```

### `domainstack openapi`

Generate OpenAPI 3.0/3.1 specification from Rust types.

```bash
domainstack openapi [OPTIONS]
```

**Options:**

- `-i, --input <PATH>` - Input directory containing Rust source files (default: `src`)
- `-o, --output <PATH>` - Output JSON file (required)
- `--openapi-31` - Use OpenAPI 3.1 (default is 3.0)
- `-w, --watch` - Watch for changes and regenerate automatically
- `-v, --verbose` - Enable verbose output
- `-h, --help` - Print help information

**Examples:**

```bash
# Generate OpenAPI 3.0 spec
domainstack openapi --output openapi.json

# Generate OpenAPI 3.1 spec
domainstack openapi --output openapi.json --openapi-31

# Watch mode
domainstack openapi -i src -o openapi.json --watch
```

**Generated output:**

```json
{
  "openapi": "3.0.3",
  "info": { "title": "Generated API Schema", "version": "1.0.0" },
  "paths": {},
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "email": { "type": "string", "format": "email", "maxLength": 255 },
          "age": { "type": "integer", "minimum": 18, "maximum": 120 }
        },
        "required": ["email", "age"],
        "additionalProperties": false
      }
    }
  }
}
```

## Supported Validation Rules

### String Validations

| Rust Attribute | Zod Output | Description |
|---------------|------------|-------------|
| `#[validate(email)]` | `.email()` | Valid email address |
| `#[validate(url)]` | `.url()` | Valid URL |
| `#[validate(min_len = N)]` | `.min(N)` | Minimum string length |
| `#[validate(max_len = N)]` | `.max(N)` | Maximum string length |
| `#[validate(length(min = N, max = M))]` | `.min(N).max(M)` | String length range |
| `#[validate(non_empty)]` | `.min(1)` | Non-empty string |
| `#[validate(non_blank)]` | `.trim().min(1)` | Non-blank string (after trim) |
| `#[validate(alphanumeric)]` | `.regex(/^[a-zA-Z0-9]*$/)` | Alphanumeric only |
| `#[validate(alpha_only)]` | `.regex(/^[a-zA-Z]*$/)` | Letters only |
| `#[validate(numeric_string)]` | `.regex(/^[0-9]*$/)` | Digits only |
| `#[validate(ascii)]` | `.regex(/^[\x00-\x7F]*$/)` | ASCII characters only |
| `#[validate(starts_with = "prefix")]` | `.startsWith("prefix")` | Must start with prefix |
| `#[validate(ends_with = "suffix")]` | `.endsWith("suffix")` | Must end with suffix |
| `#[validate(contains = "substring")]` | `.includes("substring")` | Must contain substring |
| `#[validate(matches_regex = "pattern")]` | `.regex(/pattern/)` | Custom regex pattern |
| `#[validate(no_whitespace)]` | `.regex(/^\S*$/)` | No whitespace allowed |

### Numeric Validations

| Rust Attribute | Zod Output | Description |
|---------------|------------|-------------|
| `#[validate(range(min = N, max = M))]` | `.min(N).max(M)` | Numeric range |
| `#[validate(min = N)]` | `.min(N)` | Minimum value |
| `#[validate(max = N)]` | `.max(N)` | Maximum value |
| `#[validate(positive)]` | `.positive()` | Must be positive (> 0) |
| `#[validate(negative)]` | `.negative()` | Must be negative (< 0) |
| `#[validate(non_zero)]` | `.refine(n => n !== 0, ...)` | Cannot be zero |
| `#[validate(multiple_of = N)]` | `.multipleOf(N)` | Must be multiple of N |
| `#[validate(finite)]` | `.finite()` | Must be finite (not NaN/Infinity) |

### Type Mappings

| Rust Type | Zod Type | Notes |
|-----------|----------|-------|
| `String` | `z.string()` | |
| `bool` | `z.boolean()` | |
| `u8, u16, u32, i8, i16, i32, f32, f64` | `z.number()` | |
| `u64, u128, i64, i128` | `z.number()` | With precision warning comment |
| `Option<T>` | `T.optional()` | Validations applied to inner type |
| `Vec<T>` | `z.array(T)` | |
| Custom types | `CustomTypeSchema` | References generated schema |

## Examples

### Multiple Validation Rules

You can apply multiple validation rules to a single field:

```rust
#[derive(Validate)]
struct Account {
    #[validate(email)]
    #[validate(max_len = 255)]
    #[validate(non_empty)]
    email: String,

    #[validate(min_len = 8)]
    #[validate(max_len = 128)]
    #[validate(matches_regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])")]
    password: String,
}
```

Generates:

```typescript
export const AccountSchema = z.object({
  email: z.string().email().max(255).min(1),
  password: z.string().min(8).max(128).regex(/(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])/),
});
```

### Optional Fields with Validations

Optional fields have `.optional()` applied AFTER all validations:

```rust
#[derive(Validate)]
struct Profile {
    #[validate(url)]
    website: Option<String>,

    #[validate(length(min = 10, max = 500))]
    bio: Option<String>,
}
```

Generates:

```typescript
export const ProfileSchema = z.object({
  website: z.string().url().optional(),  // Correct order
  bio: z.string().min(10).max(500).optional(),
});
```

### Arrays and Collections

```rust
#[derive(Validate)]
struct Post {
    tags: Vec<String>,

    #[validate(min = 1)]
    #[validate(max = 100)]
    scores: Vec<u8>,
}
```

Generates:

```typescript
export const PostSchema = z.object({
  tags: z.array(z.string()),
  scores: z.array(z.number()).min(1).max(100),
});
```

## Architecture

### Unified CLI Design

`domainstack-cli` is designed as a **unified code generation tool** with a single binary and multiple subcommands:

```
domainstack
├── zod          ✅ Generate Zod schemas
├── json-schema  ✅ Generate JSON Schema (Draft 2020-12)
├── openapi      ✅ Generate OpenAPI 3.0/3.1 specification
├── yup          📋 Generate Yup schemas (planned)
├── graphql      📋 Generate GraphQL schemas (planned)
└── prisma       📋 Generate Prisma schemas (planned)
```

**Benefits:**
- Single installation, multiple generators
- Shared parsing infrastructure (efficient, consistent)
- Consistent CLI interface across all generators
- Easy to add new generators

### Internal Structure

```
domainstack-cli/
├── src/
│   ├── main.rs              # CLI entry point with clap
│   ├── commands/            # Subcommand implementations
│   │   ├── zod.rs
│   │   ├── json_schema.rs
│   │   └── openapi.rs
│   ├── parser/              # Shared parsing infrastructure
│   │   ├── mod.rs           # Directory walking
│   │   ├── ast.rs           # Rust AST parsing
│   │   └── validation.rs    # Validation rule extraction
│   └── generators/          # Language-specific generators
│       ├── zod.rs
│       ├── json_schema.rs
│       └── openapi.rs
```

The parser module (`parser/`) is shared across all generators, ensuring consistent interpretation of Rust validation rules. Each generator (`generators/`) contains language-specific transformation logic.

## Future Generators

The roadmap includes support for additional generators:

### Yup (TypeScript validation)
```bash
domainstack yup --input src --output schemas.ts
```

### GraphQL Schema Definition Language
```bash
domainstack graphql --input src --output schema.graphql
```

### Prisma Schema
```bash
domainstack prisma --input src --output schema.prisma
```

## Documentation

- **[JSON_SCHEMA.md]../domainstack/docs/JSON_SCHEMA.md** - Complete JSON Schema generation guide
- **[examples/json_schema_demo.rs]./examples/json_schema_demo.rs** - Example types demonstrating tuple structs, enums, and nested validation

## Contributing

### Adding a New Generator

To add a new generator (e.g., Yup):

1. Create generator file: `src/generators/yup.rs`
2. Implement generation logic using shared parser types
3. Create command file: `src/commands/yup.rs`
4. Add subcommand to `src/main.rs`

The shared parser infrastructure (`parser::ParsedType`, `parser::ValidationRule`) makes adding new generators straightforward - focus only on the output format transformation.

### Development Setup

```bash
# Clone repository
git clone https://github.com/blackwell-systems/domainstack
cd domainstack

# Build CLI
cargo build -p domainstack-cli

# Run tests
cargo test -p domainstack-cli

# Install locally
cargo install --path domainstack/domainstack-cli
```

## Troubleshooting

### "No types found with validation rules"

Make sure your Rust types:
1. Have `#[derive(Validate)]` attribute
2. Contain at least one `#[validate(...)]` attribute
3. Are in `.rs` files within the input directory

### Generated schemas don't match expectations

Run with `--verbose` flag to see parsing details:

```bash
domainstack zod --input src --output schemas.ts --verbose
```

### Large integer precision warnings

JavaScript numbers cannot safely represent integers larger than `Number.MAX_SAFE_INTEGER` (2^53 - 1). Types like `u64`, `i64`, `u128`, `i128` will generate schemas with inline warning comments:

```typescript
big_number: z.number() /* Warning: Large integers may lose precision in JavaScript */
```

Consider using strings for large integers in your TypeScript schemas if precision is critical.

## License

This project is part of the domainstack workspace. See the root LICENSE file for details.

## Related Projects

- [domainstack]../domainstack/ - Core validation library
- [zod]https://github.com/colinhacks/zod - TypeScript-first schema validation
- [ts-rs]https://github.com/Aleph-Alpha/ts-rs - TypeScript type generation (no validation)
- [typeshare]https://github.com/1Password/typeshare - Cross-language type sharing