typed_enums 0.1.0 → 0.2.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 +6 -0
- data/README.md +201 -34
- data/lib/typed_enums/model_scanner.rb +2 -1
- data/lib/typed_enums/naming/name_builder.rb +4 -0
- data/lib/typed_enums/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 367406f60e95f76e1988c8cac92abf1bda727ca2362f0621c44388b2980a0c7f
|
|
4
|
+
data.tar.gz: be9c334bf39848c1451396a9999d718fb0cdf49480b147496c0e6cc6dcdbd9cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de4d1b59436e751875dff36e7ef0b2bc48618be1c1bbeb39676b8c023bad3017e48fbbe275a2c5a9a1c1f6c7953fac1baa40537a57260ec6b2278220de7f3bd7
|
|
7
|
+
data.tar.gz: 865b6b918499d80d66a6ed4c70c6fc59ed50709e3abda6509ab9053718ce6b8944dce2d703015a372320d396e1880754121aa336188bb3d6ec2a3a9004fc147b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- Generated runtime enum exports now end in `Enums` to avoid collisions with serializer-generated model names. Update imports and usages such as `Task` to `TaskEnums`; TypeScript enum type aliases such as `TaskWorkPriority` are unchanged.
|
|
8
|
+
|
|
3
9
|
## 0.1.0
|
|
4
10
|
|
|
5
11
|
- Initial implementation of Active Record enum scanning and TypeScript generation.
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# typed_enums
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/typed_enums)
|
|
4
|
+
|
|
3
5
|
Use Rails enums in JavaScript and TypeScript without duplicating option lists.
|
|
4
6
|
|
|
5
7
|
`typed_enums` is a Rails enum generator for JavaScript and TypeScript frontends. It exports Active Record enums to JavaScript constants and TypeScript declaration types, giving any frontend a Rails-like way to consume enum values.
|
|
@@ -56,9 +58,9 @@ end
|
|
|
56
58
|
The generated JavaScript can be imported from the generated module:
|
|
57
59
|
|
|
58
60
|
```ts
|
|
59
|
-
import {
|
|
61
|
+
import { TaskEnums } from "@/lib/enums";
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
TaskEnums.workPriorities;
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
## Generated JavaScript
|
|
@@ -69,7 +71,7 @@ The gem writes one JavaScript module containing every model enum:
|
|
|
69
71
|
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
70
72
|
// enum-schema-sha256: abc123
|
|
71
73
|
|
|
72
|
-
export const
|
|
74
|
+
export const TaskEnums = {
|
|
73
75
|
workPriorities: ["priority_1", "priority_2", "priority_3", "priority_4"],
|
|
74
76
|
};
|
|
75
77
|
```
|
|
@@ -80,16 +82,30 @@ It also writes `enums.d.ts` for TypeScript users:
|
|
|
80
82
|
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
81
83
|
// enum-schema-sha256: abc123
|
|
82
84
|
|
|
83
|
-
export declare const
|
|
85
|
+
export declare const TaskEnums: {
|
|
84
86
|
readonly workPriorities: readonly ["priority_1", "priority_2", "priority_3", "priority_4"];
|
|
85
87
|
};
|
|
86
88
|
|
|
87
|
-
export type TaskWorkPriority = (typeof
|
|
89
|
+
export type TaskWorkPriority = (typeof TaskEnums.workPriorities)[number];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Upgrading to 0.2.0
|
|
93
|
+
|
|
94
|
+
**Breaking change:** generated runtime exports now end in `Enums` so they can coexist with serializer-generated model names. Update imports and usages from `Task` to `TaskEnums`; enum type aliases, such as `TaskWorkPriority`, are unchanged.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Before 0.2.0
|
|
98
|
+
import { Task } from "@/lib/enums";
|
|
99
|
+
Task.workPriorities;
|
|
100
|
+
|
|
101
|
+
// 0.2.0 and later
|
|
102
|
+
import { TaskEnums } from "@/lib/enums";
|
|
103
|
+
TaskEnums.workPriorities;
|
|
88
104
|
```
|
|
89
105
|
|
|
90
106
|
## Naming
|
|
91
107
|
|
|
92
|
-
Rails
|
|
108
|
+
Each runtime export uses the flattened model name with an `Enums` suffix, such as `TaskEnums` or `AdminTaskEnums`. Rails enum mappings remain pluralized and camel-cased, such as `Task.work_priorities` becoming `TaskEnums.workPriorities`:
|
|
93
109
|
|
|
94
110
|
| Rails enum attribute | Rails mapping method | TypeScript property | TypeScript type |
|
|
95
111
|
| --- | --- | --- | --- |
|
|
@@ -104,7 +120,7 @@ This naming is intentionally not configurable in v1.
|
|
|
104
120
|
## Type Usage
|
|
105
121
|
|
|
106
122
|
```ts
|
|
107
|
-
import {
|
|
123
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
108
124
|
|
|
109
125
|
function setPriority(priority: TaskWorkPriority) {
|
|
110
126
|
return priority;
|
|
@@ -114,20 +130,104 @@ function setPriority(priority: TaskWorkPriority) {
|
|
|
114
130
|
## React
|
|
115
131
|
|
|
116
132
|
```tsx
|
|
117
|
-
import {
|
|
133
|
+
import { useId, useState, type FormEvent } from "react";
|
|
134
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
118
135
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
type TaskRecord = {
|
|
137
|
+
id: number;
|
|
138
|
+
workPriority: TaskWorkPriority;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type UpdateTaskPriorityParams = {
|
|
142
|
+
work_priority: TaskWorkPriority;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function EditTaskPriority(props: {
|
|
146
|
+
task: TaskRecord;
|
|
147
|
+
onSubmit: (taskId: number, params: UpdateTaskPriorityParams) => void | Promise<void>;
|
|
122
148
|
}) {
|
|
149
|
+
const priorityFieldId = useId();
|
|
150
|
+
const [workPriority, setWorkPriority] = useState<TaskWorkPriority>(props.task.workPriority);
|
|
151
|
+
|
|
152
|
+
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
void props.onSubmit(props.task.id, { work_priority: workPriority });
|
|
155
|
+
}
|
|
156
|
+
|
|
123
157
|
return (
|
|
124
|
-
<
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
<form onSubmit={handleSubmit}>
|
|
159
|
+
<label htmlFor={priorityFieldId}>Work priority</label>
|
|
160
|
+
|
|
161
|
+
<select
|
|
162
|
+
id={priorityFieldId}
|
|
163
|
+
name="work_priority"
|
|
164
|
+
value={workPriority}
|
|
165
|
+
onChange={(event) => setWorkPriority(event.currentTarget.value as TaskWorkPriority)}
|
|
166
|
+
>
|
|
167
|
+
{TaskEnums.workPriorities.map((value) => (
|
|
168
|
+
<option key={value} value={value}>
|
|
169
|
+
{value}
|
|
170
|
+
</option>
|
|
171
|
+
))}
|
|
172
|
+
</select>
|
|
173
|
+
|
|
174
|
+
<button type="submit">Save</button>
|
|
175
|
+
</form>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## React With Inertia
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import { useId, type FormEvent } from "react";
|
|
184
|
+
import { useForm } from "@inertiajs/react";
|
|
185
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
186
|
+
|
|
187
|
+
type TaskRecord = {
|
|
188
|
+
id: number;
|
|
189
|
+
workPriority: TaskWorkPriority;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type UpdateTaskPriorityParams = {
|
|
193
|
+
work_priority: TaskWorkPriority;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export function EditTaskPriority(props: {
|
|
197
|
+
task: TaskRecord;
|
|
198
|
+
}) {
|
|
199
|
+
const priorityFieldId = useId();
|
|
200
|
+
const form = useForm<UpdateTaskPriorityParams>({
|
|
201
|
+
work_priority: props.task.workPriority,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
form.put(`/tasks/${props.task.id}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<form onSubmit={handleSubmit}>
|
|
211
|
+
<label htmlFor={priorityFieldId}>Work priority</label>
|
|
212
|
+
|
|
213
|
+
<select
|
|
214
|
+
id={priorityFieldId}
|
|
215
|
+
name="work_priority"
|
|
216
|
+
value={form.data.work_priority}
|
|
217
|
+
onChange={(event) => form.setData("work_priority", event.currentTarget.value as TaskWorkPriority)}
|
|
218
|
+
disabled={form.processing}
|
|
219
|
+
>
|
|
220
|
+
{TaskEnums.workPriorities.map((value) => (
|
|
221
|
+
<option key={value} value={value}>
|
|
222
|
+
{value}
|
|
223
|
+
</option>
|
|
224
|
+
))}
|
|
225
|
+
</select>
|
|
226
|
+
|
|
227
|
+
<button type="submit" disabled={form.processing}>
|
|
228
|
+
Save
|
|
229
|
+
</button>
|
|
230
|
+
</form>
|
|
131
231
|
);
|
|
132
232
|
}
|
|
133
233
|
```
|
|
@@ -136,26 +236,87 @@ export function PrioritySelect(props: {
|
|
|
136
236
|
|
|
137
237
|
```vue
|
|
138
238
|
<script setup lang="ts">
|
|
139
|
-
import {
|
|
239
|
+
import { ref, useId } from "vue";
|
|
240
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
241
|
+
|
|
242
|
+
const props = defineProps<{
|
|
243
|
+
task: {
|
|
244
|
+
id: number;
|
|
245
|
+
workPriority: TaskWorkPriority;
|
|
246
|
+
};
|
|
247
|
+
}>();
|
|
248
|
+
|
|
249
|
+
const emit = defineEmits<{
|
|
250
|
+
submit: [taskId: number, params: { work_priority: TaskWorkPriority }];
|
|
251
|
+
}>();
|
|
252
|
+
|
|
253
|
+
const priorityFieldId = useId();
|
|
254
|
+
const workPriority = ref<TaskWorkPriority>(props.task.workPriority);
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<form @submit.prevent="emit('submit', props.task.id, { work_priority: workPriority })">
|
|
259
|
+
<label :for="priorityFieldId">Work priority</label>
|
|
140
260
|
|
|
141
|
-
|
|
261
|
+
<select :id="priorityFieldId" v-model="workPriority" name="work_priority">
|
|
262
|
+
<option v-for="value in TaskEnums.workPriorities" :key="value" :value="value">
|
|
263
|
+
{{ value }}
|
|
264
|
+
</option>
|
|
265
|
+
</select>
|
|
266
|
+
|
|
267
|
+
<button type="submit">Save</button>
|
|
268
|
+
</form>
|
|
269
|
+
</template>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Vue With Inertia
|
|
273
|
+
|
|
274
|
+
```vue
|
|
275
|
+
<script setup lang="ts">
|
|
276
|
+
import { useId } from "vue";
|
|
277
|
+
import { useForm } from "@inertiajs/vue3";
|
|
278
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
279
|
+
|
|
280
|
+
const props = defineProps<{
|
|
281
|
+
task: {
|
|
282
|
+
id: number;
|
|
283
|
+
workPriority: TaskWorkPriority;
|
|
284
|
+
};
|
|
285
|
+
}>();
|
|
286
|
+
|
|
287
|
+
const priorityFieldId = useId();
|
|
288
|
+
const form = useForm<{
|
|
289
|
+
work_priority: TaskWorkPriority;
|
|
290
|
+
}>({
|
|
291
|
+
work_priority: props.task.workPriority,
|
|
292
|
+
});
|
|
142
293
|
</script>
|
|
143
294
|
|
|
144
295
|
<template>
|
|
145
|
-
<
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
296
|
+
<form @submit.prevent="form.put(`/tasks/${props.task.id}`)">
|
|
297
|
+
<label :for="priorityFieldId">Work priority</label>
|
|
298
|
+
|
|
299
|
+
<select :id="priorityFieldId" v-model="form.work_priority" name="work_priority" :disabled="form.processing">
|
|
300
|
+
<option v-for="value in TaskEnums.workPriorities" :key="value" :value="value">
|
|
301
|
+
{{ value }}
|
|
302
|
+
</option>
|
|
303
|
+
</select>
|
|
304
|
+
|
|
305
|
+
<button type="submit" :disabled="form.processing">
|
|
306
|
+
Save
|
|
307
|
+
</button>
|
|
308
|
+
</form>
|
|
150
309
|
</template>
|
|
151
310
|
```
|
|
152
311
|
|
|
312
|
+
The route strings are intentionally plain for the examples. In Rails apps that use `js-routes`, prefer generated route helpers instead of building URLs by hand.
|
|
313
|
+
|
|
153
314
|
## Svelte Or Plain TypeScript
|
|
154
315
|
|
|
155
316
|
```ts
|
|
156
|
-
import {
|
|
317
|
+
import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
|
|
157
318
|
|
|
158
|
-
export const priorityOptions =
|
|
319
|
+
export const priorityOptions = TaskEnums.workPriorities.map((value: TaskWorkPriority) => ({
|
|
159
320
|
value,
|
|
160
321
|
label: value,
|
|
161
322
|
}));
|
|
@@ -164,9 +325,9 @@ export const priorityOptions = Task.workPriorities.map((value: TaskWorkPriority)
|
|
|
164
325
|
## Plain JavaScript
|
|
165
326
|
|
|
166
327
|
```js
|
|
167
|
-
import {
|
|
328
|
+
import { TaskEnums } from "@/lib/enums";
|
|
168
329
|
|
|
169
|
-
export const priorityOptions =
|
|
330
|
+
export const priorityOptions = TaskEnums.workPriorities.map((value) => ({
|
|
170
331
|
value,
|
|
171
332
|
label: value,
|
|
172
333
|
}));
|
|
@@ -177,9 +338,9 @@ export const priorityOptions = Task.workPriorities.map((value) => ({
|
|
|
177
338
|
The generated values are intentionally raw enum values. Use your frontend i18n layer to label them:
|
|
178
339
|
|
|
179
340
|
```ts
|
|
180
|
-
import {
|
|
341
|
+
import { TaskEnums } from "@/lib/enums";
|
|
181
342
|
|
|
182
|
-
|
|
343
|
+
TaskEnums.workPriorities.map((value) => ({
|
|
183
344
|
value,
|
|
184
345
|
label: t(`enums.task.workPriorities.${value}`),
|
|
185
346
|
}));
|
|
@@ -208,13 +369,19 @@ The installer creates `config/initializers/typed_enums.rb`:
|
|
|
208
369
|
# frozen_string_literal: true
|
|
209
370
|
|
|
210
371
|
TypedEnums.configure do |config|
|
|
372
|
+
# Generated JavaScript and TypeScript declaration files are written here.
|
|
211
373
|
config.output_dir = "app/javascript/lib"
|
|
212
|
-
|
|
374
|
+
|
|
375
|
+
# Automatically regenerate enum files in development.
|
|
213
376
|
config.auto_generate_in_development = true
|
|
377
|
+
|
|
378
|
+
# Watch app/models in development and regenerate after saves.
|
|
214
379
|
config.watch_models_in_development = true
|
|
215
380
|
end
|
|
216
381
|
```
|
|
217
382
|
|
|
383
|
+
If your enum models do not inherit from `ApplicationRecord`, configure `config.root_model_class` to the shared base class that exposes your Active Record descendants.
|
|
384
|
+
|
|
218
385
|
Keep the output directory isolated and import generated code from that directory. The generator never appends to user-owned frontend files.
|
|
219
386
|
|
|
220
387
|
## Rake Tasks
|
|
@@ -287,7 +454,7 @@ app/javascript/lib/enums.d.ts
|
|
|
287
454
|
```
|
|
288
455
|
|
|
289
456
|
```ts
|
|
290
|
-
export const
|
|
457
|
+
export const AdminTaskEnums = {};
|
|
291
458
|
```
|
|
292
459
|
|
|
293
460
|
## Empty Output
|
|
@@ -310,7 +477,7 @@ Enum values are written to frontend-visible JavaScript files. Do not put secrets
|
|
|
310
477
|
|
|
311
478
|
## Compatibility
|
|
312
479
|
|
|
313
|
-
The gem targets modern Rails applications and supports Rails 7.1 or newer. It uses Active Record enum APIs and ActiveSupport inflections, and it does not depend on any frontend framework.
|
|
480
|
+
The gem targets modern Rails applications and supports Rails 7.1 or newer. It uses Active Record enum APIs and ActiveSupport inflections, and it does not depend on any frontend framework. The React, Vue, and Inertia examples target current framework versions and are provided as copyable usage examples for generated enum values.
|
|
314
481
|
|
|
315
482
|
## Contributing
|
|
316
483
|
|
|
@@ -42,6 +42,7 @@ module TypedEnums
|
|
|
42
42
|
|
|
43
43
|
def build_definition(model:, attribute_name:, values:)
|
|
44
44
|
model_export_name = name_builder.model_export_name(model.name)
|
|
45
|
+
model_type_name = name_builder.model_type_name(model.name)
|
|
45
46
|
rails_mapping_name = name_builder.rails_mapping_name(attribute_name)
|
|
46
47
|
|
|
47
48
|
EnumDefinition.new(
|
|
@@ -50,7 +51,7 @@ module TypedEnums
|
|
|
50
51
|
attribute_name:,
|
|
51
52
|
rails_mapping_name:,
|
|
52
53
|
typescript_property_name: name_builder.property_name(rails_mapping_name),
|
|
53
|
-
typescript_type_name: name_builder.type_name(model_export_name
|
|
54
|
+
typescript_type_name: name_builder.type_name(model_export_name: model_type_name, rails_mapping_name:),
|
|
54
55
|
values: values.keys.map(&:to_s)
|
|
55
56
|
)
|
|
56
57
|
end
|
data/lib/typed_enums/version.rb
CHANGED