typed_enums 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +328 -0
- data/lib/generators/typed_enums/install_generator.rb +17 -0
- data/lib/generators/typed_enums/templates/initializer.rb +12 -0
- data/lib/typed_enums/configuration.rb +14 -0
- data/lib/typed_enums/enum_definition.rb +13 -0
- data/lib/typed_enums/error.rb +6 -0
- data/lib/typed_enums/model_scanner.rb +58 -0
- data/lib/typed_enums/naming/name_builder.rb +25 -0
- data/lib/typed_enums/output/javascript_file.rb +67 -0
- data/lib/typed_enums/output/type_declaration_file.rb +60 -0
- data/lib/typed_enums/output/writer/result.rb +38 -0
- data/lib/typed_enums/output/writer.rb +112 -0
- data/lib/typed_enums/railtie.rb +29 -0
- data/lib/typed_enums/registry.rb +29 -0
- data/lib/typed_enums/tasks.rb +47 -0
- data/lib/typed_enums/version.rb +5 -0
- data/lib/typed_enums/watcher.rb +131 -0
- data/lib/typed_enums.rb +69 -0
- metadata +161 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: feccb1b7155c9c0bce08aca4c171c1368f9a07082962903378e925eede8087e7
|
|
4
|
+
data.tar.gz: ce34f0912b5fa25ea49e3187798b81f776bfd2b755ca4284a466659fd4639378
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 48c3acf6c06bd274037335c9989e8021dbd3128dc65d5386699135caeea03b9ef4608fe9abd3faca189af1cc355efc0e74dc754502540a95885ba544dea5f1f3
|
|
7
|
+
data.tar.gz: 7f1e5519b3742de53309e81cdc28f5b70ee25e5e34ce897bcd7d13d51bffd346962d256cb6aa2d2ab4ce22eafb6ee396d03bd5ee95bef7f1e70b6019f7333b44
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 typed_enums 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,328 @@
|
|
|
1
|
+
# typed_enums
|
|
2
|
+
|
|
3
|
+
Use Rails enums in JavaScript and TypeScript without duplicating option lists.
|
|
4
|
+
|
|
5
|
+
`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.
|
|
6
|
+
|
|
7
|
+
Rails routes have `js-routes`. Rails serializers have serializer type generators. Rails enums now have `typed_enums`.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
`typed_enums` scans loaded Active Record models, reads `defined_enums`, and writes one generated JavaScript module plus a TypeScript declaration file:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
app/javascript/lib/enums.js
|
|
15
|
+
app/javascript/lib/enums.d.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It works with React, Vue, Svelte, API-only Rails apps, plain JavaScript, or any TypeScript frontend. The primary runtime output is plain JavaScript. No frontend framework, bundler, or TypeScript runtime package is required by the gem itself.
|
|
19
|
+
|
|
20
|
+
## What It Does Not Do
|
|
21
|
+
|
|
22
|
+
This gem is not a serializer, OpenAPI generator, route helper generator, form builder, or enum replacement. It does not generate React components, Vue components, Svelte components, migrations, or runtime enum editing tools.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Add the gem:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Gemfile
|
|
30
|
+
gem "typed_enums"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install and generate the initializer:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
bin/rails generate typed_enums:install
|
|
38
|
+
bin/rails typed_enums:generate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rails Setup
|
|
42
|
+
|
|
43
|
+
Given a Rails model:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class Task < ApplicationRecord
|
|
47
|
+
enum :work_priority, {
|
|
48
|
+
priority_1: 0,
|
|
49
|
+
priority_2: 1,
|
|
50
|
+
priority_3: 2,
|
|
51
|
+
priority_4: 3
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The generated JavaScript can be imported from the generated module:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Task } from "@/lib/enums";
|
|
60
|
+
|
|
61
|
+
Task.workPriorities;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Generated JavaScript
|
|
65
|
+
|
|
66
|
+
The gem writes one JavaScript module containing every model enum:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
70
|
+
// enum-schema-sha256: abc123
|
|
71
|
+
|
|
72
|
+
export const Task = {
|
|
73
|
+
workPriorities: ["priority_1", "priority_2", "priority_3", "priority_4"],
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It also writes `enums.d.ts` for TypeScript users:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
81
|
+
// enum-schema-sha256: abc123
|
|
82
|
+
|
|
83
|
+
export declare const Task: {
|
|
84
|
+
readonly workPriorities: readonly ["priority_1", "priority_2", "priority_3", "priority_4"];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type TaskWorkPriority = (typeof Task.workPriorities)[number];
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Naming
|
|
91
|
+
|
|
92
|
+
Rails exposes enum mappings through pluralized methods, such as `Task.work_priorities`. `typed_enums` mirrors that convention in camelCase:
|
|
93
|
+
|
|
94
|
+
| Rails enum attribute | Rails mapping method | TypeScript property | TypeScript type |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| `ticket_status` | `ticket_statuses` | `ticketStatuses` | `TaskTicketStatus` |
|
|
97
|
+
| `work_priority` | `work_priorities` | `workPriorities` | `TaskWorkPriority` |
|
|
98
|
+
| `fix_priority` | `fix_priorities` | `fixPriorities` | `TaskFixPriority` |
|
|
99
|
+
| `severity` | `severities` | `severities` | `TaskSeverity` |
|
|
100
|
+
| `urgency` | `urgencies` | `urgencies` | `TaskUrgency` |
|
|
101
|
+
|
|
102
|
+
This naming is intentionally not configurable in v1.
|
|
103
|
+
|
|
104
|
+
## Type Usage
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { Task, type TaskWorkPriority } from "@/lib/enums";
|
|
108
|
+
|
|
109
|
+
function setPriority(priority: TaskWorkPriority) {
|
|
110
|
+
return priority;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## React
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { Task, type TaskWorkPriority } from "@/lib/enums";
|
|
118
|
+
|
|
119
|
+
export function PrioritySelect(props: {
|
|
120
|
+
value: TaskWorkPriority;
|
|
121
|
+
onChange: (value: TaskWorkPriority) => void;
|
|
122
|
+
}) {
|
|
123
|
+
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>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Vue
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<script setup lang="ts">
|
|
139
|
+
import { Task, type TaskWorkPriority } from "@/lib/enums";
|
|
140
|
+
|
|
141
|
+
const model = defineModel<TaskWorkPriority>();
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<template>
|
|
145
|
+
<select v-model="model">
|
|
146
|
+
<option v-for="value in Task.workPriorities" :key="value" :value="value">
|
|
147
|
+
{{ value }}
|
|
148
|
+
</option>
|
|
149
|
+
</select>
|
|
150
|
+
</template>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Svelte Or Plain TypeScript
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { Task, type TaskWorkPriority } from "@/lib/enums";
|
|
157
|
+
|
|
158
|
+
export const priorityOptions = Task.workPriorities.map((value: TaskWorkPriority) => ({
|
|
159
|
+
value,
|
|
160
|
+
label: value,
|
|
161
|
+
}));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Plain JavaScript
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { Task } from "@/lib/enums";
|
|
168
|
+
|
|
169
|
+
export const priorityOptions = Task.workPriorities.map((value) => ({
|
|
170
|
+
value,
|
|
171
|
+
label: value,
|
|
172
|
+
}));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## i18n Labels
|
|
176
|
+
|
|
177
|
+
The generated values are intentionally raw enum values. Use your frontend i18n layer to label them:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { Task } from "@/lib/enums";
|
|
181
|
+
|
|
182
|
+
Task.workPriorities.map((value) => ({
|
|
183
|
+
value,
|
|
184
|
+
label: t(`enums.task.workPriorities.${value}`),
|
|
185
|
+
}));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Example keys:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"enums": {
|
|
193
|
+
"task": {
|
|
194
|
+
"workPriorities": {
|
|
195
|
+
"priority_1": "P1",
|
|
196
|
+
"priority_2": "P2"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Configuration
|
|
204
|
+
|
|
205
|
+
The installer creates `config/initializers/typed_enums.rb`:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# frozen_string_literal: true
|
|
209
|
+
|
|
210
|
+
TypedEnums.configure do |config|
|
|
211
|
+
config.output_dir = "app/javascript/lib"
|
|
212
|
+
config.root_model_class = "ApplicationRecord"
|
|
213
|
+
config.auto_generate_in_development = true
|
|
214
|
+
config.watch_models_in_development = true
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Keep the output directory isolated and import generated code from that directory. The generator never appends to user-owned frontend files.
|
|
219
|
+
|
|
220
|
+
## Rake Tasks
|
|
221
|
+
|
|
222
|
+
Generate files:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
bin/rails typed_enums:generate
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Check files in CI:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
bin/rails typed_enums:check
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The check task exits non-zero when files are missing, changed, or stale. It does not modify files.
|
|
235
|
+
|
|
236
|
+
Watch model files and regenerate after saves:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bin/rails typed_enums:watch
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This is useful when you want generated enum files to update immediately after saving a Rails model in VS Code without relying on a running Rails server.
|
|
243
|
+
|
|
244
|
+
## Development Auto-Regeneration
|
|
245
|
+
|
|
246
|
+
In development, generation runs on Rails boot and through `Rails.application.reloader.to_prepare` when `auto_generate_in_development` is enabled.
|
|
247
|
+
|
|
248
|
+
When `watch_models_in_development` is enabled, `typed_enums` also watches `app/models/**/*.rb` and regenerates enum files after model saves. This covers normal VS Code save workflows while the Rails development process is running. If you are not running Rails, use `bin/rails typed_enums:watch` in a separate terminal.
|
|
249
|
+
|
|
250
|
+
The writer compares content before writing, so unchanged files are not rewritten and file watchers should stay quiet.
|
|
251
|
+
|
|
252
|
+
Production does not auto-write files by default. Run `bin/rails typed_enums:generate` as part of your build or release process when generated enum files are not committed.
|
|
253
|
+
|
|
254
|
+
## Stale File Cleanup
|
|
255
|
+
|
|
256
|
+
`typed_enums` only overwrites files that it created.
|
|
257
|
+
|
|
258
|
+
If `enums.js` or `enums.d.ts` already exists without this generated marker, generation fails instead of overwriting the file:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Move the existing file, delete it, or configure a different `config.output_dir`.
|
|
265
|
+
|
|
266
|
+
The writer removes stale generated `.js`, `.d.ts`, and legacy `.ts` files inside the configured output directory only when they start with:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
It does not delete user-owned files or files outside the generated directory.
|
|
273
|
+
|
|
274
|
+
## Namespaced Models
|
|
275
|
+
|
|
276
|
+
Namespaced models are flattened into safe JavaScript identifiers:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
Admin::Task
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Generates exports inside:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
app/javascript/lib/enums.js
|
|
286
|
+
app/javascript/lib/enums.d.ts
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
export const AdminTask = {};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Empty Output
|
|
294
|
+
|
|
295
|
+
If no models define enums, `typed_enums` still writes `enums.js` and `enums.d.ts` files with the generated header and no exports.
|
|
296
|
+
|
|
297
|
+
## Troubleshooting
|
|
298
|
+
|
|
299
|
+
If models are missing, confirm they can be eager-loaded in the current environment. The scanner calls `Rails.application.eager_load!` before reading descendants.
|
|
300
|
+
|
|
301
|
+
If the root model class is not `ApplicationRecord`, configure `root_model_class`.
|
|
302
|
+
|
|
303
|
+
If generated files cannot be written, check filesystem permissions for `config.output_dir`.
|
|
304
|
+
|
|
305
|
+
If check mode fails in CI, run `bin/rails typed_enums:generate` locally and commit the generated files, or add generation to your build before JavaScript or TypeScript compilation.
|
|
306
|
+
|
|
307
|
+
## Security
|
|
308
|
+
|
|
309
|
+
Enum values are written to frontend-visible JavaScript files. Do not put secrets, credentials, private tokens, or private application state in enum values.
|
|
310
|
+
|
|
311
|
+
## Compatibility
|
|
312
|
+
|
|
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.
|
|
314
|
+
|
|
315
|
+
## Contributing
|
|
316
|
+
|
|
317
|
+
Run:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
bundle exec rspec
|
|
321
|
+
bundle exec rubocop
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Keep the gem small and focused: Active Record enums in, JavaScript constants and TypeScript declaration types out.
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_initializer
|
|
10
|
+
template "initializer.rb", "config/initializers/typed_enums.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_generated_directory
|
|
14
|
+
create_file "app/javascript/lib/.keep", ""
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
TypedEnums.configure do |config|
|
|
4
|
+
# Generated JavaScript and TypeScript declaration files are written here.
|
|
5
|
+
config.output_dir = "app/javascript/lib"
|
|
6
|
+
|
|
7
|
+
# Automatically regenerate enum files in development.
|
|
8
|
+
config.auto_generate_in_development = true
|
|
9
|
+
|
|
10
|
+
# Watch app/models in development and regenerate after saves.
|
|
11
|
+
config.watch_models_in_development = true
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypedEnums
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :output_dir, :root_model_class, :auto_generate_in_development, :watch_models_in_development
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@output_dir = "app/javascript/lib"
|
|
9
|
+
@root_model_class = "ApplicationRecord"
|
|
10
|
+
@auto_generate_in_development = true
|
|
11
|
+
@watch_models_in_development = true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
class ModelScanner
|
|
7
|
+
def initialize(config: TypedEnums.configuration, name_builder: Naming::NameBuilder.new)
|
|
8
|
+
@config = config
|
|
9
|
+
@name_builder = name_builder
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
eager_load_application
|
|
14
|
+
|
|
15
|
+
enum_models.flat_map do |model|
|
|
16
|
+
model.defined_enums.sort_by { |attribute_name, _values| attribute_name }.map do |attribute_name, values|
|
|
17
|
+
build_definition(model:, attribute_name:, values:)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :config, :name_builder
|
|
25
|
+
|
|
26
|
+
def eager_load_application
|
|
27
|
+
Rails.application.eager_load! if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enum_models
|
|
31
|
+
root_model_class.descendants
|
|
32
|
+
.reject(&:abstract_class?)
|
|
33
|
+
.select { |model| model.defined_enums.any? }
|
|
34
|
+
.sort_by(&:name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def root_model_class
|
|
38
|
+
config.root_model_class.to_s.constantize
|
|
39
|
+
rescue NameError => e
|
|
40
|
+
raise ConfigurationError, "Could not find root model class #{config.root_model_class.inspect}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_definition(model:, attribute_name:, values:)
|
|
44
|
+
model_export_name = name_builder.model_export_name(model.name)
|
|
45
|
+
rails_mapping_name = name_builder.rails_mapping_name(attribute_name)
|
|
46
|
+
|
|
47
|
+
EnumDefinition.new(
|
|
48
|
+
model_name: model.name,
|
|
49
|
+
model_export_name:,
|
|
50
|
+
attribute_name:,
|
|
51
|
+
rails_mapping_name:,
|
|
52
|
+
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
|
+
values: values.keys.map(&:to_s)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
module Naming
|
|
7
|
+
class NameBuilder
|
|
8
|
+
def model_export_name(model_name)
|
|
9
|
+
model_name.to_s.split("::").map(&:camelize).join
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def rails_mapping_name(attribute_name)
|
|
13
|
+
attribute_name.to_s.pluralize
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def property_name(rails_mapping_name)
|
|
17
|
+
rails_mapping_name.to_s.camelize(:lower)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def type_name(model_export_name:, rails_mapping_name:)
|
|
21
|
+
"#{model_export_name}#{rails_mapping_name.to_s.singularize.camelize}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module TypedEnums
|
|
7
|
+
module Output
|
|
8
|
+
class JavaScriptFile
|
|
9
|
+
HEADER = "// AUTO-GENERATED BY typed_enums. DO NOT EDIT."
|
|
10
|
+
|
|
11
|
+
def initialize(model_groups:)
|
|
12
|
+
@model_groups = model_groups
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render
|
|
16
|
+
<<~JAVASCRIPT
|
|
17
|
+
#{HEADER}
|
|
18
|
+
// enum-schema-sha256: #{schema_hash}
|
|
19
|
+
|
|
20
|
+
#{model_exports}
|
|
21
|
+
JAVASCRIPT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def schema_hash
|
|
25
|
+
Digest::SHA256.hexdigest(JSON.generate(schema))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :model_groups
|
|
31
|
+
|
|
32
|
+
def model_exports
|
|
33
|
+
model_groups.map do |model_export_name, definitions|
|
|
34
|
+
<<~JAVASCRIPT.chomp
|
|
35
|
+
export const #{model_export_name} = {
|
|
36
|
+
#{property_lines(definitions)}
|
|
37
|
+
};
|
|
38
|
+
JAVASCRIPT
|
|
39
|
+
end.join("\n\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def property_lines(definitions)
|
|
43
|
+
definitions.map do |definition|
|
|
44
|
+
" #{definition.typescript_property_name}: #{render_array(definition.values)},"
|
|
45
|
+
end.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_array(values)
|
|
49
|
+
"[#{values.map { |value| JSON.generate(value) }.join(', ')}]"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def schema
|
|
53
|
+
model_groups.transform_values do |definitions|
|
|
54
|
+
definitions.map do |definition|
|
|
55
|
+
{
|
|
56
|
+
attribute: definition.attribute_name,
|
|
57
|
+
rails_mapping: definition.rails_mapping_name,
|
|
58
|
+
property: definition.typescript_property_name,
|
|
59
|
+
type: definition.typescript_type_name,
|
|
60
|
+
values: definition.values
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
module Output
|
|
7
|
+
class TypeDeclarationFile
|
|
8
|
+
HEADER = JavaScriptFile::HEADER
|
|
9
|
+
|
|
10
|
+
def initialize(model_groups:, schema_hash:)
|
|
11
|
+
@model_groups = model_groups
|
|
12
|
+
@schema_hash = schema_hash
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render
|
|
16
|
+
<<~TYPESCRIPT
|
|
17
|
+
#{HEADER}
|
|
18
|
+
// enum-schema-sha256: #{schema_hash}
|
|
19
|
+
|
|
20
|
+
#{declarations}
|
|
21
|
+
TYPESCRIPT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :model_groups, :schema_hash
|
|
27
|
+
|
|
28
|
+
def declarations
|
|
29
|
+
model_groups.map do |model_export_name, definitions|
|
|
30
|
+
[constant_declaration(model_export_name, definitions), type_declarations(definitions)].join("\n\n")
|
|
31
|
+
end.join("\n\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def constant_declaration(model_export_name, definitions)
|
|
35
|
+
<<~TYPESCRIPT.chomp
|
|
36
|
+
export declare const #{model_export_name}: {
|
|
37
|
+
#{property_lines(definitions)}
|
|
38
|
+
};
|
|
39
|
+
TYPESCRIPT
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def property_lines(definitions)
|
|
43
|
+
definitions.map do |definition|
|
|
44
|
+
" readonly #{definition.typescript_property_name}: readonly #{render_tuple(definition.values)};"
|
|
45
|
+
end.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def type_declarations(definitions)
|
|
49
|
+
definitions.map do |definition|
|
|
50
|
+
"export type #{definition.typescript_type_name} = " \
|
|
51
|
+
"(typeof #{definition.model_export_name}.#{definition.typescript_property_name})[number];"
|
|
52
|
+
end.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_tuple(values)
|
|
56
|
+
"[#{values.map { |value| JSON.generate(value) }.join(', ')}]"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypedEnums
|
|
4
|
+
module Output
|
|
5
|
+
class Writer
|
|
6
|
+
Result = Data.define(:changed, :missing, :extra, :unchanged, :conflicts) do
|
|
7
|
+
def stale?
|
|
8
|
+
changed.any? || missing.any? || extra.any? || conflicts.any?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def conflict?
|
|
12
|
+
conflicts.any?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def summary
|
|
16
|
+
{
|
|
17
|
+
"conflicts" => conflicts,
|
|
18
|
+
"missing" => missing,
|
|
19
|
+
"changed" => changed,
|
|
20
|
+
"extra" => extra
|
|
21
|
+
}.filter_map do |label, files|
|
|
22
|
+
"#{label}: #{files.join(', ')}" if files.any?
|
|
23
|
+
end.join("\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def applied_summary
|
|
27
|
+
{
|
|
28
|
+
"created" => missing,
|
|
29
|
+
"updated" => changed,
|
|
30
|
+
"removed" => extra
|
|
31
|
+
}.filter_map do |label, files|
|
|
32
|
+
"#{label}: #{files.join(', ')}" if files.any?
|
|
33
|
+
end.join("\n")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module TypedEnums
|
|
7
|
+
module Output
|
|
8
|
+
class Writer
|
|
9
|
+
GENERATED_MARKER = JavaScriptFile::HEADER
|
|
10
|
+
|
|
11
|
+
def initialize(output_dir:, expected_files:, logger: default_logger, check: false)
|
|
12
|
+
@output_dir = Pathname(output_dir)
|
|
13
|
+
@expected_files = expected_files
|
|
14
|
+
@logger = logger
|
|
15
|
+
@check = check
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
ensure_output_dir unless check
|
|
20
|
+
|
|
21
|
+
result = Result.new(changed: [], missing: [], extra: [], unchanged: [], conflicts: [])
|
|
22
|
+
reconcile_expected_files(result)
|
|
23
|
+
reconcile_stale_files(result)
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :output_dir, :expected_files, :logger, :check
|
|
30
|
+
|
|
31
|
+
def reconcile_expected_files(result)
|
|
32
|
+
expected_files.sort.each do |relative_path, content|
|
|
33
|
+
path = output_dir.join(relative_path)
|
|
34
|
+
reconcile_expected_file(result:, relative_path:, path:, content:)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reconcile_expected_file(result:, relative_path:, path:, content:)
|
|
39
|
+
if !path.exist?
|
|
40
|
+
mark_missing(result:, relative_path:, path:, content:)
|
|
41
|
+
elsif path.read == content
|
|
42
|
+
result.unchanged << relative_path
|
|
43
|
+
elsif !generated_file?(path)
|
|
44
|
+
result.conflicts << relative_path
|
|
45
|
+
else
|
|
46
|
+
mark_changed(result:, relative_path:, path:, content:)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mark_missing(result:, relative_path:, path:, content:)
|
|
51
|
+
result.missing << relative_path
|
|
52
|
+
write_file(path, content) unless check
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def mark_changed(result:, relative_path:, path:, content:)
|
|
56
|
+
result.changed << relative_path
|
|
57
|
+
write_file(path, content) unless check
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reconcile_stale_files(result)
|
|
61
|
+
generated_files.each do |path|
|
|
62
|
+
relative_path = path.relative_path_from(output_dir).to_s
|
|
63
|
+
next if expected_files.key?(relative_path)
|
|
64
|
+
|
|
65
|
+
result.extra << relative_path
|
|
66
|
+
remove_file(path) unless check
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_output_dir
|
|
71
|
+
FileUtils.mkdir_p(output_dir)
|
|
72
|
+
rescue SystemCallError => e
|
|
73
|
+
raise Error, "Could not create typed_enums output directory #{output_dir}: #{e.message}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_file(path, content)
|
|
77
|
+
FileUtils.mkdir_p(path.dirname)
|
|
78
|
+
path.write(content)
|
|
79
|
+
logger&.info("typed_enums: wrote #{path}")
|
|
80
|
+
rescue SystemCallError => e
|
|
81
|
+
raise Error, "Could not write generated file #{path}: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def remove_file(path)
|
|
85
|
+
path.delete
|
|
86
|
+
logger&.info("typed_enums: removed stale #{path}")
|
|
87
|
+
rescue SystemCallError => e
|
|
88
|
+
raise Error, "Could not remove stale generated file #{path}: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def generated_files
|
|
92
|
+
return [] unless output_dir.exist?
|
|
93
|
+
|
|
94
|
+
output_dir.glob("**/*").select do |path|
|
|
95
|
+
generated_file?(path)
|
|
96
|
+
end.sort
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def generated_file?(path)
|
|
100
|
+
path.file? && generated_extension?(path) && path.read.start_with?(GENERATED_MARKER)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def generated_extension?(path)
|
|
104
|
+
[".js", ".ts"].include?(path.extname) || path.to_s.end_with?(".d.ts")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def default_logger
|
|
108
|
+
defined?(Rails) ? Rails.logger : nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
rake_tasks do
|
|
8
|
+
load File.expand_path("tasks.rb", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
config.after_initialize do
|
|
12
|
+
TypedEnums::Railtie.install_development_hooks
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.install_development_hooks
|
|
16
|
+
return unless Rails.env.development?
|
|
17
|
+
|
|
18
|
+
if TypedEnums.configuration.auto_generate_in_development
|
|
19
|
+
Rails.application.reloader.to_prepare do
|
|
20
|
+
TypedEnums.generate
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return unless TypedEnums.configuration.watch_models_in_development
|
|
25
|
+
|
|
26
|
+
TypedEnums.watch
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypedEnums
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(scanner: ModelScanner.new)
|
|
6
|
+
@scanner = scanner
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def expected_files
|
|
10
|
+
javascript_file = Output::JavaScriptFile.new(model_groups:)
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
"enums.js" => javascript_file.render,
|
|
14
|
+
"enums.d.ts" => Output::TypeDeclarationFile.new(
|
|
15
|
+
model_groups:,
|
|
16
|
+
schema_hash: javascript_file.schema_hash
|
|
17
|
+
).render
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :scanner
|
|
24
|
+
|
|
25
|
+
def model_groups
|
|
26
|
+
@model_groups ||= scanner.call.group_by(&:model_export_name).sort.to_h
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
|
|
5
|
+
namespace :typed_enums do
|
|
6
|
+
desc "Generate JavaScript enum module and TypeScript declarations from Active Record enums"
|
|
7
|
+
task generate: :environment do
|
|
8
|
+
result = TypedEnums.generate
|
|
9
|
+
if result.conflict?
|
|
10
|
+
warn "typed_enums: refusing to overwrite existing non-generated files"
|
|
11
|
+
warn result.summary
|
|
12
|
+
abort "Move the files, delete them, or change config.output_dir."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if result.stale?
|
|
16
|
+
puts "typed_enums: generated files updated"
|
|
17
|
+
puts result.applied_summary
|
|
18
|
+
else
|
|
19
|
+
puts "typed_enums: generated files are current"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Check whether generated enum files are current"
|
|
24
|
+
task check: :environment do
|
|
25
|
+
result = TypedEnums.check
|
|
26
|
+
|
|
27
|
+
if result.conflict?
|
|
28
|
+
warn "typed_enums: existing non-generated files block enum generation"
|
|
29
|
+
warn result.summary
|
|
30
|
+
abort "Move the files, delete them, or change config.output_dir."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if result.stale?
|
|
34
|
+
warn "typed_enums: generated files are stale"
|
|
35
|
+
warn result.summary
|
|
36
|
+
abort "Run bin/rails typed_enums:generate"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "typed_enums: generated files are current"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "Watch Rails model files and regenerate enum files after saves"
|
|
43
|
+
task watch: :environment do
|
|
44
|
+
puts "typed_enums: watching app/models for enum changes"
|
|
45
|
+
TypedEnums::Watcher.run_foreground(logger: Rails.logger)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module TypedEnums
|
|
6
|
+
class Watcher
|
|
7
|
+
MODEL_FILE_PATTERN = /\.rb\z/
|
|
8
|
+
DEFAULT_LATENCY = 0.25
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def start(**options)
|
|
12
|
+
mutex.synchronize do
|
|
13
|
+
return instance if instance&.started?
|
|
14
|
+
|
|
15
|
+
@instance = new(**options)
|
|
16
|
+
@instance.start
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stop
|
|
21
|
+
mutex.synchronize do
|
|
22
|
+
instance&.stop
|
|
23
|
+
@instance = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def started?
|
|
28
|
+
instance&.started? || false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_foreground(**)
|
|
32
|
+
watcher = start(**)
|
|
33
|
+
sleep
|
|
34
|
+
rescue Interrupt
|
|
35
|
+
nil
|
|
36
|
+
ensure
|
|
37
|
+
watcher&.stop
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :instance
|
|
43
|
+
|
|
44
|
+
def mutex
|
|
45
|
+
@mutex ||= Mutex.new
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(root: Rails.root, logger: Rails.logger, listener_factory: nil, generator: TypedEnums)
|
|
50
|
+
@root = Pathname(root)
|
|
51
|
+
@logger = logger
|
|
52
|
+
@listener_factory = listener_factory
|
|
53
|
+
@generator = generator
|
|
54
|
+
@started = false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def start
|
|
58
|
+
return self if started?
|
|
59
|
+
|
|
60
|
+
unless watch_path.directory?
|
|
61
|
+
logger&.warn("typed_enums: model watch path does not exist: #{watch_path}")
|
|
62
|
+
return self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@listener = build_listener
|
|
66
|
+
listener.start
|
|
67
|
+
@started = true
|
|
68
|
+
logger&.info("typed_enums: watching #{watch_path}")
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop
|
|
73
|
+
listener&.stop
|
|
74
|
+
@started = false
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def started?
|
|
79
|
+
@started
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
attr_reader :root, :logger, :listener, :listener_factory, :generator
|
|
85
|
+
|
|
86
|
+
def watch_path
|
|
87
|
+
root.join("app/models")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_listener
|
|
91
|
+
factory = listener_factory || default_listener_factory
|
|
92
|
+
factory.to(watch_path.to_s, only: MODEL_FILE_PATTERN, latency: DEFAULT_LATENCY) do |modified, added, removed|
|
|
93
|
+
regenerate(modified:, added:, removed:)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def default_listener_factory
|
|
98
|
+
require "listen"
|
|
99
|
+
Listen
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def regenerate(modified:, added:, removed:)
|
|
103
|
+
changed_files = modified + added + removed
|
|
104
|
+
return if changed_files.empty?
|
|
105
|
+
|
|
106
|
+
logger&.info("typed_enums: model file changed, regenerating enum files")
|
|
107
|
+
reload_application
|
|
108
|
+
generate
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
logger&.error("typed_enums: watch regeneration failed: #{e.class}: #{e.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reload_application
|
|
114
|
+
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
115
|
+
|
|
116
|
+
reloader = Rails.application.reloader
|
|
117
|
+
reloader.reload! if reloader.respond_to?(:reload!)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generate
|
|
121
|
+
reloader = defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.reloader
|
|
122
|
+
return generator.generate(logger:) unless reloader
|
|
123
|
+
|
|
124
|
+
if reloader.respond_to?(:wrap)
|
|
125
|
+
reloader.wrap { generator.generate(logger:) }
|
|
126
|
+
else
|
|
127
|
+
generator.generate(logger:)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/typed_enums.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typed_enums/version"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "typed_enums/configuration"
|
|
6
|
+
require "typed_enums/error"
|
|
7
|
+
require "typed_enums/enum_definition"
|
|
8
|
+
require "typed_enums/naming/name_builder"
|
|
9
|
+
require "typed_enums/model_scanner"
|
|
10
|
+
require "typed_enums/output/javascript_file"
|
|
11
|
+
require "typed_enums/output/type_declaration_file"
|
|
12
|
+
require "typed_enums/registry"
|
|
13
|
+
require "typed_enums/output/writer/result"
|
|
14
|
+
require "typed_enums/output/writer"
|
|
15
|
+
require "typed_enums/watcher"
|
|
16
|
+
|
|
17
|
+
module TypedEnums
|
|
18
|
+
class << self
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield configuration
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_configuration!
|
|
28
|
+
@configuration = Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generate(logger: nil)
|
|
32
|
+
write(check: false, logger:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check(logger: nil)
|
|
36
|
+
write(check: true, logger:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def watch(logger: nil)
|
|
40
|
+
Watcher.start(logger: logger || default_logger)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop_watcher
|
|
44
|
+
Watcher.stop
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def write(check:, logger:)
|
|
50
|
+
Output::Writer.new(
|
|
51
|
+
output_dir: output_path,
|
|
52
|
+
expected_files: Registry.new.expected_files,
|
|
53
|
+
logger: logger || default_logger,
|
|
54
|
+
check:
|
|
55
|
+
).call
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def output_path
|
|
59
|
+
configured_path = Pathname(configuration.output_dir)
|
|
60
|
+
configured_path.absolute? ? configured_path : Rails.root.join(configured_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def default_logger
|
|
64
|
+
defined?(Rails) ? Rails.logger : nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
require "typed_enums/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: typed_enums
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ackermann
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: listen
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.5'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '3.5'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rails
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: rake
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - "~>"
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '13.0'
|
|
59
|
+
type: :development
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - "~>"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '13.0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: rspec
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - "~>"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.13'
|
|
73
|
+
type: :development
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '3.13'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: rubocop
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '1.60'
|
|
87
|
+
type: :development
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '1.60'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: rubocop-rails
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '2.25'
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '2.25'
|
|
108
|
+
description: typed_enums generates a framework-agnostic JavaScript enum module from
|
|
109
|
+
Rails model enums.
|
|
110
|
+
email:
|
|
111
|
+
- ghostkominfinity@gmail.com
|
|
112
|
+
executables: []
|
|
113
|
+
extensions: []
|
|
114
|
+
extra_rdoc_files: []
|
|
115
|
+
files:
|
|
116
|
+
- CHANGELOG.md
|
|
117
|
+
- LICENSE.txt
|
|
118
|
+
- README.md
|
|
119
|
+
- lib/generators/typed_enums/install_generator.rb
|
|
120
|
+
- lib/generators/typed_enums/templates/initializer.rb
|
|
121
|
+
- lib/typed_enums.rb
|
|
122
|
+
- lib/typed_enums/configuration.rb
|
|
123
|
+
- lib/typed_enums/enum_definition.rb
|
|
124
|
+
- lib/typed_enums/error.rb
|
|
125
|
+
- lib/typed_enums/model_scanner.rb
|
|
126
|
+
- lib/typed_enums/naming/name_builder.rb
|
|
127
|
+
- lib/typed_enums/output/javascript_file.rb
|
|
128
|
+
- lib/typed_enums/output/type_declaration_file.rb
|
|
129
|
+
- lib/typed_enums/output/writer.rb
|
|
130
|
+
- lib/typed_enums/output/writer/result.rb
|
|
131
|
+
- lib/typed_enums/railtie.rb
|
|
132
|
+
- lib/typed_enums/registry.rb
|
|
133
|
+
- lib/typed_enums/tasks.rb
|
|
134
|
+
- lib/typed_enums/version.rb
|
|
135
|
+
- lib/typed_enums/watcher.rb
|
|
136
|
+
homepage: https://github.com/AckermannTM/typed_enums
|
|
137
|
+
licenses:
|
|
138
|
+
- MIT
|
|
139
|
+
metadata:
|
|
140
|
+
source_code_uri: https://github.com/AckermannTM/typed_enums
|
|
141
|
+
changelog_uri: https://github.com/AckermannTM/typed_enums/blob/main/CHANGELOG.md
|
|
142
|
+
rubygems_mfa_required: 'true'
|
|
143
|
+
rdoc_options: []
|
|
144
|
+
require_paths:
|
|
145
|
+
- lib
|
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '3.2'
|
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: '0'
|
|
156
|
+
requirements: []
|
|
157
|
+
rubygems_version: 3.7.2
|
|
158
|
+
specification_version: 4
|
|
159
|
+
summary: Export Rails Active Record enums to JavaScript constants and TypeScript declaration
|
|
160
|
+
types.
|
|
161
|
+
test_files: []
|