typelizer 0.4.2 → 0.5.1

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: a41914a150239acf4de1b43149a0bfaa81c9511a6a5434aa445cfc9dbf3ee343
4
- data.tar.gz: 82d1616845d782c041466d8500453afb8052a3d4fd8ce4e6f63c045c41a19544
3
+ metadata.gz: fa25b7b1816ce597e18fd9914b7f9434a4f9cbc4e5e7dcb80c783850a0e3d99c
4
+ data.tar.gz: 882d39a7faffcec6d7911c646a5a82a5164be04943c5f109b3c245f03728bcb8
5
5
  SHA512:
6
- metadata.gz: 1f51fef6373b00d177faceccebc84c87e22f428f684990aa3faa762a5cabca853cf609c7476f98cc34ce26d1737ace2f258c47099a23018755f3e595873009d1
7
- data.tar.gz: fff6bc2ca1844cc45d7d7a2a6bf8ac269078243013acb4e83ead8a902936f62fcfd5e15a96fbcbb1289266c52f53446e4cb3fa74584eeef09ab86882eadcfc43
6
+ metadata.gz: c99a3bc8dd6bbeaeed50394963d5a7081ebf3d1d837c9fa0b3bff6e4bd822974c8953fa355b22b07539736f18f2f7cf7f998e1bb68bdb01785a18bc1a6e5e0b7
7
+ data.tar.gz: 7b041dc084316a0ce823150b32c6af5555abacf15aaa99f4a286eec87d4a7f5b7ada172dcf3b45735139574647c91e7e5fe722d5da0a1959305a4e87d093157d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1] - 2025-09-11
11
+
12
+ ### Fixed
13
+
14
+ - Fix type inference when using virtual associations. ([@hkamberovic])
15
+
16
+ ## [0.5.0] - 2025-09-01
17
+
18
+ ### Added
19
+
20
+ - Support for multiple output writers: emit several variants (e.g., snake_case and camelCase) in parallel. ([@prog-supdex])
21
+ - Support for Rails' Attributes API. ([@skryukov])
22
+
10
23
  ## [0.4.2] - 2025-06-23
11
24
 
12
25
  ### Added
@@ -171,15 +184,20 @@ and this project adheres to [Semantic Versioning].
171
184
 
172
185
  [@davidrunger]: https://github.com/davidrunger
173
186
  [@Envek]: https://github.com/Envek
187
+ [@hkamberovic]: https://github.com/hkamberovic
174
188
  [@kristinemcbride]: https://github.com/kristinemcbride
175
189
  [@nkriege]: https://github.com/nkriege
176
190
  [@NOX73]: https://github.com/NOX73
177
191
  [@okuramasafumi]: https://github.com/okuramasafumi
178
192
  [@patvice]: https://github.com/patvice
