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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 067c0e093a70b8c8273e7504004a687c08272a536367958662e9d7c2fe293151
4
- data.tar.gz: 6dc5ef91af5f50ead351f90ee6cf8026360f1f605d06df5a30e20a9633ec168b
3
+ metadata.gz: fd7dab7ab6fabb3be94f1ca6bd6ea98337ea5b6c52811b413ed28f0804e5bd64
4
+ data.tar.gz: e1dc88644daebf2c3c34ded6cee520963c009568f45cde70e60d1276b8a859c2
5
5
  SHA512:
6
- metadata.gz: b518818c82709eea2ef5461272c929189d0fa59f53aa3f1cb6309e538ffe586b0bd2117cd520a165def41221f9f05528caf934842584f840f64370ce035488a9
7
- data.tar.gz: c89f29ad82e7fa44f3118385ccbd8a6ffb5fde9bbe1d3e400994704fdb8a709ccb9b22f029332cd36da6bdc3c8b190b5c0cf1c9492fa5e11cda4ad7d082c8898
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.4.1...HEAD
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
  [![Gem Version](https://badge.fury.io/rb/typelizer.svg)](https://rubygems.org/gems/typelizer)
4
4
 
5
- Typelizer is a Ruby gem that automatically generates TypeScript interfaces from your Ruby serializers, bridging the gap between your Ruby backend and TypeScript frontend. It supports multiple serializer libraries and provides a flexible configuration system, making it easier to maintain type consistency across your full-stack application.
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](#global-configuration)
20
- - [Config Options](#config-options)
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
- - Support for multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`, `Panko::Serializer`)
33
- - File watching and automatic regeneration in development
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
- ### Config Options
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
- `Typelizer::Config` offers fine-grained control over the gem's behavior. Here's a list of available options:
377
+ ### Option reference
232
378
 
233
379
  ```ruby
234
380
  Typelizer.configure do |config|
235
- # Determines how serializer names are mapped to TypeScript interface names
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).
@@ -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
- }.tap do |types|
12
- types.default = :unknown
13
- end
16
+ citext: :string,
17
+ uuid: :string
18
+ ).freeze
14
19
 
15
- class Config < Struct.new(
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
- ) do
34
- class << self
35
- def instance
36
- @instance ||= new(
37
- serializer_name_mapper: ->(serializer) do
38
- return "" if serializer.name.nil?
39
-
40
- serializer.name.ends_with?("Serializer") ? serializer.name&.delete_suffix("Serializer") : serializer.name&.delete_suffix("Resource")
41
- end,
42
- serializer_model_mapper: ->(serializer) do
43
- base_class = serializer_name_mapper.call(serializer)
44
- Object.const_get(base_class) if Object.const_defined?(base_class)
45
- end,
46
-
47
- model_plugin: ModelPlugins::Auto,
48
- serializer_plugin: SerializerPlugins::Auto,
49
- plugin_configs: {},
50
-
51
- type_mapping: TYPE_MAPPING,
52
- null_strategy: :nullable,
53
- inheritance_strategy: :none,
54
- associations_strategy: :database,
55
- comments: false,
56
- prefer_double_quotes: false,
57
-
58
- output_dir: js_root.join("types/serializers"),
59
-
60
- types_import_path: "@/types",
61
- types_global: %w[Array Date Record File FileList],
62
-
63
- properties_transformer: nil,
64
- verbatim_module_syntax: false
65
- )
66
- end
67
-
68
- private
69
-
70
- def js_root
71
- root_path = defined?(Rails) ? Rails.root : Pathname.pwd
72
- js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript"
73
- root_path.join(js_root)
74
- end
75
-
76
- def respond_to_missing?(name, include_private = false)
77
- Typelizer.respond_to?(name) ||
78
- instance.respond_to?(name, include_private)
79
- end
80
-
81
- def method_missing(method, *args, &block)
82
- return Typelizer.send(method, *args, &block) if Typelizer.respond_to?(method)
83
- instance.send(method, *args, &block)
84
- end
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