typelizer 0.1.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 +7 -0
- data/CHANGELOG.md +20 -0
- data/README.md +253 -0
- data/lib/tasks/generate.rake +18 -0
- data/lib/typelizer/config.rb +75 -0
- data/lib/typelizer/dsl.rb +89 -0
- data/lib/typelizer/generator.rb +66 -0
- data/lib/typelizer/interface.rb +101 -0
- data/lib/typelizer/listen.rb +72 -0
- data/lib/typelizer/model_plugins/active_record.rb +34 -0
- data/lib/typelizer/property.rb +26 -0
- data/lib/typelizer/railtie.rb +35 -0
- data/lib/typelizer/serializer_plugins/alba.rb +120 -0
- data/lib/typelizer/serializer_plugins/ams.rb +33 -0
- data/lib/typelizer/serializer_plugins/auto.rb +24 -0
- data/lib/typelizer/serializer_plugins/base.rb +34 -0
- data/lib/typelizer/serializer_plugins/oj_serializers.rb +33 -0
- data/lib/typelizer/templates/fingerprint.ts.erb +3 -0
- data/lib/typelizer/templates/index.ts.erb +3 -0
- data/lib/typelizer/templates/interface.ts.erb +26 -0
- data/lib/typelizer/version.rb +5 -0
- data/lib/typelizer/writer.rb +65 -0
- data/lib/typelizer.rb +51 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2a3d9ee492a8abe52368f76b6312cb8dea42c1321d45f9555e69ad7165c660bf
|
4
|
+
data.tar.gz: b3327bfeebc1bae33772bd3a4ddeec312583fbc9345da5e2ca16e992f845331b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43771cbcdbc2ae8952abe2af958b434a1e2cf8caaa639fab7ea477be64000e7b9c4e7d83fec13c94b3ab26ac65efbae4dfab47bd3e6976e345c0b2516e95bf40
|
7
|
+
data.tar.gz: 0450a22f5189a079f7747f2c8c2d1b01445143d5a1cbdadf34d3be17ef7056b2f28ce27d880b3950d87d69e34a166481742a1d16a8fce5b9906c130b3c18f378
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog],
|
6
|
+
and this project adheres to [Semantic Versioning].
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [0.1.0] - 2024-08-02
|
11
|
+
|
12
|
+
- Initial release ([@skryukov])
|
13
|
+
|
14
|
+
[@skryukov]: https://github.com/skryukov
|
15
|
+
|
16
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.1.0...HEAD
|
17
|
+
[0.1.0]: https://github.com/skryukov/typelizer/commits/v0.1.0
|
18
|
+
|
19
|
+
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
20
|
+
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
data/README.md
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
# Typelizer
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/typelizer)
|
4
|
+
|
5
|
+
Typelizer is a Ruby gem that automatically generates TypeScript interfaces from your Ruby serializers, bridging the gap between your Ruby backend and TypeScript frontend. It supports multiple serializer libraries and provides a flexible configuration system, making it easier to maintain type consistency across your full-stack application.
|
6
|
+
|
7
|
+
## Table of Contents
|
8
|
+
|
9
|
+
- [Features](#features)
|
10
|
+
- [Installation](#installation)
|
11
|
+
- [Usage](#usage)
|
12
|
+
- [Basic Setup](#basic-setup)
|
13
|
+
- [Manual Typing](#manual-typing)
|
14
|
+
- [TypeScript Integration](#typescript-integration)
|
15
|
+
- [Manual Generation](#manual-generation)
|
16
|
+
- [Automatic Generation in Development](#automatic-generation-in-development)
|
17
|
+
- [Configuration](#configuration)
|
18
|
+
- [Global Configuration](#global-configuration)
|
19
|
+
- [Config Options](#config-options)
|
20
|
+
- [Per-Serializer Configuration](#per-serializer-configuration)
|
21
|
+
- [Credits](#credits)
|
22
|
+
- [License](#license)
|
23
|
+
|
24
|
+
<a href="https://evilmartians.com/?utm_source=typelizer&utm_campaign=project_page">
|
25
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Built by Evil Martians" width="236" height="54">
|
26
|
+
</a>
|
27
|
+
|
28
|
+
## Features
|
29
|
+
|
30
|
+
- Automatic TypeScript interface generation
|
31
|
+
- Support for multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`)
|
32
|
+
- File watching and automatic regeneration in development
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
To install Typelizer, add the following line to your `Gemfile` and run `bundle install`:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
gem "typelizer"
|
40
|
+
```
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
### Basic Setup
|
45
|
+
|
46
|
+
Include the Typelizer DSL in your serializers:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class ApplicationResource
|
50
|
+
include Alba::Resource
|
51
|
+
include Typelizer::DSL
|
52
|
+
end
|
53
|
+
|
54
|
+
class PostResource < ApplicationResource
|
55
|
+
attributes :id, :title, :body
|
56
|
+
|
57
|
+
has_one :author, serializer: AuthorResource
|
58
|
+
end
|
59
|
+
|
60
|
+
class AuthorResource < ApplicationResource
|
61
|
+
# specify the model to infer types from (optional)
|
62
|
+
typelize_from User
|
63
|
+
|
64
|
+
attributes :id, :name
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
Typelizer will automatically generate TypeScript interfaces based on your serializer definitions using information from your models.
|
69
|
+
|
70
|
+
### Manual Typing
|
71
|
+
|
72
|
+
You can manually specify TypeScript types in your serializers:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class PostResource < ApplicationResource
|
76
|
+
attributes :id, :title, :body, :published_at
|
77
|
+
|
78
|
+
typelize "string"
|
79
|
+
attribute :author_name do |post|
|
80
|
+
post.author.name
|
81
|
+
end
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
### TypeScript Integration
|
86
|
+
|
87
|
+
Typelizer generates TypeScript interfaces in the specified output directory:
|
88
|
+
|
89
|
+
```typescript
|
90
|
+
// app/javascript/types/serializers/Post.ts
|
91
|
+
export interface Post {
|
92
|
+
id: number;
|
93
|
+
title: string;
|
94
|
+
body: string;
|
95
|
+
published_at: string | null;
|
96
|
+
author_name: string;
|
97
|
+
}
|
98
|
+
```
|
99
|
+
|
100
|
+
All generated interfaces are automatically imported in a single file:
|
101
|
+
|
102
|
+
```typescript
|
103
|
+
// app/javascript/types/serializers/index.ts
|
104
|
+
export * from "./post";
|
105
|
+
export * from "./author";
|
106
|
+
```
|
107
|
+
|
108
|
+
We recommend importing this file in a central location:
|
109
|
+
|
110
|
+
```typescript
|
111
|
+
// app/javascript/types/index.ts
|
112
|
+
import "@/types/serializers";
|
113
|
+
// Custom types can be added here
|
114
|
+
// ...
|
115
|
+
```
|
116
|
+
|
117
|
+
With such a setup, you can import all generated interfaces in your TypeScript files:
|
118
|
+
|
119
|
+
```typescript
|
120
|
+
import { Post } from "@/types";
|
121
|
+
```
|
122
|
+
|
123
|
+
This setup also allows you to use custom types in your serializers:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class PostWithMetaResource < ApplicationResource
|
127
|
+
attributes :id, :title
|
128
|
+
typelize "PostMeta"
|
129
|
+
attribute :meta do |post|
|
130
|
+
{ likes: post.likes, comments: post.comments }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
```typescript
|
136
|
+
// app/javascript/types/serializers/PostWithMeta.ts
|
137
|
+
|
138
|
+
import { PostMeta } from "@/types";
|
139
|
+
|
140
|
+
export interface Post {
|
141
|
+
id: number;
|
142
|
+
title: string;
|
143
|
+
meta: PostMeta;
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
147
|
+
The `"@/types"` import path is configurable:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Typelizer.configure do |config|
|
151
|
+
config.types_import_path = "@/types";
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
See the [Configuration](#configuration) section for more options.
|
156
|
+
|
157
|
+
### Manual Generation
|
158
|
+
|
159
|
+
To manually generate TypeScript interfaces:
|
160
|
+
|
161
|
+
```
|
162
|
+
$ rails typelizer:generate
|
163
|
+
```
|
164
|
+
|
165
|
+
### Automatic Generation in Development
|
166
|
+
|
167
|
+
When [Listen](https://github.com/guard/listen) is installed, Typelizer automatically watches for changes and regenerates interfaces in development mode. You can disable this behavior:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
Typelizer.listen = false
|
171
|
+
```
|
172
|
+
|
173
|
+
## Configuration
|
174
|
+
|
175
|
+
### Global Configuration
|
176
|
+
|
177
|
+
Typelizer provides several global configuration options:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
# Directories to search for serializers:
|
181
|
+
Typelizer.dirs = [Rails.root.join("app", "resources"), Rails.root.join("app", "serializers")]
|
182
|
+
# Reject specific classes from being typelized:
|
183
|
+
Typelizer.reject_class = ->(serializer:) { false }
|
184
|
+
# Logger for debugging:
|
185
|
+
Typelizer.logger = Logger.new($stdout, level: :info)
|
186
|
+
# Force enable or disable file watching with Listen:
|
187
|
+
Typelizer.listen = nil
|
188
|
+
```
|
189
|
+
|
190
|
+
### Config Options
|
191
|
+
|
192
|
+
`Typelizer::Config` offers fine-grained control over the gem's behavior. Here's a list of available options:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
Typelizer.configure do |config|
|
196
|
+
# Determines how serializer names are mapped to TypeScript interface names
|
197
|
+
config.serializer_name_mapper = ->(serializer) { ... }
|
198
|
+
|
199
|
+
# Maps serializers to their corresponding model classes
|
200
|
+
config.serializer_model_mapper = ->(serializer) { ... }
|
201
|
+
|
202
|
+
# Custom transformation for generated properties
|
203
|
+
config.properties_transformer = ->(properties) { ... }
|
204
|
+
|
205
|
+
# Plugin for model type inference (default: ModelPlugins::ActiveRecord)
|
206
|
+
config.model_plugin = Typelizer::ModelPlugins::ActiveRecord
|
207
|
+
|
208
|
+
# Plugin for serializer parsing (default: SerializerPlugins::Auto)
|
209
|
+
config.serializer_plugin = Typelizer::SerializerPlugins::Auto
|
210
|
+
|
211
|
+
# Additional configurations for specific plugins
|
212
|
+
config.plugin_configs = { alba: { ts_mapper: {...} } }
|
213
|
+
|
214
|
+
# Custom DB to TypeScript type mapping
|
215
|
+
config.type_mapping = config.type_mapping.merge(jsonb: "Record<string, undefined>", ... )
|
216
|
+
|
217
|
+
# Strategy for handling null values (:nullable, :optional, or :nullable_and_optional)
|
218
|
+
config.null_strategy = :nullable
|
219
|
+
|
220
|
+
# Directory where TypeScript interfaces will be generated
|
221
|
+
config.output_dir = Rails.root.join("app/javascript/types/serializers")
|
222
|
+
|
223
|
+
# Import path for generated types in TypeScript files
|
224
|
+
# (e.g., `import { MyType } from "@/types"`)
|
225
|
+
config.types_import_path = "@/types"
|
226
|
+
|
227
|
+
# List of type names that should be considered global in TypeScript
|
228
|
+
# (i.e. not prefixed with the import path)
|
229
|
+
config.types_global << %w[Array Date Record File FileList]
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
### Per-Serializer Configuration
|
234
|
+
|
235
|
+
You can also configure Typelizer on a per-serializer basis:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
class PostResource < ApplicationResource
|
239
|
+
typelizer_config do |config|
|
240
|
+
config.type_mapping = config.type_mapping.merge(jsonb: "Record<string, undefined>", ... )
|
241
|
+
config.null_strategy = :nullable
|
242
|
+
# ...
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
## Credits
|
248
|
+
|
249
|
+
Typelizer is inspired by [types_from_serializers](https://github.com/ElMassimo/types_from_serializers).
|
250
|
+
|
251
|
+
## License
|
252
|
+
|
253
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,18 @@
|
|
1
|
+
namespace :typelizer do
|
2
|
+
desc "Generate TypeScript interfaces from serializers"
|
3
|
+
task generate: :environment do
|
4
|
+
require "benchmark"
|
5
|
+
|
6
|
+
ENV["TYPELIZER"] = "true"
|
7
|
+
|
8
|
+
puts "Generating TypeScript interfaces..."
|
9
|
+
serializers = []
|
10
|
+
time = Benchmark.realtime do
|
11
|
+
serializers = Typelizer::Generator.call
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "Finished in #{time} seconds"
|
15
|
+
puts "Found #{serializers.size} serializers:"
|
16
|
+
puts serializers.map { |s| "\t#{s.name}" }.join("\n")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Typelizer
|
2
|
+
TYPE_MAPPING = {
|
3
|
+
boolean: :boolean,
|
4
|
+
date: :string,
|
5
|
+
datetime: :string,
|
6
|
+
decimal: :number,
|
7
|
+
integer: :number,
|
8
|
+
string: :string,
|
9
|
+
text: :string,
|
10
|
+
citext: :string
|
11
|
+
}.tap do |types|
|
12
|
+
types.default = :unknown
|
13
|
+
end
|
14
|
+
|
15
|
+
class Config < Struct.new(
|
16
|
+
:serializer_name_mapper,
|
17
|
+
:serializer_model_mapper,
|
18
|
+
:properties_transformer,
|
19
|
+
:model_plugin,
|
20
|
+
:serializer_plugin,
|
21
|
+
:plugin_configs,
|
22
|
+
:type_mapping,
|
23
|
+
:null_strategy,
|
24
|
+
:output_dir,
|
25
|
+
:types_import_path,
|
26
|
+
:types_global,
|
27
|
+
keyword_init: true
|
28
|
+
) do
|
29
|
+
class << self
|
30
|
+
def instance
|
31
|
+
@instance ||= new(
|
32
|
+
serializer_name_mapper: ->(serializer) { serializer.name.ends_with?("Serializer") ? serializer.name.delete_suffix("Serializer") : serializer.name.delete_suffix("Resource") },
|
33
|
+
serializer_model_mapper: ->(serializer) do
|
34
|
+
base_class = serializer_name_mapper.call(serializer)
|
35
|
+
Object.const_get(base_class) if Object.const_defined?(base_class)
|
36
|
+
end,
|
37
|
+
|
38
|
+
model_plugin: ModelPlugins::ActiveRecord,
|
39
|
+
serializer_plugin: SerializerPlugins::Auto,
|
40
|
+
plugin_configs: {},
|
41
|
+
|
42
|
+
type_mapping: TYPE_MAPPING,
|
43
|
+
null_strategy: :nullable,
|
44
|
+
|
45
|
+
output_dir: js_root.join("types/serializers"),
|
46
|
+
|
47
|
+
types_import_path: "@/types",
|
48
|
+
types_global: %w[Array Date Record File FileList],
|
49
|
+
|
50
|
+
properties_transformer: nil
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def js_root
|
57
|
+
root_path = defined?(Rails) ? Rails.root : Pathname.pwd
|
58
|
+
js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript"
|
59
|
+
root_path.join(js_root)
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_to_missing?(name, include_private = false)
|
63
|
+
Typelizer.respond_to?(name) ||
|
64
|
+
instance.respond_to?(name, include_private)
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(method, *args, &block)
|
68
|
+
return Typelizer.send(method, *args, &block) if Typelizer.respond_to?(method)
|
69
|
+
|
70
|
+
instance.send(method, *args, &block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Typelizer
|
2
|
+
module DSL
|
3
|
+
# typelize_from Model
|
4
|
+
# typelize attribute_name: ["string", "Date", optional: true, nullable: true, multi: true]
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
Typelizer.base_classes << base.to_s
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.extended(base)
|
12
|
+
Typelizer.base_classes << base.to_s
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def typelizer_config
|
18
|
+
@typelizer_config ||=
|
19
|
+
begin
|
20
|
+
parent_config = superclass.respond_to?(:typelizer_config) ? superclass.typelizer_config : Config
|
21
|
+
Config.new(parent_config.to_h.transform_values(&:dup))
|
22
|
+
end
|
23
|
+
yield @typelizer_config if block_given?
|
24
|
+
@typelizer_config
|
25
|
+
end
|
26
|
+
|
27
|
+
def typelizer_interface
|
28
|
+
@typelizer_interface ||= Interface.new(serializer: self)
|
29
|
+
end
|
30
|
+
|
31
|
+
# save association of serializer to model
|
32
|
+
def typelize_from(model)
|
33
|
+
return unless Typelizer.enabled?
|
34
|
+
|
35
|
+
define_singleton_method(:_typelizer_model_name) { model }
|
36
|
+
end
|
37
|
+
|
38
|
+
# save association of serializer attributes to type
|
39
|
+
# can be invoked multiple times
|
40
|
+
def typelize(type = nil, type_params = {}, **attributes)
|
41
|
+
if type
|
42
|
+
@keyless_type = [type, type_params.merge(attributes)]
|
43
|
+
else
|
44
|
+
assign_type_information(:_typelizer_attributes, attributes)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_accessor :keyless_type
|
49
|
+
|
50
|
+
def typelize_meta(**attributes)
|
51
|
+
assign_type_information(:_typelizer_meta_attributes, attributes)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def assign_type_information(attribute_name, attributes)
|
57
|
+
return unless Typelizer.enabled?
|
58
|
+
|
59
|
+
instance_variable = "@#{attribute_name}"
|
60
|
+
|
61
|
+
unless instance_variable_get(instance_variable)
|
62
|
+
instance_variable_set(instance_variable, {})
|
63
|
+
end
|
64
|
+
|
65
|
+
unless respond_to?(attribute_name)
|
66
|
+
define_singleton_method(attribute_name) do
|
67
|
+
result = instance_variable_get(instance_variable)
|
68
|
+
if superclass.respond_to?(attribute_name)
|
69
|
+
result.merge(superclass.send(attribute_name))
|
70
|
+
else
|
71
|
+
result
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attributes.each do |name, attrs|
|
77
|
+
next unless name
|
78
|
+
|
79
|
+
attrs = [attrs] if attrs && !attrs.is_a?(Array)
|
80
|
+
options = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
81
|
+
|
82
|
+
options[:type] = attrs.join(" | ") if attrs.any?
|
83
|
+
instance_variable_get(instance_variable)[name.to_sym] ||= {}
|
84
|
+
instance_variable_get(instance_variable)[name.to_sym].merge!(options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
class Generator
|
5
|
+
def self.call
|
6
|
+
new.call
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(config = Typelizer::Config)
|
10
|
+
@config = config
|
11
|
+
@writer = Writer.new
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :config, :writer
|
15
|
+
|
16
|
+
def call(force: false)
|
17
|
+
return unless Typelizer.enabled?
|
18
|
+
|
19
|
+
read_serializers
|
20
|
+
|
21
|
+
interfaces = target_serializers.map(&:typelizer_interface).reject(&:empty?)
|
22
|
+
writer.call(interfaces, force: force)
|
23
|
+
|
24
|
+
interfaces
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def target_serializers
|
30
|
+
base_classes = Typelizer.base_classes.filter_map do |base_class|
|
31
|
+
Object.const_get(base_class) if Object.const_defined?(base_class)
|
32
|
+
end.tap do |base_classes|
|
33
|
+
raise ArgumentError, "Please ensure all your serializers include Typelizer::DSL." if base_classes.none?
|
34
|
+
end
|
35
|
+
|
36
|
+
(base_classes + base_classes.flat_map(&:descendants)).uniq.sort_by(&:name)
|
37
|
+
.reject { |serializer| Typelizer.reject_class.call(serializer: serializer) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_serializers(files = nil)
|
41
|
+
files ||= Typelizer.dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }
|
42
|
+
|
43
|
+
files.each do |file|
|
44
|
+
trace = TracePoint.new(:call) do |tp|
|
45
|
+
begin
|
46
|
+
next unless tp.self.respond_to?(:typelizer_interface) && !tp.self.send(:respond_to_missing?, :typelizer_interface, false)
|
47
|
+
rescue WeakRef::RefError
|
48
|
+
next
|
49
|
+
end
|
50
|
+
serializer_plugin = tp.self.typelizer_interface.serializer_plugin
|
51
|
+
|
52
|
+
if tp.callee_id.in?(serializer_plugin.methods_to_typelize)
|
53
|
+
type, attrs = tp.self.keyless_type
|
54
|
+
name = tp.binding.local_variable_get(:name) if tp.binding.local_variable_defined?(:name)
|
55
|
+
tp.self.typelize(**serializer_plugin.typelize_method_transform(method: tp.callee_id, binding: tp.binding, name: name, type: type, attrs: attrs || {}))
|
56
|
+
tp.self.keyless_type = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
trace.enable
|
61
|
+
require file
|
62
|
+
trace.disable
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Typelizer
|
2
|
+
class Interface
|
3
|
+
attr_reader :serializer, :serializer_plugin
|
4
|
+
|
5
|
+
def config
|
6
|
+
serializer.typelizer_config
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(serializer:)
|
10
|
+
@serializer = serializer
|
11
|
+
@serializer_plugin = config.serializer_plugin.new(serializer: serializer, config: config)
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
config.serializer_name_mapper.call(serializer).tr_s(":", "")
|
16
|
+
end
|
17
|
+
|
18
|
+
def filename
|
19
|
+
name.gsub("::", "/")
|
20
|
+
end
|
21
|
+
|
22
|
+
def root_key
|
23
|
+
serializer_plugin.root_key
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
meta_fields.empty? && properties.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def meta_fields
|
31
|
+
@meta_fields ||= begin
|
32
|
+
props = serializer_plugin.meta_fields || []
|
33
|
+
props = infer_types(props, :_typelizer_meta_attributes)
|
34
|
+
props = config.properties_transformer.call(props) if config.properties_transformer
|
35
|
+
props
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def properties
|
40
|
+
@properties ||= begin
|
41
|
+
props = serializer_plugin.properties
|
42
|
+
props = infer_types(props)
|
43
|
+
props = config.properties_transformer.call(props) if config.properties_transformer
|
44
|
+
props
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def imports
|
49
|
+
association_serializers, attribute_types = properties.filter_map(&:type)
|
50
|
+
.uniq
|
51
|
+
.partition { |type| type.is_a?(Interface) }
|
52
|
+
|
53
|
+
serializer_types = association_serializers
|
54
|
+
.filter_map { |interface| interface.name if interface.name != name }
|
55
|
+
|
56
|
+
custom_type_imports = attribute_types
|
57
|
+
.flat_map { |type| extract_typescript_types(type.to_s) }
|
58
|
+
.uniq
|
59
|
+
.reject { |type| global_type?(type) }
|
60
|
+
|
61
|
+
custom_type_imports + serializer_types
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"<#{self.class.name} #{name} properties=#{properties.inspect}>"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def extract_typescript_types(type)
|
71
|
+
type.split(/[<>\[\],\s|]+/)
|
72
|
+
end
|
73
|
+
|
74
|
+
def global_type?(type)
|
75
|
+
type[0] == type[0].downcase || config.types_global.include?(type)
|
76
|
+
end
|
77
|
+
|
78
|
+
def infer_types(props, hash_name = :_typelizer_attributes)
|
79
|
+
props.map do |prop|
|
80
|
+
if serializer.respond_to?(hash_name)
|
81
|
+
dsl_type = serializer.public_send(hash_name)[prop.name.to_sym]
|
82
|
+
next Property.new(prop.to_h.merge(dsl_type)) if dsl_type&.any?
|
83
|
+
end
|
84
|
+
|
85
|
+
model_plugin.infer_types(prop)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def model_class
|
90
|
+
return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
|
91
|
+
|
92
|
+
config.serializer_model_mapper.call(serializer)
|
93
|
+
rescue NameError
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def model_plugin
|
98
|
+
@model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
module Listen
|
5
|
+
class << self
|
6
|
+
attr_accessor :started
|
7
|
+
|
8
|
+
def call(
|
9
|
+
run_on_start: true,
|
10
|
+
options: {},
|
11
|
+
&block
|
12
|
+
)
|
13
|
+
return if started
|
14
|
+
return unless Typelizer.enabled?
|
15
|
+
|
16
|
+
@block = block
|
17
|
+
@generator = Typelizer::Generator.new
|
18
|
+
|
19
|
+
gem "listen"
|
20
|
+
require "listen"
|
21
|
+
|
22
|
+
self.started = true
|
23
|
+
|
24
|
+
locales_dirs = Typelizer.dirs.filter(&:exist?).map { |path| File.expand_path(path) }
|
25
|
+
|
26
|
+
relative_paths = locales_dirs.map { |path| relative_path(path) }
|
27
|
+
|
28
|
+
debug("Watching #{relative_paths.inspect}")
|
29
|
+
|
30
|
+
listener(locales_dirs.map(&:to_s), options).start
|
31
|
+
@generator.call if run_on_start
|
32
|
+
end
|
33
|
+
|
34
|
+
def relative_path(path)
|
35
|
+
root_path = defined?(Rails) ? Rails.root : Pathname.pwd
|
36
|
+
Pathname.new(path).relative_path_from(root_path).to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def debug(message)
|
40
|
+
Typelizer.logger.debug(message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def listener(paths, options)
|
44
|
+
::Listen.to(*paths, options) do |changed, added, removed|
|
45
|
+
changes = compute_changes(paths, changed, added, removed)
|
46
|
+
|
47
|
+
next unless changes.any?
|
48
|
+
|
49
|
+
debug(changes.map { |key, value| "#{key}=#{value.inspect}" }.join(", "))
|
50
|
+
|
51
|
+
@block.call
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def compute_changes(paths, changed, added, removed)
|
56
|
+
paths = paths.map { |path| relative_path(path) }
|
57
|
+
|
58
|
+
{
|
59
|
+
changed: included_on_watched_paths(paths, changed),
|
60
|
+
added: included_on_watched_paths(paths, added),
|
61
|
+
removed: included_on_watched_paths(paths, removed)
|
62
|
+
}.select { |_k, v| v.any? }
|
63
|
+
end
|
64
|
+
|
65
|
+
def included_on_watched_paths(paths, changes)
|
66
|
+
changes.map { |change| relative_path(change) }.select do |change|
|
67
|
+
paths.any? { |path| change.start_with?(path) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Typelizer
|
2
|
+
module ModelPlugins
|
3
|
+
class ActiveRecord
|
4
|
+
def initialize(model_class:, config:)
|
5
|
+
@columns_hash = model_class&.columns_hash || {}
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :columns_hash, :config
|
10
|
+
|
11
|
+
def infer_types(prop)
|
12
|
+
column = columns_hash[prop.column_name.to_s]
|
13
|
+
return prop unless column
|
14
|
+
|
15
|
+
prop.multi = !!column.try(:array)
|
16
|
+
case config.null_strategy
|
17
|
+
when :nullable
|
18
|
+
prop.nullable = column.null
|
19
|
+
when :optional
|
20
|
+
prop.optional = column.null
|
21
|
+
when :nullable_and_optional
|
22
|
+
prop.nullable = column.null
|
23
|
+
prop.optional = column.null
|
24
|
+
else
|
25
|
+
raise "Unknown null strategy: #{config.null_strategy}"
|
26
|
+
end
|
27
|
+
|
28
|
+
prop.type = @config.type_mapping[column.type]
|
29
|
+
|
30
|
+
prop
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Typelizer
|
2
|
+
Property = Struct.new(
|
3
|
+
:name, :type, :optional, :nullable,
|
4
|
+
:multi, :column_name,
|
5
|
+
keyword_init: true
|
6
|
+
) do
|
7
|
+
def inspect
|
8
|
+
props = to_h.merge(type: type_name).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
9
|
+
"<#{self.class.name} #{props}>"
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
type_str = type_name
|
14
|
+
type_str = "Array<#{type_str}>" if multi
|
15
|
+
type_str = "#{type_str} | null" if nullable
|
16
|
+
|
17
|
+
"#{name}#{"?" if optional}: #{type_str}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def type_name
|
23
|
+
type.respond_to?(:name) ? type.name : type || "unknown"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Typelizer
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
rake_tasks do
|
4
|
+
load "tasks/generate.rake"
|
5
|
+
end
|
6
|
+
|
7
|
+
initializer "typelizer.configure" do
|
8
|
+
Typelizer.configure do |c|
|
9
|
+
c.dirs = [
|
10
|
+
Rails.root.join("app", "resources"),
|
11
|
+
Rails.root.join("app", "serializers")
|
12
|
+
]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer "typelizer.generate" do |app|
|
17
|
+
next unless Typelizer.enabled?
|
18
|
+
|
19
|
+
generator = Typelizer::Generator.new
|
20
|
+
|
21
|
+
if Typelizer.listen == true || Gem.loaded_specs["listen"] && Typelizer.listen != false
|
22
|
+
require_relative "listen"
|
23
|
+
app.config.after_initialize do
|
24
|
+
Typelizer::Listen.call do
|
25
|
+
Rails.application.reloader.reload!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
app.config.to_prepare do
|
31
|
+
generator.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
module SerializerPlugins
|
5
|
+
class Alba < Base
|
6
|
+
ALBA_TS_MAPPER = {
|
7
|
+
"String" => {type: :string},
|
8
|
+
"Integer" => {type: :number},
|
9
|
+
"Boolean" => {type: :boolean},
|
10
|
+
"ArrayOfString" => {type: :string, multi: true},
|
11
|
+
"ArrayOfInteger" => {type: :number, multi: true}
|
12
|
+
}
|
13
|
+
|
14
|
+
def properties
|
15
|
+
serializer._attributes.map do |name, attr|
|
16
|
+
build_property(name, attr)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def methods_to_typelize
|
21
|
+
[
|
22
|
+
:association, :one, :has_one,
|
23
|
+
:many, :has_many,
|
24
|
+
:attributes, :attribute,
|
25
|
+
:nested_attribute, :nested,
|
26
|
+
:meta
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
31
|
+
return {name => [type, attrs.merge(multi: true)]} if [:many, :has_many].include?(method)
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def root_key
|
37
|
+
serializer.new({}).send(:_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def meta_fields
|
41
|
+
return nil unless serializer._meta
|
42
|
+
|
43
|
+
name = serializer._meta.first
|
44
|
+
[
|
45
|
+
build_property(name, name)
|
46
|
+
]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_property(name, attr, **options)
|
52
|
+
case attr
|
53
|
+
when Symbol
|
54
|
+
Property.new(
|
55
|
+
name: name,
|
56
|
+
type: nil,
|
57
|
+
optional: false,
|
58
|
+
nullable: false,
|
59
|
+
multi: false,
|
60
|
+
column_name: name,
|
61
|
+
**options
|
62
|
+
)
|
63
|
+
when Proc
|
64
|
+
Property.new(
|
65
|
+
name: name,
|
66
|
+
type: nil,
|
67
|
+
optional: false,
|
68
|
+
nullable: false,
|
69
|
+
multi: false,
|
70
|
+
column_name: nil,
|
71
|
+
**options
|
72
|
+
)
|
73
|
+
when ::Alba::Association
|
74
|
+
resource = attr.instance_variable_get(:@resource)
|
75
|
+
Property.new(
|
76
|
+
name: name,
|
77
|
+
type: Interface.new(serializer: resource),
|
78
|
+
optional: false,
|
79
|
+
nullable: false,
|
80
|
+
multi: false, # we override this in typelize_method_transform
|
81
|
+
column_name: name,
|
82
|
+
**options
|
83
|
+
)
|
84
|
+
when ::Alba::TypedAttribute
|
85
|
+
alba_type = attr.instance_variable_get(:@type)
|
86
|
+
Property.new(
|
87
|
+
name: name,
|
88
|
+
optional: false,
|
89
|
+
# not sure if that's a good default tbh
|
90
|
+
nullable: !alba_type.instance_variable_get(:@auto_convert),
|
91
|
+
multi: false,
|
92
|
+
column_name: name,
|
93
|
+
**ts_mapper[alba_type.name.to_s],
|
94
|
+
**options
|
95
|
+
)
|
96
|
+
when ::Alba::NestedAttribute
|
97
|
+
Property.new(
|
98
|
+
name: name,
|
99
|
+
type: nil,
|
100
|
+
optional: false,
|
101
|
+
nullable: false,
|
102
|
+
multi: false,
|
103
|
+
column_name: nil,
|
104
|
+
**options
|
105
|
+
)
|
106
|
+
when ::Alba::ConditionalAttribute
|
107
|
+
build_property(name, attr.instance_variable_get(:@body), optional: true)
|
108
|
+
else
|
109
|
+
raise ArgumentError, "Unsupported attribute type: #{attr.class}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def ts_mapper
|
116
|
+
config.plugin_configs.dig(:alba, :ts_mapper) || ALBA_TS_MAPPER
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
module SerializerPlugins
|
5
|
+
class AMS < Base
|
6
|
+
def methods_to_typelize
|
7
|
+
[
|
8
|
+
:has_many, :has_one, :belongs_to,
|
9
|
+
:attribute, :attributes
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
14
|
+
return {binding.local_variable_get(:attr) => [type, attrs]} if method == :attribute
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def properties
|
20
|
+
serializer._attributes_data.merge(serializer._reflections).flat_map do |key, association|
|
21
|
+
type = association.options[:serializer] ? Interface.new(serializer: association.options[:serializer]) : nil
|
22
|
+
Property.new(
|
23
|
+
name: key.to_s,
|
24
|
+
type: type,
|
25
|
+
optional: association.options.key?(:if) || association.options.key?(:unless),
|
26
|
+
multi: association.respond_to?(:collection?) && association.collection?,
|
27
|
+
column_name: association.name.to_s
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Typelizer
|
2
|
+
module SerializerPlugins
|
3
|
+
module Auto
|
4
|
+
class << self
|
5
|
+
def new(serializer:, config:)
|
6
|
+
plugin(serializer).new(serializer: serializer, config: config)
|
7
|
+
end
|
8
|
+
|
9
|
+
def plugin(serializer)
|
10
|
+
if defined?(::Oj::Serializer) && serializer.ancestors.include?(::Oj::Serializer)
|
11
|
+
OjSerializers
|
12
|
+
elsif defined?(::Alba) && serializer.ancestors.include?(::Alba::Resource)
|
13
|
+
Alba
|
14
|
+
elsif defined?(ActiveModel::Serializer) && serializer.ancestors.include?(ActiveModel::Serializer)
|
15
|
+
AMS
|
16
|
+
else
|
17
|
+
raise "Can't guess serializer plugin for #{serializer}. " \
|
18
|
+
"Please specify it with `config.serializer_plugin`."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Typelizer
|
2
|
+
module SerializerPlugins
|
3
|
+
class Base
|
4
|
+
def initialize(serializer:, config:)
|
5
|
+
@serializer = serializer
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def root_key
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def meta_fields
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
18
|
+
{name => [type, attrs]}
|
19
|
+
end
|
20
|
+
|
21
|
+
def methods_to_typelize
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
def properties
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :serializer, :config
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module Typelizer
|
4
|
+
module SerializerPlugins
|
5
|
+
class OjSerializers < Base
|
6
|
+
def methods_to_typelize
|
7
|
+
[
|
8
|
+
:has_many, :has_one, :belongs_to,
|
9
|
+
:flat_one, :attribute, :attributes
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def properties
|
14
|
+
serializer._attributes
|
15
|
+
.flat_map do |key, options|
|
16
|
+
if options[:association] == :flat
|
17
|
+
Interface.new(serializer: options.fetch(:serializer)).properties
|
18
|
+
else
|
19
|
+
type = options[:serializer] ? Interface.new(serializer: options[:serializer]) : options[:type]
|
20
|
+
Property.new(
|
21
|
+
name: key,
|
22
|
+
type: type,
|
23
|
+
optional: options[:optional] || options.key?(:if),
|
24
|
+
nullable: options[:nullable],
|
25
|
+
multi: options[:association] == :many,
|
26
|
+
column_name: options.fetch(:value_from)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<%- if interface.imports.any? -%>
|
2
|
+
import type {<%= interface.imports.join(", ") %>} from '<%= interface.config.types_import_path %>'
|
3
|
+
<%- end -%>
|
4
|
+
|
5
|
+
<%- if interface.root_key -%>
|
6
|
+
type <%= interface.name %>Data = {
|
7
|
+
<%- interface.properties.each do |property| -%>
|
8
|
+
<%= property %>;
|
9
|
+
<%- end -%>
|
10
|
+
}
|
11
|
+
|
12
|
+
type <%= interface.name %> = {
|
13
|
+
<%= interface.root_key %>: <%= interface.name %>Data;
|
14
|
+
<%- interface.meta_fields&.each do |property| -%>
|
15
|
+
<%= property %>;
|
16
|
+
<%- end -%>
|
17
|
+
}
|
18
|
+
<%- else -%>
|
19
|
+
type <%= interface.name %> = {
|
20
|
+
<%- interface.properties.each do |property| -%>
|
21
|
+
<%= property %>;
|
22
|
+
<%- end -%>
|
23
|
+
}
|
24
|
+
<%- end -%>
|
25
|
+
|
26
|
+
export default <%= interface.name %>;
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "erb"
|
5
|
+
|
6
|
+
module Typelizer
|
7
|
+
class Writer
|
8
|
+
def initialize
|
9
|
+
@template_cache = {}
|
10
|
+
@config = Config
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :config, :template_cache
|
14
|
+
|
15
|
+
def call(interfaces, force:)
|
16
|
+
cleanup_output_dir if force
|
17
|
+
|
18
|
+
written_files = interfaces.map { |interface| write_interface(interface) }
|
19
|
+
written_files << write_index(interfaces)
|
20
|
+
|
21
|
+
existing_files = Dir[File.join(config.output_dir, "**/*.ts")]
|
22
|
+
files_to_delete = existing_files - written_files
|
23
|
+
|
24
|
+
File.delete(*files_to_delete) unless files_to_delete.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def write_index(interfaces)
|
30
|
+
write_file("index.ts", interfaces.map(&:filename).join) do
|
31
|
+
render_template("index.ts.erb", interfaces: interfaces)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_interface(interface)
|
36
|
+
write_file("#{interface.filename}.ts", interface.inspect) do
|
37
|
+
render_template("interface.ts.erb", interface: interface)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def write_file(filename, fingerprint)
|
42
|
+
output_file = File.join(config.output_dir, filename)
|
43
|
+
existing_content = File.exist?(output_file) ? File.read(output_file) : nil
|
44
|
+
digest = render_template("fingerprint.ts.erb", fingerprint: fingerprint)
|
45
|
+
|
46
|
+
return output_file if existing_content&.start_with?(digest)
|
47
|
+
|
48
|
+
content = yield
|
49
|
+
|
50
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
51
|
+
|
52
|
+
File.write(output_file, digest + content)
|
53
|
+
output_file
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_template(template, **context)
|
57
|
+
template_cache[template] ||= ERB.new(File.read(File.join(File.dirname(__FILE__), "templates/#{template}")), trim_mode: "-")
|
58
|
+
template_cache[template].result_with_hash(context)
|
59
|
+
end
|
60
|
+
|
61
|
+
def cleanup_output_dir
|
62
|
+
FileUtils.rm_rf(config.output_dir)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/typelizer.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "typelizer/version"
|
4
|
+
require_relative "typelizer/config"
|
5
|
+
require_relative "typelizer/property"
|
6
|
+
require_relative "typelizer/interface"
|
7
|
+
require_relative "typelizer/writer"
|
8
|
+
require_relative "typelizer/generator"
|
9
|
+
|
10
|
+
require_relative "typelizer/dsl"
|
11
|
+
|
12
|
+
require_relative "typelizer/serializer_plugins/auto"
|
13
|
+
require_relative "typelizer/serializer_plugins/oj_serializers"
|
14
|
+
require_relative "typelizer/serializer_plugins/alba"
|
15
|
+
require_relative "typelizer/serializer_plugins/ams"
|
16
|
+
|
17
|
+
require_relative "typelizer/model_plugins/active_record"
|
18
|
+
|
19
|
+
require_relative "typelizer/railtie" if defined?(Rails)
|
20
|
+
|
21
|
+
module Typelizer
|
22
|
+
class << self
|
23
|
+
def enabled?
|
24
|
+
ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["TYPELIZER"] == "true"
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :dirs
|
28
|
+
attr_accessor :reject_class
|
29
|
+
attr_accessor :logger
|
30
|
+
attr_accessor :listen
|
31
|
+
|
32
|
+
# @private
|
33
|
+
attr_reader :base_classes
|
34
|
+
|
35
|
+
def configure
|
36
|
+
yield Config
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_writer :base_classes
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set in the Railtie
|
45
|
+
self.dirs = []
|
46
|
+
self.reject_class = ->(serializer:) { false }
|
47
|
+
self.logger = Logger.new($stdout, level: :info)
|
48
|
+
self.listen = nil
|
49
|
+
|
50
|
+
self.base_classes = Set.new
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typelizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Svyatoslav Kryukov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-08-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: railties
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.0.0
|
27
|
+
description: A TypeScript type generator for Ruby serializers.
|
28
|
+
email:
|
29
|
+
- me@skryukov.dev
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- README.md
|
36
|
+
- lib/tasks/generate.rake
|
37
|
+
- lib/typelizer.rb
|
38
|
+
- lib/typelizer/config.rb
|
39
|
+
- lib/typelizer/dsl.rb
|
40
|
+
- lib/typelizer/generator.rb
|
41
|
+
- lib/typelizer/interface.rb
|
42
|
+
- lib/typelizer/listen.rb
|
43
|
+
- lib/typelizer/model_plugins/active_record.rb
|
44
|
+
- lib/typelizer/property.rb
|
45
|
+
- lib/typelizer/railtie.rb
|
46
|
+
- lib/typelizer/serializer_plugins/alba.rb
|
47
|
+
- lib/typelizer/serializer_plugins/ams.rb
|
48
|
+
- lib/typelizer/serializer_plugins/auto.rb
|
49
|
+
- lib/typelizer/serializer_plugins/base.rb
|
50
|
+
- lib/typelizer/serializer_plugins/oj_serializers.rb
|
51
|
+
- lib/typelizer/templates/fingerprint.ts.erb
|
52
|
+
- lib/typelizer/templates/index.ts.erb
|
53
|
+
- lib/typelizer/templates/interface.ts.erb
|
54
|
+
- lib/typelizer/version.rb
|
55
|
+
- lib/typelizer/writer.rb
|
56
|
+
homepage: https://github.com/skryukov/typelizer
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata:
|
60
|
+
bug_tracker_uri: https://github.com/skryukov/typelizer/issues
|
61
|
+
changelog_uri: https://github.com/skryukov/typelizer/blob/main/CHANGELOG.md
|
62
|
+
documentation_uri: https://github.com/skryukov/typelizer/blob/main/README.md
|
63
|
+
homepage_uri: https://github.com/skryukov/typelizer
|
64
|
+
source_code_uri: https://github.com/skryukov/typelizer
|
65
|
+
rubygems_mfa_required: 'true'
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.7.0
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubygems_version: 3.5.7
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: A TypeScript type generator for Ruby serializers.
|
85
|
+
test_files: []
|