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 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.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]