typelizer 0.4.1 → 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 +23 -1
- data/README.md +156 -24
- data/lib/typelizer/config.rb +78 -59
- 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 +3 -2
- 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,23 @@ 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
|
+
|
17
|
+
## [0.4.2] - 2025-06-23
|
18
|
+
|
19
|
+
### Added
|
20
|
+
|
21
|
+
- Map `uuid` type to `string` by default. ([@ventsislaf])
|
22
|
+
|
23
|
+
### Fixed
|
24
|
+
|
25
|
+
- Alba: fix `has_many` with a custom `key` generates single value type instead of array. ([@skryukov])
|
26
|
+
|
10
27
|
## [0.4.1] - 2025-06-10
|
11
28
|
|
12
29
|
### Added
|
@@ -166,9 +183,14 @@ and this project adheres to [Semantic Versioning].
|
|
166
183
|
[@NOX73]: https://github.com/NOX73
|
167
184
|
[@okuramasafumi]: https://github.com/okuramasafumi
|
168
185
|
[@patvice]: https://github.com/patvice
|
186
|
+
[@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
|
169
187
|
[@skryukov]: https://github.com/skryukov
|
188
|
+
[@supdex]: https://github.com/supdex
|
189
|
+
[@ventsislaf]: https://github.com/ventsislaf
|
170
190
|
|
171
|
-
[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
|
193
|
+
[0.4.2]: https://github.com/skryukov/typelizer/compare/v0.4.1...v0.4.2
|
172
194
|
[0.4.1]: https://github.com/skryukov/typelizer/compare/v0.4.0...v0.4.1
|
173
195
|
[0.4.0]: https://github.com/skryukov/typelizer/compare/v0.3.0...v0.4.0
|
174
196
|
[0.3.0]: https://github.com/skryukov/typelizer/compare/v0.2.0...v0.3.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,18 +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
|
-
citext: :string
|
11
|
-
|
12
|
-
|
13
|
-
end
|
16
|
+
citext: :string,
|
17
|
+
uuid: :string
|
18
|
+
).freeze
|
14
19
|
|
15
|
-
|
20
|
+
DEFAULT_TYPES_GLOBAL = %w[Array Date Record File FileList].freeze
|
21
|
+
|
22
|
+
Config = Struct.new(
|
16
23
|
:serializer_name_mapper,
|
17
24
|
:serializer_model_mapper,
|
18
25
|
:properties_transformer,
|
@@ -30,59 +37,71 @@ module Typelizer
|
|
30
37
|
:comments,
|
31
38
|
:prefer_double_quotes,
|
32
39
|
keyword_init: true
|
33
|
-
)
|
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
|
-
|
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
|
85
105
|
end
|
86
|
-
end
|
87
106
|
end
|
88
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
|