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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feccb1b7155c9c0bce08aca4c171c1368f9a07082962903378e925eede8087e7
4
- data.tar.gz: ce34f0912b5fa25ea49e3187798b81f776bfd2b755ca4284a466659fd4639378
3
+ metadata.gz: 367406f60e95f76e1988c8cac92abf1bda727ca2362f0621c44388b2980a0c7f
4
+ data.tar.gz: be9c334bf39848c1451396a9999d718fb0cdf49480b147496c0e6cc6dcdbd9cb
5
5
  SHA512:
6
- metadata.gz: 48c3acf6c06bd274037335c9989e8021dbd3128dc65d5386699135caeea03b9ef4608fe9abd3faca189af1cc355efc0e74dc754502540a95885ba544dea5f1f3
7
- data.tar.gz: 7f1e5519b3742de53309e81cdc28f5b70ee25e5e34ce897bcd7d13d51bffd346962d256cb6aa2d2ab4ce22eafb6ee396d03bd5ee95bef7f1e70b6019f7333b44
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
+ [![Gem Version](https://badge.fury.io/rb/typed_enums.svg?icon=si%3Arubygems)](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 { Task } from "@/lib/enums";
61
+ import { TaskEnums } from "@/lib/enums";
60
62
 
61
- Task.workPriorities;
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 Task = {
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 Task: {
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 Task.workPriorities)[number];
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 exposes enum mappings through pluralized methods, such as `Task.work_priorities`. `typed_enums` mirrors that convention in camelCase:
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 { Task, type TaskWorkPriority } from "@/lib/enums";
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 { Task, type TaskWorkPriority } from "@/lib/enums";
133
+ import { useId, useState, type FormEvent } from "react";
134
+ import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
118
135
 
119
- export function PrioritySelect(props: {
120
- value: TaskWorkPriority;
121
- onChange: (value: TaskWorkPriority) => void;
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
- <select value={props.value} onChange={(event) => props.onChange(event.target.value as TaskWorkPriority)}>
125
- {Task.workPriorities.map((value) => (
126
- <option key={value} value={value}>
127
- {value}
128
- </option>
129
- ))}
130
- </select>
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 { Task, type TaskWorkPriority } from "@/lib/enums";
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
- const model = defineModel<TaskWorkPriority>();
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
- <select v-model="model">
146
- <option v-for="value in Task.workPriorities" :key="value" :value="value">
147
- {{ value }}
148
- </option>
149
- </select>
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 { Task, type TaskWorkPriority } from "@/lib/enums";
317
+ import { TaskEnums, type TaskWorkPriority } from "@/lib/enums";
157
318
 
158
- export const priorityOptions = Task.workPriorities.map((value: TaskWorkPriority) => ({
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 { Task } from "@/lib/enums";
328
+ import { TaskEnums } from "@/lib/enums";
168
329
 
169
- export const priorityOptions = Task.workPriorities.map((value) => ({
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 { Task } from "@/lib/enums";
341
+ import { TaskEnums } from "@/lib/enums";
181
342
 
182
- Task.workPriorities.map((value) => ({
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
- config.root_model_class = "ApplicationRecord"
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 AdminTask = {};
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:, rails_mapping_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
@@ -6,6 +6,10 @@ module TypedEnums
6
6
  module Naming
7
7
  class NameBuilder
8
8
  def model_export_name(model_name)
9
+ "#{model_type_name(model_name)}Enums"
10
+ end
11
+
12
+ def model_type_name(model_name)
9
13
  model_name.to_s.split("::").map(&:camelize).join
10
14
  end
11
15
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedEnums
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_enums
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ackermann