rbs_ts_generator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|