zod_rails 0.1.4
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/.github/workflows/ci.yml +32 -0
- data/.github/workflows/release.yml +33 -0
- data/LICENSE +21 -0
- data/README.md +282 -0
- data/Rakefile +12 -0
- data/ZOD_RAILS.md +1178 -0
- data/lib/tasks/zod_rails.rake +36 -0
- data/lib/zod_rails/configuration.rb +17 -0
- data/lib/zod_rails/generation/file_writer.rb +37 -0
- data/lib/zod_rails/generation/schema_builder.rb +89 -0
- data/lib/zod_rails/generation/typescript_emitter.rb +42 -0
- data/lib/zod_rails/generator.rb +39 -0
- data/lib/zod_rails/introspection/column_info.rb +39 -0
- data/lib/zod_rails/introspection/model_inspector.rb +34 -0
- data/lib/zod_rails/introspection/validation_info.rb +48 -0
- data/lib/zod_rails/mapping/enum_mapper.rb +31 -0
- data/lib/zod_rails/mapping/type_mapper.rb +46 -0
- data/lib/zod_rails/mapping/validation_mapper.rb +163 -0
- data/lib/zod_rails/railtie.rb +19 -0
- data/lib/zod_rails/version.rb +5 -0
- data/lib/zod_rails.rb +40 -0
- data/sig/zod_rails.rbs +4 -0
- data/zod_rails.png +0 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7f965cc7e0d0a036e1880f54359e0ffc173723d34016050d818adc7dc2fd3838
|
|
4
|
+
data.tar.gz: f8c5c18a640bc5820d98360b4964b3e6486ecaefc6395de1ae26c6090daa70b3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 80e23eafed4d17da2f08bf74b469210754c76bb5cfd917b745d68e6ff14dd609a48c59bb0dd6560017dd8e926548a6052e35e09879f03ffc1fb6ae4948a3be7d
|
|
7
|
+
data.tar.gz: 1fded1d84ceb8d517838e7a0cd697846fe4c1f2636852d270af80ec4e6e7c5534bb86461b0c011d83db87c8532e5302b8065a7a6d9a31acd0896cbc86caa4b6f
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [trunk]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [trunk]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Ruby ${{ matrix.ruby-version }}
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
ruby-version: ['3.2', '3.3', '3.4']
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: bundle exec rspec --format documentation
|
|
30
|
+
|
|
31
|
+
- name: Run linter
|
|
32
|
+
run: bundle exec rubocop --parallel
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
uses: ./.github/workflows/ci.yml
|
|
11
|
+
|
|
12
|
+
release:
|
|
13
|
+
needs: test
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
id-token: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Ruby
|
|
24
|
+
uses: ruby/setup-ruby@v1
|
|
25
|
+
with:
|
|
26
|
+
ruby-version: "3.3"
|
|
27
|
+
bundler-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Build gem
|
|
30
|
+
run: gem build *.gemspec
|
|
31
|
+
|
|
32
|
+
- name: Publish to RubyGems
|
|
33
|
+
uses: rubygems/release-gem@v1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ZodRails 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. 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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="zod_rails.png" alt="ZodRails" width="400">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/mathisto/zod_rails/actions/workflows/ci.yml"><img src="https://github.com/mathisto/zod_rails/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="https://rubygems.org/gems/zod_rails"><img src="https://img.shields.io/gem/v/zod_rails.svg?style=flat" alt="Gem Version"></a>
|
|
8
|
+
<a href="https://rubygems.org/gems/zod_rails"><img src="https://img.shields.io/gem/dt/zod_rails.svg?style=flat" alt="Downloads"></a>
|
|
9
|
+
<a href="https://github.com/mathisto/zod_rails/blob/trunk/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg" alt="Ruby 3.2+">
|
|
11
|
+
<img src="https://img.shields.io/badge/rails-%3E%3D%207.0-red.svg" alt="Rails 7.0+">
|
|
12
|
+
<img src="https://img.shields.io/badge/zod-4.x-3068b7.svg" alt="Zod 4">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<strong>Generate Zod schemas from ActiveRecord models</strong><br>
|
|
17
|
+
Bridge the gap between your Rails backend and TypeScript frontend with type-safe validation.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
Generate [Zod](https://zod.dev) schemas from your ActiveRecord models. Bridge the gap between your Rails backend and TypeScript frontend with type-safe validation that stays in sync with your database schema and model validations.
|
|
23
|
+
|
|
24
|
+
## Why ZodRails?
|
|
25
|
+
|
|
26
|
+
When building Rails APIs consumed by TypeScript frontends, you often duplicate validation logic: once in your Rails models, again in your frontend forms. ZodRails eliminates this duplication by generating Zod schemas directly from your ActiveRecord models, including:
|
|
27
|
+
|
|
28
|
+
- Database column types mapped to Zod types
|
|
29
|
+
- Rails validations (presence, length, numericality, format) mapped to Zod constraints
|
|
30
|
+
- Enums generated as `z.enum()` with proper string literals
|
|
31
|
+
- Nullable columns and default values handled correctly
|
|
32
|
+
- Separate schemas for API responses vs. form inputs
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Ruby 3.2+
|
|
37
|
+
- Rails 7.0+ (uses ActiveRecord and Railtie)
|
|
38
|
+
- Zod 4.x in your frontend project
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Add to your Gemfile:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
gem "zod_rails"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### 1. Configure the gem
|
|
57
|
+
|
|
58
|
+
Create an initializer at `config/initializers/zod_rails.rb`:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
ZodRails.configure do |config|
|
|
62
|
+
config.output_dir = Rails.root.join("app/javascript/schemas").to_s
|
|
63
|
+
config.models = %w[User Post Comment]
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Generate schemas
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bin/rails zod_rails:generate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Use in your frontend
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { UserSchema, UserInputSchema, type User } from "./schemas/user";
|
|
77
|
+
|
|
78
|
+
// Validate API response
|
|
79
|
+
const user = UserSchema.parse(apiResponse);
|
|
80
|
+
|
|
81
|
+
// Validate form input
|
|
82
|
+
const formData = UserInputSchema.parse(formValues);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configuration Options
|
|
86
|
+
|
|
87
|
+
| Option | Default | Description |
|
|
88
|
+
|--------|---------|-------------|
|
|
89
|
+
| `output_dir` | `app/javascript/schemas` | Directory for generated TypeScript files |
|
|
90
|
+
| `models` | `[]` | Array of model names to generate schemas for |
|
|
91
|
+
| `schema_suffix` | `Schema` | Suffix for response schemas (e.g., `UserSchema`) |
|
|
92
|
+
| `input_schema_suffix` | `InputSchema` | Suffix for input schemas (e.g., `UserInputSchema`) |
|
|
93
|
+
| `generate_input_schemas` | `true` | Whether to generate input schemas |
|
|
94
|
+
| `excluded_columns` | `["id", "created_at", "updated_at"]` | Columns to exclude from input schemas |
|
|
95
|
+
|
|
96
|
+
### Full Configuration Example
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
ZodRails.configure do |config|
|
|
100
|
+
config.output_dir = Rails.root.join("frontend/src/schemas").to_s
|
|
101
|
+
config.models = %w[User Post Comment Tag]
|
|
102
|
+
config.schema_suffix = "Schema"
|
|
103
|
+
config.input_schema_suffix = "FormSchema"
|
|
104
|
+
config.generate_input_schemas = true
|
|
105
|
+
config.excluded_columns = %w[id created_at updated_at deleted_at]
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Type Mappings
|
|
110
|
+
|
|
111
|
+
| Rails/DB Type | Zod Type |
|
|
112
|
+
|---------------|----------|
|
|
113
|
+
| `string`, `text` | `z.string()` |
|
|
114
|
+
| `integer`, `bigint` | `z.int()` |
|
|
115
|
+
| `float`, `decimal` | `z.number()` |
|
|
116
|
+
| `boolean` | `z.boolean()` |
|
|
117
|
+
| `date` | `z.iso.date()` |
|
|
118
|
+
| `datetime`, `timestamp` | `z.iso.datetime()` |
|
|
119
|
+
| `json`, `jsonb` | `z.json()` |
|
|
120
|
+
| `uuid` | `z.uuid()` |
|
|
121
|
+
| `enum` | `z.enum([...])` |
|
|
122
|
+
|
|
123
|
+
## Validation Mappings
|
|
124
|
+
|
|
125
|
+
ZodRails introspects your model validations and maps them to Zod constraints:
|
|
126
|
+
|
|
127
|
+
| Rails Validation | Zod Constraint |
|
|
128
|
+
|------------------|----------------|
|
|
129
|
+
| `presence: true` | `.min(1)` for strings |
|
|
130
|
+
| `length: { minimum: n }` | `.min(n)` |
|
|
131
|
+
| `length: { maximum: n }` | `.max(n)` |
|
|
132
|
+
| `length: { is: n }` | `.length(n)` |
|
|
133
|
+
| `numericality: { greater_than: n }` | `.gt(n)` |
|
|
134
|
+
| `numericality: { greater_than_or_equal_to: n }` | `.gte(n)` |
|
|
135
|
+
| `numericality: { less_than: n }` | `.lt(n)` |
|
|
136
|
+
| `numericality: { less_than_or_equal_to: n }` | `.lte(n)` |
|
|
137
|
+
| `format: { with: /regex/ }` | `.regex(/regex/)` |
|
|
138
|
+
| `inclusion: { in: n..m }` | `.min(n).max(m)` |
|
|
139
|
+
|
|
140
|
+
## Generated Output Example
|
|
141
|
+
|
|
142
|
+
Given this Rails model:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class User < ApplicationRecord
|
|
146
|
+
enum :role, { member: 0, admin: 1, moderator: 2 }
|
|
147
|
+
|
|
148
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
149
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
150
|
+
validates :age, numericality: { greater_than: 0, less_than: 150 }, allow_nil: true
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
ZodRails generates:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { z } from "zod";
|
|
158
|
+
|
|
159
|
+
export const UserSchema = z.object({
|
|
160
|
+
id: z.int(),
|
|
161
|
+
email: z.string().min(1).regex(/\A[^@\s]+@[^@\s]+\z/),
|
|
162
|
+
name: z.string().min(2).max(100),
|
|
163
|
+
age: z.int().gt(0).lt(150).nullable(),
|
|
164
|
+
role: z.enum(["member", "admin", "moderator"]),
|
|
165
|
+
created_at: z.iso.datetime(),
|
|
166
|
+
updated_at: z.iso.datetime()
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export type User = z.infer<typeof UserSchema>;
|
|
170
|
+
|
|
171
|
+
export const UserInputSchema = z.object({
|
|
172
|
+
email: z.string().min(1).regex(/\A[^@\s]+@[^@\s]+\z/),
|
|
173
|
+
name: z.string().min(2).max(100),
|
|
174
|
+
age: z.int().gt(0).lt(150).nullish(),
|
|
175
|
+
role: z.enum(["member", "admin", "moderator"]).optional()
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export type UserInput = z.infer<typeof UserInputSchema>;
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Schema vs InputSchema
|
|
182
|
+
|
|
183
|
+
ZodRails generates two schema variants:
|
|
184
|
+
|
|
185
|
+
**Schema** (e.g., `UserSchema`)
|
|
186
|
+
- Represents data as returned from your API
|
|
187
|
+
- Includes all columns (`id`, timestamps, etc.)
|
|
188
|
+
- Uses `.nullable()` for nullable columns
|
|
189
|
+
|
|
190
|
+
**InputSchema** (e.g., `UserInputSchema`)
|
|
191
|
+
- Represents data for form submission
|
|
192
|
+
- Excludes configured columns (defaults: `id`, `created_at`, `updated_at`)
|
|
193
|
+
- Uses `.optional()` for columns with database defaults
|
|
194
|
+
- Uses `.nullish()` for nullable columns without defaults
|
|
195
|
+
|
|
196
|
+
## Integrating with Forms
|
|
197
|
+
|
|
198
|
+
ZodRails pairs well with form libraries that support Zod:
|
|
199
|
+
|
|
200
|
+
### React Hook Form
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { useForm } from "react-hook-form";
|
|
204
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
205
|
+
import { UserInputSchema, type UserInput } from "./schemas/user";
|
|
206
|
+
|
|
207
|
+
function UserForm() {
|
|
208
|
+
const { register, handleSubmit, formState: { errors } } = useForm<UserInput>({
|
|
209
|
+
resolver: zodResolver(UserInputSchema)
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const onSubmit = (data: UserInput) => {
|
|
213
|
+
// data is fully typed and validated
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Validating API Responses
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { UserSchema, type User } from "./schemas/user";
|
|
224
|
+
|
|
225
|
+
async function fetchUser(id: number): Promise<User> {
|
|
226
|
+
const response = await fetch(`/api/users/${id}`);
|
|
227
|
+
const data = await response.json();
|
|
228
|
+
return UserSchema.parse(data); // Throws if invalid
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## CI/CD Integration
|
|
233
|
+
|
|
234
|
+
Add schema generation to your build process to catch type mismatches early:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# .github/workflows/ci.yml
|
|
238
|
+
- name: Generate Zod schemas
|
|
239
|
+
run: bin/rails zod_rails:generate
|
|
240
|
+
|
|
241
|
+
- name: Check for uncommitted schema changes
|
|
242
|
+
run: git diff --exit-code app/javascript/schemas/
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Troubleshooting
|
|
246
|
+
|
|
247
|
+
### Schemas not updating after model changes
|
|
248
|
+
|
|
249
|
+
Re-run the generator after any model changes:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
bin/rails zod_rails:generate
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Validation constraints not appearing
|
|
256
|
+
|
|
257
|
+
Ensure validations are defined on the model class, not in concerns that might not be loaded. Conditional validations (`:if`, `:unless`) are detected and excluded by default.
|
|
258
|
+
|
|
259
|
+
### Custom column types
|
|
260
|
+
|
|
261
|
+
For custom types not in the mapping table, ZodRails falls back to `z.unknown()`. Open an issue if you need support for additional types.
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
After checking out the repo:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
bundle install
|
|
269
|
+
bundle exec rspec
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Contributing
|
|
273
|
+
|
|
274
|
+
1. Fork it
|
|
275
|
+
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
276
|
+
3. Commit your changes (`git commit -am 'Add my feature'`)
|
|
277
|
+
4. Push to the branch (`git push origin feature/my-feature`)
|
|
278
|
+
5. Create a Pull Request
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT License. See [LICENSE](LICENSE) for details.
|