zod_rails 0.2.0 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +19 -11
- data/lib/zod_rails/generation/file_writer.rb +3 -2
- data/lib/zod_rails/generation/schema_builder.rb +42 -20
- data/lib/zod_rails/generation/typescript_emitter.rb +5 -5
- data/lib/zod_rails/generator.rb +37 -13
- data/lib/zod_rails/introspection/column_info.rb +2 -1
- data/lib/zod_rails/mapping/enum_mapper.rb +5 -8
- data/lib/zod_rails/mapping/regexp_mapper.rb +36 -0
- data/lib/zod_rails/mapping/type_mapper.rb +2 -1
- data/lib/zod_rails/mapping/validation_mapper.rb +39 -39
- data/lib/zod_rails/model_resolver.rb +27 -3
- data/lib/zod_rails/version.rb +1 -1
- data/lib/zod_rails.rb +3 -0
- data/sig/zod_rails.rbs +35 -1
- metadata +45 -8
- data/.github/workflows/ci.yml +0 -32
- data/.github/workflows/release.yml +0 -33
- data/Rakefile +0 -12
- data/lib/tasks/zod_rails.rake +0 -93
- data/zod_rails.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8e1e956f2954b2200c39a4b467529911806337883bd2674b31b7a271ef560a8
|
|
4
|
+
data.tar.gz: 681139d70905f6c34ee190f629f437e015a2eb1313a76940d42b0eaf6a8ffabe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30a4ce819ee99ac5bcdd83e482c16cd2029136af74e6e64c04a03960c8640991cef144eab6562eb431a0205c655c857687e78f023ffe66aaa9d8c53a953a7b1e
|
|
7
|
+
data.tar.gz: 7744055379a755592beadb1010992deeba5ef0fac4d0ea5719bd67d98191264d9671d0a4ee6806e2c62a7e97e0a69997d0bd8f80eca7ed6002c52d59ae7c236c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ZodRails are documented here.
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.3.0 - 2026-08-03
|
|
8
|
+
|
|
9
|
+
### Upgrade Notes
|
|
10
|
+
|
|
11
|
+
- Regenerate committed schemas after upgrading. Presence, range, regular-expression, and datetime output has changed
|
|
12
|
+
to more closely match Rails behavior.
|
|
13
|
+
- `schema_suffix`, `input_schema_suffix`, and `generate_input_schemas` now take effect. Applications that configured
|
|
14
|
+
these previously ignored options will see the configured output for the first time.
|
|
15
|
+
- Configured model names must resolve to concrete ActiveRecord models; nonmodels and abstract models now produce a
|
|
16
|
+
targeted configuration error.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Honor custom schema suffixes and `generate_input_schemas`.
|
|
21
|
+
- Generate safe TypeScript for unusual column names, enum values, and regular expressions.
|
|
22
|
+
- Preserve negative, zero, exclusive, beginless, and endless numeric range bounds.
|
|
23
|
+
- Match Rails presence semantics for whitespace-only strings and required nullable columns.
|
|
24
|
+
- Accept ISO datetimes with timezone offsets and detect database expression defaults.
|
|
25
|
+
- Keep preserved custom blocks from causing false drift reports.
|
|
26
|
+
- Detect output filename collisions before writing files.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Declare ActiveRecord and Railties as runtime dependencies.
|
|
31
|
+
- Reject configured constants that are not concrete ActiveRecord models.
|
|
32
|
+
- Skip dynamic numericality constraints and Ruby extended-mode regular expressions instead of emitting invalid TypeScript.
|
|
33
|
+
|
|
34
|
+
## 0.2.0
|
|
35
|
+
|
|
36
|
+
- Added response and input schema generation, enums, validation mapping, namespaced models, drift detection,
|
|
37
|
+
custom block preservation, dry runs, and formatter integration.
|
data/README.md
CHANGED
|
@@ -118,7 +118,7 @@ end
|
|
|
118
118
|
| `decimal` | `z.string()` (preserves `BigDecimal` precision) |
|
|
119
119
|
| `boolean` | `z.boolean()` |
|
|
120
120
|
| `date` | `z.iso.date()` |
|
|
121
|
-
| `datetime`, `timestamp` | `z.iso.datetime()` |
|
|
121
|
+
| `datetime`, `timestamp` | `z.iso.datetime({ offset: true })` |
|
|
122
122
|
| `json`, `jsonb` | `z.json()` |
|
|
123
123
|
| `uuid` | `z.uuid()` |
|
|
124
124
|
| `time` | `z.string()` |
|
|
@@ -131,7 +131,7 @@ ZodRails introspects your model validations and maps them to Zod constraints:
|
|
|
131
131
|
|
|
132
132
|
| Rails Validation | Zod Constraint |
|
|
133
133
|
|------------------|----------------|
|
|
134
|
-
| `presence: true` | `.min(1)` for string/text columns |
|
|
134
|
+
| `presence: true` | `.min(1).refine(...)` for string/text columns, including whitespace-only rejection |
|
|
135
135
|
| `length: { minimum: n }` | `.min(n)` |
|
|
136
136
|
| `length: { maximum: n }` | `.max(n)` |
|
|
137
137
|
| `length: { is: n }` | `.length(n)` |
|
|
@@ -139,8 +139,8 @@ ZodRails introspects your model validations and maps them to Zod constraints:
|
|
|
139
139
|
| `numericality: { greater_than_or_equal_to: n }` | `.gte(n)` |
|
|
140
140
|
| `numericality: { less_than: n }` | `.lt(n)` |
|
|
141
141
|
| `numericality: { less_than_or_equal_to: n }` | `.lte(n)` |
|
|
142
|
-
| `format: { with: /regex/ }` | `.regex(
|
|
143
|
-
| `inclusion: { in: n..m }` (Range) | `.
|
|
142
|
+
| `format: { with: /regex/ }` | `.regex(new RegExp(...))` with safe escaping and compatible flags |
|
|
143
|
+
| `inclusion: { in: n..m }` (Range) | `.gte(n).lte(m)`; exclusive and open-ended bounds are preserved |
|
|
144
144
|
| `inclusion: { in: %w[a b c] }` (Array, string column) | `z.enum(["a", "b", "c"])` as the base type |
|
|
145
145
|
| `inclusion: { in: [1, 5, 10] }` (Array, integer column) | `.pipe(z.union([z.literal(1), z.literal(5), z.literal(10)]))` |
|
|
146
146
|
|
|
@@ -149,7 +149,7 @@ ZodRails introspects your model validations and maps them to Zod constraints:
|
|
|
149
149
|
The Rails `enum` macro and a string column with `validates :foo, inclusion: { in: %w[...] }` both end up as `z.enum([...])` in the generated TypeScript:
|
|
150
150
|
|
|
151
151
|
- `enum :role, { member: 0, admin: 1 }` introspects through ActiveRecord's `defined_enums` and emits `z.enum(["member", "admin"])`.
|
|
152
|
-
- `validates :decision, inclusion: { in: %w[pending approved] }` on a string column is detected by `SchemaBuilder` and
|
|
152
|
+
- `validates :decision, inclusion: { in: %w[pending approved] }` on a string column is detected by `SchemaBuilder` and restricts the result with `z.enum(["pending", "approved"])`. Other compatible validators on the attribute are retained before the enum restriction.
|
|
153
153
|
|
|
154
154
|
If you mix both (`enum` macro AND a separate `inclusion` validator on the same column), the `enum` macro wins.
|
|
155
155
|
|
|
@@ -175,20 +175,20 @@ import { z } from "zod";
|
|
|
175
175
|
|
|
176
176
|
export const UserSchema = z.object({
|
|
177
177
|
id: z.int(),
|
|
178
|
-
email: z.string().min(1).regex(
|
|
179
|
-
name: z.string().min(2).max(100),
|
|
178
|
+
email: z.string().min(1).refine((value) => value.trim().length > 0).regex(new RegExp("^[^@\\s]+@[^@\\s]+$")),
|
|
179
|
+
name: z.string().min(2).max(100).refine((value) => value.trim().length > 0),
|
|
180
180
|
age: z.int().gt(0).lt(150).nullable(),
|
|
181
181
|
status: z.enum(["pending", "active", "suspended"]),
|
|
182
182
|
role: z.enum(["member", "admin", "moderator"]),
|
|
183
|
-
created_at: z.iso.datetime(),
|
|
184
|
-
updated_at: z.iso.datetime()
|
|
183
|
+
created_at: z.iso.datetime({ offset: true }),
|
|
184
|
+
updated_at: z.iso.datetime({ offset: true })
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
export type User = z.infer<typeof UserSchema>;
|
|
188
188
|
|
|
189
189
|
export const UserInputSchema = z.object({
|
|
190
|
-
email: z.string().min(1).regex(
|
|
191
|
-
name: z.string().min(2).max(100),
|
|
190
|
+
email: z.string().min(1).refine((value) => value.trim().length > 0).regex(new RegExp("^[^@\\s]+@[^@\\s]+$")),
|
|
191
|
+
name: z.string().min(2).max(100).refine((value) => value.trim().length > 0),
|
|
192
192
|
age: z.int().gt(0).lt(150).nullish(),
|
|
193
193
|
status: z.enum(["pending", "active", "suspended"]),
|
|
194
194
|
role: z.enum(["member", "admin", "moderator"]).optional()
|
|
@@ -266,6 +266,8 @@ end
|
|
|
266
266
|
|
|
267
267
|
The command runs with your project's working directory. A nonzero exit raises `ZodRails::Error` so CI catches misconfiguration, and the generated files are still written before the formatter runs.
|
|
268
268
|
|
|
269
|
+
Because `zod_rails:check` compares generated bytes, a formatter that rewrites those bytes can report drift. Prefer a formatter configuration that accepts the generated style, or run the same deterministic formatter before checking committed files.
|
|
270
|
+
|
|
269
271
|
## Integrating with Forms
|
|
270
272
|
|
|
271
273
|
ZodRails pairs well with form libraries that support Zod:
|
|
@@ -340,6 +342,12 @@ Ensure validations are defined on the model class, not in concerns that might no
|
|
|
340
342
|
|
|
341
343
|
For custom types not in the mapping table, ZodRails falls back to `z.unknown()`. Open an issue if you need support for additional types.
|
|
342
344
|
|
|
345
|
+
### Serialization and validation limits
|
|
346
|
+
|
|
347
|
+
ZodRails generates a useful static approximation of database columns and unconditional model validations. It cannot reproduce validations that require database access, another attribute, or runtime model state. Conditional and context-specific validations, dynamic numericality values, uniqueness, and unsupported Ruby regular-expression modes are skipped.
|
|
348
|
+
|
|
349
|
+
The generated wire types must match your serializers. In particular, `bigint` and `decimal` map to strings to avoid JavaScript precision loss; configure your API serializer to emit strings for those attributes. Review generated schemas when using adapter-specific or custom ActiveRecord types.
|
|
350
|
+
|
|
343
351
|
### Misconfigured model names
|
|
344
352
|
|
|
345
353
|
A typo in `config.models` no longer raises an `uninitialized constant` backtrace. The generator collects every unresolvable name and prints them all in one report:
|
|
@@ -23,8 +23,9 @@ module ZodRails
|
|
|
23
23
|
File.write(full_path, final)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def preview(filename:, content:)
|
|
27
|
-
|
|
26
|
+
def preview(filename:, content:)
|
|
27
|
+
full_path = File.join(output_dir, filename)
|
|
28
|
+
File.exist?(full_path) ? splice_custom_blocks(content, File.read(full_path)) : content
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def output_path_for(model_name)
|
|
@@ -5,12 +5,15 @@ module ZodRails
|
|
|
5
5
|
class SchemaBuilder
|
|
6
6
|
STRING_TYPES = %i[string text].freeze
|
|
7
7
|
NULLABILITY_SUFFIX_RE = /(\.(?:nullable|nullish|optional)\(\))\z/
|
|
8
|
+
TYPESCRIPT_IDENTIFIER_RE = /\A[$A-Z_a-z][$\w]*\z/
|
|
8
9
|
|
|
9
|
-
attr_reader :inspector, :excluded_columns
|
|
10
|
+
attr_reader :inspector, :excluded_columns, :schema_suffix, :input_schema_suffix
|
|
10
11
|
|
|
11
|
-
def initialize(inspector, excluded_columns: [])
|
|
12
|
+
def initialize(inspector, excluded_columns: [], schema_suffix: "Schema", input_schema_suffix: "InputSchema")
|
|
12
13
|
@inspector = inspector
|
|
13
14
|
@excluded_columns = excluded_columns.map(&:to_s)
|
|
15
|
+
@schema_suffix = schema_suffix
|
|
16
|
+
@input_schema_suffix = input_schema_suffix
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def build(input_schema: false)
|
|
@@ -23,32 +26,42 @@ module ZodRails
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def schema_name(input_schema: false)
|
|
26
|
-
suffix = input_schema ?
|
|
27
|
-
"#{inspector.model_name.gsub("::", "")}#{suffix}"
|
|
29
|
+
suffix = input_schema ? input_schema_suffix : schema_suffix
|
|
30
|
+
name = "#{inspector.model_name.gsub("::", "")}#{suffix}"
|
|
31
|
+
return name if name.match?(TYPESCRIPT_IDENTIFIER_RE)
|
|
32
|
+
|
|
33
|
+
raise ZodRails::Error, "Invalid TypeScript schema name: #{name.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def type_name(input_schema: false)
|
|
37
|
+
name = inspector.model_name.gsub("::", "")
|
|
38
|
+
input_schema ? "#{name}Input" : name
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
private
|
|
31
42
|
|
|
32
43
|
def field_definition(column, input_schema:)
|
|
33
44
|
type_str = build_type_string(column, input_schema: input_schema)
|
|
34
|
-
|
|
45
|
+
key = column.name.match?(TYPESCRIPT_IDENTIFIER_RE) ? column.name : JSON.generate(column.name)
|
|
46
|
+
"#{key}: #{type_str}"
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def build_type_string(column, input_schema:)
|
|
50
|
+
validations = inspector.validations_for(column.name)
|
|
51
|
+
|
|
38
52
|
if enum_column?(column.name)
|
|
39
|
-
build_enum_type(column, input_schema: input_schema)
|
|
40
|
-
elsif (
|
|
41
|
-
build_inclusion_enum_type(column,
|
|
53
|
+
build_enum_type(column, validations, input_schema: input_schema)
|
|
54
|
+
elsif (inclusion = string_array_inclusion(column, validations))
|
|
55
|
+
build_inclusion_enum_type(column, inclusion, validations, input_schema: input_schema)
|
|
42
56
|
else
|
|
43
|
-
build_regular_type(column, input_schema: input_schema)
|
|
57
|
+
build_regular_type(column, validations, input_schema: input_schema)
|
|
44
58
|
end
|
|
45
59
|
end
|
|
46
60
|
|
|
47
|
-
def
|
|
61
|
+
def string_array_inclusion(column, validations)
|
|
48
62
|
return nil unless STRING_TYPES.include?(column.type)
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
inclusion&.options&.[](:in)
|
|
64
|
+
validations.find { |validation| string_array_inclusion?(validation) }
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
def string_array_inclusion?(validation)
|
|
@@ -58,30 +71,32 @@ module ZodRails
|
|
|
58
71
|
values.is_a?(Array) && !values.empty? && values.all? { |x| x.is_a?(String) }
|
|
59
72
|
end
|
|
60
73
|
|
|
61
|
-
def build_inclusion_enum_type(column,
|
|
74
|
+
def build_inclusion_enum_type(column, inclusion, validations, input_schema:)
|
|
75
|
+
remaining = validations.reject { |validation| validation.equal?(inclusion) }
|
|
62
76
|
Mapping::EnumMapper.call(
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
inclusion.options[:in],
|
|
78
|
+
validation_chain: Mapping::ValidationMapper.call_all(remaining, base_type: :string),
|
|
79
|
+
nullable: nullable?(column, validations),
|
|
65
80
|
input_schema: input_schema,
|
|
66
81
|
has_default: column.has_default
|
|
67
82
|
)
|
|
68
83
|
end
|
|
69
84
|
|
|
70
|
-
def build_enum_type(column, input_schema:)
|
|
85
|
+
def build_enum_type(column, validations, input_schema:)
|
|
71
86
|
values = inspector.enums[column.name]
|
|
72
87
|
Mapping::EnumMapper.call(
|
|
73
88
|
values,
|
|
74
|
-
|
|
89
|
+
validation_chain: Mapping::ValidationMapper.call_all(validations, base_type: :string),
|
|
90
|
+
nullable: nullable?(column, validations),
|
|
75
91
|
input_schema: input_schema,
|
|
76
92
|
has_default: column.has_default
|
|
77
93
|
)
|
|
78
94
|
end
|
|
79
95
|
|
|
80
|
-
def build_regular_type(column, input_schema:)
|
|
81
|
-
validations = inspector.validations_for(column.name)
|
|
96
|
+
def build_regular_type(column, validations, input_schema:)
|
|
82
97
|
base_type = Mapping::TypeMapper.call(
|
|
83
98
|
column.type,
|
|
84
|
-
nullable: column
|
|
99
|
+
nullable: nullable?(column, validations),
|
|
85
100
|
input_schema: input_schema,
|
|
86
101
|
has_default: column.has_default
|
|
87
102
|
)
|
|
@@ -104,6 +119,13 @@ module ZodRails
|
|
|
104
119
|
inspector.enums.key?(column_name)
|
|
105
120
|
end
|
|
106
121
|
|
|
122
|
+
def nullable?(column, validations)
|
|
123
|
+
column.nullable && validations.none? do |validation|
|
|
124
|
+
validation.kind == :presence && !validation.conditional? &&
|
|
125
|
+
!validation.options[:allow_nil] && !validation.options[:allow_blank]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
107
129
|
def filtered_columns
|
|
108
130
|
inspector.columns.reject { |col| excluded_columns.include?(col.name) }
|
|
109
131
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module ZodRails
|
|
4
4
|
module Generation
|
|
5
5
|
class TypescriptEmitter
|
|
6
|
-
def emit(schema_name:, schema_body:)
|
|
7
|
-
type_name
|
|
6
|
+
def emit(schema_name:, schema_body:, type_name: nil)
|
|
7
|
+
type_name ||= derive_type_name(schema_name)
|
|
8
8
|
|
|
9
9
|
<<~TYPESCRIPT
|
|
10
10
|
import { z } from "zod";
|
|
@@ -16,8 +16,8 @@ module ZodRails
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def emit_combined(response:, input:)
|
|
19
|
-
response_type = derive_type_name(response[:name])
|
|
20
|
-
input_type = derive_type_name(input[:name])
|
|
19
|
+
response_type = response[:type_name] || derive_type_name(response[:name])
|
|
20
|
+
input_type = input[:type_name] || derive_type_name(input[:name])
|
|
21
21
|
|
|
22
22
|
<<~TYPESCRIPT
|
|
23
23
|
import { z } from "zod";
|
|
@@ -35,7 +35,7 @@ module ZodRails
|
|
|
35
35
|
private
|
|
36
36
|
|
|
37
37
|
def derive_type_name(schema_name)
|
|
38
|
-
schema_name.sub(/
|
|
38
|
+
schema_name.sub(/InputSchema$/, "Input").sub(/Schema$/, "")
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
data/lib/zod_rails/generator.rb
CHANGED
|
@@ -18,27 +18,32 @@ module ZodRails
|
|
|
18
18
|
|
|
19
19
|
def generate_content(model_class)
|
|
20
20
|
inspector = Introspection::ModelInspector.new(model_class)
|
|
21
|
-
|
|
22
|
-
builder = Generation::SchemaBuilder.new(
|
|
21
|
+
config = ZodRails.configuration
|
|
22
|
+
builder = Generation::SchemaBuilder.new(
|
|
23
|
+
inspector,
|
|
24
|
+
excluded_columns: config.excluded_columns,
|
|
25
|
+
schema_suffix: config.schema_suffix,
|
|
26
|
+
input_schema_suffix: config.input_schema_suffix
|
|
27
|
+
)
|
|
23
28
|
|
|
24
29
|
response_schema = {
|
|
25
30
|
name: builder.schema_name,
|
|
31
|
+
type_name: builder.type_name,
|
|
26
32
|
body: builder.build
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
name: builder.schema_name(input_schema: true),
|
|
31
|
-
body: builder.build(input_schema: true)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
content = emitter.emit_combined(response: response_schema, input: input_schema)
|
|
35
|
-
filename = file_writer.output_path_for(inspector.model_name)
|
|
36
|
-
|
|
37
|
-
{ filename: filename, content: content }
|
|
34
|
+
content = emit_content(builder, response_schema, generate_input: config.generate_input_schemas)
|
|
35
|
+
{ filename: file_writer.output_path_for(inspector.model_name), content: content }
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
def generate_all(model_classes)
|
|
41
|
-
|
|
39
|
+
targets = model_classes.map { |klass| generate_content(klass) }
|
|
40
|
+
duplicate = targets.group_by { |target| target[:filename] }.find { |_filename, matches| matches.size > 1 }
|
|
41
|
+
raise ZodRails::Error, "Output filename collision: #{duplicate.first}" if duplicate
|
|
42
|
+
|
|
43
|
+
files = targets.map do |target|
|
|
44
|
+
file_writer.write(filename: target[:filename], content: target[:content])
|
|
45
|
+
target[:filename]
|
|
46
|
+
end
|
|
42
47
|
run_post_generate_command
|
|
43
48
|
files
|
|
44
49
|
end
|
|
@@ -59,6 +64,25 @@ module ZodRails
|
|
|
59
64
|
|
|
60
65
|
private
|
|
61
66
|
|
|
67
|
+
def emit_content(builder, response, generate_input:)
|
|
68
|
+
unless generate_input
|
|
69
|
+
return emitter.emit(
|
|
70
|
+
schema_name: response[:name], schema_body: response[:body], type_name: response[:type_name]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
input = {
|
|
75
|
+
name: builder.schema_name(input_schema: true),
|
|
76
|
+
type_name: builder.type_name(input_schema: true),
|
|
77
|
+
body: builder.build(input_schema: true)
|
|
78
|
+
}
|
|
79
|
+
if input[:name] == response[:name]
|
|
80
|
+
raise ZodRails::Error, "Response and input schemas have the same export name: #{input[:name]}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
emitter.emit_combined(response: response, input: input)
|
|
84
|
+
end
|
|
85
|
+
|
|
62
86
|
def run_post_generate_command
|
|
63
87
|
cmd = ZodRails.configuration.post_generate_command
|
|
64
88
|
return if cmd.nil? || cmd.to_s.strip.empty?
|
|
@@ -3,19 +3,16 @@
|
|
|
3
3
|
module ZodRails
|
|
4
4
|
module Mapping
|
|
5
5
|
class EnumMapper
|
|
6
|
-
def self.call(values, nullable: false, input_schema: false, has_default: false)
|
|
6
|
+
def self.call(values, nullable: false, input_schema: false, has_default: false, validation_chain: "")
|
|
7
7
|
names = values.is_a?(Hash) ? values.keys : values
|
|
8
|
-
quoted = names.map { |
|
|
9
|
-
|
|
8
|
+
quoted = names.map { |name| JSON.generate(name.to_s) }
|
|
9
|
+
enum = "z.enum([#{quoted.join(", ")}])"
|
|
10
|
+
base = validation_chain.empty? ? enum : "z.string()#{validation_chain}.pipe(#{enum})"
|
|
10
11
|
|
|
11
12
|
suffix = determine_suffix(nullable: nullable, input_schema: input_schema, has_default: has_default)
|
|
12
13
|
"#{base}#{suffix}"
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
def self.escape_quotes(str)
|
|
16
|
-
str.to_s.gsub('"', '\\"')
|
|
17
|
-
end
|
|
18
|
-
|
|
19
16
|
def self.determine_suffix(nullable:, input_schema:, has_default:)
|
|
20
17
|
return "" unless nullable || has_default
|
|
21
18
|
|
|
@@ -26,7 +23,7 @@ module ZodRails
|
|
|
26
23
|
end
|
|
27
24
|
end
|
|
28
25
|
|
|
29
|
-
private_class_method :
|
|
26
|
+
private_class_method :determine_suffix
|
|
30
27
|
end
|
|
31
28
|
end
|
|
32
29
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZodRails
|
|
4
|
+
module Mapping
|
|
5
|
+
class RegexpMapper
|
|
6
|
+
UNSUPPORTED_TOKENS = %w[\\K \\R (?> (?~].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(regex)
|
|
9
|
+
if unsupported?(regex)
|
|
10
|
+
ZodRails.logger.warn("ZodRails: Skipping regexp with Ruby-only semantics: #{regex.inspect}")
|
|
11
|
+
return nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
pattern = regex.source
|
|
15
|
+
.gsub("\\A", "^")
|
|
16
|
+
.gsub("\\z", "(?![\\s\\S])")
|
|
17
|
+
.gsub("\\Z", "(?=(?:\\n)?(?![\\s\\S]))")
|
|
18
|
+
flags = flags_for(regex)
|
|
19
|
+
args = [JSON.generate(pattern), (JSON.generate(flags) unless flags.empty?)].compact
|
|
20
|
+
"new RegExp(#{args.join(", ")})"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.flags_for(regex)
|
|
24
|
+
flags = +""
|
|
25
|
+
flags << "i" if regex.options.anybits?(Regexp::IGNORECASE)
|
|
26
|
+
flags << "s" if regex.options.anybits?(Regexp::MULTILINE)
|
|
27
|
+
flags
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.unsupported?(regex)
|
|
31
|
+
regex.options.anybits?(Regexp::EXTENDED) || UNSUPPORTED_TOKENS.any? { |token| regex.source.include?(token) }
|
|
32
|
+
end
|
|
33
|
+
private_class_method :flags_for, :unsupported?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -12,7 +12,8 @@ module ZodRails
|
|
|
12
12
|
decimal: "z.string()",
|
|
13
13
|
boolean: "z.boolean()",
|
|
14
14
|
date: "z.iso.date()",
|
|
15
|
-
datetime: "z.iso.datetime()",
|
|
15
|
+
datetime: "z.iso.datetime({ offset: true })",
|
|
16
|
+
timestamp: "z.iso.datetime({ offset: true })",
|
|
16
17
|
time: "z.string()",
|
|
17
18
|
json: "z.json()",
|
|
18
19
|
jsonb: "z.json()",
|
|
@@ -14,7 +14,7 @@ module ZodRails
|
|
|
14
14
|
STRING_ZOD_TYPES = %i[string text].freeze
|
|
15
15
|
|
|
16
16
|
def self.call(validation, base_type:)
|
|
17
|
-
return "" if validation.conditional?
|
|
17
|
+
return "" if validation.conditional? || no_op_presence?(validation)
|
|
18
18
|
|
|
19
19
|
case validation.kind
|
|
20
20
|
when :presence then map_presence(validation, base_type)
|
|
@@ -37,7 +37,7 @@ module ZodRails
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def self.map_presence(_validation, base_type)
|
|
40
|
-
STRING_ZOD_TYPES.include?(base_type) ? ".min(1)" : ""
|
|
40
|
+
STRING_ZOD_TYPES.include?(base_type) ? ".min(1)#{presence_suffix}" : ""
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def self.map_length(validation, base_type)
|
|
@@ -61,7 +61,7 @@ module ZodRails
|
|
|
61
61
|
|
|
62
62
|
validation.options.filter_map do |key, value|
|
|
63
63
|
method = NUMERICALITY_MAP[key]
|
|
64
|
-
".#{method}(#{value})" if method
|
|
64
|
+
".#{method}(#{value})" if method && static_number?(value)
|
|
65
65
|
end.join
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -71,8 +71,7 @@ module ZodRails
|
|
|
71
71
|
regex = validation.options[:with]
|
|
72
72
|
return "" unless regex
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
".regex(/#{js_pattern}/#{regex_flags_for(regex)})"
|
|
74
|
+
RegexpMapper.call(regex)&.then { |expression| ".regex(#{expression})" } || ""
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
def self.map_inclusion(validation, base_type)
|
|
@@ -101,35 +100,20 @@ module ZodRails
|
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
def self.build_string_enum_suffix(values)
|
|
104
|
-
quoted = values.map { |
|
|
103
|
+
quoted = values.map { |value| JSON.generate(value) }.join(", ")
|
|
105
104
|
".pipe(z.enum([#{quoted}]))"
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
def self.build_numeric_literal_suffix(values)
|
|
108
|
+
return nil unless values.all? { |value| static_number?(value) }
|
|
109
109
|
return ".pipe(z.literal(#{values.first}))" if values.length == 1
|
|
110
110
|
|
|
111
111
|
literals = values.map { |v| "z.literal(#{v})" }.join(", ")
|
|
112
112
|
".pipe(z.union([#{literals}]))"
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def self.escape_quotes(str)
|
|
116
|
-
str.to_s.gsub('"', '\\"')
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def self.convert_ruby_regex_to_js(regex)
|
|
120
|
-
pattern = regex.source
|
|
121
|
-
pattern = pattern.gsub("\\A", "^")
|
|
122
|
-
pattern.gsub(/\\z/i, "$")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def self.regex_flags_for(regex)
|
|
126
|
-
flags = +""
|
|
127
|
-
flags << "i" if regex.options.anybits?(Regexp::IGNORECASE)
|
|
128
|
-
flags
|
|
129
|
-
end
|
|
130
|
-
|
|
131
115
|
def self.collect_constraints(validation, base_type, constraints)
|
|
132
|
-
return if validation.conditional?
|
|
116
|
+
return if validation.conditional? || no_op_presence?(validation)
|
|
133
117
|
|
|
134
118
|
case validation.kind
|
|
135
119
|
when :presence then handle_presence_constraint(base_type, constraints)
|
|
@@ -141,7 +125,10 @@ module ZodRails
|
|
|
141
125
|
end
|
|
142
126
|
|
|
143
127
|
def self.handle_presence_constraint(base_type, constraints)
|
|
144
|
-
|
|
128
|
+
return unless STRING_ZOD_TYPES.include?(base_type)
|
|
129
|
+
|
|
130
|
+
constraints[:min] = [constraints[:min] || 0, 1].max
|
|
131
|
+
constraints[:others] << presence_suffix
|
|
145
132
|
end
|
|
146
133
|
|
|
147
134
|
def self.handle_length_constraint(validation, base_type, constraints)
|
|
@@ -159,8 +146,8 @@ module ZodRails
|
|
|
159
146
|
regex = validation.options[:with]
|
|
160
147
|
return unless regex
|
|
161
148
|
|
|
162
|
-
|
|
163
|
-
constraints[:others] << ".regex(
|
|
149
|
+
expression = RegexpMapper.call(regex)
|
|
150
|
+
constraints[:others] << ".regex(#{expression})" if expression
|
|
164
151
|
end
|
|
165
152
|
|
|
166
153
|
def self.build_chain(constraints)
|
|
@@ -169,7 +156,7 @@ module ZodRails
|
|
|
169
156
|
if constraints[:length]
|
|
170
157
|
parts << ".length(#{constraints[:length]})"
|
|
171
158
|
else
|
|
172
|
-
parts << ".min(#{constraints[:min]})"
|
|
159
|
+
parts << ".min(#{constraints[:min]})" unless constraints[:min].nil?
|
|
173
160
|
parts << ".max(#{constraints[:max].to_i})" if constraints[:max] && constraints[:max] != Float::INFINITY
|
|
174
161
|
end
|
|
175
162
|
|
|
@@ -181,16 +168,15 @@ module ZodRails
|
|
|
181
168
|
values = validation.options[:in] || validation.options[:within]
|
|
182
169
|
|
|
183
170
|
case values
|
|
184
|
-
when Range then apply_range_inclusion(values, constraints)
|
|
171
|
+
when Range then apply_range_inclusion(values, base_type, constraints)
|
|
185
172
|
when Array then apply_array_inclusion(values, base_type, constraints)
|
|
186
173
|
end
|
|
187
174
|
end
|
|
188
175
|
|
|
189
|
-
def self.apply_range_inclusion(range, constraints)
|
|
190
|
-
return unless
|
|
176
|
+
def self.apply_range_inclusion(range, base_type, constraints)
|
|
177
|
+
return unless NUMERIC_ZOD_TYPES.include?(base_type)
|
|
191
178
|
|
|
192
|
-
|
|
193
|
-
constraints[:max] = [constraints[:max] || Float::INFINITY, range.end].min
|
|
179
|
+
apply_numeric_range(range, constraints)
|
|
194
180
|
end
|
|
195
181
|
|
|
196
182
|
def self.apply_array_inclusion(values, base_type, constraints)
|
|
@@ -204,27 +190,41 @@ module ZodRails
|
|
|
204
190
|
validation.options.each do |key, value|
|
|
205
191
|
if key == :in
|
|
206
192
|
apply_numeric_range(value, constraints)
|
|
207
|
-
elsif (method = NUMERICALITY_MAP[key])
|
|
193
|
+
elsif (method = NUMERICALITY_MAP[key]) && static_number?(value)
|
|
208
194
|
constraints[:others] << ".#{method}(#{value})"
|
|
209
195
|
end
|
|
210
196
|
end
|
|
211
197
|
end
|
|
212
198
|
|
|
213
199
|
def self.apply_numeric_range(range, constraints)
|
|
214
|
-
return unless range.is_a?(Range)
|
|
200
|
+
return unless range.is_a?(Range)
|
|
201
|
+
|
|
202
|
+
constraints[:others] << ".gte(#{range.begin})" if static_number?(range.begin)
|
|
203
|
+
return unless static_number?(range.end)
|
|
204
|
+
|
|
205
|
+
constraints[:others] << ".#{range.exclude_end? ? "lt" : "lte"}(#{range.end})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.presence_suffix
|
|
209
|
+
'.refine((value) => value.trim().length > 0, { message: "can\'t be blank" })'
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.static_number?(value)
|
|
213
|
+
value.is_a?(Numeric) && (!value.respond_to?(:finite?) || value.finite?)
|
|
214
|
+
end
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
def self.no_op_presence?(validation)
|
|
217
|
+
validation.kind == :presence && validation.options[:allow_blank]
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
private_class_method :map_presence, :map_length, :map_numericality, :map_format, :map_inclusion,
|
|
221
221
|
:build_array_inclusion_suffix, :string_array_for_string_type?,
|
|
222
222
|
:numeric_array_for_numeric_type?, :build_string_enum_suffix,
|
|
223
|
-
:build_numeric_literal_suffix, :
|
|
224
|
-
:convert_ruby_regex_to_js, :regex_flags_for, :collect_constraints, :build_chain,
|
|
223
|
+
:build_numeric_literal_suffix, :collect_constraints, :build_chain,
|
|
225
224
|
:handle_presence_constraint, :handle_length_constraint, :handle_format_constraint,
|
|
226
225
|
:handle_inclusion_constraint, :apply_range_inclusion, :apply_array_inclusion,
|
|
227
|
-
:handle_numericality_constraint, :apply_numeric_range
|
|
226
|
+
:handle_numericality_constraint, :apply_numeric_range, :presence_suffix, :static_number?,
|
|
227
|
+
:no_op_presence?
|
|
228
228
|
end
|
|
229
229
|
end
|
|
230
230
|
end
|
|
@@ -5,14 +5,38 @@ module ZodRails
|
|
|
5
5
|
def self.resolve(names)
|
|
6
6
|
resolved = []
|
|
7
7
|
missing = []
|
|
8
|
+
invalid = []
|
|
8
9
|
|
|
9
10
|
names.each do |name|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
model = resolve_constant(name)
|
|
12
|
+
if active_record_model?(model)
|
|
13
|
+
resolved << model
|
|
14
|
+
else
|
|
15
|
+
invalid << name
|
|
16
|
+
end
|
|
17
|
+
rescue NameError => e
|
|
18
|
+
raise unless missing_constant?(e, name)
|
|
19
|
+
|
|
12
20
|
missing << name
|
|
13
21
|
end
|
|
14
22
|
|
|
15
|
-
{ resolved: resolved, missing: missing }
|
|
23
|
+
{ resolved: resolved, missing: missing, invalid: invalid }
|
|
16
24
|
end
|
|
25
|
+
|
|
26
|
+
def self.resolve_constant(name)
|
|
27
|
+
name.split("::").reject(&:empty?).reduce(Object) do |namespace, constant_name|
|
|
28
|
+
namespace.const_get(constant_name, false)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.active_record_model?(constant)
|
|
33
|
+
constant.is_a?(Class) && constant < ActiveRecord::Base && !constant.abstract_class?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.missing_constant?(error, requested_name)
|
|
37
|
+
requested_name.split("::").include?(error.name.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private_class_method :resolve_constant, :active_record_model?, :missing_constant?
|
|
17
41
|
end
|
|
18
42
|
end
|
data/lib/zod_rails/version.rb
CHANGED
data/lib/zod_rails.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
|
+
require "json"
|
|
5
|
+
require "active_record"
|
|
4
6
|
require_relative "zod_rails/version"
|
|
5
7
|
require_relative "zod_rails/configuration"
|
|
6
8
|
require_relative "zod_rails/mapping/type_mapper"
|
|
9
|
+
require_relative "zod_rails/mapping/regexp_mapper"
|
|
7
10
|
require_relative "zod_rails/mapping/validation_mapper"
|
|
8
11
|
require_relative "zod_rails/mapping/enum_mapper"
|
|
9
12
|
require_relative "zod_rails/introspection/column_info"
|
data/sig/zod_rails.rbs
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
module ZodRails
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.logger: () -> Logger
|
|
8
|
+
def self.logger=: (Logger) -> Logger
|
|
9
|
+
def self.configuration: () -> Configuration
|
|
10
|
+
def self.configure: [A] () { (Configuration) -> A } -> A
|
|
11
|
+
def self.reset_configuration!: () -> Configuration
|
|
12
|
+
|
|
13
|
+
class Configuration
|
|
14
|
+
attr_accessor output_dir: String
|
|
15
|
+
attr_accessor schema_suffix: String
|
|
16
|
+
attr_accessor input_schema_suffix: String
|
|
17
|
+
attr_accessor generate_input_schemas: bool
|
|
18
|
+
attr_accessor excluded_columns: Array[String]
|
|
19
|
+
attr_accessor models: Array[String]
|
|
20
|
+
attr_accessor post_generate_command: String?
|
|
21
|
+
|
|
22
|
+
def initialize: () -> void
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Generator
|
|
26
|
+
attr_reader output_dir: String
|
|
27
|
+
|
|
28
|
+
def initialize: (output_dir: String) -> void
|
|
29
|
+
def generate: (singleton(ActiveRecord::Base)) -> String
|
|
30
|
+
def generate_content: (singleton(ActiveRecord::Base)) -> Hash[Symbol, String]
|
|
31
|
+
def generate_all: (Array[singleton(ActiveRecord::Base)]) -> Array[String]
|
|
32
|
+
def check: (Array[singleton(ActiveRecord::Base)]) -> Array[Hash[Symbol, String | Symbol]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ModelResolver
|
|
36
|
+
def self.resolve: (Array[String]) -> Hash[Symbol, Array[untyped]]
|
|
37
|
+
end
|
|
4
38
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zod_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Kelly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
13
33
|
- !ruby/object:Gem::Dependency
|
|
14
34
|
name: logger
|
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -24,6 +44,26 @@ dependencies:
|
|
|
24
44
|
- - "~>"
|
|
25
45
|
- !ruby/object:Gem::Version
|
|
26
46
|
version: '1.6'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: railties
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
- - "<"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '9'
|
|
57
|
+
type: :runtime
|
|
58
|
+
prerelease: false
|
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '7.0'
|
|
64
|
+
- - "<"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '9'
|
|
27
67
|
description: Ruby gem that introspects ActiveRecord models and generates TypeScript
|
|
28
68
|
files with Zod schemas for type-safe frontend validation
|
|
29
69
|
email:
|
|
@@ -32,12 +72,9 @@ executables: []
|
|
|
32
72
|
extensions: []
|
|
33
73
|
extra_rdoc_files: []
|
|
34
74
|
files:
|
|
35
|
-
-
|
|
36
|
-
- ".github/workflows/release.yml"
|
|
75
|
+
- CHANGELOG.md
|
|
37
76
|
- LICENSE
|
|
38
77
|
- README.md
|
|
39
|
-
- Rakefile
|
|
40
|
-
- lib/tasks/zod_rails.rake
|
|
41
78
|
- lib/zod_rails.rb
|
|
42
79
|
- lib/zod_rails/configuration.rb
|
|
43
80
|
- lib/zod_rails/generation/file_writer.rb
|
|
@@ -48,20 +85,20 @@ files:
|
|
|
48
85
|
- lib/zod_rails/introspection/model_inspector.rb
|
|
49
86
|
- lib/zod_rails/introspection/validation_info.rb
|
|
50
87
|
- lib/zod_rails/mapping/enum_mapper.rb
|
|
88
|
+
- lib/zod_rails/mapping/regexp_mapper.rb
|
|
51
89
|
- lib/zod_rails/mapping/type_mapper.rb
|
|
52
90
|
- lib/zod_rails/mapping/validation_mapper.rb
|
|
53
91
|
- lib/zod_rails/model_resolver.rb
|
|
54
92
|
- lib/zod_rails/railtie.rb
|
|
55
93
|
- lib/zod_rails/version.rb
|
|
56
94
|
- sig/zod_rails.rbs
|
|
57
|
-
- zod_rails.png
|
|
58
95
|
homepage: https://github.com/mathisto/zod_rails
|
|
59
96
|
licenses:
|
|
60
97
|
- MIT
|
|
61
98
|
metadata:
|
|
62
99
|
homepage_uri: https://github.com/mathisto/zod_rails
|
|
63
100
|
source_code_uri: https://github.com/mathisto/zod_rails
|
|
64
|
-
changelog_uri: https://github.com/mathisto/zod_rails/blob/
|
|
101
|
+
changelog_uri: https://github.com/mathisto/zod_rails/blob/trunk/CHANGELOG.md
|
|
65
102
|
rubygems_mfa_required: 'true'
|
|
66
103
|
post_install_message:
|
|
67
104
|
rdoc_options: []
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
|
@@ -1,33 +0,0 @@
|
|
|
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/Rakefile
DELETED
data/lib/tasks/zod_rails.rake
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
namespace :zod_rails do
|
|
4
|
-
desc "Generate Zod schemas for configured models (DRY_RUN=1 to preview only)"
|
|
5
|
-
task generate: :environment do
|
|
6
|
-
config = ZodRails.configuration
|
|
7
|
-
generator = ZodRails::Generator.new(output_dir: config.output_dir)
|
|
8
|
-
|
|
9
|
-
if config.models.empty?
|
|
10
|
-
puts "No models configured. Add models to ZodRails.configure { |c| c.models = ['User', 'Article'] }"
|
|
11
|
-
exit 1
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
resolution = ZodRails::ModelResolver.resolve(config.models)
|
|
15
|
-
|
|
16
|
-
unless resolution[:missing].empty?
|
|
17
|
-
puts "ZodRails: #{resolution[:missing].size} model(s) in config.models could not be loaded:"
|
|
18
|
-
resolution[:missing].each { |m| puts " - #{m}" }
|
|
19
|
-
puts ""
|
|
20
|
-
puts "Check the model names in config/initializers/zod_rails.rb."
|
|
21
|
-
exit 1
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
models = resolution[:resolved]
|
|
25
|
-
|
|
26
|
-
if ENV["DRY_RUN"] == "1"
|
|
27
|
-
drift = generator.check(models)
|
|
28
|
-
if drift.empty?
|
|
29
|
-
puts "ZodRails (dry run): no changes."
|
|
30
|
-
else
|
|
31
|
-
puts "ZodRails (dry run): would update #{drift.size} file(s):"
|
|
32
|
-
drift.each { |d| puts " - #{d[:filename]} (#{d[:status]})" }
|
|
33
|
-
end
|
|
34
|
-
else
|
|
35
|
-
generated = generator.generate_all(models)
|
|
36
|
-
puts "Generated #{generated.size} schema file(s):"
|
|
37
|
-
generated.each { |f| puts " - #{f}" }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
desc "Check whether generated schemas match the current models (exits nonzero on drift)"
|
|
42
|
-
task check: :environment do
|
|
43
|
-
config = ZodRails.configuration
|
|
44
|
-
generator = ZodRails::Generator.new(output_dir: config.output_dir)
|
|
45
|
-
|
|
46
|
-
if config.models.empty?
|
|
47
|
-
puts "No models configured. Nothing to check."
|
|
48
|
-
exit 0
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
resolution = ZodRails::ModelResolver.resolve(config.models)
|
|
52
|
-
|
|
53
|
-
unless resolution[:missing].empty?
|
|
54
|
-
puts "ZodRails: #{resolution[:missing].size} model(s) in config.models could not be loaded:"
|
|
55
|
-
resolution[:missing].each { |m| puts " - #{m}" }
|
|
56
|
-
exit 1
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
drift = generator.check(resolution[:resolved])
|
|
60
|
-
|
|
61
|
-
if drift.empty?
|
|
62
|
-
puts "ZodRails: generated schemas are up to date."
|
|
63
|
-
exit 0
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
puts "ZodRails: #{drift.size} file(s) out of date:"
|
|
67
|
-
drift.each { |d| puts " - #{d[:filename]} (#{d[:status]})" }
|
|
68
|
-
puts ""
|
|
69
|
-
puts "Run `bin/rails zod_rails:generate` to update."
|
|
70
|
-
exit 1
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
desc "Generate Zod schema for a specific model"
|
|
74
|
-
task :generate_model, [:model_name] => :environment do |_t, args|
|
|
75
|
-
model_name = args[:model_name]
|
|
76
|
-
unless model_name
|
|
77
|
-
puts "Usage: rails zod_rails:generate_model[ModelName]"
|
|
78
|
-
exit 1
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
config = ZodRails.configuration
|
|
82
|
-
generator = ZodRails::Generator.new(output_dir: config.output_dir)
|
|
83
|
-
|
|
84
|
-
resolution = ZodRails::ModelResolver.resolve([model_name])
|
|
85
|
-
if resolution[:missing].any?
|
|
86
|
-
puts "ZodRails: model '#{model_name}' could not be loaded. Check the spelling."
|
|
87
|
-
exit 1
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
filename = generator.generate(resolution[:resolved].first)
|
|
91
|
-
puts "Generated: #{filename}"
|
|
92
|
-
end
|
|
93
|
-
end
|
data/zod_rails.png
DELETED
|
Binary file
|