typelizer 0.1.5 → 0.2.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: 52c5a98a49d5f7a6f040da23ce1ff5c7b8453a758c0daae251788189be0fc874
4
- data.tar.gz: f0b023ec773a121a30d5ee587118dad4c30b36f33309e34e4d07fb77b32a93fd
3
+ metadata.gz: d00f7bda8328c5148fe2bb6053d27066dd128fd834432c92d39417c3bbdc9405
4
+ data.tar.gz: 8b7fc9baa4b646cd91965b1316d9c9dcb22867a5bc8b65407c3fa36bbff9d85c
5
5
  SHA512:
6
- metadata.gz: 6c4b5d39763829b212109356ac5f81967cbcd2340562bddf6bb116cd98d18e209e42f0d694d7fdb62241c661849591e75728250eb856929b68cdbdb4d54fd03a
7
- data.tar.gz: 66635cbafdbc3babb70f92dd2a73b924954f7e7b3f22623397ef53c99d62f8c62c3daea754634833dd3909cdaaa4e03f6aa114c1d7156202aa8274ee2a77cc2e
6
+ metadata.gz: 9174cd537b757ea89f7a886c1443e12a78f16b269bc16ccb2e49866128c47471df83e55c32cc60ef7148ea4e8cd15d5cc1552f907dad903931c043af97ab6bc2
7
+ data.tar.gz: f4d59939214975e140d1a9db3cdc8c5bd9a3278baf6f59d686af7434499917ae30581fcb2a2681c1e106372fcda819250a47f5ba61a4844819dac660f8dea93a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2024-11-26
11
+
12
+ ## Added
13
+
14
+ - Add support for enum attributes declared using `ActiveRecord::Enum` or explicitly in serializers ([@envek])
15
+ - Add support for comments in generated TypeScript interfaces ([@envek])
16
+ - Add TypeScript verbatim module syntax support through `verbatim_module_syntax` config option ([@patvice])
17
+ - Add `typelizer:generate:refresh` command to clean output directory and regenerate all interfaces ([@patvice])
18
+ - Allow disabling Typelizer in Rails development with `DISABLE_TYPELIZER` environment variable to `true` ([@okuramasafumi])
19
+
20
+ ## Fixes
21
+
22
+ - Do not override `Typelizer.dirs` in the railtie initializer ([@patvice])
23
+ - Do not raise on empty nested serializers ([@skryukov])
24
+ - Attribute options merging in inherited serializers ([@envek])
25
+ - Allow recursive type definition ([@okuramasafumi])
26
+
10
27
  ## [0.1.5] - 2024-10-07
11
28
 
12
29
  ## Fixed
@@ -58,10 +75,13 @@ and this project adheres to [Semantic Versioning].
58
75
  - Initial release ([@skryukov])
59
76
 
60
77
  [@davidrunger]: https://github.com/davidrunger
61
- [@okuramasafumi]: https://github.com/@okuramasafumi
78
+ [@envek]: https://github.com/envek
79
+ [@okuramasafumi]: https://github.com/okuramasafumi
80
+ [@patvice]: https://github.com/patvice
62
81
  [@skryukov]: https://github.com/skryukov
63
82
 
64
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.1.5...HEAD
83
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.2.0...HEAD
84
+ [0.2.0]: https://github.com/skryukov/typelizer/compare/v0.1.5...v0.2.0
65
85
  [0.1.5]: https://github.com/skryukov/typelizer/compare/v0.1.4...v0.1.5
66
86
  [0.1.4]: https://github.com/skryukov/typelizer/compare/v0.1.3...v0.1.4
67
87
  [0.1.3]: https://github.com/skryukov/typelizer/compare/v0.1.2...v0.1.3
