typelizer 0.11.0 → 0.13.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.
@@ -1,33 +1,68 @@
1
1
  namespace :typelizer do
2
2
  desc "Generate TypeScript interfaces from serializers"
3
- task generate: :environment do
4
- benchmark do
5
- Typelizer::Generator.call
3
+ task types: :environment do
4
+ benchmark_types do
5
+ Typelizer::Generator.call(skip_check: true)
6
6
  end
7
7
  end
8
8
 
9
- desc "Removes all files in output folder and refreshs all generate TypeScript interfaces from serializers"
10
- task "generate:refresh": :environment do
11
- benchmark do
12
- Typelizer::Generator.call(force: true)
9
+ desc "Regenerate all TypeScript interfaces from serializers"
10
+ task "types:refresh": :environment do
11
+ benchmark_types do
12
+ Typelizer::Generator.call(force: true, skip_check: true)
13
13
  end
14
14
  end
15
15
 
16
- def benchmark(&block)
17
- require "benchmark"
18
-
19
- ENV["DISABLE_TYPELIZER"] = "false"
16
+ desc "Generate TypeScript route helpers"
17
+ task routes: :environment do
18
+ benchmark_routes do
19
+ Typelizer::RouteGenerator.call(skip_check: true)
20
+ end
21
+ end
20
22
 
21
- puts "Generating TypeScript interfaces..."
22
- time = Benchmark.realtime do
23
- block.call
23
+ desc "Regenerate all TypeScript route helpers"
24
+ task "routes:refresh": :environment do
25
+ benchmark_routes do
26
+ Typelizer::RouteGenerator.call(force: true, skip_check: true)
24
27
  end
28
+ end
29
+
30
+ desc "Generate all TypeScript files"
31
+ task generate: %i[types routes]
32
+
33
+ desc "Regenerate all TypeScript files"
34
+ task "generate:refresh": ["types:refresh", "routes:refresh"]
35
+
36
+ def benchmark_types(&block)
37
+ require "benchmark"
25
38
 
26
39
  interfaces = Typelizer.interfaces
27
- raise ArgumentError, "No serializers found. Please ensure all your serializers include Typelizer::DSL." if interfaces.empty?
40
+ if interfaces.empty?
41
+ puts "No serializers found, skipping type generation."
42
+ return
43
+ end
44
+
45
+ puts "Generating TypeScript interfaces..."
46
+ time = Benchmark.realtime { block.call }
28
47
 
29
48
  puts "Finished in #{time} seconds"
30
49
  puts "Found #{interfaces.size} serializers:"
31
50
  puts interfaces.map { |i| "\t#{i.name}" }.join("\n")
32
51
  end
52
+
53
+ def benchmark_routes(&block)
54
+ require "benchmark"
55
+
56
+ config = Typelizer.configuration.routes
57
+ unless config.enabled
58
+ puts "Route generation is disabled, skipping."
59
+ return
60
+ end
61
+
62
+ puts "Generating TypeScript route helpers..."
63
+ time = Benchmark.realtime { block.call }
64
+
65
+ puts "Finished in #{time} seconds"
66
+ puts "Generated route helpers in #{config.output_dir}"
67
+ end
33
68
  end
@@ -31,12 +31,17 @@ module Typelizer
31
31
  properties_sort_order
32
32
  ].freeze
33
33
 
34
- # Subset of CONFIGS_AFFECTING_OUTPUT that specifically affect index.ts output.
35
- CONFIGS_AFFECTING_INDEX_OUTPUT = %i[
34
+ # Config keys that affect only index.ts and Enums.ts (not per-interface .ts files).
35
+ CONFIGS_AFFECTING_INDEX_ONLY_OUTPUT = %i[
36
+ enum_runtime
37
+ ].freeze
38
+
39
+ # Config keys that affect index.ts output (superset: per-interface keys + index-only keys).
40
+ CONFIGS_AFFECTING_INDEX_OUTPUT = (%i[
36
41
  verbatim_module_syntax
37
42
  prefer_double_quotes
38
43
  imports_sort_order
39
- ].freeze
44
+ ] + CONFIGS_AFFECTING_INDEX_ONLY_OUTPUT).freeze
40
45
 
