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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 391ff3345874135d72e3875c60aff109ebfd8ce98b0766e51dd1d2a8909e2af9
4
- data.tar.gz: 1a3c12dad42402fc5485f3124660e2376db4f8dec7b36d016a8d48a33d568fa0
3
+ metadata.gz: ed3e7b6339e4a79168c10afd541bba9ea8c509b00f689465df037501327ac6e2
4
+ data.tar.gz: a7f079a7ad377d1f2754cc1efcb98a7bc0e8af11c3910e8f56b648090ddb7f46
5
5
  SHA512:
6
- metadata.gz: 6111652d2f134f20fa6450c1eccaf592a1bade20e0325b49b43a55628e9ac4f7eab1dfbfad09df0aa28893ae898571ccb486d92aa79f23cce682ab3d0c61b8d4
7
- data.tar.gz: 1b73e070ce4dc50cc87d1a589a286afffcca8f42f596a6172e9b236e9911c72b45ea391d8e7b20ac8321eb83bb488f871c48d67e2a5ece6672530e0e897a6689
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
- ## [Unreleased]
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.3.0...HEAD
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
 
@@ -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"),
@@ -17,7 +17,7 @@ module Typelizer
17
17
 
18
18
  def name
19
19
  if inline?
20
- Renderer.new("inline_type.ts.erb").call(properties: properties).strip
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
- association_serializers, attribute_types = properties.filter_map(&:type)
58
- .uniq
59
- .partition { |type| type.is_a?(Interface) }
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
- serializer_types = association_serializers
62
- .filter_map { |interface| interface.name if interface.name != name && !interface.inline? }
86
+ serializer_types = association_serializers
87
+ .filter_map { |interface| interface.name if interface.name != name && !interface.inline? }
63
88
 
64
- custom_type_imports = attribute_types
65
- .flat_map { |type| extract_typescript_types(type.to_s) }
66
- .uniq
67
- .reject { |type| global_type?(type) }
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
- (custom_type_imports + serializer_types).uniq - Array(self_type_name)
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=[#{properties.map(&:fingerprint).join(", ")}]>"
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
 
@@ -1,7 +1,7 @@
1
1
  module Typelizer
2
2
  module ModelPlugins
3
3
  class Poro
4
- # We don't care about intialization
4
+ # We don't care about initialization
5
5
  def initialize(...)
6
6
  end
7
7
 
@@ -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
@@ -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,7 +13,7 @@ module Typelizer
13
13
 
14
14
  def properties
15
15
  serializer._attributes.map do |name, attr|
16
- build_property(name, attr)
16
+ build_property(name.is_a?(Symbol) ? name.name : name, attr)
17
17
  end
18
18
  end
19
19
 
@@ -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
- <%- if interface.imports.any? -%>
1
+ <% if interface.imports.any? -%>
2
2
  import type {<%= interface.imports.join(", ") %>} from '<%= interface.config.types_import_path %>'
3
- <%- end -%>
3
+ <% end -%>
4
4
 
5
- <%- if interface.root_key -%>
6
- type <%= interface.name %>Data = {
7
- <%- interface.properties.each do |property| -%>
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
- <%- end -%>
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
- <%- interface.properties.each do |property| -%>
21
- <%- comment = "" -%>
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
- <%- end -%>
20
+ <% end -%>
27
21
  }
28
- <%- end -%>
22
+ <% end -%>
29
23
 
30
- <%-if interface.config.verbatim_module_syntax -%>
24
+ <% if interface.config.verbatim_module_syntax -%>
31
25
  export type { <%= interface.name %> };
32
- <%- else -%>
26
+ <% else -%>
33
27
  export default <%= interface.name %>;
34
- <%- end -%>
28
+ <% end -%>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.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-28 00:00:00.000000000 Z
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