193
+ [@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
179
194
  [@skryukov]: https://github.com/skryukov
195
+ [@prog-supdex]: https://github.com/prog-supdex
180
196
  [@ventsislaf]: https://github.com/ventsislaf
181
197
 
182
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.4.2...HEAD
198
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.1...HEAD
199
+ [0.5.1]: https://github.com/skryukov/typelizer/compare/v0.5.0...v0.5.1
200
+ [0.5.0]: https://github.com/skryukov/typelizer/compare/v0.4.2...v0.5.0
183
201
  [0.4.2]: https://github.com/skryukov/typelizer/compare/v0.4.1...v0.4.2
184
202
  [0.4.1]: https://github.com/skryukov/typelizer/compare/v0.4.0...v0.4.1
185
203
  [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
  [![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,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
- }.tap do |types|
13
- types.default = :unknown
14
- end
18
+ ).freeze
15
19
 
16
- class Config < Struct.new(
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
- ) do
35
- class << self
36
- def instance
37
- @instance ||= new(
38
- serializer_name_mapper: ->(serializer) do
39
- return "" if serializer.name.nil?
40
-
41
- serializer.name.ends_with?("Serializer") ? serializer.name&.delete_suffix("Serializer") : serializer.name&.delete_suffix("Resource")
42
- end,
43
- serializer_model_mapper: ->(serializer) do
44
- base_class = serializer_name_mapper.call(serializer)
45
- Object.const_get(base_class) if Object.const_defined?(base_class)
46
- end,
47
-
48
- model_plugin: ModelPlugins::Auto,
49
- serializer_plugin: SerializerPlugins::Auto,
50
- plugin_configs: {},
51
-
52
- type_mapping: TYPE_MAPPING,
53
- null_strategy: :nullable,
54
- inheritance_strategy: :none,
55
- associations_strategy: :database,
56
- comments: false,
57
- prefer_double_quotes: false,
58
-
59
- output_dir: js_root.join("types/serializers"),
60
-
61
- types_import_path: "@/types",
62
- types_global: %w[Array Date Record File FileList],
63
-
64
- properties_transformer: nil,
65
- verbatim_module_syntax: false
66
- )
67
- end
68
-
69
- private
70
-
71
- def js_root
72
- root_path = defined?(Rails) ? Rails.root : Pathname.pwd
73
- js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript"
74
- root_path.join(js_root)
75
- end
76
-
77
- def respond_to_missing?(name, include_private = false)
78
- Typelizer.respond_to?(name) ||
79
- instance.respond_to?(name, include_private)
80
- end
81
-
82
- def method_missing(method, *args, &block)
83
- return Typelizer.send(method, *args, &block) if Typelizer.respond_to?(method)
84
- instance.send(method, *args, &block)
85
- 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
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
- @typelizer_config ||=
19
- begin
20
- parent_config = superclass.respond_to?(:typelizer_config) ? superclass.typelizer_config : Config
21
- Config.new(parent_config.to_h.transform_values(&:dup))
22
- end
23
- yield @typelizer_config if block_given?
24
- @typelizer_config
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
- def typelizer_interface
28
- @typelizer_interface ||= Interface.new(serializer: self)
26
+ @config_layer
29
27
  end
30
28
 
31
29
  # save association of serializer to model
@@ -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
- found_interfaces = interfaces
20
- writer.call(found_interfaces, force: force)
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.map(&:typelizer_interface).reject(&:empty?)
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?(:typelizer_interface) && tp.self.typelizer_interface.is_a?(Interface)
49
+ next unless tp.self.is_a?(Class) && tp.self.respond_to?(:typelizer_config)
48
50
 
49
- serializer_plugin = tp.self.typelizer_interface.serializer_plugin
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
@@ -1,14 +1,22 @@
1
1
  module Typelizer
2
2
  class Interface
3
- attr_reader :serializer, :serializer_plugin
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.typelizer_config
11
+ context.config_for(serializer)
7
12
  end
8
13
 
9
- def initialize(serializer:)
10
- @serializer = serializer
11
- @serializer_plugin = config.serializer_plugin.new(serializer: serializer, config: config)
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
- interface = serializer.superclass.typelizer_interface
75
- return if interface.empty?
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
- interface
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.serializer_model_mapper.call(serializer)
144
- rescue NameError
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,32 +9,59 @@ module Typelizer
9
9
  attr_reader :model_class, :config
10
10
 
11
11
  def infer_types(prop)
12
- if (association = model_class&.reflect_on_association(prop.column_name.to_sym))
13
- case association.macro
14
- when :belongs_to
15
- foreign_key = association.foreign_key
16
- column = model_class&.columns_hash&.dig(foreign_key.to_s)
17
- if config.associations_strategy == :database
18
- prop.nullable = column.null if column
19
- elsif config.associations_strategy == :active_record
20
- prop.nullable = association.options[:optional] === true || association.options[:required] === false
21
- else
22
- raise "Unknown associations strategy: #{config.associations_strategy}"
23
- end
24
- when :has_one
25
- if config.associations_strategy == :database
26
- prop.nullable = true
27
- elsif config.associations_strategy == :active_record
28
- prop.nullable = !association.options[:required]
29
- else
30
- raise "Unknown associations strategy: #{config.associations_strategy}"
31
- end
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 prop unless column
64
+ return nil unless column
38
65
 
39
66
  prop.multi = !!column.try(:array)
40
67
  case config.null_strategy
@@ -57,17 +84,20 @@ module Typelizer
57
84
  prop
58
85
  end
59
86
 
60
- def comment_for(prop)
61
- column = model_class&.columns_hash&.dig(prop.column_name.to_s)
62
- return nil unless column
87
+ def infer_types_for_attribute(prop)
88
+ return nil unless model_class.respond_to?(:attribute_types)
63
89
 
64
- prop.comment = column.comment
65
- end
90
+ attribute_type_obj = model_class.attribute_types.fetch(prop.column_name.to_s, nil)
91
+ return nil unless attribute_type_obj
66
92
 
67
- def enum_for(prop)
68
- return unless model_class&.defined_enums&.key?(prop.column_name.to_s)
93
+ if attribute_type_obj.respond_to?(:subtype)
94
+ prop.type = @config.type_mapping[attribute_type_obj.subtype.type]
95
+ prop.multi = true
96
+ elsif attribute_type_obj.respond_to?(:type)
97
+ prop.type = @config.type_mapping[attribute_type_obj.type]
98
+ end
69
99
 
70
- prop.enum = model_class.defined_enums[prop.column_name.to_s].keys
100
+ prop
71
101
  end
72
102
  end
73
103
  end
@@ -26,8 +26,9 @@ module Typelizer
26
26
  def fingerprint
27
27
  props = to_h
28
28
  props[:type] = type_name
29
- props = props.filter_map { |k, v| "#{k}=#{v.inspect}" unless v.nil? }
30
- "<#{self.class.name} #{props.join(" ")}>"
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: Interface.new(serializer: resource),
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] ? Interface.new(serializer: association.options[:serializer]) : nil
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
- Interface.new(serializer: options.fetch(:serializer)).properties
21
+ context.interface_for(options.fetch(:serializer)).properties
22
22
  else
23
- type = options[:serializer] ? Interface.new(serializer: options[:serializer]) : options[:type]
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 ? Interface.new(serializer: serializer) : nil
51
+ type = serializer ? context.interface_for(serializer) : nil
52
52
  Property.new(
53
53
  name: key,
54
54
  type: type,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -4,27 +4,51 @@ require "fileutils"
4
4
 
5
5
  module Typelizer
6
6
  class Writer
7
- def initialize
7
+ class WriterError < StandardError; end
8
+
9
+ def initialize(config)
8
10
  @template_cache = {}
9
- @config = 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
- written_files = interfaces.map { |interface| write_interface(interface) }
18
- written_files << write_index(interfaces)
17
+ valid_interfaces = interfaces.reject(&:empty?)
18
+ return [] if valid_interfaces.empty?
19
19
 
20
- existing_files = Dir[File.join(config.output_dir, "**/*.ts")]
21
- files_to_delete = existing_files - written_files
20
+ written_files = []
21
+
22
+ begin
23
+ written_files.concat(valid_interfaces.map { |interface| write_interface(interface) })
22
24
 
23
- File.delete(*files_to_delete) unless files_to_delete.empty?
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 Config
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.2
4
+ version: 0.5.1
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.7
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: []