41
46
  # Config keys that don't affect file content (runtime behavior, or effects captured via properties).
42
47
  CONFIGS_NOT_AFFECTING_OUTPUT = %i[
@@ -76,6 +81,7 @@ module Typelizer
76
81
  :reject_class,
77
82
  :comments,
78
83
  :prefer_double_quotes,
84
+ :enum_runtime,
79
85
  keyword_init: true
80
86
  )
81
87
 
@@ -114,6 +120,7 @@ module Typelizer
114
120
  reject_class: ->(serializer:) { false },
115
121
  comments: false,
116
122
  prefer_double_quotes: false,
123
+ enum_runtime: false,
117
124
 
118
125
  output_dir: -> { default_output_dir },
119
126
 
@@ -21,6 +21,10 @@ module Typelizer
21
21
  attr_accessor :dirs, :listen
22
22
  attr_reader :writers, :global_settings
23
23
 
24
+ def routes
25
+ @routes ||= RouteConfig.build
26
+ end
27
+
24
28
  def initialize
25
29
  @dirs = []
26
30
  @listen = nil
data/lib/typelizer/dsl.rb CHANGED
@@ -33,8 +33,6 @@ module Typelizer
33
33
 
34
34
  # save association of serializer to model
35
35
  def typelize_from(model)
36
- return unless Typelizer.enabled?
37
-
38
36
  define_singleton_method(:_typelizer_model_name) { model }
39
37
  end
40
38
 
@@ -82,12 +80,13 @@ module Typelizer
82
80
  private
83
81
 
84
82
  def assign_type_information(attribute_name, attributes)
85
- return unless Typelizer.enabled?
86
-
87
83
  attributes.each do |name, attrs|
88
84
  next unless name
89
85
 
90
- store_type(attribute_name, name, TypeParser.parse_declaration(attrs))
86
+ clean_name, optional_from_key = TypeParser.parse_key(name)
87
+ parsed = TypeParser.apply_optional_key(TypeParser.parse_declaration(attrs), optional_from_key)
88
+
89
+ store_type(attribute_name, clean_name, parsed)
91
90
  end
92
91
  end
93
92
 
@@ -112,5 +111,15 @@ module Typelizer
112
111
  end
113
112
  end
114
113
  end
114
+
115
+ module Disabled
116
+ %i[typelize_from typelize typelize_meta].each do |name|
117
+ define_method(name) { |*, **| }
118
+ end
119
+ end
120
+
121
+ def self.disable!
122
+ ClassMethods.prepend(Disabled)
123
+ end
115
124
  end
116
125
  end
@@ -6,8 +6,8 @@ module Typelizer
6
6
  new.call(**args)
7
7
  end
8
8
 
9
- def call(force: false)
10
- return [] unless Typelizer.enabled?
9
+ def call(force: false, skip_check: false)
10
+ return [] unless skip_check || Typelizer.enabled?
11
11
 
12
12
  Typelizer.configuration.writers.each do |writer_name, writer_config|
13
13
  interfaces = Typelizer.interfaces(writer_name: writer_name)
@@ -64,7 +64,7 @@ module Typelizer
64
64
  @meta_fields ||= begin
65
65
  props = serializer_plugin.meta_fields || []
66
66
  props = infer_types(props, :_typelizer_meta_attributes)
67
- props = config.properties_transformer.call(props) if config.properties_transformer
67
+ props = transform_properties(props)
68
68
  PropertySorter.sort(props, config.properties_sort_order)
69
69
  end
70
70
  end
@@ -88,7 +88,7 @@ module Typelizer
88
88
  @properties ||= begin
89
89
  props = serializer_plugin.properties
90
90
  props = infer_types(props)
91
- props = config.properties_transformer.call(props) if config.properties_transformer
91
+ props = transform_properties(props)
92
92
  PropertySorter.sort(props, config.properties_sort_order)
