rbs_ts_generator 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/MIT-LICENSE +20 -0
- data/README.md +191 -0
- data/Rakefile +27 -0
- data/lib/generators/rbs_ts/USAGE +9 -0
- data/lib/generators/rbs_ts/rbs_ts_generator.rb +199 -0
- data/lib/generators/rbs_ts/rbs_types_convertible.rb +249 -0
- data/lib/generators/rbs_ts/templates/rbs_ts_runtime.ts +34 -0
- data/lib/generators/rbs_ts/type_script_visitor.rb +40 -0
- data/lib/rbs_ts_generator.rb +0 -0
- data/lib/tasks/rbs_ts_generator_tasks.rake +4 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d8212448863dcae6fbb3ddafc3b281fdb3b04eb0886859bda832c0126e145d24
|
4
|
+
data.tar.gz: 715cbeefbfd98e88942ad4b13d9ae8514452a1f7527653218a79ac54a93b2400
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3703dce9fa9f65e75d1456c429a426ae4c6d26c87d087fe308d2a508d031d6b55a428fc70c1fab38312c1c5ffd2ab2068c9860a9e126419e239f8ea1746984c1
|
7
|
+
data.tar.gz: fdc6850e87c3e89a144e2cb1555e686fda91bab1a2e37ce70de8892cc95824e18880245a42521af86b606c069eb1dc9a2276513c5478f06339d4b257fec6ce54
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020 Seiei Miyagi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# RbsTsGenerator
|
2
|
+
|
3
|
+
Generate TypeScript that includes routes definition and request / response JSON type from type signature of Rails controller actions.
|
4
|
+
|
5
|
+
Sample repository: [hanachin/rbs_ts_bbs](https://github.com/hanachin/rbs_ts_bbs)
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Write type signature of your controller actions in [ruby/rbs](https://github.com/ruby/rbs).
|
10
|
+
|
11
|
+
```rbs
|
12
|
+
# sig/app/controllers/boards_controller.rbs
|
13
|
+
class BoardsController < ApplicationController
|
14
|
+
@board: Board
|
15
|
+
@boards: Board::ActiveRecord_Relation
|
16
|
+
|
17
|
+
def index: () -> Array[{ id: Integer, title: String }]
|
18
|
+
def create: (String title) -> ({ url: String, message: String } | Array[String])
|
19
|
+
def update: (Integer id, String title) -> ({ url: String, message: String } | Array[String])
|
20
|
+
def destroy: (Integer id) -> { url: String, message: String }
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
The return type of the action method is type of json record.
|
25
|
+
But action does not explicitly return json record.
|
26
|
+
To pass the ruby type checking, add `| void` to each signatures.
|
27
|
+
|
28
|
+
```rbs
|
29
|
+
class BoardsController < ApplicationController
|
30
|
+
@board: Board
|
31
|
+
@boards: Board::ActiveRecord_Relation
|
32
|
+
|
33
|
+
def index: () -> (Array[{ id: Integer, title: String }] | void)
|
34
|
+
def create: (String title) -> ({ url: String, message: String } | Array[String] | void)
|
35
|
+
def update: (Integer id, String title) -> ({ url: String, message: String } | Array[String] | void)
|
36
|
+
def destroy: (Integer id) -> ({ url: String, message: String } | void)
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
I use [Steep](https://github.com/soutaro/steep) to type checking the ruby code.
|
41
|
+
Setup the Steepfile like following and run `steep check`.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Steepfile
|
45
|
+
target :app do
|
46
|
+
signature "sig"
|
47
|
+
|
48
|
+
check "app"
|
49
|
+
typing_options :strict
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
```console
|
54
|
+
$ bundle exec steep check
|
55
|
+
[Steep 0.17.1] [target=app] [target#type_check(target_sources: [app/channels/application_cable/channel.rb, app/channels/application_cable/connection.rb, app/controllers/application_controller.rb, app/controllers/boards_controller.rb, app/helpers/application_helper.rb, app/helpers/boards_helper.rb, app/jobs/application_job.rb, app/mailers/application_mailer.rb, app/models/application_record.rb, app/models/board.rb, app/mailboxes/application_mailbox.rb], validate_signatures: true)] [synthesize:(1:1)] [synthesize:(2:3)] [synthesize:(2:3)] [(*::Symbol, ?model_name: ::string, **untyped) -> void] Method call with rest keywords type is detected. Rough approximation to be improved.
|
56
|
+
```
|
57
|
+
|
58
|
+
When you passed the ruby type check, next generate TypeScript from those signatures.
|
59
|
+
|
60
|
+
```console
|
61
|
+
$ rails generate rbs_ts
|
62
|
+
```
|
63
|
+
|
64
|
+
This will generate those routes definition in `app/javascript/packs/rbs_ts_routes.ts`.
|
65
|
+
|
66
|
+
```typescript
|
67
|
+
type BoardsUpdateParams = { id: number; title: string }
|
68
|
+
type BoardsDestroyParams = { id: number }
|
69
|
+
type BoardsIndexParams = {}
|
70
|
+
type BoardsCreateParams = { title: string }
|
71
|
+
|
72
|
+
type BoardsUpdateReturn = Exclude<{ url: string; message: string } | string[] | void, void>
|
73
|
+
type BoardsDestroyReturn = Exclude<{ url: string; message: string } | void, void>
|
74
|
+
type BoardsIndexReturn = Exclude<{ id: number; title: string }[] | void, void>
|
75
|
+
type BoardsCreateReturn = Exclude<{ url: string; message: string } | string[] | void, void>
|
76
|
+
|
77
|
+
export const boards = {
|
78
|
+
path: ({ format }: any) => "/" + "boards" + (() => { try { return "." + (() => { if (format) return format; throw "format" })() } catch { return "" } })(),
|
79
|
+
names: ["format"]
|
80
|
+
} as {
|
81
|
+
path: (args: any) => string
|
82
|
+
names: ["format"]
|
83
|
+
Methods?: "GET" | "POST"
|
84
|
+
Params?: {
|
85
|
+
GET: BoardsIndexParams,
|
86
|
+
POST: BoardsCreateParams
|
87
|
+
}
|
88
|
+
Return?: {
|
89
|
+
GET: BoardsIndexReturn,
|
90
|
+
POST: BoardsCreateReturn
|
91
|
+
}
|
92
|
+
}
|
93
|
+
export const board = {
|
94
|
+
path: ({ id, format }: any) => "/" + "boards" + "/" + (() => { if (id) return id; throw "id" })() + (() => { try { return "." + (() => { if (format) return format; throw "format" })() } catch { return "" } })(),
|
95
|
+
names: ["id","format"]
|
96
|
+
} as {
|
97
|
+
path: (args: any) => string
|
98
|
+
names: ["id","format"]
|
99
|
+
Methods?: "PATCH" | "PUT" | "DELETE"
|
100
|
+
Params?: {
|
101
|
+
PATCH: BoardsUpdateParams,
|
102
|
+
PUT: BoardsUpdateParams,
|
103
|
+
DELETE: BoardsDestroyParams
|
104
|
+
}
|
105
|
+
Return?: {
|
106
|
+
PATCH: BoardsUpdateReturn,
|
107
|
+
PUT: BoardsUpdateReturn,
|
108
|
+
DELETE: BoardsDestroyReturn
|
109
|
+
}
|
110
|
+
}
|
111
|
+
```
|
112
|
+
|
113
|
+
And generate default runtime in `app/javascript/packs/rbs_ts_runtime.ts`
|
114
|
+
|
115
|
+
```typescript
|
116
|
+
type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
|
117
|
+
type BaseResource = {
|
118
|
+
path: (args: any) => string
|
119
|
+
names: string[]
|
120
|
+
Methods?: any
|
121
|
+
Params?: { [method in HttpMethods]?: any }
|
122
|
+
Return?: { [method in HttpMethods]?: any }
|
123
|
+
}
|
124
|
+
export async function railsApi<
|
125
|
+
Method extends Exclude<Resource['Methods'], undefined>,
|
126
|
+
Resource extends BaseResource,
|
127
|
+
Params extends Exclude<Resource['Params'], undefined>[Method],
|
128
|
+
Return extends Exclude<Resource['Return'], undefined>[Method]
|
129
|
+
>(method: Method, { path, names }: Resource, params: Params): Promise<{ status: number, json: Return }> {
|
130
|
+
const tag = document.querySelector<HTMLMetaElement>('meta[name=csrf-token]')
|
131
|
+
const paramsNotInNames = Object.keys(params).reduce<object>((ps, key) => names.indexOf(key) === - 1 ? { ...ps, [key]: params[key] } : ps, {})
|
132
|
+
const searchParams = new URLSearchParams()
|
133
|
+
for (const name of Object.keys(paramsNotInNames)) {
|
134
|
+
searchParams.append(name, paramsNotInNames[name])
|
135
|
+
}
|
136
|
+
const query = method === 'GET' && Object.keys(paramsNotInNames).length ? `?${searchParams.toString()}` : ''
|
137
|
+
const body = method === 'GET' ? undefined : JSON.stringify(paramsNotInNames)
|
138
|
+
const response = await fetch(path(params) + query, {
|
139
|
+
method,
|
140
|
+
body,
|
141
|
+
headers: {
|
142
|
+
'Accept': 'application/json',
|
143
|
+
'Content-Type': 'application/json',
|
144
|
+
'X-CSRF-Token': tag.content
|
145
|
+
}
|
146
|
+
})
|
147
|
+
const json = await response.json() as Return
|
148
|
+
return new Promise((resolve) => resolve({ status: response.status, json: json }))
|
149
|
+
}
|
150
|
+
```
|
151
|
+
|
152
|
+
In your TypeScript code, you can use those routes definition and the default runtime like following
|
153
|
+
|
154
|
+
```typescript
|
155
|
+
import { boards } from './rbs_ts_routes'
|
156
|
+
import { railsApi } from './rbs_ts_runtime'
|
157
|
+
|
158
|
+
const params = { title: 'test' }
|
159
|
+
railsApi('POST' as const, boards, params).then(({ json }) => {
|
160
|
+
if (json instanceof Array) {
|
161
|
+
return Promise.reject(json)
|
162
|
+
} else {
|
163
|
+
window.location.href = json.url
|
164
|
+
return Promise.resolve()
|
165
|
+
}
|
166
|
+
})
|
167
|
+
```
|
168
|
+
|
169
|
+
## Installation
|
170
|
+
Add this line to your application's Gemfile:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
gem 'rbs_ts_generator', group: :development
|
174
|
+
```
|
175
|
+
|
176
|
+
And then execute:
|
177
|
+
```bash
|
178
|
+
$ bundle
|
179
|
+
```
|
180
|
+
|
181
|
+
Or install it yourself as:
|
182
|
+
```bash
|
183
|
+
$ gem install rbs_ts_generator
|
184
|
+
```
|
185
|
+
|
186
|
+
## Contributing
|
187
|
+
|
188
|
+
https://github.com/hanachin/rbs_ts_generator
|
189
|
+
|
190
|
+
## License
|
191
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'RbsTsGenerator'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'test'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: :test
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'rbs'
|
3
|
+
require_relative './rbs_types_convertible'
|
4
|
+
require_relative './type_script_visitor'
|
5
|
+
|
6
|
+
using RbsTsGenerator::RbsTypesConvertible
|
7
|
+
|
8
|
+
using Module.new {
|
9
|
+
refine(Object) do
|
10
|
+
def parse_type_name(string)
|
11
|
+
RBS::Namespace.parse(string).yield_self do |namespace|
|
12
|
+
last = namespace.path.last
|
13
|
+
RBS::TypeName.new(name: last, namespace: namespace.parent)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def type_script_params_type_name(controller, action)
|
18
|
+
"#{controller.to_s.camelcase}#{action.to_s.camelcase}Params"
|
19
|
+
end
|
20
|
+
|
21
|
+
def type_script_return_type_name(controller, action)
|
22
|
+
"#{controller.to_s.camelcase}#{action.to_s.camelcase}Return"
|
23
|
+
end
|
24
|
+
|
25
|
+
def typescript_path_function(route_info, verb_type)
|
26
|
+
name = route_info.fetch(:name).camelize(:lower)
|
27
|
+
parts = route_info.fetch(:parts)
|
28
|
+
body = RbsTsGenerator::TypeScriptVisitor::INSTANCE.accept(route_info.fetch(:spec), '')
|
29
|
+
<<~TS
|
30
|
+
export const #{name} = {
|
31
|
+
path: (#{ parts.empty? ? '' : "{ #{parts.join(', ')} }: any" }) => #{ body },
|
32
|
+
names: [#{ parts.map(&:to_json).join(",") }]
|
33
|
+
} as {
|
34
|
+
path: (args: any) => string
|
35
|
+
names: [#{ parts.map(&:to_json).join(",") }]
|
36
|
+
Methods?: #{verb_type.keys.map(&:to_json).join(' | ')}
|
37
|
+
Params?: {
|
38
|
+
#{verb_type.map { |v, t| " #{v}: #{t.fetch(:params_type)}" }.join(",\n")}
|
39
|
+
}
|
40
|
+
Return?: {
|
41
|
+
#{verb_type.map { |v, t| " #{v}: #{t.fetch(:return_type)}" }.join(",\n")}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
TS
|
45
|
+
end
|
46
|
+
|
47
|
+
def routes_info
|
48
|
+
rs = Rails.application.routes.routes.filter_map { |r|
|
49
|
+
{
|
50
|
+
verb: r.verb,
|
51
|
+
name: r.name,
|
52
|
+
parts: r.parts,
|
53
|
+
reqs: r.requirements,
|
54
|
+
spec: r.path.spec
|
55
|
+
} if !r.internal && !r.app.engine?
|
56
|
+
}
|
57
|
+
rs.inject([]) do |new_rs, r|
|
58
|
+
prev_r = new_rs.last
|
59
|
+
if prev_r && r[:name].nil? && r.fetch(:spec).to_s != prev_r.fetch(:spec).to_s
|
60
|
+
# puts prev_r[:name]
|
61
|
+
# puts r.fetch(:spec).to_s
|
62
|
+
# puts prev_r.fetch(:spec).to_s
|
63
|
+
# raise
|
64
|
+
next new_rs
|
65
|
+
end
|
66
|
+
r[:name] = prev_r[:name] if prev_r && r[:name].nil?
|
67
|
+
next new_rs if r[:name].nil?
|
68
|
+
new_rs << r
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def rbs_loader
|
73
|
+
RBS::EnvironmentLoader.new.tap do |loader|
|
74
|
+
dir = Pathname('sig')
|
75
|
+
loader.add(path: dir)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def rbs_env
|
80
|
+
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
81
|
+
end
|
82
|
+
|
83
|
+
def rbs_builder
|
84
|
+
RBS::DefinitionBuilder.new(env: rbs_env)
|
85
|
+
end
|
86
|
+
|
87
|
+
def collect_controller_action_method_type
|
88
|
+
builder = rbs_builder
|
89
|
+
types = {}
|
90
|
+
ApplicationController.subclasses.each do |subclass|
|
91
|
+
subclass_type_name = parse_type_name(subclass.inspect).absolute!
|
92
|
+
definition = builder.build_instance(subclass_type_name)
|
93
|
+
actions = subclass.public_instance_methods(false)
|
94
|
+
actions.each do |action|
|
95
|
+
method = definition.methods[action]
|
96
|
+
next unless method
|
97
|
+
raise 'Unsupported' unless method.method_types.size == 1
|
98
|
+
method_type = method.method_types.first
|
99
|
+
raise 'Unsupported' unless method_type.is_a?(RBS::MethodType)
|
100
|
+
controller = subclass.to_s.sub(/Controller$/, '').underscore.to_s
|
101
|
+
types[controller] ||= {}
|
102
|
+
types[controller][action.to_s] = method_type
|
103
|
+
rescue
|
104
|
+
$stderr.puts $!.backtrace.join("\n")
|
105
|
+
$stderr.puts "#{subclass}##{action} not supported"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
types
|
109
|
+
end
|
110
|
+
|
111
|
+
def collect_controller_action_params_types
|
112
|
+
types = {}
|
113
|
+
collect_controller_action_method_type.each do |controller, action_method_types|
|
114
|
+
action_method_types.each do |action, method_type|
|
115
|
+
type = method_type.to_ts_params_type
|
116
|
+
types[controller] ||= {}
|
117
|
+
types[controller][action] ||= {}
|
118
|
+
types[controller][action] = type.empty? ? '{}' : type
|
119
|
+
end
|
120
|
+
end
|
121
|
+
types
|
122
|
+
end
|
123
|
+
|
124
|
+
def collect_controller_action_return_types
|
125
|
+
types = {}
|
126
|
+
collect_controller_action_method_type.each do |controller, action_method_types|
|
127
|
+
action_method_types.each do |action, method_type|
|
128
|
+
type = method_type.to_ts_return_type
|
129
|
+
types[controller] ||= {}
|
130
|
+
types[controller][action] ||= {}
|
131
|
+
types[controller][action] = type.empty? ? '{}' : type
|
132
|
+
end
|
133
|
+
end
|
134
|
+
types
|
135
|
+
end
|
136
|
+
end
|
137
|
+
}
|
138
|
+
|
139
|
+
using Module.new {
|
140
|
+
refine(Object) do
|
141
|
+
def generate_params_types
|
142
|
+
output = StringIO.new
|
143
|
+
types = collect_controller_action_params_types
|
144
|
+
types.each do |controller, actions|
|
145
|
+
actions.each do |action, type|
|
146
|
+
output.puts "type #{type_script_params_type_name(controller, action)} = #{type}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
output.string
|
150
|
+
end
|
151
|
+
|
152
|
+
def generate_return_types
|
153
|
+
output = StringIO.new
|
154
|
+
types = collect_controller_action_return_types
|
155
|
+
types.each do |controller, actions|
|
156
|
+
actions.each do |action, type|
|
157
|
+
output.puts "type #{type_script_return_type_name(controller, action)} = Exclude<#{type}, void>"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
output.string
|
161
|
+
end
|
162
|
+
|
163
|
+
def generate_request_functions
|
164
|
+
output = StringIO.new
|
165
|
+
type = collect_controller_action_params_types
|
166
|
+
routes_info.group_by { |r| r.fetch(:name) }.each do |name, routes|
|
167
|
+
route = routes.first
|
168
|
+
verb_type = routes.each_with_object({}) do |r, vt|
|
169
|
+
controller = r.dig(:reqs, :controller)
|
170
|
+
action = r.dig(:reqs, :action)
|
171
|
+
next unless type.dig(controller, action)
|
172
|
+
vt[r.fetch(:verb)] = {
|
173
|
+
params_type: type_script_params_type_name(controller, action),
|
174
|
+
return_type: type_script_return_type_name(controller, action)
|
175
|
+
}
|
176
|
+
end
|
177
|
+
next if verb_type.empty?
|
178
|
+
output.puts typescript_path_function(route, verb_type)
|
179
|
+
end
|
180
|
+
output.string
|
181
|
+
end
|
182
|
+
end
|
183
|
+
}
|
184
|
+
|
185
|
+
class RbsTsGenerator < Rails::Generators::Base
|
186
|
+
source_root File.expand_path('templates', __dir__)
|
187
|
+
|
188
|
+
def copy_rbs_ts_runtime_ts
|
189
|
+
copy_file 'rbs_ts_runtime.ts', 'app/javascript/packs/rbs_ts_runtime.ts'
|
190
|
+
end
|
191
|
+
|
192
|
+
def create_rbs_ts_routes_ts
|
193
|
+
Rails.application.eager_load!
|
194
|
+
params_types = generate_params_types
|
195
|
+
return_types = generate_return_types
|
196
|
+
request_functions = generate_request_functions
|
197
|
+
create_file 'app/javascript/packs/rbs_ts_routes.ts', [params_types, return_types, request_functions].join("\n")
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'rbs'
|
2
|
+
|
3
|
+
unless RBS::Types.constants.sort == [:NoSubst, :NoTypeName, :EmptyEachType, :Literal, :Bases, :Interface, :Tuple, :Union, :ClassSingleton, :Application, :ClassInstance, :Record, :Function, :Optional, :Variable, :Alias, :Proc, :Intersection, :NoFreeVariables].sort
|
4
|
+
raise 'Unsupported'
|
5
|
+
end
|
6
|
+
|
7
|
+
unless RBS::Types::Bases.constants.sort == [:Instance, :Base, :Class, :Void, :Any, :Nil, :Top, :Bottom, :Self, :Bool].sort
|
8
|
+
raise 'Unsupported'
|
9
|
+
end
|
10
|
+
|
11
|
+
class RbsTsGenerator < Rails::Generators::Base
|
12
|
+
module RbsTypesConvertible
|
13
|
+
using self
|
14
|
+
|
15
|
+
refine(RBS::MethodType) do
|
16
|
+
def to_ts_params_type
|
17
|
+
raise 'Unsupported' unless type_params.empty?
|
18
|
+
|
19
|
+
s = case
|
20
|
+
when block && block.required
|
21
|
+
raise 'Unsupported'
|
22
|
+
when block
|
23
|
+
raise 'Unsupported'
|
24
|
+
else
|
25
|
+
type.param_to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
if type_params.empty?
|
29
|
+
s
|
30
|
+
else
|
31
|
+
raise 'Unsuported'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_ts_return_type
|
36
|
+
type.return_to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
refine(RBS::Types::Function::Param) do
|
41
|
+
def to_s
|
42
|
+
type.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
refine(RBS::Types::Function) do
|
47
|
+
def param_to_s
|
48
|
+
params = []
|
49
|
+
params.push(*required_positionals.map { |param| "#{param.name}: #{param.type}" })
|
50
|
+
params.push(*optional_positionals.map {|param| "#{param.name}?: #{param.type}" })
|
51
|
+
raise 'Unsupported' if rest_positionals
|
52
|
+
params.push(*trailing_positionals.map { |param| "#{param.name}: #{param.type}" })
|
53
|
+
params.push(*required_keywords.map {|name, param| "#{name}: #{param}" })
|
54
|
+
params.push(*optional_keywords.map {|name, param| "#{name}?: #{param}" })
|
55
|
+
raise 'Unsupported' if rest_keywords
|
56
|
+
|
57
|
+
return '' if params.empty?
|
58
|
+
|
59
|
+
"{ #{params.join("; ")} }"
|
60
|
+
end
|
61
|
+
|
62
|
+
def return_to_s
|
63
|
+
return_type.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# RBS::Types.constants.map { RBS::Types.const_get(_1) }.select { _1.public_instance_methods(false).include?(:to_s) }
|
68
|
+
refine(RBS::Types::Literal) do
|
69
|
+
def to_s(level = 0)
|
70
|
+
case literal
|
71
|
+
when Symbol, String
|
72
|
+
literal.to_s.inspect
|
73
|
+
when Integer, TrueClass, FalseClass
|
74
|
+
literal.inspect
|
75
|
+
else
|
76
|
+
raise 'Unsupported'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
refine(RBS::Types::Interface) do
|
82
|
+
def to_s(level = 0)
|
83
|
+
raise 'Unsupported'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
refine(RBS::Types::Tuple) do
|
88
|
+
# copy from super to use refinements
|
89
|
+
def to_s(level = 0)
|
90
|
+
if types.empty?
|
91
|
+
"[ ]"
|
92
|
+
else
|
93
|
+
"[ #{types.map(&:to_s).join(", ")} ]"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
refine(RBS::Types::Record) do
|
99
|
+
def to_s(level = 0)
|
100
|
+
return "{ }" if self.fields.empty?
|
101
|
+
|
102
|
+
fields = self.fields.map do |key, type|
|
103
|
+
if key.is_a?(Symbol) && key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) && !key.match?(RBS::Parser::KEYWORDS_RE)
|
104
|
+
"#{key.to_s}: #{type}"
|
105
|
+
else
|
106
|
+
"#{key.to_s.inspect}: #{type}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
"{ #{fields.join("; ")} }"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
refine(RBS::Types::Union) do
|
114
|
+
# copy from super to use refinements
|
115
|
+
def to_s(level = 0)
|
116
|
+
if level > 0
|
117
|
+
"(#{types.map(&:to_s).join(" | ")})"
|
118
|
+
else
|
119
|
+
types.map(&:to_s).join(" | ")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
refine(RBS::Types::ClassSingleton) do
|
125
|
+
def to_s(level = 0)
|
126
|
+
raise 'Unsupported'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
refine(RBS::Types::Application) do
|
131
|
+
def to_s(level = 0)
|
132
|
+
case name.to_s
|
133
|
+
when '::Integer'
|
134
|
+
'number'
|
135
|
+
when '::String'
|
136
|
+
'string'
|
137
|
+
when '::Array'
|
138
|
+
raise 'Unsupported' unless args.one?
|
139
|
+
|
140
|
+
args[0].to_s + '[]'
|
141
|
+
else
|
142
|
+
raise 'Unsupported'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
refine(RBS::Types::Optional) do
|
148
|
+
# copy from super to use refinements
|
149
|
+
def to_s(level = 0)
|
150
|
+
if type.is_a?(RBS::Types::Literal) && type.literal.is_a?(Symbol)
|
151
|
+
"#{type.to_s(1)} ?"
|
152
|
+
else
|
153
|
+
"#{type.to_s(1)}?"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
refine(RBS::Types::Variable) do
|
159
|
+
def to_s(level = 0)
|
160
|
+
raise 'Unsupported'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
refine(RBS::Types::Alias) do
|
165
|
+
def to_s(level = 0)
|
166
|
+
raise 'Unsupported'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
refine(RBS::Types::Proc) do
|
171
|
+
def to_s(level = 0)
|
172
|
+
raise 'Unsupported'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
refine(RBS::Types::Intersection) do
|
177
|
+
# copy from super to use refinements
|
178
|
+
def to_s(level = 0)
|
179
|
+
strs = types.map {|ty| ty.to_s(2) }
|
180
|
+
if level > 0
|
181
|
+
"(#{strs.join(" & ")})"
|
182
|
+
else
|
183
|
+
strs.join(" & ")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# RBS::Types::Bases.constants.map { RBS::Types::Bases.const_get(_1) }
|
189
|
+
refine(RBS::Types::Bases::Instance) do
|
190
|
+
def to_s(level = 0)
|
191
|
+
raise 'Unsupported'
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
refine(RBS::Types::Bases::Base) do
|
196
|
+
def to_s(level = 0)
|
197
|
+
raise 'Unsupported'
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
refine(RBS::Types::Bases::Class) do
|
202
|
+
def to_s(level = 0)
|
203
|
+
raise 'Unsupported'
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
refine(RBS::Types::Bases::Void) do
|
208
|
+
def to_s(level = 0)
|
209
|
+
'void'
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
refine(RBS::Types::Bases::Any) do
|
214
|
+
def to_s(level = 0)
|
215
|
+
'any'
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
refine(RBS::Types::Bases::Nil) do
|
220
|
+
def to_s(level = 0)
|
221
|
+
'null'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
refine(RBS::Types::Bases::Top) do
|
226
|
+
def to_s(level = 0)
|
227
|
+
raise 'Unsupported'
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
refine(RBS::Types::Bases::Bottom) do
|
232
|
+
def to_s(level = 0)
|
233
|
+
raise 'Unsupported'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
refine(RBS::Types::Bases::Self) do
|
238
|
+
def to_s(level = 0)
|
239
|
+
raise 'Unsupported'
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
refine(RBS::Types::Bases::Bool) do
|
244
|
+
def to_s(level = 0)
|
245
|
+
'boolean'
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
|
2
|
+
type BaseResource = {
|
3
|
+
path: (args: any) => string
|
4
|
+
names: string[]
|
5
|
+
Methods?: any
|
6
|
+
Params?: { [method in HttpMethods]?: any }
|
7
|
+
Return?: { [method in HttpMethods]?: any }
|
8
|
+
}
|
9
|
+
export async function railsApi<
|
10
|
+
Method extends Exclude<Resource['Methods'], undefined>,
|
11
|
+
Resource extends BaseResource,
|
12
|
+
Params extends Exclude<Resource['Params'], undefined>[Method],
|
13
|
+
Return extends Exclude<Resource['Return'], undefined>[Method]
|
14
|
+
>(method: Method, { path, names }: Resource, params: Params): Promise<{ status: number, json: Return }> {
|
15
|
+
const tag = document.querySelector<HTMLMetaElement>('meta[name=csrf-token]')
|
16
|
+
const paramsNotInNames = Object.keys(params).reduce<object>((ps, key) => names.indexOf(key) === - 1 ? { ...ps, [key]: params[key] } : ps, {})
|
17
|
+
const searchParams = new URLSearchParams()
|
18
|
+
for (const name of Object.keys(paramsNotInNames)) {
|
19
|
+
searchParams.append(name, paramsNotInNames[name])
|
20
|
+
}
|
21
|
+
const query = method === 'GET' && Object.keys(paramsNotInNames).length ? `?${searchParams.toString()}` : ''
|
22
|
+
const body = method === 'GET' ? undefined : JSON.stringify(paramsNotInNames)
|
23
|
+
const response = await fetch(path(params) + query, {
|
24
|
+
method,
|
25
|
+
body,
|
26
|
+
headers: {
|
27
|
+
'Accept': 'application/json',
|
28
|
+
'Content-Type': 'application/json',
|
29
|
+
'X-CSRF-Token': tag.content
|
30
|
+
}
|
31
|
+
})
|
32
|
+
const json = await response.json() as Return
|
33
|
+
return new Promise((resolve) => resolve({ status: response.status, json: json }))
|
34
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class RbsTsGenerator < Rails::Generators::Base
|
2
|
+
class TypeScriptVisitor < ActionDispatch::Journey::Visitors::FunctionalVisitor
|
3
|
+
private
|
4
|
+
|
5
|
+
def binary(node, seed)
|
6
|
+
visit(node.right, visit(node.left, seed) + ' + ')
|
7
|
+
end
|
8
|
+
|
9
|
+
def nary(node, seed)
|
10
|
+
last_child = node.children.last
|
11
|
+
node.children.inject(seed) { |s, c|
|
12
|
+
string = visit(c, s)
|
13
|
+
string << '|' unless last_child == c
|
14
|
+
string
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def terminal(node, seed)
|
19
|
+
seed + node.left.to_s.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_GROUP(node, seed)
|
23
|
+
# TODO: support nested level 2
|
24
|
+
visit(node.left, seed.dup << '(() => { try { return ') << ' } catch { return "" } })()'
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_SYMBOL(n, seed); variable(n, seed); end
|
28
|
+
|
29
|
+
def variable(node, seed)
|
30
|
+
if node.left.to_s[0] == '*'
|
31
|
+
seed + '(' + node.left.to_s[1..-1] + ' ?? "")'
|
32
|
+
else
|
33
|
+
v = node.left.to_s[1..-1]
|
34
|
+
seed + "(() => { if (#{v}) return #{v}; throw #{v.to_json} })()"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
INSTANCE = new
|
39
|
+
end
|
40
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rbs_ts_generator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Seiei Miyagi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "<="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.1.0.alpha
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.3.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "<="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 6.1.0.alpha
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.3.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rbs
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "<"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.7.0
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.6.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "<"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.7.0
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.6.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: sqlite3
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
description:
|
68
|
+
email:
|
69
|
+
- hanachin@gmail.com
|
70
|
+
executables: []
|
71
|
+
extensions: []
|
72
|
+
extra_rdoc_files: []
|
73
|
+
files:
|
74
|
+
- MIT-LICENSE
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- lib/generators/rbs_ts/USAGE
|
78
|
+
- lib/generators/rbs_ts/rbs_ts_generator.rb
|
79
|
+
- lib/generators/rbs_ts/rbs_types_convertible.rb
|
80
|
+
- lib/generators/rbs_ts/templates/rbs_ts_runtime.ts
|
81
|
+
- lib/generators/rbs_ts/type_script_visitor.rb
|
82
|
+
- lib/rbs_ts_generator.rb
|
83
|
+
- lib/tasks/rbs_ts_generator_tasks.rake
|
84
|
+
homepage: https://github.com/hanachin/rbs_ts_generator
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.1.2
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Generate TypeScript that includes routes definition and request / response
|
107
|
+
JSON type from type signature of Rails controller actions.
|
108
|
+
test_files: []
|