🐶

scaffdogを使ってrails generate scaffold相当を手軽に実現する

2022/01/13に公開

scaffdogというscaffoldingツールがとても良かったので紹介します。

https://github.com/cats-oss/scaffdog

scaffdogの使い方

  • 以下のようなmarkdownファイルを用意して yarn run scaffdog generate crud を実行
  • モデル名を入力してください と聞かれるので、そこで User と答える
  • src/models/User.ts src/controllers/UserController.ts が作成される
---
name: 'crud'
root: '.'
output: '.'
ignore: []
questions:
  name: 'モデル名を入力してください。'
---

# `src/models/{{ inputs.name }}.ts`

```ts
export class {{ inputs.name }} {
  // TODO
}
``

# `src/controllers/{{ inputs.name }}Controller.ts`

```ts
export class {{ inputs.name }}Controller {
  // TODO
}
``

markdownの中ではmustache記法({{ }})を使って入力された値を埋め込みできます。
説明するほど難しいところはないので、詳しい使い方は公式をどうぞ。

https://github.com/cats-oss/scaffdog

便利な点

  • 設定ファイルの書き方がシンプルで分かりやすい。学習コストなしで良い。
  • 設定ファイルがmarkdownなので、syntax highlightつきで書ける・読める。
  • pascal camel snake など、組み込みのヘルパー関数が便利。
  • ヘルパー関数をカスタマイズできるため、柔軟に出力できる。

なぜrails generate scaffold相当をしたいか

現在prisma,GraphQL(Apollo Server, Apollo Client),nextを使ってWebサービス開発をしており、あるモデル(仮に User とする)に対するCRUDを作るために、例えば以下のファイル群を作る・編集する必要があります。

  • サーバーサイド
    • prisma.schema
    • User.ts, UserInput.ts (nexusの objectTypeinputObjectType), UsersQuery.ts UpsertUserMutation.ts
    • query.ts mutation.ts types.ts inputs.ts
  • フロントエンド
    • fetchUsers.graphql, upsertUser.graphql, UserFields.graphql
    • pages/users/index.tsx pages/users/[id]/edit.tsx
    • user関連のコンポーネント群

14ファイルに渡って毎回同じような記述をするのは非人間的なので、効率化したい。可能ならscaffoldでモデル名とカラム名・型を指定したら、それに沿って全てのファイルが出力されて欲しい。

ということで、rails generate scaffold相当の挙動になるようにしてみる。

テクニック1. 既存ファイルへの追記

scaffdogは新規作成だけでなく、追記も可能です。
追記のためには、 read という組み込みのヘルパー関数を使ってファイルを開いて上書きすればOK。

以下、prismaファイルの末尾にモデルを追加する例。

# `src/infra/schema.prisma`

```prisma
{{ '/foo/bar/src/infra/schema.prisma' | read }}

model {{ inputs.name | pascal }} {
  id             String       @id @default(uuid())
  // ...
}
``

テクニック2. カラムの作成

bin/rails generate scaffold User name:string age:integer 的なことをしたいところですが、これもヘルパー関数を自作すれば楽勝です。
ヘルパー関数は .scaffdog/config.js で拡張することができます。

ここでは name:string age:integer のような引数を受け取って、prismaの属性を出力する prismaAttributes ヘルパー関数を定義してみます。

module.exports = {
  files: ['./*'],
  helpers: [
    (registry) => {
      registry.set('prismaAttributes', (_context, value, _size, _str) => {
        const attributes = value.split(',').map((line) => {
          const [name, nexusType] = line.split(':')
          return `${name} ${prismaType(nexusType)}`
        })

        return attributes.join(`\n`)
      })
    },
  ],
}

function prismaType(nexusType) {
  return (
    {
      string: 'String',
      int: 'Int',
      date: 'DateTime',
      datetime: 'DateTime',
      boolean: 'Boolean',
      json: 'Json',
    }[nexusType] ?? nexusType
  )
}

これを以下のようなmarkdownで記入すればOKです。

---
name: 'crud'
root: '.'
output: '.'
ignore: []
questions:
  name: 'モデル名を入力してください。'
  attributes: '属性を入力してください。'
---

# `src/infra/prisma/schema.prisma`

```prisma
{{ '/foo/bar/src/infra/prisma/schema.prisma' | read }}

model {{ inputs.name | pascal }} {
  id             String       @id @default(uuid())
  {{ inputs.attributes | prismaAttributes }}
}
``

Discussion