93
93
  end
94
94
  end
@@ -125,7 +125,10 @@ module Typelizer
125
125
  # recursively including nested sub-properties
126
126
  all_properties = collect_all_properties(properties_to_print + trait_interfaces.flat_map(&:properties))
127
127
 
128
- flat_types = all_properties.filter_map(&:type).flat_map { |t| Array(t) }.uniq
128
+ flat_types = all_properties.filter_map(&:type)
129
+ .flat_map { |t| Array(t) }
130
+ .reject { |t| t.is_a?(Shape) }
131
+ .uniq
129
132
  association_serializers, attribute_types = flat_types.partition { |type| type.is_a?(Interface) }
130
133
 
131
134
  serializer_types = association_serializers
@@ -136,13 +139,10 @@ module Typelizer
136
139
  .uniq
137
140
  .reject { |type| global_type?(type) }
138
141
 
139
- # Collect trait types from properties with with_traits (skip self-references)
140
142
  trait_imports = all_properties.flat_map do |prop|
141
- next [] unless prop.with_traits&.any? && prop.type.is_a?(Interface)
142
- # Skip if the trait types are from the current interface (same file)
143
- next [] if prop.type.name == name
143
+ next [] if prop.type.is_a?(Interface) && prop.type.name == name
144
144
 
145
- prop.with_traits.map { |t| "#{prop.type.name}#{t.to_s.camelize}Trait" }
145
+ prop.trait_type_names
146
146
  end
147
147
 
148
148
  # Collect enum type names from properties
@@ -177,13 +177,15 @@ module Typelizer
177
177
 
178
178
  def collect_all_properties(props)
179
179
  props.flat_map do |prop|
180
- if prop.nested_properties&.any?
181
- [prop] + collect_all_properties(prop.nested_properties)
182
- elsif prop.type.is_a?(Interface) && prop.type.inline?
183
- [prop] + collect_all_properties(prop.type.properties)
184
- else
185
- [prop]
186
- end
180
+ children = nested_properties_of(prop.type)
181
+ children ? [prop] + collect_all_properties(children) : [prop]
182
+ end
183
+ end
184
+
185
+ def nested_properties_of(type)
186
+ case type
187
+ when Shape then type.properties
188
+ when Interface then type.properties if type.inline?
187
189
  end
188
190
  end
189
191
 
@@ -204,7 +206,7 @@ module Typelizer
204
206
  multi_attrs = serializer.respond_to?(:_typelizer_multi_attributes) ? serializer._typelizer_multi_attributes : Set.new
205
207
 
206
208
  props.map do |prop|
207
- has_dsl = dsl_attrs_for(prop, dsl_attrs)&.any?
209
+ has_dsl = prop.lookup_in(dsl_attrs)&.any?
208
210
 
209
211
  prop
210
212
  .then { |p| apply_dsl_type(p, dsl_attrs) }
@@ -215,12 +217,8 @@ module Typelizer
215
217
  end
216
218
  end
217
219
 
218
- def dsl_attrs_for(prop, dsl_attrs)
219
- dsl_attrs[prop.column_name.to_sym] || dsl_attrs[prop.name.to_sym]
220
- end
221
-
222
220
  def apply_dsl_type(prop, dsl_attrs)
223
- dsl_type = dsl_attrs_for(prop, dsl_attrs)
221
+ dsl_type = prop.lookup_in(dsl_attrs)
224
222
  return prop unless dsl_type&.any?
225
223
 
226
224
  dsl_type = resolve_class_type(dsl_type)
@@ -235,11 +233,20 @@ module Typelizer
235
233
  resolve_union_class_types(attrs)
236
234
  when String, Symbol
237
235
  resolve_single_class_type(attrs)
236
+ when Shape
237
+ attrs.merge(type: resolve_shape(type))
238
238
  else
239
239
  attrs
240
240
  end
241
241
  end
242
242
 
