RbsTsGenerator

Generate TypeScript that includes routes definition and request / response JSON type from type signature of Rails controller actions.

Sample repository: hanachin/rbs_ts_bbs

Usage

Write type signature of your controller actions in ruby/rbs.

# sig/app/controllers/boards_controller.rbs
class BoardsController < ::ApplicationController
  @board: Board
  @boards: Board::ActiveRecord_Relation

  def index: () -> Array[{ id: Integer, title: String }]
  def create: (String title) -> { id: Integer, message: String } | Array[String]
  def update: (Integer id, String title) -> { id: Integer, message: String } | Array[String]
  def destroy: (Integer id) -> { message: String }
end

The return type of the action method is type of json record. But action does not explicitly return json record. To pass the ruby type checking, add | void to each signatures.

class BoardsController < ::ApplicationController
  @board: Board
  @boards: Board::ActiveRecord_Relation

  def index: () -> (Array[{ id: Integer, title: String }] | void)
  def create: (String title) -> ({ id: Integer, message: String } | Array[String] | void)
  def update: (Integer id, String title) -> ({ id: Integer, message: String } | Array[String] | void)
  def destroy: (Integer id) -> ({ message: String } | void)
end

I use Steep to type checking the ruby code. Setup the Steepfile like following and run steep check.

# Steepfile
target :app do
  signature "sig"

  check "app"
  typing_options :strict
end
$ bundle exec steep check
[Steep 0.20.0] [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/mailboxes/application_mailbox.rb, app/mailers/application_mailer.rb, app/models/application_record.rb, app/models/board.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.

When you passed the ruby type check, next generate TypeScript from those signatures.

$ rails generate rbs_ts

This will generate those routes definition in app/javascript/packs/rbs_ts_routes.ts.

type BoardsUpdateParams = { id: number; title: string }
type BoardsDestroyParams = { id: number }
type BoardsIndexParams = {}
type BoardsCreateParams = { title: string }

type BoardsUpdateReturn = Exclude<{ url: string; message: string } | string[] | void, void>
type BoardsDestroyReturn = Exclude<{ url: string; message: string } | void, void>
type BoardsIndexReturn = Exclude<{ id: number; title: string }[] | void, void>
type BoardsCreateReturn = Exclude<{ url: string; message: string } | string[] | void, void>

export const boards = {
  path: ({ format }: any) => "/" + "boards" + (() => { try { return "." + (() => { if (format) return format; throw "format" })() } catch { return "" } })(),
  names: ["format"]
} as {
  path: (args: any) => string
  names: ["format"]
  Methods?: "GET" | "POST"
  Params?: {
    GET: BoardsIndexParams,
    POST: BoardsCreateParams
  }
  Return?: {
    GET: BoardsIndexReturn,
    POST: BoardsCreateReturn
  }
}
export const board = {
  path: ({ id, format }: any) => "/" + "boards" + "/" + (() => { if (id) return id; throw "id" })() + (() => { try { return "." + (() => { if (format) return format; throw "format" })() } catch { return "" } })(),
  names: ["id","format"]
} as {
  path: (args: any) => string
  names: ["id","format"]
  Methods?: "PATCH" | "PUT" | "DELETE"
  Params?: {
    PATCH: BoardsUpdateParams,
    PUT: BoardsUpdateParams,
    DELETE: BoardsDestroyParams
  }
  Return?: {
    PATCH: BoardsUpdateReturn,
    PUT: BoardsUpdateReturn,
    DELETE: BoardsDestroyReturn
  }
}

And generate default runtime in app/javascript/packs/rbs_ts_runtime.ts

In your TypeScript code, you can use those routes definition and the default runtime like following

import { boards } from './rbs_ts_routes'
import { railsApi } from './rbs_ts_runtime'

const params = { title: 'test' }
railsApi('POST', boards, params).then(({ json }) => {
  if (json instanceof Array) {
    return Promise.reject(json)
  } else {
    window.location.href = json.url
    return Promise.resolve()
  }
})

Installation

Add this line to your application's Gemfile:

gem 'rbs_ts_generator', group: :development

And then execute:

$ bundle

Or install it yourself as:

$ gem install rbs_ts_generator

Contributing

https://github.com/hanachin/rbs_ts_generator

License

The gem is available as open source under the terms of the MIT License.