data/README.md CHANGED
@@ -14,6 +14,7 @@ Typelizer is a Ruby gem that automatically generates TypeScript interfaces from
14
14
  - [TypeScript Integration](#typescript-integration)
15
15
  - [Manual Generation](#manual-generation)
16
16
  - [Automatic Generation in Development](#automatic-generation-in-development)
17
+ - [Disabling Typelizer](#disabling-typelizer)
17
18
  - [Configuration](#configuration)
18
19
  - [Global Configuration](#global-configuration)
19
20
  - [Config Options](#config-options)
@@ -79,9 +80,34 @@ class PostResource < ApplicationResource
79
80
  attribute :author_name do |post|
80
81
  post.author.name
81
82
  end
83
+
84
+ typelize :string, nullable: true, comment: "Author's avatar URL"
85
+ attribute :avatar do
86
+ "https://example.com/avatar.png" if active?
87
+ end
82
88
  end
83
89
  ```
84
90
 
91
+ `typelize` can be used with a Hash to specify multiple types at once.
92
+
93
+ ```ruby
94
+ class PostResource < ApplicationResource
95
+ attributes :id, :title, :body, :published_at
96
+
97
+ attribute :author_name do |post|
98
+ post.author.name
99
+ end
100
+
101
+ typelize author_name: :string, published_at: :string
102
+ end
103
+ ```
104
+
105
+ You can also specify more complex type definitions using a lower-level API:
106
+
107
+ ```ruby
108
+ typelize attribute_name: ["string", "Date", optional: true, nullable: true, multi: true, enum: %w[foo bar], comment: "Attribute description"]
109
+ ```
110
+
85
111
  ### TypeScript Integration
86
112
 
87
113
  Typelizer generates TypeScript interfaces in the specified output directory:
@@ -91,6 +117,7 @@ Typelizer generates TypeScript interfaces in the specified output directory:
91
117
  export interface Post {
92
118
  id: number;
93
119
  title: string;
120
+ category?: "news" | "article" | "blog" | null;
94
121
  body: string;
95
122
  published_at: string | null;
96
123
  author_name: string;
@@ -156,11 +183,15 @@ See the [Configuration](#configuration) section for more options.
156
183
 
157
184
  ### Manual Generation
158
185
 
159
- To manually generate TypeScript interfaces:
186
+ To manually generate TypeScript interfaces use one of the following commands:
160
187
 
161
- ```
162
- $ rails typelizer:generate
163
- ```
188
+ ```bash
189
+ # Generate new interfaces
190
+ rails typelizer:generate
191
+
192
+ # Clean output directory and regenerate all interfaces
193
+ rails typelizer:generate:refresh
194
+ ````
164
195
 
165
196
  ### Automatic Generation in Development
166
197
 
@@ -170,6 +201,10 @@ When [Listen](https://github.com/guard/listen) is installed, Typelizer automatic
170
201
  Typelizer.listen = false
171
202
  ```
172
203
 
204
+ ### Disabling Typelizer
205
+
206
+ Sometimes we want to use Typelizer only with manual generation. To disable Typelizer during development, we can set `DISABLE_TYPELIZER` environment variable to `true`. This doesn't affect manual generation.
207
+
173
208
  ## Configuration
174
209
 
175
210
  ### Global Configuration
@@ -227,6 +262,14 @@ Typelizer.configure do |config|
227
262
  # List of type names that should be considered global in TypeScript
228
263
  # (i.e. not prefixed with the import path)
229
264
  config.types_global << %w[Array Date Record File FileList]
265
+
266
+ # Support TypeScript's Verbatim module syntax option (default: false)
267
+ # Will change imports and exports of types from default to support this syntax option
268
+ config.verbatim_module_syntax = false
269
+
270
+ # Support comments in generated TypeScript interfaces (default: false)
271
+ # Will add comments to the generated interfaces
272
+ config.comments = false
230
273
  end
231
274
  ```
232
275
 
@@ -1,14 +1,27 @@
1
1
  namespace :typelizer do
2
2
  desc "Generate TypeScript interfaces from serializers"
3
3
  task generate: :environment do
4
+ benchmark do
5
+ Typelizer::Generator.call
6
+ end
7
+ end
8
+
9
+ desc "Removes all files in output folder and refreshs all generate TypeScript interfaces from serializers"
10
+ task "generate:refresh": :environment do
11
+ benchmark do
12
+ Typelizer::Generator.call(force: true)
13
+ end
14
+ end
15
+
16
+ def benchmark(&block)
4
17
  require "benchmark"
5
18
 
6
- ENV["TYPELIZER"] = "true"
19
+ ENV["DISABLE_TYPELIZER"] = "false"
7
20
 
8
21
  puts "Generating TypeScript interfaces..."
9
22
  serializers = []
10
23
  time = Benchmark.realtime do
11
- serializers = Typelizer::Generator.call
24
+ serializers = block.call
12
25
  end
13
26
 
14
27
  puts "Finished in #{time} seconds"
@@ -24,6 +24,8 @@ module Typelizer
24
24
  :output_dir,
25
25
  :types_import_path,
26
26
  :types_global,
27
+ :verbatim_module_syntax,
28
+ :comments,
27
29
  keyword_init: true
28
30
  ) do
29
31
  class << self
@@ -45,13 +47,15 @@ module Typelizer
45
47
 
46
48
  type_mapping: TYPE_MAPPING,
47
49
  null_strategy: :nullable,
50
+ comments: false,
48
51
 
49
52
  output_dir: js_root.join("types/serializers"),
50
53
 
51
54
  types_import_path: "@/types",
52
55
  types_global: %w[Array Date Record File FileList],
53
56
 
54
- properties_transformer: nil
57
+ properties_transformer: nil,
58
+ verbatim_module_syntax: false
55
59
  )
56
60
  end
57
61
 
@@ -70,7 +74,6 @@ module Typelizer
70
74
 
71
75
  def method_missing(method, *args, &block)
72
76
  return Typelizer.send(method, *args, &block) if Typelizer.respond_to?(method)
73
-
74
77
  instance.send(method, *args, &block)
75
78
  end
76
79
  end
data/lib/typelizer/dsl.rb CHANGED
@@ -64,9 +64,11 @@ module Typelizer
64
64
 
65
65
  unless respond_to?(attribute_name)
66
66
  define_singleton_method(attribute_name) do
67
- result = instance_variable_get(instance_variable)
67
+ result = instance_variable_get(instance_variable) || {}
68
68
  if superclass.respond_to?(attribute_name)
69
- result.merge(superclass.send(attribute_name))
69
+ result.merge(superclass.send(attribute_name)) do |key, currentdef, supervaldef|
70
+ supervaldef.merge(currentdef)
71
+ end
70
72
  else
71
73
  result
72
74
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Typelizer
4
4
  class Generator
5
- def self.call
6
- new.call
5
+ def self.call(**args)
6
+ new.call(**args)
7
7
  end
8
8
 
9
9
  def initialize(config = Typelizer::Config)
@@ -16,14 +16,18 @@ module Typelizer
16
16
  def call(force: false)
17
17
  return unless Typelizer.enabled?
18
18
 
19
- read_serializers
20
-
21
- interfaces = target_serializers.map(&:typelizer_interface).reject(&:empty?)
22
19
  writer.call(interfaces, force: force)
23
20
 
24
21
  interfaces
25
22
  end
26
23
 
24
+ def interfaces
25
+ @interfaces ||= begin
26
+ read_serializers
27
+ target_serializers.map(&:typelizer_interface).reject(&:empty?)
28
+ end
29
+ end
30
+
27
31
  private
28
32
 
29
33
  def target_serializers
@@ -66,7 +66,7 @@ module Typelizer
66
66
  .uniq
67
67
  .reject { |type| global_type?(type) }
68
68
 
69
- (custom_type_imports + serializer_types).uniq
69
+ (custom_type_imports + serializer_types).uniq - Array(self_type_name)
70
70
  end
71
71
 
72
72
  def inspect
@@ -75,6 +75,10 @@ module Typelizer
75
75
 
76
76
  private
77
77
 
78
+ def self_type_name
79
+ serializer.name.match(/(\w+::)?(\w+)(Serializer|Resource)/)[2]
80
+ end
81
+
78
82
  def extract_typescript_types(type)
79
83
  type.split(/[<>\[\],\s|]+/)
80
84
  end
@@ -87,7 +91,12 @@ module Typelizer
87
91
  props.map do |prop|
88
92
  if serializer.respond_to?(hash_name)
89
93
  dsl_type = serializer.public_send(hash_name)[prop.name.to_sym]
90
- next Property.new(prop.to_h.merge(dsl_type)) if dsl_type&.any?
94
+ if dsl_type&.any?
95
+ next Property.new(prop.to_h.merge(dsl_type)).tap do |property|
96
+ property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
97
+ property.enum ||= model_plugin.enum_for(property) if property.enum != false
98
+ end
99
+ end
91
100
  end
92
101
 
93
102
  model_plugin.infer_types(prop)
@@ -2,14 +2,14 @@ module Typelizer
2
2
  module ModelPlugins
3
3
  class ActiveRecord
4
4
  def initialize(model_class:, config:)
5
- @columns_hash = model_class&.columns_hash || {}
5
+ @model_class = model_class
6
6
  @config = config
7
7
  end
8
8
 
9
- attr_reader :columns_hash, :config
9
+ attr_reader :model_class, :config
10
10
 
11
11
  def infer_types(prop)
12
- column = columns_hash[prop.column_name.to_s]
12
+ column = model_class&.columns_hash&.dig(prop.column_name.to_s)
13
13
  return prop unless column
14
14
 
15
15
  prop.multi = !!column.try(:array)
@@ -26,9 +26,25 @@ module Typelizer
26
26
  end
27
27
 
28
28
  prop.type = @config.type_mapping[column.type]
29
+ prop.comment = comment_for(prop)
30
+ prop.enum = enum_for(prop)
31
+ prop.type = :string if prop.enum # Ignore underlying column type for enums
29
32
 
30
33
  prop
31
34
  end
35
+
36
+ def comment_for(prop)
37
+ column = model_class&.columns_hash&.dig(prop.column_name.to_s)
38
+ return nil unless column
39
+
40
+ prop.comment = column.comment
41
+ end
42
+
43
+ def enum_for(prop)
44
+ return unless model_class&.defined_enums&.key?(prop.column_name.to_s)
45
+
46
+ prop.enum = model_class.defined_enums[prop.column_name.to_s].keys
47
+ end
32
48
  end
33
49
  end
34
50
  end
@@ -8,6 +8,14 @@ module Typelizer
8
8
  def infer_types(prop)
9
9
  prop
10
10
  end
11
+
12
+ def comment_for(prop)
13
+ nil
14
+ end
15
+
16
+ def enum_for(prop)
17
+ nil
18
+ end
11
19
  end
12
20
  end
13
21
  end
@@ -1,7 +1,7 @@
1
1
  module Typelizer
2
2
  Property = Struct.new(
3
3
  :name, :type, :optional, :nullable,
4
- :multi, :column_name,
4
+ :multi, :column_name, :comment, :enum,
5
5
  keyword_init: true
6
6
  ) do
7
7
  def inspect
@@ -20,6 +20,8 @@ module Typelizer
20
20
  private
21
21
 
22
22
  def type_name
23
+ return enum.map { |v| v.to_s.inspect }.join(" | ") if enum
24
+
23
25
  type.respond_to?(:name) ? type.name : type || "unknown"
24
26
  end
25
27
  end
@@ -6,10 +6,12 @@ module Typelizer
6
6
 
7
7
  initializer "typelizer.configure" do
8
8
  Typelizer.configure do |c|
9
- c.dirs = [
10
- Rails.root.join("app", "resources"),
11
- Rails.root.join("app", "serializers")
12
- ]
9
+ if c.dirs.empty?
10
+ c.dirs = [
11
+ Rails.root.join("app", "resources"),
12
+ Rails.root.join("app", "serializers")
13
+ ]
14
+ end
13
15
  end
14
16
  end
15
17
 
@@ -1,3 +1,7 @@
1
1
  <%- interfaces.each do |interface| -%>
2
+ <%- if interface.config.verbatim_module_syntax -%>
3
+ export type { <%= interface.name %> } from './<%= interface.filename %>'
4
+ <%- else -%>
2
5
  export type { default as <%= interface.name %> } from './<%= interface.filename %>'
3
6
  <%- end -%>
7
+ <%- end -%>
@@ -18,9 +18,14 @@ type <%= interface.name %> = {
18
18
  <%- else -%>
19
19
  type <%= interface.name %> = {
20
20
  <%- interface.properties.each do |property| -%>
21
+ <%= indent("/** #{property.comment.split("\n").map(&:strip).join("\n * ")} */\n") if interface.config.comments && property.comment -%>
21
22
  <%= indent(property) %>;
22
23
  <%- end -%>
23
24
  }
24
25
  <%- end -%>
25
26
 
27
+ <%-if interface.config.verbatim_module_syntax -%>
28
+ export type { <%= interface.name %> };
29
+ <%- else -%>
26
30
  export default <%= interface.name %>;
31
+ <%- end -%>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/typelizer.rb CHANGED
@@ -26,7 +26,9 @@ require "logger"
26
26
  module Typelizer
27
27
  class << self
28
28
  def enabled?
29
- ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["TYPELIZER"] == "true"
29
+ return false if ENV["DISABLE_TYPELIZER"] == "true" || ENV["DISABLE_TYPELIZER"] == "1"
30
+
31
+ ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["DISABLE_TYPELIZER"] == "false"
30
32
  end
31
33
 
32
34
  attr_accessor :dirs
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typelizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-07 00:00:00.000000000 Z
11
+ date: 2024-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  requirements: []
85
- rubygems_version: 3.5.17
85
+ rubygems_version: 3.5.23
86
86
  signing_key:
87
87
  specification_version: 4
88
88
  summary: A TypeScript type generator for Ruby serializers.