243
+ def resolve_shape(shape)
244
+ shape.map_properties do |p|
245
+ resolved = resolve_class_type(type: p.type)
246
+ p.with(type: resolved[:type])
247
+ end
248
+ end
249
+
243
250
  def resolve_single_class_type(attrs)
244
251
  attrs.merge(type: resolve_type_part(attrs[:type]))
245
252
  end
@@ -11,7 +11,8 @@ module Typelizer
11
11
  &block
12
12
  )
13
13
  return if started
14
- return unless Typelizer.enabled?
14
+ return if Typelizer.listen == false
15
+ return unless Typelizer.listen || Gem.loaded_specs["listen"]
15
16
 
16
17
  @block = block
17
18
  @generator = Typelizer::Generator.new
@@ -21,19 +22,25 @@ module Typelizer
21
22
 
22
23
  self.started = true
23
24
 
24
- locales_dirs = Typelizer.dirs.filter(&:exist?).map { |path| File.expand_path(path) }
25
-
26
- relative_paths = locales_dirs.map { |path| relative_path(path) }
25
+ watched_dirs = Typelizer.dirs.filter(&:exist?).map { |path| File.expand_path(path) }
27
26
 
27
+ relative_paths = watched_dirs.map { |path| relative_path(path) }
28
28
  debug("Watching #{relative_paths.inspect}")
29
29
 
30
- listener(locales_dirs.map(&:to_s), options).start
30
+ listener(watched_dirs.map(&:to_s), options).start
31
31
  @generator.call if run_on_start
32
+
33
+ if Typelizer.configuration.routes.enabled
34
+ RouteGenerator.call if run_on_start
35
+ start_route_listener(options)
36
+ end
32
37
  end
33
38
 
39
+ private
40
+
34
41
  def relative_path(path)
35
- root_path = defined?(Rails) ? Rails.root : Pathname.pwd
36
- Pathname.new(path).relative_path_from(root_path).to_s
42
+ @root_path ||= defined?(Rails) ? Rails.root : Pathname.pwd
43
+ Pathname.new(path).relative_path_from(@root_path).to_s
37
44
  end
38
45
 
39
46
  def debug(message)
@@ -67,6 +74,18 @@ module Typelizer
67
74
  paths.any? { |path| change.start_with?(path) }
68
75
  end
69
76
  end
77
+
78
+ def start_route_listener(options)
79
+ config_dir = @root_path.join("config")
80
+ return unless config_dir.exist?
81
+
82
+ debug("Watching #{relative_path(config_dir)} for route changes")
83
+
84
+ ::Listen.to(config_dir.to_s, only: /routes/, **options) do |changed, added, removed|
85
+ debug("Routes changed: #{(changed + added + removed).map { |f| relative_path(f) }.inspect}")
86
+ RouteGenerator.call
87
+ end.start
88
+ end
70
89
  end
71
90
  end
72
91
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ class TypeGenerationError < StandardError; end
5
+
6
+ class Middleware
7
+ class << self
8
+ attr_accessor :instance
9
+ end
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ @mutex = Mutex.new
14
+ @pending = true
15
+ self.class.instance = self
16
+ end
17
+
18
+ def call(env)
19
+ if @pending
20
+ @mutex.synchronize do
21
+ generate! if @pending
22
+ end
23
+ end
24
+ @app.call(env)
25
+ end
26
+
27
+ def mark_pending!
28
+ @pending = true
29
+ end
30
+
31
+ private
32
+
33
+ def generate!
34
+ Generator.new.call
35
+ RouteGenerator.call
36
+ @pending = false
37
+ rescue *db_error_classes => e
38
+ raise TypeGenerationError, "Typelizer could not generate types: #{e.message}\n" \
39
+ "Fix the database issue, then reload the page."
40
+ end
41
+
42
+ def db_error_classes
43
+ return [] unless defined?(ActiveRecord)
44
+
45
+ [
46
+ ActiveRecord::NoDatabaseError,
47
+ ActiveRecord::ConnectionNotEstablished,
48
+ ActiveRecord::StatementInvalid
49
+ ]
50
+ end
51
+ end
52
+ end
@@ -38,6 +38,7 @@ module Typelizer
38
38
  def columns_hash
