typelizer 0.12.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 +38 -1
- data/README.md +18 -0
- data/lib/typelizer/config.rb +10 -3
- data/lib/typelizer/dsl.rb +14 -5
- data/lib/typelizer/interface.rb +28 -21
- 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 +11 -9
- 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 +2 -43
- data/lib/typelizer/templates/route_runtime.js +0 -8
- data/lib/typelizer/templates/route_runtime.ts +0 -13
- 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 +3 -3
- data/lib/typelizer.rb +3 -19
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9a7d9dfc60b5b5b8432575016f4f63697b6c6e356d74044abf6646b7d8e10a6
|
|
4
|
+
data.tar.gz: 374654bfb99f5bb3363b7a21174bb4c4a52718d2b852d4597e80aa155ce6fa34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3886301d0db29ef0ed101f5edf76e6228501672f5750c0109a854e142f880c1cd24a71fea9ca352caa888697f801302283b679e36ac61ea19c87e8c9b73ecfb4
|
|
7
|
+
data.tar.gz: bf5b5cdc3748500355aa1bac6d2f0dc035a4776080e06bee523c369cf33177ed24f1e92702e42366e5a7b1e72994696ff32dc52f55295f66bbf48f7961c5e2a5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning].
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.0] - 2026-05-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Runtime enums** (`config.enum_runtime = true`, default `false`): emit an `as const` object alongside each named enum type in `Enums.ts` and re-export it as a value from `index.ts`. Lets consumers compare against enum values at runtime (`if (user.role === UserRole.admin)`) without hand-maintaining a parallel constants file. Existing type-only output is unchanged when the flag is off. ([@skryukov])
|
|
15
|
+
|
|
16
|
+
- **Inline object types**: pass a positional hash to `typelize` to describe an inline TypeScript object type. Nested hashes nest, and options like `multi:`/`nullable:` compose. Useful for JSON columns and ad-hoc computed shapes that don't warrant a separate resource. ([@skryukov])
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
typelize({id: :number, label?: :string})
|
|
20
|
+
attribute :category
|
|
21
|
+
# → category: { id: number; label?: string }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **`?` suffix on attribute keys** as a shorthand for `optional: true`, mirroring TypeScript's own syntax. Works both in keyed `typelize` calls and inside inline shape hashes. ([@skryukov])
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
typelize name?: :string
|
|
28
|
+
# → name?: string
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- [BREAKING] Type generation is now deferred to the first HTTP request via a Rack middleware, instead of running during Rails boot. Boot no longer touches the database, so a missing or pending-migration database no longer crashes server startup. Generation failures raise `Typelizer::TypeGenerationError` (rendered like the standard pending-migrations page) and are retried on the next request. Rake tasks are unaffected. ([@julik])
|
|
34
|
+
- [BREAKING] Bumped `railties` requirement to `>= 6.1.0` to use the `server` Railtie block. ([@julik])
|
|
35
|
+
- [BREAKING] Dropped `DISABLE_TYPELIZER` environment variable support (deprecated since 0.12.0). Use `TYPELIZER=false` instead. ([@skryukov])
|
|
36
|
+
- [BREAKING] Removed the `.form` route variant, `FormDefinition` type, and `formAction` runtime helper. ([@skryukov])
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `typelize` declarations silently dropped during rake tasks, producing `unknown` for every field. ([#114](https://github.com/skryukov/typelizer/issues/114)) ([@skryukov])
|
|
41
|
+
- `properties_transformer` now applied to nested attribute sub-properties, meta nested blocks, and Alba trait properties. Previously only top-level keys were transformed, producing inconsistent output. ([#89](https://github.com/skryukov/typelizer/issues/89)) ([@skryukov])
|
|
42
|
+
- `typelize "Name[]"` paired with `with_traits:` no longer emits a phantom trait intersection with a missing import. Explicit `typelize` overrides are now trusted as-is — the generated type is exactly what you wrote. ([#113](https://github.com/skryukov/typelizer/issues/113)) ([@skryukov])
|
|
43
|
+
- ActiveRecord models whose tables don't exist yet (fresh checkout, pending migration) are now skipped during type inference instead of issuing live DB queries. ([@julik])
|
|
44
|
+
|
|
10
45
|
## [0.12.0] - 2026-03-29
|
|
11
46
|
|
|
12
47
|
### Added
|
|
@@ -455,6 +490,7 @@ and this project adheres to [Semantic Versioning].
|
|
|
455
490
|
[@Envek]: https://github.com/Envek
|
|
456
491
|
[@hkamberovic]: https://github.com/hkamberovic
|
|
457
492
|
[@jonmarkgo]: https://github.com/jonmarkgo
|
|
493
|
+
[@julik]: https://github.com/julik
|
|
458
494
|
[@kristinemcbride]: https://github.com/kristinemcbride
|
|
459
495
|
[@nkriege]: https://github.com/nkriege
|
|
460
496
|
[@NOX73]: https://github.com/NOX73
|
|
@@ -467,7 +503,8 @@ and this project adheres to [Semantic Versioning].
|
|
|
467
503
|
[@skryukov]: https://github.com/skryukov
|
|
468
504
|
[@ventsislaf]: https://github.com/ventsislaf
|
|
469
505
|
|
|
470
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
|
506
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.13.0...HEAD
|
|
507
|
+
[0.13.0]: https://github.com/skryukov/typelizer/compare/v0.12.0...v0.13.0
|
|
471
508
|
[0.12.0]: https://github.com/skryukov/typelizer/compare/v0.11.0...v0.12.0
|
|
472
509
|
[0.11.0]: https://github.com/skryukov/typelizer/compare/v0.10.0...v0.11.0
|
|
473
510
|
[0.10.0]: https://github.com/skryukov/typelizer/compare/v0.9.3...v0.10.0
|
data/README.md
CHANGED
|
@@ -50,6 +50,24 @@ rails typelizer:generate
|
|
|
50
50
|
- [Configuration](https://typelizer.dev/reference/configuration)
|
|
51
51
|
- [Type Mapping](https://typelizer.dev/reference/type-mapping)
|
|
52
52
|
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
You need PostgreSQL running locally. Then:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bundle install
|
|
59
|
+
cd spec/app && RAILS_ENV=test bundle exec rails db:create db:migrate && cd ../..
|
|
60
|
+
bundle exec rspec
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The test suite uses a dummy Rails app in `spec/app/` with models, migrations, and serializers for all four supported frameworks (Alba, AMS, OjSerializers, Panko). Linting is done with StandardRB:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bundle exec standardrb
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`bundle exec rake` runs both the tests and the linter.
|
|
70
|
+
|
|
53
71
|
## Credits
|
|
54
72
|
|
|
55
73
|
Typelizer is inspired by [types_from_serializers](https://github.com/ElMassimo/types_from_serializers), [js-routes](https://github.com/railsware/js-routes), and [Wayfinder](https://github.com/nicholasvansanten/wayfinder).
|
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/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
|
|
@@ -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,23 +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
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
|
-
|
|
34
|
-
RouteGenerator.call
|
|
35
|
+
Rails.application.config.to_prepare do
|
|
36
|
+
Typelizer::Middleware.instance&.mark_pending!
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
end
|
|
@@ -29,6 +29,7 @@ module Typelizer
|
|
|
29
29
|
@properties ||= begin
|
|
30
30
|
props, typelizes = plugin.trait_properties(trait_name)
|
|
31
31
|
props = infer_types(props, typelizes)
|
|
32
|
+
props = transform_properties(props)
|
|
32
33
|
PropertySorter.sort(props, config.properties_sort_order)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -37,7 +38,7 @@ module Typelizer
|
|
|
37
38
|
|
|
38
39
|
def infer_types(props, typelizes)
|
|
39
40
|
props.map do |prop|
|
|
40
|
-
dsl_type =
|
|
41
|
+
dsl_type = prop.lookup_in(typelizes)
|
|
41
42
|
prop
|
|
42
43
|
.then { |p| dsl_type&.any? ? p.with(**dsl_type) : apply_model_inference(p) }
|
|
43
44
|
.then { |p| apply_metadata(p) }
|
|
@@ -47,7 +47,6 @@ module Typelizer
|
|
|
47
47
|
trait_block = traits[trait_name]
|
|
48
48
|
return [], {} unless trait_block
|
|
49
49
|
|
|
50
|
-
# Create a collector to capture attributes defined in the trait block
|
|
51
50
|
collector = BlockAttributeCollector.new
|
|
52
51
|
collector.instance_exec(&trait_block)
|
|
53
52
|
|
|
@@ -76,16 +75,13 @@ module Typelizer
|
|
|
76
75
|
)
|
|
77
76
|
when BlockAttributeCollector::BlockNestedAttribute
|
|
78
77
|
prop_name = has_transform_key?(serializer) ? fetch_key(serializer, name) : name
|
|
79
|
-
nested_props, nested_typelizes = collect_nested_block(attr.block)
|
|
80
78
|
Property.new(
|
|
81
79
|
name: prop_name,
|
|
82
|
-
type:
|
|
80
|
+
type: Shape.new(properties: collect_nested_block(attr.block)),
|
|
83
81
|
optional: false,
|
|
84
82
|
nullable: false,
|
|
85
83
|
multi: false,
|
|
86
|
-
column_name: name
|
|
87
|
-
nested_properties: nested_props,
|
|
88
|
-
nested_typelizes: nested_typelizes
|
|
84
|
+
column_name: name
|
|
89
85
|
)
|
|
90
86
|
else
|
|
91
87
|
build_property(name, attr)
|
|
@@ -179,16 +175,13 @@ module Typelizer
|
|
|
179
175
|
)
|
|
180
176
|
when ::Alba::NestedAttribute
|
|
181
177
|
block = attr.instance_variable_get(:@block)
|
|
182
|
-
nested_props, nested_typelizes = collect_nested_block(block)
|
|
183
178
|
Property.new(
|
|
184
179
|
name: name,
|
|
185
|
-
type:
|
|
180
|
+
type: Shape.new(properties: collect_nested_block(block)),
|
|
186
181
|
optional: false,
|
|
187
182
|
nullable: false,
|
|
188
183
|
multi: false,
|
|
189
184
|
column_name: column_name,
|
|
190
|
-
nested_properties: nested_props,
|
|
191
|
-
nested_typelizes: nested_typelizes,
|
|
192
185
|
**options
|
|
193
186
|
)
|
|
194
187
|
when ::Alba::ConditionalAttribute
|
|
@@ -217,13 +210,14 @@ module Typelizer
|
|
|
217
210
|
def collect_nested_block(block)
|
|
218
211
|
collector = BlockAttributeCollector.new
|
|
219
212
|
collector.instance_exec(&block)
|
|
213
|
+
typelizes = collector.collected_typelizes
|
|
220
214
|
|
|
221
|
-
|
|
215
|
+
collector.collected_attributes.map do |attr_name, attr|
|
|
222
216
|
attr_name_str = attr_name.is_a?(Symbol) ? attr_name.name : attr_name
|
|
223
|
-
build_collected_property(attr_name_str, attr)
|
|
217
|
+
prop = build_collected_property(attr_name_str, attr)
|
|
218
|
+
override = prop.lookup_in(typelizes)
|
|
219
|
+
override&.any? ? prop.with(**override) : prop
|
|
224
220
|
end
|
|
225
|
-
|
|
226
|
-
[props, collector.collected_typelizes]
|
|
227
221
|
end
|
|
228
222
|
end
|
|
229
223
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
class Shape
|
|
5
|
+
attr_reader :properties
|
|
6
|
+
|
|
7
|
+
def initialize(properties:)
|
|
8
|
+
@properties = properties.freeze
|
|
9
|
+
@fingerprint = ["Shape", properties.map(&:fingerprint)].freeze
|
|
10
|
+
@hash = @fingerprint.hash
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def map_properties
|
|
15
|
+
self.class.new(properties: properties.map { |p| yield p })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(sort_order: :none, prefer_double_quotes: false)
|
|
19
|
+
inner = properties.map { |p|
|
|
20
|
+
(p.render(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) + ";")
|
|
21
|
+
.gsub(/^/, " ")
|
|
22
|
+
}.join("\n")
|
|
23
|
+
"{\n#{inner}\n}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
alias_method :to_s, :render
|
|
27
|
+
|
|
28
|
+
attr_reader :fingerprint, :hash
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(Shape) && fingerprint == other.fingerprint
|
|
32
|
+
end
|
|
33
|
+
alias_method :eql?, :==
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
"<Typelizer::Shape properties=#{properties.inspect}>"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
<%- enums.each do |property| -%>
|
|
2
2
|
export <%= property.enum_definition(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) %>;
|
|
3
|
+
<%- if enum_runtime -%>
|
|
4
|
+
export <%= property.enum_runtime_definition(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) %>;
|
|
5
|
+
<%- end -%>
|
|
3
6
|
<%- end -%>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%- if enums.any? -%>
|
|
2
2
|
<%- enums_path = prefer_double_quotes ? '"./Enums"' : "'./Enums'" -%>
|
|
3
|
-
|
|
3
|
+
<%- enum_keyword = enum_runtime ? "export" : "export type" -%>
|
|
4
|
+
<%= enum_keyword %> { <%= ImportSorter.sort(enums.map(&:enum_type_name), imports_sort_order).join(", ") %> } from <%= enums_path %>
|
|
4
5
|
<%- end -%>
|
|
5
6
|
<%- interfaces.each do |interface| -%>
|
|
6
7
|
<%- sorted_traits = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
<%- has_form = routes.any? { |r| !%w[get post].include?(r[:verb]) } -%>
|
|
2
1
|
<%- if ts -%>
|
|
3
|
-
import type { RouteDefinition
|
|
2
|
+
import type { RouteDefinition, RouteOptions } from '<%= runtime_import %>'
|
|
4
3
|
<%- end -%>
|
|
5
|
-
import { buildUrl
|
|
4
|
+
import { buildUrl } from '<%= runtime_import %>'
|
|
6
5
|
|
|
7
6
|
export default {
|
|
8
7
|
<%- routes.each_with_index do |route, idx| -%>
|
|
@@ -10,48 +9,8 @@ export default {
|
|
|
10
9
|
<%- parts = route[:required_params].map { |p| "#{p}#{ts ? ": string | number" : ""}" } -%>
|
|
11
10
|
<%- parts += route[:optional_params].map { |p| "#{p}#{ts ? "?: string | number" : ""}" } -%>
|
|
12
11
|
<%- has_params = parts.any? -%>
|
|
13
|
-
<%- needs_form = !%w[get post].include?(route[:verb]) -%>
|
|
14
12
|
/** <%= route[:verb].upcase %> <%= route[:path] %> */
|
|
15
|
-
<%- if needs_form -%>
|
|
16
13
|
<%- if has_params -%>
|
|
17
|
-
<%- if ts -%>
|
|
18
|
-
<%= route[:key] %>: Object.assign(
|
|
19
|
-
(
|
|
20
|
-
params: { <%= parts.join('; ') %> }<%= " | string | number" if route[:single_required] %>,
|
|
21
|
-
options?: RouteOptions,
|
|
22
|
-
): RouteDefinition<<%= "'#{route[:verb]}'" %>> => ({
|
|
23
|
-
url: buildUrl('<%= route[:path] %>', params, options),
|
|
24
|
-
method: '<%= route[:verb] %>',
|
|
25
|
-
}),
|
|
26
|
-
{ form: (
|
|
27
|
-
params: { <%= parts.join('; ') %> }<%= " | string | number" if route[:single_required] %>,
|
|
28
|
-
options?: RouteOptions,
|
|
29
|
-
): FormDefinition =>
|
|
30
|
-
formAction(buildUrl('<%= route[:path] %>', params, options), '<%= route[:verb] %>') },
|
|
31
|
-
),
|
|
32
|
-
<%- else -%>
|
|
33
|
-
<%= route[:key] %>: Object.assign(
|
|
34
|
-
(params, options) => ({
|
|
35
|
-
url: buildUrl('<%= route[:path] %>', params, options),
|
|
36
|
-
method: '<%= route[:verb] %>',
|
|
37
|
-
}),
|
|
38
|
-
{ form: (params, options) =>
|
|
39
|
-
formAction(buildUrl('<%= route[:path] %>', params, options), '<%= route[:verb] %>') },
|
|
40
|
-
),
|
|
41
|
-
<%- end -%>
|
|
42
|
-
<%- else -%>
|
|
43
|
-
<%- if ts -%>
|
|
44
|
-
<%= route[:key] %>: Object.assign(
|
|
45
|
-
(options?: RouteOptions): RouteDefinition<<%= "'#{route[:verb]}'" %>> => ({
|
|
46
|
-
url: buildUrl('<%= route[:path] %>', {}, options),
|
|
47
|
-
method: '<%= route[:verb] %>',
|
|
48
|
-
}),
|
|
49
|
-
{ form: (options?: RouteOptions): FormDefinition =>
|
|
50
|
-
formAction(buildUrl('<%= route[:path] %>', {}, options), '<%= route[:verb] %>') },
|
|
51
|
-
),
|
|
52
|
-
<%- end -%>
|
|
53
|
-
<%- end -%>
|
|
54
|
-
<%- elsif has_params -%>
|
|
55
14
|
<%- if ts -%>
|
|
56
15
|
<%= route[:key] %>: (
|
|
57
16
|
params: { <%= parts.join('; ') %> }<%= " | string | number" if route[:single_required] %>,
|
|
@@ -20,14 +20,6 @@ export function addUrlDefault(key, value) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function formAction(url, method) {
|
|
24
|
-
if (method === 'get' || method === 'post') {
|
|
25
|
-
return { action: url, method }
|
|
26
|
-
}
|
|
27
|
-
const sep = url.includes('?') ? '&' : '?'
|
|
28
|
-
return { action: `${url}${sep}_method=${method.toUpperCase()}`, method: 'post' }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
export function buildUrl(
|
|
32
24
|
template,
|
|
33
25
|
params,
|
|
@@ -5,11 +5,6 @@ export type RouteDefinition<M extends Method> = {
|
|
|
5
5
|
method: M
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export type FormDefinition = {
|
|
9
|
-
action: string
|
|
10
|
-
method: 'get' | 'post'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
export type RouteOptions = {
|
|
14
9
|
query?: Record<string, unknown>
|
|
15
10
|
anchor?: string
|
|
@@ -37,14 +32,6 @@ export function addUrlDefault(key: string, value: unknown): void {
|
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
export function formAction(url: string, method: Method): FormDefinition {
|
|
41
|
-
if (method === 'get' || method === 'post') {
|
|
42
|
-
return { action: url, method }
|
|
43
|
-
}
|
|
44
|
-
const sep = url.includes('?') ? '&' : '?'
|
|
45
|
-
return { action: `${url}${sep}_method=${method.toUpperCase()}`, method: 'post' }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
35
|
export function buildUrl(
|
|
49
36
|
template: string,
|
|
50
37
|
params: Record<string, unknown> | string | number,
|
|
@@ -15,19 +15,27 @@ module Typelizer
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def transform_properties(props)
|
|
19
|
+
return props unless config.properties_transformer
|
|
20
|
+
|
|
21
|
+
props = config.properties_transformer.call(props)
|
|
22
|
+
props.map do |prop|
|
|
23
|
+
next prop unless prop.type.is_a?(Shape)
|
|
24
|
+
|
|
25
|
+
prop.with(type: Shape.new(properties: transform_properties(prop.type.properties)))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
def infer_nested_property_types(prop)
|
|
19
|
-
return prop unless prop.
|
|
30
|
+
return prop unless prop.type.is_a?(Shape)
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
inferred = prop.nested_properties.map do |sub_prop|
|
|
23
|
-
dsl_type = typelizes[sub_prop.column_name.to_sym] || typelizes[sub_prop.name.to_sym]
|
|
32
|
+
inferred = prop.type.map_properties do |sub_prop|
|
|
24
33
|
sub_prop
|
|
25
|
-
.then { |p|
|
|
34
|
+
.then { |p| p.type ? p : apply_model_inference(p) }
|
|
26
35
|
.then { |p| apply_metadata(p) }
|
|
27
36
|
.then { |p| infer_nested_property_types(p) }
|
|
28
37
|
end
|
|
29
|
-
|
|
30
|
-
prop.with(nested_properties: inferred)
|
|
38
|
+
prop.with(type: inferred)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def model_class
|
|
@@ -23,6 +23,7 @@ module Typelizer
|
|
|
23
23
|
|
|
24
24
|
def parse(type_def, **options)
|
|
25
25
|
return options if type_def.nil?
|
|
26
|
+
return parse_shape(type_def, **options) if type_def.is_a?(Hash)
|
|
26
27
|
return parse_array(type_def, **options) if type_def.is_a?(Array)
|
|
27
28
|
|
|
28
29
|
type_str = type_def.to_s
|
|
@@ -49,8 +50,35 @@ module Typelizer
|
|
|
49
50
|
type_str.end_with?("?", "[]")
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Strips a trailing `?` from an attribute key, returning [clean_name, optional?].
|
|
54
|
+
def parse_key(name)
|
|
55
|
+
str = name.to_s
|
|
56
|
+
str.end_with?("?") ? [str.chomp("?").to_sym, true] : [name.to_sym, false]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_optional_key(parsed, optional_from_key)
|
|
60
|
+
parsed[:optional] = true if optional_from_key && !parsed.key?(:optional)
|
|
61
|
+
parsed
|
|
62
|
+
end
|
|
63
|
+
|
|
52
64
|
private
|
|
53
65
|
|
|
66
|
+
def parse_shape(hash, **options)
|
|
67
|
+
properties = hash.map do |name, value|
|
|
68
|
+
clean_name, optional_from_key = parse_key(name)
|
|
69
|
+
|
|
70
|
+
# parse_declaration returns Hash args verbatim (options-bag form); nested
|
|
71
|
+
# shapes need parse to dispatch back here and build a Shape.
|
|
72
|
+
parsed = value.is_a?(Hash) ? parse(value) : parse_declaration(value)
|
|
73
|
+
apply_optional_key(parsed, optional_from_key)
|
|
74
|
+
|
|
75
|
+
property_attrs = parsed.slice(*Property.members).tap { |h| h[:name] = clean_name }
|
|
76
|
+
Property.new(optional: false, nullable: false, multi: false, **property_attrs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
{type: Shape.new(properties: properties)}.merge(options)
|
|
80
|
+
end
|
|
81
|
+
|
|
54
82
|
def parse_array(type_defs, **options)
|
|
55
83
|
raise ArgumentError, "Empty array passed to typelize" if type_defs.empty?
|
|
56
84
|
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer/writer.rb
CHANGED
|
@@ -60,9 +60,9 @@ module Typelizer
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def write_enums(enums)
|
|
63
|
-
fingerprint = [enums.map(&:fingerprint), config.properties_sort_order, config.prefer_double_quotes].inspect
|
|
63
|
+
fingerprint = [enums.map(&:fingerprint), config.properties_sort_order, config.prefer_double_quotes, config.enum_runtime].inspect
|
|
64
64
|
write_file("Enums.ts", fingerprint) do
|
|
65
|
-
render_template("enums.ts.erb", enums: enums, sort_order: config.properties_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
65
|
+
render_template("enums.ts.erb", enums: enums, sort_order: config.properties_sort_order, prefer_double_quotes: config.prefer_double_quotes, enum_runtime: config.enum_runtime)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -74,7 +74,7 @@ module Typelizer
|
|
|
74
74
|
}
|
|
75
75
|
].inspect
|
|
76
76
|
write_file("index.ts", fingerprint) do
|
|
77
|
-
render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
77
|
+
render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes, enum_runtime: config.enum_runtime)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
data/lib/typelizer.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "typelizer/version"
|
|
4
4
|
require_relative "typelizer/union_type_sorter"
|
|
5
|
+
require_relative "typelizer/shape"
|
|
5
6
|
require_relative "typelizer/property"
|
|
6
7
|
require_relative "typelizer/model_plugins/auto"
|
|
7
8
|
require_relative "typelizer/serializer_plugins/auto"
|
|
@@ -50,10 +51,7 @@ module Typelizer
|
|
|
50
51
|
# Is Typelizer active?
|
|
51
52
|
#
|
|
52
53
|
# Precedence: TYPELIZER env var > development? detection
|
|
53
|
-
# Legacy DISABLE_TYPELIZER is mapped to TYPELIZER with a deprecation warning.
|
|
54
54
|
def enabled?
|
|
55
|
-
migrate_legacy_env!
|
|
56
|
-
|
|
57
55
|
val = ENV["TYPELIZER"]
|
|
58
56
|
return val == "true" || val == "1" if val
|
|
59
57
|
|
|
@@ -95,23 +93,9 @@ module Typelizer
|
|
|
95
93
|
private
|
|
96
94
|
|
|
97
95
|
def development?
|
|
98
|
-
|
|
99
|
-
end
|
|
96
|
+
return Rails.env.development? if defined?(Rails) && Rails.respond_to?(:env)
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
# Only takes effect if TYPELIZER is not already set.
|
|
103
|
-
def migrate_legacy_env!
|
|
104
|
-
return if @legacy_env_migrated
|
|
105
|
-
@legacy_env_migrated = true
|
|
106
|
-
|
|
107
|
-
val = ENV["DISABLE_TYPELIZER"]
|
|
108
|
-
return unless val
|
|
109
|
-
|
|
110
|
-
new_val = (val == "true" || val == "1") ? "false" : "true"
|
|
111
|
-
logger.warn(
|
|
112
|
-
"[Typelizer] DISABLE_TYPELIZER is deprecated, use TYPELIZER=#{new_val} instead."
|
|
113
|
-
)
|
|
114
|
-
ENV["TYPELIZER"] ||= new_val
|
|
98
|
+
ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development"
|
|
115
99
|
end
|
|
116
100
|
|
|
117
101
|
def load_serializers
|
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
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 6.
|
|
18
|
+
version: 6.1.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 6.
|
|
25
|
+
version: 6.1.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: benchmark
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- lib/typelizer/import_sorter.rb
|
|
63
63
|
- lib/typelizer/interface.rb
|
|
64
64
|
- lib/typelizer/listen.rb
|
|
65
|
+
- lib/typelizer/middleware.rb
|
|
65
66
|
- lib/typelizer/model_plugins/active_record.rb
|
|
66
67
|
- lib/typelizer/model_plugins/auto.rb
|
|
67
68
|
- lib/typelizer/model_plugins/poro.rb
|
|
@@ -82,6 +83,7 @@ files:
|
|
|
82
83
|
- lib/typelizer/serializer_plugins/base.rb
|
|
83
84
|
- lib/typelizer/serializer_plugins/oj_serializers.rb
|
|
84
85
|
- lib/typelizer/serializer_plugins/panko.rb
|
|
86
|
+
- lib/typelizer/shape.rb
|
|
85
87
|
- lib/typelizer/templates/comment.ts.erb
|
|
86
88
|
- lib/typelizer/templates/enums.ts.erb
|
|
87
89
|
- lib/typelizer/templates/fingerprint.erb
|