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.
@@ -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: []