typelizer 0.4.2 → 0.5.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 +11 -1
- data/README.md +156 -24
- data/lib/typelizer/config.rb +76 -58
- data/lib/typelizer/configuration.rb +159 -0
- data/lib/typelizer/contexts/scan_context.rb +20 -0
- data/lib/typelizer/contexts/writer_context.rb +89 -0
- data/lib/typelizer/dsl.rb +9 -11
- data/lib/typelizer/generator.rb +36 -16
- data/lib/typelizer/interface.rb +25 -11
- data/lib/typelizer/model_plugins/active_record.rb +61 -30
- data/lib/typelizer/property.rb +3 -2
- data/lib/typelizer/serializer_config_layer.rb +45 -0
- data/lib/typelizer/serializer_plugins/alba.rb +2 -1
- data/lib/typelizer/serializer_plugins/ams.rb +1 -1
- data/lib/typelizer/serializer_plugins/auto.rb +2 -2
- data/lib/typelizer/serializer_plugins/base.rb +3 -2
- data/lib/typelizer/serializer_plugins/oj_serializers.rb +2 -2
- data/lib/typelizer/serializer_plugins/panko.rb +1 -1
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer/writer.rb +37 -9
- data/lib/typelizer.rb +23 -11
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd7dab7ab6fabb3be94f1ca6bd6ea98337ea5b6c52811b413ed28f0804e5bd64
|
4
|
+
data.tar.gz: e1dc88644daebf2c3c34ded6cee520963c009568f45cde70e60d1276b8a859c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a910d1e73e070287d5c01d3d10f7b8c181b81a78474f4cf53f45c774fea5362d1498345a16f495630e10637f4314548266c779893dccb4dad2d8b95c15cf09da
|
7
|
+
data.tar.gz: 214105e604e21a4901ff437fbf26d8d19886be704c10b8981a26025bfc17fde3046fb1c9e90bf3db0f9a4abd4a954940d6c4168672e58136ff90c9961bcefc38
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning].
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.5.0] - 2025-09-01
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Support for multiple output writers: emit several variants (e.g., snake_case and camelCase) in parallel. ([@supdex])
|
15
|
+
- Support for Rails' Attributes API. ([@skryukov])
|
16
|
+
|
10
17
|
## [0.4.2] - 2025-06-23
|
11
18
|
|
12
19
|
### Added
|
@@ -176,10 +183,13 @@ and this project adheres to [Semantic Versioning].
|
|
176
183
|
[@NOX73]: https://github.com/NOX73
|
177
184
|
[@okuramasafumi]: https://github.com/okuramasafumi
|
178
185
|
[@patvice]: https://github.com/patvice
|
186
|
+
[@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
|
179
187
|
[@skryukov]: https://github.com/skryukov
|
188
|
+
[@supdex]: https://github.com/supdex
|
180
189
|
[@ventsislaf]: https://github.com/ventsislaf
|
181
190
|
|
182
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
191
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.0...HEAD
|
192
|
+
[0.5.0]: https://github.com/skryukov/typelizer/compare/v0.4.2...v0.5.0
|
183
193
|
[0.4.2]: https://github.com/skryukov/typelizer/compare/v0.4.1...v0.4.2
|
184
194
|
[0.4.1]: https://github.com/skryukov/typelizer/compare/v0.4.0...v0.4.1
|
185
195
|
[0.4.0]: https://github.com/skryukov/typelizer/compare/v0.3.0...v0.4.0
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/typelizer)
|
4
4
|
|
5
|
-
Typelizer
|
5
|
+
Typelizer generates TypeScript types from your Ruby serializers. It supports multiple serializer libraries and a flexible, layered configuration model so you can keep your backend and frontend in sync without hand‑maintaining types.
|
6
6
|
|
7
7
|
## Table of Contents
|
8
8
|
|
@@ -16,8 +16,8 @@ Typelizer is a Ruby gem that automatically generates TypeScript interfaces from
|
|
16
16
|
- [Automatic Generation in Development](#automatic-generation-in-development)
|
17
17
|
- [Disabling Typelizer](#disabling-typelizer)
|
18
18
|
- [Configuration](#configuration)
|
19
|
-
- [Global Configuration](#
|
20
|
-
- [
|
19
|
+
- [Global Configuration](#simple-configuration)
|
20
|
+
- [Writers (multiple outputs)](#defining-multiple-writers)
|
21
21
|
- [Per-Serializer Configuration](#per-serializer-configuration)
|
22
22
|
- [Credits](#credits)
|
23
23
|
- [License](#license)
|
@@ -29,8 +29,10 @@ Typelizer is a Ruby gem that automatically generates TypeScript interfaces from
|
|
29
29
|
## Features
|
30
30
|
|
31
31
|
- Automatic TypeScript interface generation
|
32
|
-
-
|
33
|
-
-
|
32
|
+
- Infers types from database columns and associations, with support for the Attributes API
|
33
|
+
- Supports multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`, `Panko::Serializer`)
|
34
|
+
- File watching with automatic regeneration in development
|
35
|
+
- Multiple output writers: emit several variants (e.g., snake_case and camelCase) in parallel
|
34
36
|
|
35
37
|
## Installation
|
36
38
|
|
@@ -211,8 +213,6 @@ Sometimes we want to use Typelizer only with manual generation. To disable Typel
|
|
211
213
|
|
212
214
|
## Configuration
|
213
215
|
|
214
|
-
### Global Configuration
|
215
|
-
|
216
216
|
Typelizer provides several global configuration options:
|
217
217
|
|
218
218
|
```ruby
|
@@ -226,13 +226,159 @@ Typelizer.logger = Logger.new($stdout, level: :info)
|
|
226
226
|
Typelizer.listen = nil
|
227
227
|
```
|
228
228
|
|
229
|
-
###
|
229
|
+
### Configuration Layers
|
230
|
+
|
231
|
+
Typelizer uses a hierarchical system to resolve settings. Settings are applied in the following order of precedence, where higher numbers override lower ones:
|
232
|
+
|
233
|
+
1. **Per-Serializer Overrides**: Settings defined using `typelizer_config` directly within a serializer class. This layer has the highest priority.
|
234
|
+
2. **Writer-Specific Settings**: Settings defined within a `config.writer(:name) { ... }` block.
|
235
|
+
3. **Global Settings**: Application-wide settings defined by direct assignment (e.g., `config.comments = true`) within the `Typelizer.configure` block.
|
236
|
+
4. **Library Defaults**: The gem's built-in default values.
|
237
|
+
|
238
|
+
### Simple Configuration (Single Output)
|
239
|
+
|
240
|
+
For most apps, a single output is enough. All settings in an initializer apply to the `:default` writer and also act as a global baseline.
|
241
|
+
|
242
|
+
- Settings like `dirs` are considered **Global** and establish a baseline for all writers.
|
243
|
+
- Settings like `output_dir` or `comments` configure the implicit **`:default` writer**.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
# config/initializers/typelizer.rb
|
247
|
+
Typelizer.configure do |config|
|
248
|
+
# This is a GLOBAL SETTING. It applies to ALL writers.
|
249
|
+
config.dirs = [Rails.root.join("app/serializers")]
|
250
|
+
|
251
|
+
# This setting configures the :default writer and ALSO acts as a global setting.
|
252
|
+
config.output_dir = "app/javascript/types/generated"
|
253
|
+
config.comments = true
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
### Defining Multiple Writers
|
258
|
+
|
259
|
+
The multi-writer system allows for the generation of multiple, distinct TypeScript outputs. Each output is managed by a named writer with an isolated configuration.
|
260
|
+
|
261
|
+
|
262
|
+
#### Writer Inheritance Rules
|
263
|
+
|
264
|
+
- By default, a new writer inherits its base settings from the Global Settings.
|
265
|
+
- To inherit from another existing writer, use the `from:` option.
|
266
|
+
|
267
|
+
|
268
|
+
**A Note on the :default Writer and Inheritance**
|
269
|
+
- You usually do not need to declare `writer(:default)`. The implicit default writer automatically uses your global settings.
|
270
|
+
- Declare `writer(:default)` when you want to apply specific overrides to it that should not be inherited by other new writers. This provides a way to separate your application's global baseline from settings that are truly unique to the default output
|
271
|
+
|
272
|
+
#### Example of the distinction:
|
273
|
+
```ruby
|
274
|
+
Typelizer.configure do |config|
|
275
|
+
# === Global Setting ===
|
276
|
+
# `comments: true` applies to :default and will be inherited by :camel_case.
|
277
|
+
config.comments = true
|
278
|
+
|
279
|
+
# === Default-Writer-Only Setting ===
|
280
|
+
# `prefer_double_quotes: true` applies ONLY to the :default writer.
|
281
|
+
# It is NOT a global setting and will NOT be inherited by :camel_case.
|
282
|
+
config.writer(:default) do |c|
|
283
|
+
c.prefer_double_quotes = true
|
284
|
+
end
|
285
|
+
|
286
|
+
# === New Writer Definition ===
|
287
|
+
config.writer(:camel_case) do |c|
|
288
|
+
c.output_dir = "app/javascript/types/camel_case"
|
289
|
+
# This writer inherits `comments: true` from globals.
|
290
|
+
# It does NOT inherit `prefer_double_quotes: true` from the :default writer's block.
|
291
|
+
# Its `prefer_double_quotes` will be `false` (the library default).
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
#### Configuring Writers
|
297
|
+
You can define writers either inside the configure block or directly on the Typelizer module.
|
298
|
+
|
299
|
+
1. **Inside the configure block**
|
300
|
+
|
301
|
+
This is the approach for keeping all configuration centralized.
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
# config/initializers/typelizer.rb
|
305
|
+
Typelizer.configure do |config|
|
306
|
+
# ... global settings ...
|
307
|
+
|
308
|
+
config.writer(:camel_case) do |c|
|
309
|
+
c.output_dir = "app/javascript/types/camel_case"
|
310
|
+
c.properties_transformer = ->(properties) { # ... transform ... }
|
311
|
+
end
|
312
|
+
|
313
|
+
config.writer(:admin, from: :camel_case) do |c|
|
314
|
+
c.output_dir = "app/javascript/types/admin"
|
315
|
+
c.null_strategy = :optional
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
2. Top-Level Helper
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
Typelizer.writer(:admin, from: :default) do |c|
|
324
|
+
c.output_dir = Rails.root.join("app/javascript/types/admin")
|
325
|
+
c.prefer_double_quotes = true
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
#### Comprehensive Example
|
330
|
+
This example configures three distinct outputs, demonstrating all inheritance mechanisms.
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
# config/initializers/typelizer.rb
|
334
|
+
Typelizer.configure do |config|
|
335
|
+
# === 1. Global Settings (Baseline for ALL writers) ===
|
336
|
+
config.comments = true
|
337
|
+
config.dirs = [Rails.root.join("app/serializers")]
|
338
|
+
|
339
|
+
# === 2. The :default writer (snake_case output) ===
|
340
|
+
config.writer(:default) do |c|
|
341
|
+
c.output_dir = "app/javascript/types/snake_case"
|
342
|
+
end
|
343
|
+
|
344
|
+
# === 3. A new :camel_case writer ===
|
345
|
+
# Inherits `comments: true` and `dirs` from the Global Settings.
|
346
|
+
config.writer(:camel_case) do |c|
|
347
|
+
c.output_dir = "app/javascript/types/camel_case"
|
348
|
+
c.properties_transformer = lambda do |properties|
|
349
|
+
properties.map { |prop| prop.with_overrides(name: prop.name.to_s.camelize(:lower)) }
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# === 4. An "admin" writer that clones :camel_case ===
|
354
|
+
# Use `from:` to explicitly inherit another writer's complete configuration.
|
355
|
+
config.writer(:admin, from: :camel_case) do |c|
|
356
|
+
c.output_dir = "app/javascript/types/admin"
|
357
|
+
# This writer inherits the properties_transformer from :camel_case.
|
358
|
+
c.null_strategy = :optional
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### Per-serializer configuration
|
364
|
+
|
365
|
+
Use `typelizer_config` within a serializer class to apply overrides with the highest possible priority.
|
366
|
+
These settings will supersede any conflicting settings from the active writer, global settings, or library defaults.
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
class PostResource < ApplicationResource
|
370
|
+
typelizer_config do |c|
|
371
|
+
c.null_strategy = :nullable_and_optional
|
372
|
+
c.plugin_configs = { alba: { ts_mapper: { "UUID" => { type: :string } } } }
|
373
|
+
end
|
374
|
+
end
|
375
|
+
```
|
230
376
|
|
231
|
-
|
377
|
+
### Option reference
|
232
378
|
|
233
379
|
```ruby
|
234
380
|
Typelizer.configure do |config|
|
235
|
-
#
|
381
|
+
# Name to type mapping for serializer classes
|
236
382
|
config.serializer_name_mapper = ->(serializer) { ... }
|
237
383
|
|
238
384
|
# Maps serializers to their corresponding model classes
|
@@ -290,20 +436,6 @@ Typelizer.configure do |config|
|
|
290
436
|
end
|
291
437
|
```
|
292
438
|
|
293
|
-
### Per-Serializer Configuration
|
294
|
-
|
295
|
-
You can also configure Typelizer on a per-serializer basis:
|
296
|
-
|
297
|
-
```ruby
|
298
|
-
class PostResource < ApplicationResource
|
299
|
-
typelizer_config do |config|
|
300
|
-
config.type_mapping = config.type_mapping.merge(jsonb: "Record<string, undefined>", ... )
|
301
|
-
config.null_strategy = :nullable
|
302
|
-
# ...
|
303
|
-
end
|
304
|
-
end
|
305
|
-
```
|
306
|
-
|
307
439
|
## Credits
|
308
440
|
|
309
441
|
Typelizer is inspired by [types_from_serializers](https://github.com/ElMassimo/types_from_serializers).
|
data/lib/typelizer/config.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
1
5
|
module Typelizer
|
2
|
-
TYPE_MAPPING =
|
6
|
+
TYPE_MAPPING = Hash.new(:unknown).update(
|
3
7
|
boolean: :boolean,
|
4
8
|
date: :string,
|
5
9
|
datetime: :string,
|
10
|
+
time: :string,
|
6
11
|
decimal: :number,
|
12
|
+
float: :number,
|
7
13
|
integer: :number,
|
8
14
|
string: :string,
|
9
15
|
text: :string,
|
10
16
|
citext: :string,
|
11
17
|
uuid: :string
|
12
|
-
|
13
|
-
types.default = :unknown
|
14
|
-
end
|
18
|
+
).freeze
|
15
19
|
|
16
|
-
|
20
|
+
DEFAULT_TYPES_GLOBAL = %w[Array Date Record File FileList].freeze
|
21
|
+
|
22
|
+
Config = Struct.new(
|
17
23
|
:serializer_name_mapper,
|
18
24
|
:serializer_model_mapper,
|
19
25
|
:properties_transformer,
|
@@ -31,59 +37,71 @@ module Typelizer
|
|
31
37
|
:comments,
|
32
38
|
:prefer_double_quotes,
|
33
39
|
keyword_init: true
|
34
|
-
)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
40
|
+
)
|
41
|
+
|
42
|
+
# Immutable configuration object for a single writer
|
43
|
+
#
|
44
|
+
# Use .build to construct from defaults, and #with_overrides to copy with overrides.
|
45
|
+
class Config
|
46
|
+
# Returns library defaults (built-in) for building a Config.
|
47
|
+
# This method creates a fresh Hash each time to avoid sharing mutable state
|
48
|
+
# across builds
|
49
|
+
def self.defaults
|
50
|
+
{
|
51
|
+
serializer_name_mapper: lambda do |serializer|
|
52
|
+
name = serializer.name.to_s
|
53
|
+
|
54
|
+
return name if name.empty?
|
55
|
+
|
56
|
+
# remove only the end of the line
|
57
|
+
name.sub(/(Serializer|Resource)\z/, "")
|
58
|
+
end,
|
59
|
+
|
60
|
+
serializer_model_mapper: lambda do |serializer|
|
61
|
+
base_class = serializer_name_mapper.call(serializer)
|
62
|
+
Object.const_get(base_class) if Object.const_defined?(base_class)
|
63
|
+
end,
|
64
|
+
|
65
|
+
model_plugin: ModelPlugins::Auto,
|
66
|
+
serializer_plugin: SerializerPlugins::Auto,
|
67
|
+
plugin_configs: {}.freeze,
|
68
|
+
type_mapping: TYPE_MAPPING,
|
69
|
+
null_strategy: :nullable,
|
70
|
+
inheritance_strategy: :none,
|
71
|
+
associations_strategy: :database,
|
72
|
+
comments: false,
|
73
|
+
prefer_double_quotes: false,
|
74
|
+
|
75
|
+
output_dir: -> { default_output_dir },
|
76
|
+
|
77
|
+
types_import_path: "@/types",
|
78
|
+
types_global: DEFAULT_TYPES_GLOBAL,
|
79
|
+
properties_transformer: nil,
|
80
|
+
verbatim_module_syntax: false
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.build(**overrides)
|
85
|
+
new(**defaults.merge(overrides))
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.default_output_dir
|
89
|
+
root_path = defined?(Rails) ? Rails.root : Pathname.pwd
|
90
|
+
js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript"
|
91
|
+
|
92
|
+
root_path.join(js_root, "types/serializers")
|
93
|
+
end
|
94
|
+
|
95
|
+
def with_overrides(**overrides)
|
96
|
+
props = to_h
|
97
|
+
props.merge!(overrides) unless overrides.empty?
|
98
|
+
|
99
|
+
self.class.new(**props)
|
100
|
+
end
|
101
|
+
|
102
|
+
def output_dir
|
103
|
+
v = self[:output_dir]
|
104
|
+
v.respond_to?(:call) ? v.call : v
|
86
105
|
end
|
87
|
-
end
|
88
106
|
end
|
89
107
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module Typelizer
|
7
|
+
# Central registry for Typelizer multi-writer configuration
|
8
|
+
#
|
9
|
+
# Responsibilities:
|
10
|
+
# - Holds immutable Config per writer name (always includes :default)
|
11
|
+
# - Maintain flat DSL setters for :default (e.g., config.output_dir = ...)
|
12
|
+
# - Allows defining/updating named writers via writer(:name) { |cfg| ... }
|
13
|
+
# - Check unique output_dir across writers to avoid file conflicts
|
14
|
+
#
|
15
|
+
# Config priorities:
|
16
|
+
# - WriterContext merges in order: library defaults < global_settings < writer < DSL inheritance
|
17
|
+
# - global_settings are only updated by flat setters, not by writer(:default) blocks
|
18
|
+
class Configuration
|
19
|
+
DEFAULT_WRITER_NAME = :default
|
20
|
+
|
21
|
+
attr_accessor :dirs, :reject_class, :listen
|
22
|
+
attr_reader :writers, :global_settings
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@dirs = []
|
26
|
+
@reject_class = ->(serializer:) { false }
|
27
|
+
@listen = nil
|
28
|
+
|
29
|
+
default = Config.build
|
30
|
+
|
31
|
+
@writers = {DEFAULT_WRITER_NAME => default.freeze}
|
32
|
+
@global_settings = {}
|
33
|
+
|
34
|
+
@writer_output_dirs = {DEFAULT_WRITER_NAME => normalize_path(default.output_dir)}
|
35
|
+
@used_output_dirs = Set.new(@writer_output_dirs.values.compact)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines or updates a writer configuration.
|
39
|
+
#
|
40
|
+
# Inherits from the existing writer config (or global_settigns if absent), yields a mutable copy,
|
41
|
+
# then freezes and stores it. output_dir is unique and mandatory
|
42
|
+
# Also accepts "from" argument, which allows us to inherit configuration from any writer
|
43
|
+
def writer(name = DEFAULT_WRITER_NAME, from: nil, &block)
|
44
|
+
writer_name = normalize_writer_name(name)
|
45
|
+
|
46
|
+
# Inherit from existing writer config or from "from" attribute or global (flatt) config
|
47
|
+
base_config =
|
48
|
+
if @writers.key?(writer_name)
|
49
|
+
@writers[writer_name]
|
50
|
+
elsif from && @writers.key?(from.to_sym)
|
51
|
+
@writers[from.to_sym]
|
52
|
+
else
|
53
|
+
Config.build(**@global_settings)
|
54
|
+
end
|
55
|
+
|
56
|
+
mutable_config = base_config.with_overrides
|
57
|
+
|
58
|
+
block&.call(mutable_config)
|
59
|
+
|
60
|
+
# Register output directory for uniqueness checking
|
61
|
+
register_output_dir!(writer_name, mutable_config.output_dir)
|
62
|
+
|
63
|
+
# Store and return frozen configuration
|
64
|
+
@writers[writer_name] = mutable_config.freeze
|
65
|
+
end
|
66
|
+
|
67
|
+
def writer_config(name = DEFAULT_WRITER_NAME)
|
68
|
+
@writers.fetch((name || DEFAULT_WRITER_NAME).to_sym)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Reset writers and keep only `default` writer
|
72
|
+
def reset_writers!
|
73
|
+
@writers.keep_if { |key, _| key == DEFAULT_WRITER_NAME }
|
74
|
+
|
75
|
+
@writer_output_dirs = {
|
76
|
+
DEFAULT_WRITER_NAME => normalize_path(@writers[DEFAULT_WRITER_NAME].output_dir)
|
77
|
+
}
|
78
|
+
|
79
|
+
@used_output_dirs = Set.new(@writer_output_dirs.values.compact)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Setters and readers to Writer(:default) config
|
85
|
+
# Keep the "flat" setters for the :default writer, for example:
|
86
|
+
# config.output_dir = ...
|
87
|
+
# config.prefer_double_quotes = true
|
88
|
+
def method_missing(name, *args, &block)
|
89
|
+
name = name.to_s
|
90
|
+
config_key = normalize_method_name(name)
|
91
|
+
|
92
|
+
# Setters
|
93
|
+
if name.end_with?("=") && args.length.positive?
|
94
|
+
return super unless config_attribute?(config_key)
|
95
|
+
|
96
|
+
val = args.first
|
97
|
+
new_default = @writers[DEFAULT_WRITER_NAME].with_overrides(config_key => val)
|
98
|
+
|
99
|
+
register_output_dir!(DEFAULT_WRITER_NAME, new_default.output_dir) if config_key == :output_dir
|
100
|
+
|
101
|
+
@writers[DEFAULT_WRITER_NAME] = new_default.freeze
|
102
|
+
|
103
|
+
return @global_settings[config_key] = val
|
104
|
+
end
|
105
|
+
|
106
|
+
# Readers
|
107
|
+
return @writers[DEFAULT_WRITER_NAME].public_send(config_key) if args.empty? && config_attribute?(config_key)
|
108
|
+
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
def respond_to_missing?(name, include_private = false)
|
113
|
+
str = name.to_s
|
114
|
+
key = normalize_method_name(str)
|
115
|
+
(config_attribute?(key) && (str.end_with?("=") || true)) || super
|
116
|
+
end
|
117
|
+
|
118
|
+
# Normalizes and validates writer name
|
119
|
+
def normalize_writer_name(name)
|
120
|
+
writer_name = (name || DEFAULT_WRITER_NAME).to_sym
|
121
|
+
|
122
|
+
raise ArgumentError, "Writer name cannot be empty" if writer_name.to_s.strip.empty?
|
123
|
+
|
124
|
+
writer_name
|
125
|
+
end
|
126
|
+
|
127
|
+
# Validates and registers output directory for uniqueness across writers
|
128
|
+
def register_output_dir!(writer_name, dir)
|
129
|
+
raise ArgumentError, "output_dir must be configured for writer :#{writer_name}" if dir.to_s.strip.empty?
|
130
|
+
|
131
|
+
path = normalize_path(dir)
|
132
|
+
|
133
|
+
current = @writer_output_dirs[writer_name]
|
134
|
+
return if current == path
|
135
|
+
|
136
|
+
if @writer_output_dirs.any? { |k, v| k != writer_name && v == path }
|
137
|
+
holder = @writer_output_dirs.key(path)
|
138
|
+
|
139
|
+
raise ArgumentError, "output_dir '#{path}' is already in use by writer :#{holder}"
|
140
|
+
end
|
141
|
+
|
142
|
+
@used_output_dirs.delete(current) if current
|
143
|
+
@writer_output_dirs[writer_name] = path
|
144
|
+
@used_output_dirs << path
|
145
|
+
end
|
146
|
+
|
147
|
+
def normalize_path(dir)
|
148
|
+
Pathname(dir).expand_path.to_s
|
149
|
+
end
|
150
|
+
|
151
|
+
def normalize_method_name(name)
|
152
|
+
name.to_s.chomp("=").to_sym
|
153
|
+
end
|
154
|
+
|
155
|
+
def config_attribute?(name)
|
156
|
+
Config.members.include?(name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
# Builds a minimal plugin used during scan time
|
5
|
+
class ScanContext
|
6
|
+
class InvalidOperationError < StandardError; end
|
7
|
+
|
8
|
+
# Interface creation is not available during DSL scanning phase (TracePoint)
|
9
|
+
def self.interface_for(serializer_class)
|
10
|
+
class_name = serializer_class&.name || "unknown class"
|
11
|
+
raise InvalidOperationError,
|
12
|
+
"Interface creation is not allowed during DSL scan (#{class_name})"
|
13
|
+
end
|
14
|
+
|
15
|
+
# just in case, if we call ScanContext like an object
|
16
|
+
def interface_for(serializer_class)
|
17
|
+
self.class.interface_for(serializer_class)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
# Context for a single writer during a generation pass.
|
5
|
+
# - Caches one Interface per serializer class (prevents duplicates/loops)
|
6
|
+
# - Computes per-serializer effective Config:
|
7
|
+
# library defaults < global (flat setters) < writer < DSL (parent → child)
|
8
|
+
class WriterContext
|
9
|
+
attr_reader :writer_config, :writer_name
|
10
|
+
|
11
|
+
def initialize(writer_name: nil, configuration: Typelizer.configuration)
|
12
|
+
@configuration = configuration
|
13
|
+
@writer_name = (writer_name || Configuration::DEFAULT_WRITER_NAME).to_sym
|
14
|
+
@writer_config = configuration.writer_config(@writer_name)
|
15
|
+
|
16
|
+
@interface_cache = {}
|
17
|
+
@config_cache = {}
|
18
|
+
@dsl_cache = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a memoized Interface for the given serializer class within this writer context
|
22
|
+
# Guarantees a single Interface instance per serializer (in this context), which:
|
23
|
+
# - preserves object identity across associations,
|
24
|
+
# - prevents infinite loops on cyclic relations,
|
25
|
+
# - and avoids redundant recomputation
|
26
|
+
# The cache is scoped to WriterContext (i.e., per writer and per generation run)
|
27
|
+
def interface_for(serializer_class)
|
28
|
+
raise ArgumentError, "Serializer class cannot be nil" if serializer_class.nil?
|
29
|
+
|
30
|
+
@interface_cache[serializer_class] ||= Interface.new(
|
31
|
+
serializer: serializer_class,
|
32
|
+
context: self
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Resolves the effective configuration for a serializer class by merging
|
37
|
+
# configuration layers in priority order:
|
38
|
+
# Library defaults
|
39
|
+
# Global configuration settings
|
40
|
+
# Writer-specific configuration
|
41
|
+
# DSL configuration with inheritance (highest priority)
|
42
|
+
def config_for(serializer_class)
|
43
|
+
raise ArgumentError, "Serializer class cannot be nil" unless serializer_class
|
44
|
+
|
45
|
+
@config_cache[serializer_class] ||= build_config(serializer_class)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Builds the correct configuration by merging all configuration layers
|
51
|
+
def build_config(serializer_class)
|
52
|
+
global_settings = @configuration.global_settings
|
53
|
+
writer_settings = @writer_config.to_h
|
54
|
+
dsl_settings = dsl_config_for(serializer_class)
|
55
|
+
|
56
|
+
# Merge in priority order: global < writer < DSL
|
57
|
+
merged_config = deep_merge(global_settings, writer_settings)
|
58
|
+
merged_config = deep_merge(merged_config, dsl_settings)
|
59
|
+
|
60
|
+
Config.build(**merged_config).freeze
|
61
|
+
end
|
62
|
+
|
63
|
+
def dsl_config_for(klass)
|
64
|
+
return @dsl_cache[klass] if @dsl_cache.key?(klass)
|
65
|
+
|
66
|
+
# Recursively get the parent's DSL config. If no parent or parent is not
|
67
|
+
# a Typelizer serializer, the base is an empty hash.
|
68
|
+
parent_dsl = (parent = klass.superclass).respond_to?(:typelizer_config) ? dsl_config_for(parent) : {}
|
69
|
+
|
70
|
+
# Get this class's own local overrides.
|
71
|
+
local_dsl = klass.respond_to?(:typelizer_config) ? klass.typelizer_config.to_h : {}
|
72
|
+
|
73
|
+
@dsl_cache[klass] = deep_merge(parent_dsl, local_dsl).freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
def deep_merge(hash_one, hash_two)
|
77
|
+
# If Active Support's `deep_merge` exists, use it
|
78
|
+
return hash_one.deep_merge(hash_two) if hash_one.respond_to?(:deep_merge)
|
79
|
+
|
80
|
+
return hash_one if hash_one == hash_two
|
81
|
+
return hash_one if hash_two.empty?
|
82
|
+
return hash_two if hash_one.empty?
|
83
|
+
|
84
|
+
hash_one.merge(hash_two) do |_, old_v, new_v|
|
85
|
+
(old_v.is_a?(Hash) && new_v.is_a?(Hash)) ? deep_merge(old_v, new_v) : new_v
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/typelizer/dsl.rb
CHANGED
@@ -14,18 +14,16 @@ module Typelizer
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
|
-
def typelizer_config
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@
|
25
|
-
end
|
17
|
+
def typelizer_config(&block)
|
18
|
+
# Lazily initializes and memoizes the hash for local overrides at the class level.
|
19
|
+
# This ensures that all subsequent DSL calls for this specific serializer class
|
20
|
+
# modify the same single hash, allowing settings to be accumulated
|
21
|
+
@serializer_overrides ||= {}
|
22
|
+
|
23
|
+
@config_layer ||= SerializerConfigLayer.new(@serializer_overrides)
|
24
|
+
@config_layer.instance_eval(&block) if block
|
26
25
|
|
27
|
-
|
28
|
-
@typelizer_interface ||= Interface.new(serializer: self)
|
26
|
+
@config_layer
|
29
27
|
end
|
30
28
|
|
31
29
|
# save association of serializer to model
|
data/lib/typelizer/generator.rb
CHANGED
@@ -6,24 +6,26 @@ module Typelizer
|
|
6
6
|
new.call(**args)
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize(config = Typelizer::Config)
|
10
|
-
@config = config
|
11
|
-
@writer = Writer.new
|
12
|
-
end
|
13
|
-
|
14
|
-
attr_reader :config, :writer
|
15
|
-
|
16
9
|
def call(force: false)
|
17
|
-
return unless Typelizer.enabled?
|
10
|
+
return [] unless Typelizer.enabled?
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
found_interfaces
|
22
|
-
end
|
12
|
+
# plugin scan per run cache
|
13
|
+
@scan_plugin_cache = {}
|
23
14
|
|
24
|
-
def interfaces
|
25
15
|
read_serializers
|
26
|
-
target_serializers
|
16
|
+
serializers = target_serializers
|
17
|
+
|
18
|
+
# For each writer, build a dedicated WriterContext. The context holds that writer's
|
19
|
+
# configuration and resolves the effective Config for every Interface (per serializer)
|
20
|
+
# by merging global, writer, and per-serializer (DSL) overrides
|
21
|
+
Typelizer.configuration.writers.each do |writer_name, writer_config|
|
22
|
+
context = WriterContext.new(writer_name: writer_name)
|
23
|
+
interfaces = serializers.map { |klass| context.interface_for(klass) }
|
24
|
+
|
25
|
+
Writer.new(writer_config).call(interfaces, force: force)
|
26
|
+
end
|
27
|
+
|
28
|
+
serializers
|
27
29
|
end
|
28
30
|
|
29
31
|
private
|
@@ -44,9 +46,10 @@ module Typelizer
|
|
44
46
|
files ||= Typelizer.dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }
|
45
47
|
files.each do |file|
|
46
48
|
trace = TracePoint.new(:call) do |tp|
|
47
|
-
next unless tp.self.is_a?(Class) && tp.self.respond_to?(:
|
49
|
+
next unless tp.self.is_a?(Class) && tp.self.respond_to?(:typelizer_config)
|
48
50
|
|
49
|
-
serializer_plugin = tp.self
|
51
|
+
serializer_plugin = build_scan_plugin_for(tp.self)
|
52
|
+
next unless serializer_plugin
|
50
53
|
|
51
54
|
if tp.callee_id.in?(serializer_plugin.methods_to_typelize)
|
52
55
|
type, attrs = tp.self.keyless_type
|
@@ -61,5 +64,22 @@ module Typelizer
|
|
61
64
|
trace.disable
|
62
65
|
end
|
63
66
|
end
|
67
|
+
|
68
|
+
# Builds a minimal plugin instance used only during scan time for TracePoint
|
69
|
+
def build_scan_plugin_for(serializer_klass)
|
70
|
+
return @scan_plugin_cache[serializer_klass] if @scan_plugin_cache&.key?(serializer_klass)
|
71
|
+
|
72
|
+
base = Typelizer.configuration.writer_config(:default)
|
73
|
+
local_configuration = serializer_klass.typelizer_config.to_h.slice(:serializer_plugin, :plugin_configs)
|
74
|
+
cfg = base.with_overrides(**local_configuration)
|
75
|
+
|
76
|
+
@scan_plugin_cache[serializer_klass] = cfg.serializer_plugin.new(
|
77
|
+
serializer: serializer_klass,
|
78
|
+
config: cfg,
|
79
|
+
context: Typelizer::ScanContext
|
80
|
+
)
|
81
|
+
rescue NameError
|
82
|
+
nil
|
83
|
+
end
|
64
84
|
end
|
65
85
|
end
|
data/lib/typelizer/interface.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
1
|
module Typelizer
|
2
2
|
class Interface
|
3
|
-
attr_reader :serializer, :
|
3
|
+
attr_reader :serializer, :context
|
4
|
+
|
5
|
+
def initialize(serializer:, context:)
|
6
|
+
@serializer = serializer
|
7
|
+
@context = context
|
8
|
+
end
|
4
9
|
|
5
10
|
def config
|
6
|
-
serializer
|
11
|
+
context.config_for(serializer)
|
7
12
|
end
|
8
13
|
|
9
|
-
def
|
10
|
-
@
|
11
|
-
|
14
|
+
def serializer_plugin
|
15
|
+
@serializer_plugin ||= config.serializer_plugin.new(
|
16
|
+
serializer: serializer,
|
17
|
+
config: config,
|
18
|
+
context: context
|
19
|
+
)
|
12
20
|
end
|
13
21
|
|
14
22
|
def inline?
|
@@ -69,12 +77,14 @@ module Typelizer
|
|
69
77
|
|
70
78
|
def parent_interface
|
71
79
|
return if config.inheritance_strategy == :none
|
72
|
-
return unless serializer.superclass.respond_to?(:typelizer_interface)
|
73
80
|
|
74
|
-
|
75
|
-
return
|
81
|
+
parent_class = serializer.superclass
|
82
|
+
return unless parent_class.respond_to?(:typelizer_config)
|
83
|
+
|
84
|
+
parent_interface = context.interface_for(parent_class)
|
85
|
+
return if parent_interface.empty?
|
76
86
|
|
77
|
-
|
87
|
+
parent_interface
|
78
88
|
end
|
79
89
|
|
80
90
|
def imports
|
@@ -140,8 +150,12 @@ module Typelizer
|
|
140
150
|
def model_class
|
141
151
|
return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
|
142
152
|
|
143
|
-
config
|
144
|
-
|
153
|
+
# Execute the `serializer_model_mapper` lambda in the context of the `config` object
|
154
|
+
# This giving a possibility to access other lambdas, for example, `serializer_name_mapper`
|
155
|
+
config.instance_exec(serializer, &config.serializer_model_mapper)
|
156
|
+
rescue NameError => e
|
157
|
+
Typelizer.logger.debug("model_mapper failed for serializer #{serializer.name}: #{e.class}: #{e.message}")
|
158
|
+
|
145
159
|
nil
|
146
160
|
end
|
147
161
|
|
@@ -9,33 +9,61 @@ module Typelizer
|
|
9
9
|
attr_reader :model_class, :config
|
10
10
|
|
11
11
|
def infer_types(prop)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
12
|
+
infer_types_for_association(prop) ||
|
13
|
+
infer_types_for_column(prop) ||
|
14
|
+
infer_types_for_attribute(prop)
|
15
|
+
|
16
|
+
prop
|
17
|
+
end
|
18
|
+
|
19
|
+
def comment_for(prop)
|
20
|
+
column = model_class&.columns_hash&.dig(prop.column_name.to_s)
|
21
|
+
return nil unless column
|
22
|
+
|
23
|
+
prop.comment = column.comment
|
24
|
+
end
|
25
|
+
|
26
|
+
def enum_for(prop)
|
27
|
+
return unless model_class&.defined_enums&.key?(prop.column_name.to_s)
|
28
|
+
|
29
|
+
prop.enum = model_class.defined_enums[prop.column_name.to_s].keys
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def infer_types_for_association(prop)
|
35
|
+
association = model_class&.reflect_on_association(prop.column_name.to_sym)
|
36
|
+
return nil unless association
|
37
|
+
|
38
|
+
case association.macro
|
39
|
+
when :belongs_to
|
40
|
+
foreign_key = association.foreign_key
|
41
|
+
column = model_class&.columns_hash&.dig(foreign_key.to_s)
|
42
|
+
if config.associations_strategy == :database
|
43
|
+
prop.nullable = column.null if column
|
44
|
+
elsif config.associations_strategy == :active_record
|
45
|
+
prop.nullable = association.options[:optional] === true || association.options[:required] === false
|
46
|
+
else
|
47
|
+
raise "Unknown associations strategy: #{config.associations_strategy}"
|
48
|
+
end
|
49
|
+
when :has_one
|
50
|
+
if config.associations_strategy == :database
|
51
|
+
prop.nullable = true
|
52
|
+
elsif config.associations_strategy == :active_record
|
53
|
+
prop.nullable = !association.options[:required]
|
54
|
+
else
|
55
|
+
raise "Unknown associations strategy: #{config.associations_strategy}"
|
32
56
|
end
|
33
|
-
return prop
|
34
57
|
end
|
35
58
|
|
59
|
+
prop
|
60
|
+
end
|
61
|
+
|
62
|
+
def infer_types_for_column(prop)
|
36
63
|
column = model_class&.columns_hash&.dig(prop.column_name.to_s)
|
37
|
-
return
|
64
|
+
return nil unless column
|
38
65
|
|
66
|
+
column = model_class&.columns_hash&.dig(prop.column_name.to_s)
|
39
67
|
prop.multi = !!column.try(:array)
|
40
68
|
case config.null_strategy
|
41
69
|
when :nullable
|
@@ -57,17 +85,20 @@ module Typelizer
|
|
57
85
|
prop
|
58
86
|
end
|
59
87
|
|
60
|
-
def
|
61
|
-
|
62
|
-
return nil unless column
|
88
|
+
def infer_types_for_attribute(prop)
|
89
|
+
return nil unless model_class.respond_to?(:attribute_types)
|
63
90
|
|
64
|
-
|
65
|
-
|
91
|
+
attribute_type_obj = model_class.attribute_types[prop.column_name.to_s]
|
92
|
+
return nil unless attribute_type_obj
|
66
93
|
|
67
|
-
|
68
|
-
|
94
|
+
if attribute_type_obj.respond_to?(:subtype)
|
95
|
+
prop.type = @config.type_mapping[attribute_type_obj.subtype.type]
|
96
|
+
prop.multi = true
|
97
|
+
elsif attribute_type_obj.respond_to?(:type)
|
98
|
+
prop.type = @config.type_mapping[attribute_type_obj.type]
|
99
|
+
end
|
69
100
|
|
70
|
-
prop
|
101
|
+
prop
|
71
102
|
end
|
72
103
|
end
|
73
104
|
end
|
data/lib/typelizer/property.rb
CHANGED
@@ -26,8 +26,9 @@ module Typelizer
|
|
26
26
|
def fingerprint
|
27
27
|
props = to_h
|
28
28
|
props[:type] = type_name
|
29
|
-
props
|
30
|
-
|
29
|
+
props.each_with_object(+"<#{self.class.name}") do |(k, v), fp|
|
30
|
+
fp << " #{k}=#{v.inspect}" unless v.nil?
|
31
|
+
end << ">"
|
31
32
|
end
|
32
33
|
|
33
34
|
private
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
# SerializerConfigLayer
|
5
|
+
#
|
6
|
+
# Lightweight, validated container for per-serializer overrides defined via the DSL.
|
7
|
+
#
|
8
|
+
# - Backed by a plain Hash for cheap deep-merge later (see WriterContext).
|
9
|
+
# - Only keys from Config.members are allowed; unknown keys raise NoMethodError.
|
10
|
+
# - Supports flat setters/getters in the DSL (e.g., c.null_strategy = :nullable_and_optional).
|
11
|
+
# - Mutable only via the DSL; #to_h returns a frozen hash to prevent external mutation.
|
12
|
+
#
|
13
|
+
# Rationale: we don't allocate another Config here; this layer is merged on top of
|
14
|
+
# library/global/writer settings when computing the effective config.
|
15
|
+
class SerializerConfigLayer
|
16
|
+
VALID_KEYS = Config.members.to_set
|
17
|
+
|
18
|
+
def initialize(target_hash)
|
19
|
+
@target_hash = target_hash
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
@target_hash.dup.freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def method_missing(name, *args)
|
29
|
+
name = name.to_s
|
30
|
+
key = name.chomp("=").to_sym
|
31
|
+
|
32
|
+
raise NoMethodError, "Unknown configuration key: '#{key}'" unless VALID_KEYS.include?(key)
|
33
|
+
|
34
|
+
return @target_hash[key] = args.first if name.end_with?("=") && args.length == 1
|
35
|
+
|
36
|
+
return @target_hash[key] if args.empty?
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(name, include_private = false)
|
42
|
+
VALID_KEYS.include?(name.to_s.chomp("=").to_sym) || super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -92,9 +92,10 @@ module Typelizer
|
|
92
92
|
)
|
93
93
|
when ::Alba::Association
|
94
94
|
resource = attr.instance_variable_get(:@resource)
|
95
|
+
|
95
96
|
Property.new(
|
96
97
|
name: name,
|
97
|
-
type:
|
98
|
+
type: context.interface_for(resource),
|
98
99
|
optional: false,
|
99
100
|
nullable: false,
|
100
101
|
multi: false, # we override this in typelize_method_transform
|
@@ -18,7 +18,7 @@ module Typelizer
|
|
18
18
|
|
19
19
|
def properties
|
20
20
|
serializer._attributes_data.merge(serializer._reflections).flat_map do |key, association|
|
21
|
-
type = association.options[:serializer] ?
|
21
|
+
type = association.options[:serializer] ? context.interface_for(association.options[:serializer]) : nil
|
22
22
|
adapter = ActiveModelSerializers::Adapter.configured_adapter
|
23
23
|
Property.new(
|
24
24
|
name: adapter.transform_key_casing!(key.to_s, association.options),
|
@@ -2,8 +2,8 @@ module Typelizer
|
|
2
2
|
module SerializerPlugins
|
3
3
|
module Auto
|
4
4
|
class << self
|
5
|
-
def new(serializer:, config:)
|
6
|
-
plugin(serializer).new(serializer: serializer, config: config)
|
5
|
+
def new(serializer:, config:, context:)
|
6
|
+
plugin(serializer).new(serializer: serializer, config: config, context: context)
|
7
7
|
end
|
8
8
|
|
9
9
|
def plugin(serializer)
|
@@ -1,9 +1,10 @@
|
|
1
1
|
module Typelizer
|
2
2
|
module SerializerPlugins
|
3
3
|
class Base
|
4
|
-
def initialize(serializer:, config:)
|
4
|
+
def initialize(serializer:, config:, context:)
|
5
5
|
@serializer = serializer
|
6
6
|
@config = config
|
7
|
+
@context = context
|
7
8
|
end
|
8
9
|
|
9
10
|
def root_key
|
@@ -28,7 +29,7 @@ module Typelizer
|
|
28
29
|
|
29
30
|
private
|
30
31
|
|
31
|
-
attr_reader :serializer, :config
|
32
|
+
attr_reader :serializer, :config, :context
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
@@ -18,9 +18,9 @@ module Typelizer
|
|
18
18
|
attributes
|
19
19
|
.flat_map do |key, options|
|
20
20
|
if options[:association] == :flat
|
21
|
-
|
21
|
+
context.interface_for(options.fetch(:serializer)).properties
|
22
22
|
else
|
23
|
-
type = options[:serializer] ?
|
23
|
+
type = options[:serializer] ? context.interface_for(options[:serializer]) : options[:type]
|
24
24
|
Property.new(
|
25
25
|
name: key,
|
26
26
|
type: type,
|
@@ -48,7 +48,7 @@ module Typelizer
|
|
48
48
|
def association_property(assoc, multi: false)
|
49
49
|
key = assoc.name_str
|
50
50
|
serializer = assoc.descriptor.type
|
51
|
-
type = serializer ?
|
51
|
+
type = serializer ? context.interface_for(serializer) : nil
|
52
52
|
Property.new(
|
53
53
|
name: key,
|
54
54
|
type: type,
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer/writer.rb
CHANGED
@@ -4,27 +4,51 @@ require "fileutils"
|
|
4
4
|
|
5
5
|
module Typelizer
|
6
6
|
class Writer
|
7
|
-
|
7
|
+
class WriterError < StandardError; end
|
8
|
+
|
9
|
+
def initialize(config)
|
8
10
|
@template_cache = {}
|
9
|
-
@config =
|
11
|
+
@config = config
|
10
12
|
end
|
11
13
|
|
12
|
-
attr_reader :config, :template_cache
|
13
|
-
|
14
14
|
def call(interfaces, force:)
|
15
15
|
cleanup_output_dir if force
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
valid_interfaces = interfaces.reject(&:empty?)
|
18
|
+
return [] if valid_interfaces.empty?
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
written_files = []
|
21
|
+
|
22
|
+
begin
|
23
|
+
written_files.concat(valid_interfaces.map { |interface| write_interface(interface) })
|
22
24
|
|
23
|
-
|
25
|
+
written_files << write_index(valid_interfaces)
|
26
|
+
|
27
|
+
cleanup_stale_files(written_files) unless force
|
28
|
+
|
29
|
+
Typelizer.logger.debug("Generated #{written_files.size} TypeScript files in #{config.output_dir}")
|
30
|
+
|
31
|
+
written_files
|
32
|
+
rescue => e
|
33
|
+
# if during the file generations an error appears, we remove generated files
|
34
|
+
cleanup_partial_writes(written_files)
|
35
|
+
raise WriterError, "Failed to write TypeScript files (#{e.class}): #{e.message}"
|
36
|
+
end
|
24
37
|
end
|
25
38
|
|
26
39
|
private
|
27
40
|
|
41
|
+
attr_reader :config, :template_cache
|
42
|
+
|
43
|
+
def cleanup_stale_files(written_files)
|
44
|
+
return unless File.directory?(config.output_dir)
|
45
|
+
|
46
|
+
existing_files = Dir[File.join(config.output_dir, "**/*.ts")]
|
47
|
+
stale_files = existing_files - written_files
|
48
|
+
|
49
|
+
File.delete(*stale_files) unless stale_files.empty?
|
50
|
+
end
|
51
|
+
|
28
52
|
def write_index(interfaces)
|
29
53
|
write_file("index.ts", interfaces.map(&:filename).join) do
|
30
54
|
render_template("index.ts.erb", interfaces: interfaces)
|
@@ -60,5 +84,9 @@ module Typelizer
|
|
60
84
|
def cleanup_output_dir
|
61
85
|
FileUtils.rm_rf(config.output_dir)
|
62
86
|
end
|
87
|
+
|
88
|
+
def cleanup_partial_writes(partial_files)
|
89
|
+
File.delete(*partial_files) unless partial_files.empty?
|
90
|
+
end
|
63
91
|
end
|
64
92
|
end
|
data/lib/typelizer.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "typelizer/version"
|
4
|
-
require_relative "typelizer/config"
|
5
4
|
require_relative "typelizer/property"
|
5
|
+
require_relative "typelizer/model_plugins/auto"
|
6
|
+
require_relative "typelizer/serializer_plugins/auto"
|
7
|
+
|
8
|
+
require_relative "typelizer/config"
|
9
|
+
require_relative "typelizer/configuration"
|
10
|
+
require_relative "typelizer/serializer_config_layer"
|
11
|
+
|
12
|
+
require_relative "typelizer/contexts/writer_context"
|
13
|
+
require_relative "typelizer/contexts/scan_context"
|
6
14
|
require_relative "typelizer/interface"
|
7
15
|
require_relative "typelizer/renderer"
|
8
16
|
require_relative "typelizer/writer"
|
9
17
|
require_relative "typelizer/generator"
|
10
|
-
|
11
18
|
require_relative "typelizer/dsl"
|
12
19
|
|
13
|
-
require_relative "typelizer/serializer_plugins/auto"
|
14
20
|
require_relative "typelizer/serializer_plugins/oj_serializers"
|
15
21
|
require_relative "typelizer/serializer_plugins/alba"
|
16
22
|
require_relative "typelizer/serializer_plugins/ams"
|
@@ -18,30 +24,39 @@ require_relative "typelizer/serializer_plugins/panko"
|
|
18
24
|
|
19
25
|
require_relative "typelizer/model_plugins/active_record"
|
20
26
|
require_relative "typelizer/model_plugins/poro"
|
21
|
-
require_relative "typelizer/model_plugins/auto"
|
22
27
|
|
23
28
|
require_relative "typelizer/railtie" if defined?(Rails)
|
24
29
|
|
25
30
|
require "logger"
|
31
|
+
require "forwardable"
|
26
32
|
|
27
33
|
module Typelizer
|
28
34
|
class << self
|
35
|
+
extend Forwardable
|
36
|
+
|
37
|
+
# readers
|
38
|
+
def_delegators :configuration, :dirs, :reject_class, :listen, :writer
|
39
|
+
|
40
|
+
# writers
|
41
|
+
def_delegators :configuration, :dirs=, :reject_class=, :listen=
|
42
|
+
|
29
43
|
def enabled?
|
30
44
|
return false if ENV["DISABLE_TYPELIZER"] == "true" || ENV["DISABLE_TYPELIZER"] == "1"
|
31
45
|
|
32
46
|
ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["DISABLE_TYPELIZER"] == "false"
|
33
47
|
end
|
34
48
|
|
35
|
-
attr_accessor :dirs
|
36
|
-
attr_accessor :reject_class
|
37
49
|
attr_accessor :logger
|
38
|
-
attr_accessor :listen
|
39
50
|
|
40
51
|
# @private
|
41
52
|
attr_reader :base_classes
|
42
53
|
|
54
|
+
def configuration
|
55
|
+
@configuration ||= Configuration.new
|
56
|
+
end
|
57
|
+
|
43
58
|
def configure
|
44
|
-
yield
|
59
|
+
yield configuration
|
45
60
|
end
|
46
61
|
|
47
62
|
private
|
@@ -50,10 +65,7 @@ module Typelizer
|
|
50
65
|
end
|
51
66
|
|
52
67
|
# Set in the Railtie
|
53
|
-
self.dirs = []
|
54
|
-
self.reject_class = ->(serializer:) { false }
|
55
68
|
self.logger = Logger.new($stdout, level: :info)
|
56
|
-
self.listen = nil
|
57
69
|
|
58
70
|
self.base_classes = Set.new
|
59
71
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typelizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Svyatoslav Kryukov
|
@@ -35,6 +35,9 @@ files:
|
|
35
35
|
- lib/tasks/generate.rake
|
36
36
|
- lib/typelizer.rb
|
37
37
|
- lib/typelizer/config.rb
|
38
|
+
- lib/typelizer/configuration.rb
|
39
|
+
- lib/typelizer/contexts/scan_context.rb
|
40
|
+
- lib/typelizer/contexts/writer_context.rb
|
38
41
|
- lib/typelizer/dsl.rb
|
39
42
|
- lib/typelizer/generator.rb
|
40
43
|
- lib/typelizer/interface.rb
|
@@ -45,6 +48,7 @@ files:
|
|
45
48
|
- lib/typelizer/property.rb
|
46
49
|
- lib/typelizer/railtie.rb
|
47
50
|
- lib/typelizer/renderer.rb
|
51
|
+
- lib/typelizer/serializer_config_layer.rb
|
48
52
|
- lib/typelizer/serializer_plugins/alba.rb
|
49
53
|
- lib/typelizer/serializer_plugins/ams.rb
|
50
54
|
- lib/typelizer/serializer_plugins/auto.rb
|
@@ -83,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
87
|
- !ruby/object:Gem::Version
|
84
88
|
version: '0'
|
85
89
|
requirements: []
|
86
|
-
rubygems_version: 3.6.
|
90
|
+
rubygems_version: 3.6.9
|
87
91
|
specification_version: 4
|
88
92
|
summary: A TypeScript type generator for Ruby serializers.
|
89
93
|
test_files: []
|