rbs_ts_generator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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).
@@ -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,9 @@
1
+ Description:
2
+ Generate TypeScript routes definition and API request runtime from .rbs
3
+
4
+ Example:
5
+ bin/rails generate rbs_ts
6
+
7
+ This will create:
8
+ app/javascript/packs/rbs_ts_routes.ts
9
+ app/javascript/packs/rbs_ts_runtime.ts
@@ -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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :rbs_ts_generator do
3
+ # # Task goes here
4
+ # end
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: []