39
39
  return nil unless model_class
40
40
  return nil if model_class.abstract_class?
41
+ return nil unless model_class.table_exists?
41
42
 
42
43
  model_class.columns_hash
43
44
  end
@@ -45,6 +46,7 @@ module Typelizer
45
46
  def attribute_types
46
47
  return nil unless model_class&.respond_to?(:attribute_types)
47
48
  return nil if model_class.abstract_class?
49
+ return nil unless model_class.table_exists?
48
50
 
49
51
  model_class.attribute_types
50
52
  end
@@ -126,6 +128,7 @@ module Typelizer
126
128
  return nil unless assoc
127
129
 
128
130
  target = assoc.klass
131
+ return nil unless target.table_exists?
129
132
  col = target.columns_hash[info[:original].to_s]
130
133
  return nil unless col
131
134
 
@@ -32,13 +32,7 @@ module Typelizer
32
32
  validate_version!(openapi_version)
33
33
 
34
34
  type_mapping = interface.respond_to?(:config) ? interface.config.type_mapping : Typelizer.configuration.type_mapping
35
- required_props = interface.properties.reject(&:optional).map(&:name)
36
- schema = {
37
- type: :object,
38
- properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version, type_mapping: type_mapping)] }
39
- }
40
- schema[:required] = required_props if required_props.any?
41
- schema
35
+ object_schema(interface.properties, openapi_version: openapi_version, type_mapping: type_mapping)
42
36
  end
43
37
 
44
38
  def property_schema(property, openapi_version: "3.0", type_mapping: Typelizer.configuration.type_mapping)
@@ -120,11 +114,10 @@ module Typelizer
120
114
  end
121
115
 
122
116
  def wrap_traits(definition, property, openapi_version:)
123
- return definition unless property.respond_to?(:with_traits) && property.with_traits&.any? && property.type.respond_to?(:name)
117
+ trait_names = property.trait_type_names
118
+ return definition if trait_names.empty?
124
119
 
125
- trait_refs = property.with_traits.map do |t|
126
- {"$ref" => "#/components/schemas/#{property.type.name}#{t.to_s.camelize}Trait"}
127
- end
120
+ trait_refs = trait_names.map { |name| {"$ref" => "#/components/schemas/#{name}"} }
128
121
 
129
122
  base_ref = definition.delete("$ref")
130
123
  if base_ref
@@ -173,14 +166,15 @@ module Typelizer
173
166
  end
174
167
 
175
168
  def base_type(property, openapi_version:, type_mapping:)
176
- if property.type.respond_to?(:properties)
169
+ # Shape check must precede respond_to?(:properties) — Shape also responds to :properties.
170
+ if property.type.is_a?(Shape)
171
+ object_schema(property.type.properties, openapi_version: openapi_version, type_mapping: type_mapping)
172
+ elsif property.type.respond_to?(:properties)
177
173
  if property.type.respond_to?(:inline?) && property.type.inline?
178
174
  schema_for(property.type, openapi_version: openapi_version)
179
175
  else
180
176
  {"$ref" => "#/components/schemas/#{property.type.name}"}
181
177
  end
182
- elsif property.type.nil? && property.respond_to?(:nested_properties) && property.nested_properties&.any?
183
- nested_schema(property, openapi_version: openapi_version, type_mapping: type_mapping)
184
178
  elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type) &&
185
179
  !type_mapping_overridden?(property, type_mapping)
186
180
  result = COLUMN_TYPE_MAP[property.column_type].dup
@@ -196,11 +190,11 @@ module Typelizer
196
190
  end
197
191
  end
198
192
 
199
- def nested_schema(property, openapi_version:, type_mapping:)
200
- required = property.nested_properties.reject(&:optional).map(&:name)
193
+ def object_schema(properties, openapi_version:, type_mapping:)
194
+ required = properties.reject(&:optional).map(&:name)
201
195
  schema = {
202
196
  type: :object,
203
- properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version, type_mapping: type_mapping)] }
197
+ properties: properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version, type_mapping: type_mapping)] }
204
198
  }
