typelizer 0.3.0 → 0.4.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 +37 -2
- data/README.md +15 -1
- data/lib/typelizer/config.rb +4 -0
- data/lib/typelizer/interface.rb +38 -12
- data/lib/typelizer/model_plugins/active_record.rb +24 -0
- data/lib/typelizer/model_plugins/poro.rb +1 -1
- data/lib/typelizer/property.rb +6 -0
- data/lib/typelizer/renderer.rb +8 -0
- data/lib/typelizer/serializer_plugins/alba.rb +1 -1
- data/lib/typelizer/serializer_plugins/auto.rb +2 -0
- data/lib/typelizer/serializer_plugins/panko.rb +63 -0
- data/lib/typelizer/templates/comment.ts.erb +8 -0
- data/lib/typelizer/templates/inheritance.ts.erb +1 -0
- data/lib/typelizer/templates/interface.ts.erb +18 -24
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed3e7b6339e4a79168c10afd541bba9ea8c509b00f689465df037501327ac6e2
|
4
|
+
data.tar.gz: a7f079a7ad377d1f2754cc1efcb98a7bc0e8af11c3910e8f56b648090ddb7f46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7687ff8e061c46e11f56421684a5629a823debc4d76ac3018e3ab7ac960416a05088662372b38bb4029a650ba6d81c74fbccb49d9821d7864f86023fdddd819e
|
7
|
+
data.tar.gz: 860d8dc24a04302c2e70d30a4ef6811f595ee4e55788ab47d6966c10a405370dc756419b06e15b343b00fc38d495897e706d5be5a5fb1816b9a0da3315c009ce
|
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog],
|
6
6
|
and this project adheres to [Semantic Versioning].
|
7
7
|
|
8
|
-
## [
|
8
|
+
## [0.4.0] - 2025-05-03
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Support for `panko_serializer` gem ([@PedroAugustoRamalhoDuarte], [@skryukov])
|
13
|
+
- Mark `has_one` and `belongs_to` association as nullable. ([@skryukov])
|
14
|
+
|
15
|
+
By default, `has_one` associations are marked as nullable in TypeScript interfaces.
|
16
|
+
`belongs_to` associations are marked as nullable if the database column is nullable.
|
17
|
+
Use the new `config.associations_strategy = :active_record` configuration option to mark associations as nullable based on the `required`/`optional` options.
|
18
|
+
You can also use the type hint `typelize latest_post: {nullable: false}` in the serializer to override the defaults.
|
19
|
+
|
20
|
+
- Support inherited typelization. ([@skryukov])
|
21
|
+
|
22
|
+
Set `config.inheritance_strategy = :inheritance` to make Typelizer respect the inheritance hierarchy of serializers:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class AdminSerializer < UserSerializer
|
26
|
+
attributes :admin_level
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
```typescript
|
31
|
+
// app/javascript/types/serializers/Admin.ts
|
32
|
+
import { User } from "@/types";
|
33
|
+
|
34
|
+
export type Admin = User & {
|
35
|
+
admin_level: number;
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
### Fixed
|
40
|
+
|
41
|
+
- Alba: always use strings for keys in properties. ([@skryukov])
|
42
|
+
This change will fire update of all hashes for Alba serializers, but it's necessary to support inheritance strategy.
|
9
43
|
|
10
44
|
## [0.3.0] - 2025-02-28
|
11
45
|
|
@@ -116,7 +150,8 @@ and this project adheres to [Semantic Versioning].
|
|
116
150
|
[@patvice]: https://github.com/patvice
|
117
151
|
[@skryukov]: https://github.com/skryukov
|
118
152
|
|
119
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
153
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.4.0...HEAD
|
154
|
+
[0.4.0]: https://github.com/skryukov/typelizer/compare/v0.3.0...v0.4.0
|
120
155
|
[0.3.0]: https://github.com/skryukov/typelizer/compare/v0.2.0...v0.3.0
|
121
156
|
[0.2.0]: https://github.com/skryukov/typelizer/compare/v0.1.5...v0.2.0
|
122
157
|
[0.1.5]: https://github.com/skryukov/typelizer/compare/v0.1.4...v0.1.5
|
data/README.md
CHANGED
@@ -29,7 +29,7 @@ Typelizer is a Ruby gem that automatically generates TypeScript interfaces from
|
|
29
29
|
## Features
|
30
30
|
|
31
31
|
- Automatic TypeScript interface generation
|
32
|
-
- Support for multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`)
|
32
|
+
- Support for multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`, `Panko::Serializer`)
|
33
33
|
- File watching and automatic regeneration in development
|
34
34
|
|
35
35
|
## Installation
|
@@ -50,6 +50,10 @@ Include the Typelizer DSL in your serializers:
|
|
50
50
|
class ApplicationResource
|
51
51
|
include Alba::Resource
|
52
52
|
include Typelizer::DSL
|
53
|
+
|
54
|
+
# For Alba, we recommend using the `helper` method instead of `include`.
|
55
|
+
# See the documentation: https://github.com/okuramasafumi/alba/blob/main/README.md#helper
|
56
|
+
# helper Typelizer::DSL
|
53
57
|
end
|
54
58
|
|
55
59
|
class PostResource < ApplicationResource
|
@@ -252,6 +256,16 @@ Typelizer.configure do |config|
|
|
252
256
|
# Strategy for handling null values (:nullable, :optional, or :nullable_and_optional)
|
253
257
|
config.null_strategy = :nullable
|
254
258
|
|
259
|
+
# Strategy for handling serializer inheritance (:none, :inheritance)
|
260
|
+
# :none - lists all attributes of the serializer in the type
|
261
|
+
# :inheritance - extends the type from the parent serializer
|
262
|
+
config.inheritance_strategy = :none
|
263
|
+
|
264
|
+
# Strategy for handling `has_one` and `belongs_to` associations nullability (:database, :active_record)
|
265
|
+
# :database - uses the database column nullability
|
266
|
+
# :active_record - uses the `required` / `optional` association options
|
267
|
+
config.associations_strategy = :database
|
268
|
+
|
255
269
|
# Directory where TypeScript interfaces will be generated
|
256
270
|
config.output_dir = Rails.root.join("app/javascript/types/serializers")
|
257
271
|
|
data/lib/typelizer/config.rb
CHANGED
@@ -25,6 +25,8 @@ module Typelizer
|
|
25
25
|
:types_import_path,
|
26
26
|
:types_global,
|
27
27
|
:verbatim_module_syntax,
|
28
|
+
:inheritance_strategy,
|
29
|
+
:associations_strategy,
|
28
30
|
:comments,
|
29
31
|
keyword_init: true
|
30
32
|
) do
|
@@ -47,6 +49,8 @@ module Typelizer
|
|
47
49
|
|
48
50
|
type_mapping: TYPE_MAPPING,
|
49
51
|
null_strategy: :nullable,
|
52
|
+
inheritance_strategy: :none,
|
53
|
+
associations_strategy: :database,
|
50
54
|
comments: false,
|
51
55
|
|
52
56
|
output_dir: js_root.join("types/serializers"),
|
data/lib/typelizer/interface.rb
CHANGED
@@ -17,7 +17,7 @@ module Typelizer
|
|
17
17
|
|
18
18
|
def name
|
19
19
|
if inline?
|
20
|
-
Renderer.
|
20
|
+
Renderer.call("inline_type.ts.erb", properties: properties).strip
|
21
21
|
else
|
22
22
|
config.serializer_name_mapper.call(serializer).tr_s(":", "")
|
23
23
|
end
|
@@ -53,20 +53,46 @@ module Typelizer
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def overwritten_properties
|
57
|
+
return [] unless parent_interface
|
58
|
+
|
59
|
+
@overwritten_properties ||= parent_interface.properties - properties
|
60
|
+
end
|
61
|
+
|
62
|
+
def own_properties
|
63
|
+
@own_properties ||= properties - (parent_interface&.properties || [])
|
64
|
+
end
|
65
|
+
|
66
|
+
def properties_to_print
|
67
|
+
parent_interface ? own_properties : properties
|
68
|
+
end
|
69
|
+
|
70
|
+
def parent_interface
|
71
|
+
return if config.inheritance_strategy == :none
|
72
|
+
return unless serializer.superclass.respond_to?(:typelizer_interface)
|
73
|
+
|
74
|
+
interface = serializer.superclass.typelizer_interface
|
75
|
+
return if interface.empty?
|
76
|
+
|
77
|
+
interface
|
78
|
+
end
|
79
|
+
|
56
80
|
def imports
|
57
|
-
|
58
|
-
.
|
59
|
-
|
81
|
+
@imports ||= begin
|
82
|
+
association_serializers, attribute_types = properties_to_print.filter_map(&:type)
|
83
|
+
.uniq
|
84
|
+
.partition { |type| type.is_a?(Interface) }
|
60
85
|
|
61
|
-
|
62
|
-
|
86
|
+
serializer_types = association_serializers
|
87
|
+
.filter_map { |interface| interface.name if interface.name != name && !interface.inline? }
|
63
88
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
89
|
+
custom_type_imports = attribute_types
|
90
|
+
.flat_map { |type| extract_typescript_types(type.to_s) }
|
91
|
+
.uniq
|
92
|
+
.reject { |type| global_type?(type) }
|
68
93
|
|
69
|
-
|
94
|
+
(custom_type_imports + serializer_types + Array(parent_interface&.name)).uniq - Array(self_type_name)
|
95
|
+
end
|
70
96
|
end
|
71
97
|
|
72
98
|
def inspect
|
@@ -74,7 +100,7 @@ module Typelizer
|
|
74
100
|
end
|
75
101
|
|
76
102
|
def fingerprint
|
77
|
-
"<#{self.class.name} #{name} properties=[#{
|
103
|
+
"<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}]>"
|
78
104
|
end
|
79
105
|
|
80
106
|
private
|
@@ -9,6 +9,30 @@ module Typelizer
|
|
9
9
|
attr_reader :model_class, :config
|
10
10
|
|
11
11
|
def infer_types(prop)
|
12
|
+
if (association = model_class&.reflect_on_association(prop.column_name.to_sym))
|
13
|
+
case association.macro
|
14
|
+
when :belongs_to
|
15
|
+
foreign_key = association.foreign_key
|
16
|
+
column = model_class&.columns_hash&.dig(foreign_key.to_s)
|
17
|
+
if config.associations_strategy == :database
|
18
|
+
prop.nullable = column.null if column
|
19
|
+
elsif config.associations_strategy == :active_record
|
20
|
+
prop.nullable = !association.options[:required] || association.options[:optional]
|
21
|
+
else
|
22
|
+
raise "Unknown associations strategy: #{config.associations_strategy}"
|
23
|
+
end
|
24
|
+
when :has_one
|
25
|
+
if config.associations_strategy == :database
|
26
|
+
prop.nullable = true
|
27
|
+
elsif config.associations_strategy == :active_record
|
28
|
+
prop.nullable = !association.options[:required]
|
29
|
+
else
|
30
|
+
raise "Unknown associations strategy: #{config.associations_strategy}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return prop
|
34
|
+
end
|
35
|
+
|
12
36
|
column = model_class&.columns_hash&.dig(prop.column_name.to_s)
|
13
37
|
return prop unless column
|
14
38
|
|
data/lib/typelizer/property.rb
CHANGED
@@ -9,6 +9,12 @@ module Typelizer
|
|
9
9
|
"<#{self.class.name} #{props}>"
|
10
10
|
end
|
11
11
|
|
12
|
+
def eql?(other)
|
13
|
+
return false unless other.is_a?(self.class)
|
14
|
+
|
15
|
+
fingerprint == other.fingerprint
|
16
|
+
end
|
17
|
+
|
12
18
|
def to_s
|
13
19
|
type_str = type_name
|
14
20
|
type_str = "Array<#{type_str}>" if multi
|
data/lib/typelizer/renderer.rb
CHANGED
@@ -4,6 +4,10 @@ require "erb"
|
|
4
4
|
|
5
5
|
module Typelizer
|
6
6
|
class Renderer
|
7
|
+
def self.call(template, **context)
|
8
|
+
new(template).call(**context)
|
9
|
+
end
|
10
|
+
|
7
11
|
def initialize(template)
|
8
12
|
@erb = ERB.new(File.read(File.join(File.dirname(__FILE__), "templates/#{template}")), trim_mode: "-")
|
9
13
|
end
|
@@ -24,5 +28,9 @@ module Typelizer
|
|
24
28
|
spaces = " " * multiplier
|
25
29
|
content.to_s.each_line.map { |line| line.blank? ? line : "#{spaces}#{line}" }.join
|
26
30
|
end
|
31
|
+
|
32
|
+
def render(template, **context)
|
33
|
+
Renderer.call(template, **context)
|
34
|
+
end
|
27
35
|
end
|
28
36
|
end
|
@@ -13,6 +13,8 @@ module Typelizer
|
|
13
13
|
Alba
|
14
14
|
elsif defined?(ActiveModel::Serializer) && serializer.ancestors.include?(ActiveModel::Serializer)
|
15
15
|
AMS
|
16
|
+
elsif defined?(::Panko::Serializer) && serializer.ancestors.include?(::Panko::Serializer)
|
17
|
+
Panko
|
16
18
|
else
|
17
19
|
raise "Can't guess serializer plugin for #{serializer}. " \
|
18
20
|
"Please specify it with `config.serializer_plugin`."
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
module SerializerPlugins
|
5
|
+
class Panko < Base
|
6
|
+
def methods_to_typelize
|
7
|
+
[:has_many, :has_one, :attributes, :method_added]
|
8
|
+
end
|
9
|
+
|
10
|
+
def properties
|
11
|
+
descriptor = serializer.new.instance_variable_get(:@descriptor)
|
12
|
+
attributes = descriptor.attributes
|
13
|
+
methods_attributes = descriptor.method_fields
|
14
|
+
has_many_associations = descriptor.has_many_associations
|
15
|
+
has_one_associations = descriptor.has_one_associations
|
16
|
+
|
17
|
+
attributes.map do |att|
|
18
|
+
attribute_property(att)
|
19
|
+
end + methods_attributes.map do |att|
|
20
|
+
attribute_property(att)
|
21
|
+
end + has_many_associations.map do |assoc|
|
22
|
+
association_property(assoc, multi: true)
|
23
|
+
end + has_one_associations.map do |assoc|
|
24
|
+
association_property(assoc, multi: false)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
29
|
+
if method == :method_added && binding.local_variable_defined?(:method)
|
30
|
+
name = binding.local_variable_get(:method)
|
31
|
+
end
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def attribute_property(att)
|
39
|
+
Property.new(
|
40
|
+
name: att.alias_name || att.name,
|
41
|
+
optional: false,
|
42
|
+
nullable: false,
|
43
|
+
multi: false,
|
44
|
+
column_name: att.name
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def association_property(assoc, multi: false)
|
49
|
+
key = assoc.name_str
|
50
|
+
serializer = assoc.descriptor.type
|
51
|
+
type = serializer ? Interface.new(serializer: serializer) : nil
|
52
|
+
Property.new(
|
53
|
+
name: key,
|
54
|
+
type: type,
|
55
|
+
optional: false,
|
56
|
+
nullable: false,
|
57
|
+
multi: multi,
|
58
|
+
column_name: key
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%
|
2
|
+
comment = ""
|
3
|
+
comment += property.comment.to_s if interface.config.comments
|
4
|
+
if property.deprecated
|
5
|
+
comment += "\n@deprecated #{property.deprecated.is_a?(String) ? property.deprecated : ''}"
|
6
|
+
end
|
7
|
+
-%>
|
8
|
+
<%= indent("/** #{comment.strip.split("\n").map(&:strip).join("\n * ")} */\n") unless comment.empty? -%>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= interface.overwritten_properties.any? ? "Omit<" : "" %><%= interface.parent_interface.name %><%= "['#{interface.parent_interface.root_key}']" if interface.parent_interface.root_key %><%= interface.overwritten_properties.any? ? ", " + interface.overwritten_properties.map { |pr| "'#{pr.name}'" }.join(' | ') + ">" : ""%>
|
@@ -1,34 +1,28 @@
|
|
1
|
-
|
1
|
+
<% if interface.imports.any? -%>
|
2
2
|
import type {<%= interface.imports.join(", ") %>} from '<%= interface.config.types_import_path %>'
|
3
|
-
|
3
|
+
<% end -%>
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
type <%= interface.name %><%= "Data" if interface.root_key %> = <%=
|
6
|
+
render("inheritance.ts.erb", interface: interface).strip if interface.parent_interface
|
7
|
+
-%>
|
8
|
+
<% unless interface.parent_interface && interface.properties_to_print.empty? -%>
|
9
|
+
<%= " & " if interface.parent_interface %>{
|
10
|
+
<% interface.properties_to_print.each do |property| -%>
|
11
|
+
<%= render("comment.ts.erb", interface: interface, property: property) -%>
|
8
12
|
<%= indent(property) %>;
|
9
|
-
|
13
|
+
<% end -%>
|
10
14
|
}
|
11
|
-
|
12
|
-
type <%= interface.name %> = {
|
13
|
-
<%= interface.root_key %>: <%= interface.name %>Data;
|
14
|
-
<%- interface.meta_fields&.each do |property| -%>
|
15
|
-
<%= indent(property) %>;
|
16
|
-
<%- end -%>
|
17
|
-
}
|
18
|
-
<%- else -%>
|
15
|
+
<% end %><% if interface.root_key %>
|
19
16
|
type <%= interface.name %> = {
|
20
|
-
|
21
|
-
|
22
|
-
<%- comment += property.comment.to_s if interface.config.comments -%>
|
23
|
-
<%- comment += "\n@deprecated #{property.deprecated.is_a?(String) ? property.deprecated : ''}" if property.deprecated -%>
|
24
|
-
<%= indent("/** #{comment.strip.split("\n").map(&:strip).join("\n * ")} */\n") unless comment.empty? -%>
|
17
|
+
<%= indent(interface.root_key) %>: <%= interface.name %>Data;
|
18
|
+
<% interface.meta_fields&.each do |property| -%>
|
25
19
|
<%= indent(property) %>;
|
26
|
-
|
20
|
+
<% end -%>
|
27
21
|
}
|
28
|
-
|
22
|
+
<% end -%>
|
29
23
|
|
30
|
-
|
24
|
+
<% if interface.config.verbatim_module_syntax -%>
|
31
25
|
export type { <%= interface.name %> };
|
32
|
-
|
26
|
+
<% else -%>
|
33
27
|
export default <%= interface.name %>;
|
34
|
-
|
28
|
+
<% end -%>
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative "typelizer/serializer_plugins/auto"
|
|
14
14
|
require_relative "typelizer/serializer_plugins/oj_serializers"
|
15
15
|
require_relative "typelizer/serializer_plugins/alba"
|
16
16
|
require_relative "typelizer/serializer_plugins/ams"
|
17
|
+
require_relative "typelizer/serializer_plugins/panko"
|
17
18
|
|
18
19
|
require_relative "typelizer/model_plugins/active_record"
|
19
20
|
require_relative "typelizer/model_plugins/poro"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typelizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Svyatoslav Kryukov
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02
|
10
|
+
date: 2025-05-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: railties
|
@@ -50,8 +50,11 @@ files:
|
|
50
50
|
- lib/typelizer/serializer_plugins/auto.rb
|
51
51
|
- lib/typelizer/serializer_plugins/base.rb
|
52
52
|
- lib/typelizer/serializer_plugins/oj_serializers.rb
|
53
|
+
- lib/typelizer/serializer_plugins/panko.rb
|
54
|
+
- lib/typelizer/templates/comment.ts.erb
|
53
55
|
- lib/typelizer/templates/fingerprint.ts.erb
|
54
56
|
- lib/typelizer/templates/index.ts.erb
|
57
|
+
- lib/typelizer/templates/inheritance.ts.erb
|
55
58
|
- lib/typelizer/templates/inline_type.ts.erb
|
56
59
|
- lib/typelizer/templates/interface.ts.erb
|
57
60
|
- lib/typelizer/version.rb
|