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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -1
- data/README.md +32 -680
- data/lib/tasks/generate.rake +50 -15
- data/lib/typelizer/config.rb +10 -3
- data/lib/typelizer/configuration.rb +4 -0
- data/lib/typelizer/dsl.rb +14 -5
- data/lib/typelizer/generator.rb +2 -2
- data/lib/typelizer/interface.rb +28 -21
- data/lib/typelizer/listen.rb +26 -7
- data/lib/typelizer/middleware.rb +52 -0
- data/lib/typelizer/model_plugins/active_record.rb +3 -0
- data/lib/typelizer/openapi.rb +11 -17
- data/lib/typelizer/property.rb +40 -27
- data/lib/typelizer/railtie.rb +12 -9
- data/lib/typelizer/route_config.rb +50 -0
- data/lib/typelizer/route_generator.rb +149 -0
- data/lib/typelizer/route_writer.rb +171 -0
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +2 -1
- data/lib/typelizer/serializer_plugins/alba.rb +8 -14
- data/lib/typelizer/shape.rb +39 -0
- data/lib/typelizer/templates/enums.ts.erb +3 -0
- data/lib/typelizer/templates/index.ts.erb +2 -1
- data/lib/typelizer/templates/route_controller.erb +36 -0
- data/lib/typelizer/templates/route_index.erb +14 -0
- data/lib/typelizer/templates/route_runtime.js +97 -0
- data/lib/typelizer/templates/route_runtime.ts +109 -0
- data/lib/typelizer/type_inference.rb +15 -7
- data/lib/typelizer/type_parser.rb +28 -0
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer/writer.rb +4 -4
- data/lib/typelizer.rb +16 -2
- metadata +13 -4
- /data/lib/typelizer/templates/{fingerprint.ts.erb → fingerprint.erb} +0 -0
data/lib/tasks/generate.rake
CHANGED
|
@@ -1,33 +1,68 @@
|
|
|
1
1
|
namespace :typelizer do
|
|
2
2
|
desc "Generate TypeScript interfaces from serializers"
|
|
3
|
-
task
|
|
4
|
-
|
|
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 "
|
|
10
|
-
task "
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
data/lib/typelizer/config.rb
CHANGED
|
@@ -31,12 +31,17 @@ module Typelizer
|
|
|
31
31
|
properties_sort_order
|
|
32
32
|
].freeze
|
|
33
33
|
|
|
34
|
-
#
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/typelizer/generator.rb
CHANGED
|
@@ -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)
|
data/lib/typelizer/interface.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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)
|
|
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 []
|
|
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.
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 =
|
|
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 =
|
|
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
|
data/lib/typelizer/listen.rb
CHANGED
|
@@ -11,7 +11,8 @@ module Typelizer
|
|
|
11
11
|
&block
|
|
12
12
|
)
|
|
13
13
|
return if started
|
|
14
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
data/lib/typelizer/openapi.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
117
|
+
trait_names = property.trait_type_names
|
|
118
|
+
return definition if trait_names.empty?
|
|
124
119
|
|
|
125
|
-
trait_refs =
|
|
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
|
-
|
|
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
|
|
200
|
-
required =
|
|
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:
|
|
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
|
data/lib/typelizer/property.rb
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
#
|
|
56
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
data/lib/typelizer/railtie.rb
CHANGED
|
@@ -15,22 +15,25 @@ module Typelizer
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
initializer "typelizer.
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
35
|
+
Rails.application.config.to_prepare do
|
|
36
|
+
Typelizer::Middleware.instance&.mark_pending!
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
end
|