205
199
  schema[:required] = required if required.any?
206
200
  schema
@@ -2,13 +2,17 @@ module Typelizer
2
2
  Property = Struct.new(
3
3
  :name, :type, :optional, :nullable,
4
4
  :multi, :column_name, :column_type, :comment, :enum, :enum_type_name, :deprecated,
5
- :with_traits, :nested_properties, :nested_typelizes,
5
+ :with_traits,
6
6
  keyword_init: true
7
7
  ) do
8
8
  def with(**attrs)
9
9
  dup.tap { |p| attrs.each { |k, v| p[k] = v } }
10
10
  end
11
11
 
12
+ def lookup_in(hash)
13
+ hash[column_name.to_sym] || hash[name.to_sym]
14
+ end
15
+
12
16
  def inspect
13
17
  props = to_h.merge(type: type_name).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
14
18
  "<#{self.class.name} #{props}>"
@@ -25,6 +29,12 @@ module Typelizer
25
29
  render(sort_order: :none)
26
30
  end
27
31
 
32
+ def trait_type_names
33
+ return [] unless with_traits&.any? && type.is_a?(Interface)
34
+
35
+ with_traits.map { |t| "#{type.name}#{t.to_s.camelize}Trait" }
36
+ end
37
+
28
38
  # Renders the property as a TypeScript property string
29
39
  # @param sort_order [Symbol, Proc, nil] Sort order for union types (:none, :alphabetical, or Proc)
30
40
  # @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
@@ -32,11 +42,8 @@ module Typelizer
32
42
  def render(sort_order: :none, prefer_double_quotes: false)
33
43
  type_str = type_name(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes)
34
44
 
35
- # Handle intersection types for traits
36
- if with_traits&.any? && type.respond_to?(:name)
37
- trait_types = with_traits.map { |t| "#{type.name}#{t.to_s.camelize}Trait" }
38
- type_str = ([type_str] + trait_types).join(" & ")
39
- end
45
+ trait_types = trait_type_names
46
+ type_str = ([type_str] + trait_types).join(" & ") if trait_types.any?
40
47
 
41
48
  type_str = "Array<#{type_str}>" if multi
42
49
 
@@ -51,14 +58,10 @@ module Typelizer
51
58
 
52
59
  def fingerprint
53
60
  # Use array format for consistent output across Ruby versions
54
- # (Hash#inspect format changed in Ruby 3.4)
55
- # Exclude fields that do not affect generated TypeScript output.
56
- # Exclude nested_properties/nested_typelizes from to_h to avoid changing
57
- # fingerprints for properties that don't use them.
58
- # nested_typelizes is excluded entirely as it only affects inference, not output.
59
- to_h.except(:column_type, :nested_properties, :nested_typelizes)
61
+ # (Hash#inspect format changed in Ruby 3.4).
62
+ # column_type is excluded because it only informs inference, not output.
63
+ to_h.except(:column_type)
60
64
  .merge(type: UnionTypeSorter.sort(type_name(sort_order: :alphabetical), :alphabetical))
61
- .then { |h| nested_properties&.any? ? h.merge(nested_properties: nested_properties.map(&:fingerprint)) : h }
62
65
  .to_a.inspect
63
66
  end
64
67
 
@@ -69,17 +72,36 @@ module Typelizer
69
72
  def enum_definition(sort_order: :none, prefer_double_quotes: false)
70
73
  return unless enum && enum_type_name
71
74
 
72
- values = enum.map { |v| quote_string(v.to_s, prefer_double_quotes) }
73
- values = values.sort_by(&:downcase) if sort_order == :alphabetical
75
+ values = sorted_enum_keys(sort_order).map { |k| quote_string(k, prefer_double_quotes) }
74
76
  "type #{enum_type_name} = #{values.join(" | ")}"
75
77
  end
76
78
 
79
+ # Generates a TypeScript runtime constant for named enums
80
+ # @param sort_order [Symbol, Proc, nil] Sort order for enum keys (:none, :alphabetical, or Proc)
81
+ # @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
82
+ # @return [String, nil] The const like "const UserRole = { admin: 'admin', user: 'user' } as const"
83
+ def enum_runtime_definition(sort_order: :none, prefer_double_quotes: false)
84
+ return unless enum && enum_type_name
85
+
86
+ entries = sorted_enum_keys(sort_order).map { |k| "#{js_key(k, prefer_double_quotes)}: #{quote_string(k, prefer_double_quotes)}" }
87
+ "const #{enum_type_name} = { #{entries.join(", ")} } as const"
88
+ end
89
+
77
90
  private
78
91
 
92
+ def sorted_enum_keys(sort_order)
93
+ keys = enum.map(&:to_s)
94
+ (sort_order == :alphabetical) ? keys.sort_by(&:downcase) : keys
95
+ end
96
+
79
97
  def quote_string(str, prefer_double_quotes)
80
98
  prefer_double_quotes ? "\"#{str}\"" : "'#{str}'"
81
99
  end
82
100
 
101
+ def js_key(str, prefer_double_quotes)
102
+ str.match?(/\A[A-Za-z_$][\w$]*\z/) ? str : quote_string(str, prefer_double_quotes)
103
+ end
104
+
83
105
  # Returns the type name, optionally sorting union members
84
106
  # @param sort_order [Symbol, Proc, nil] Sort order for union types
85
107
  # @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
@@ -89,21 +111,12 @@ module Typelizer
89
111
  return enum_type_name if enum_type_name
90
112
 
91
113
  if enum
92
- # Sort enum values if alphabetical sorting is requested
93
- enum_values = enum.map { |v| quote_string(v.to_s, prefer_double_quotes) }
94
- enum_values = enum_values.sort_by(&:downcase) if sort_order == :alphabetical
95
- return enum_values.join(" | ")
96
- end
97
-
98
- if type.nil? && nested_properties&.any?
99
- inner = nested_properties.map { |p|
100
- rendered = p.render(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) + ";"
101
- rendered.gsub(/^/, " ")
102
- }.join("\n")
103
- return "{\n#{inner}\n}"
114
+ return sorted_enum_keys(sort_order).map { |k| quote_string(k, prefer_double_quotes) }.join(" | ")
104
115
  end
105
116
 
106
117
  case type
118
+ when Shape
119
+ type.render(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes)
107
120
  when Array
108
121
  type.map { |t| t.respond_to?(:name) ? t.name : t.to_s }.join(" | ")
109
122
  else
@@ -15,22 +15,25 @@ module Typelizer
15
15
  end
16
16
  end
17
17
 
18
- initializer "typelizer.generate" do |app|
18
+ initializer "typelizer.configure_dsl" do
19
+ Typelizer::DSL.disable! unless Typelizer.enabled?
20
+ end
21
+
22
+ server do
19
23
  next unless Typelizer.enabled?
20
24
 
21
- generator = Typelizer::Generator.new
25
+ require_relative "middleware"
26
+ Rails.application.config.app_middleware.use(Typelizer::Middleware)
22
27
 
23
- if Typelizer.listen == true || Gem.loaded_specs["listen"] && Typelizer.listen != false
28
+ if Typelizer.listen == true || (Gem.loaded_specs["listen"] && Typelizer.listen != false)
24
29
  require_relative "listen"
25
- app.config.after_initialize do
26
- Typelizer::Listen.call do
27
- Rails.application.reloader.reload!
28
- end
30
+ Typelizer::Listen.call(run_on_start: false) do
31
+ Rails.application.reloader.reload!
29
32
  end
30
33
  end
31
34
 
32
- app.config.to_prepare do
33
- generator.call
35
+ Rails.application.config.to_prepare do
36
+ Typelizer::Middleware.instance&.mark_pending!
34
37
  end
35
38
  end
36
39
  end