Madogiwa Blog

主に技術系の学習メモに使っていきます。

オレオレWebフレームワークのv0.1.2をリリースしました🥳

オレオレWebフレームワークのv0.1.2をリリースしました🥳

github.com

今回のアップデートで、unicornpumaといったRack::Handlerに対応したアプリケーションサーバーであれば好きなものを使えるようになりました🙌

変更方法は簡単でGemfileアプリケーションサーバーのgemを追加してMakanai::Settingsの値を変更してあげるだけです👍

pumaを使う場合は、Gemfilepumaを追加して、

gem 'puma'

下記のようにhandlerpumaを指定して、

require 'makanai/main'

Makanai::Settings.rack_app_config = { 
  handler: :puma,
  host: '0.0.0.0',
  port: '8080' 
}

router.get '/' do
  'Hello Makanai!'
end

起動してあげるだけです🙆‍♂️

$ ruby app.rb 
Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:8080
Use Ctrl-C to stop

unicornを使う場合は、素のunicornRack::Handlerに対応してないので、unicorn-railsを使うと良い感じに起動できます🦄

gem 'unicorn-rails'
require 'makanai/main'
require 'unicorn-rails'

Makanai::Settings.rack_app_config = { 
  handler: :unicorn,
  host: '0.0.0.0',
  port: '8080' 
}

router.get '/' do
  'Hello Makanai!'
end
$ be ruby app.rb 
I, [2020-02-23T18:36:35.750377 #4936]  INFO -- : listening on addr=0.0.0.0:8080 fd=13
I, [2020-02-23T18:36:35.750546 #4936]  INFO -- : worker=0 spawning...
I, [2020-02-23T18:36:35.751820 #4936]  INFO -- : master process ready
I, [2020-02-23T18:36:35.752928 #4950]  INFO -- : worker=0 spawned pid=4950
I, [2020-02-23T18:36:35.753498 #4950]  INFO -- : worker=0 ready

仕組みとしてはhandlerで指定した値でRack::Handler.getを利用してRack::Handlerに対応したサーバーのClassを取得して起動しているという感じですね👀

www.rubydoc.info

Rack便利!!✨

(次は複数のDBMSに対応しようと思っているのですがRackみたいに共通のルールが整備されてないので大変😇)

CommitteeでOpenAPIによるAPI定義ドキュメントをもとにリクエスト/レスポンス形式をチェックする

最近、Nuxt.jsとRailsでSPAのアプリケーションを作っていて、schema管理をOpenAPIで行ってみているのですが、 せっかくなのでCommitteeを使ってCIで検証するようしてみたので導入方法とかをメモしておきます📝

Committeeとは?

CommitteeとはOpenAPIを使ったアプリケーション構築を支援してくれるGemのようです。

A collection of middleware to help build services with JSON Schema, OpenAPI 2, OpenAPI 3.

github.com

記載の通りCommitteeはOpenAPIで定義したドキュメントをもとにリクエスト/レスポンスの形式を検証するミドルウェアを提供してくれます。

この提供されているミドルウェアを使うことでRspec等のテスティングフレームワークでで検証することができます🙌

Committeeの使い方

Committeeを導入する

CommitteeRailsで使う場合には良い感じにしてくれているcommittee-railsというgemがあるので、今回はそちらを使いました💎

github.com

committeeと合わせてGemfileに記載してbundle installしてあげればOKです🙆‍♂️

gem 'committee'
gem 'committee-rails'

Committeeを使うための設定をする

Committeeを使うための設定はcommittee-railsのおかげでとても簡単です。

rails_helperに下記のような形で記載するだけです。

rails_helper.rb

  # configured for committee-rails
  config.add_setting :committee_options
  config.committee_options = {
    schema_path: Rails.root.join('schemas', 'api', 'v1', 'schema.yml').to_s,
    prefix: '/api/v1'
  }

schema_pathにはOpenAPIで定義ドキュメントへのファイルパス、prefixにはリクエスト時に付与するプレフィックス(api/v1等)を設定します。 ※prefixは設定しなくても大丈夫です。

Committeeを使ってテストする

先程の設定を行うことによりassert_schema_conformといったCommitteeのテスティングメソッドが使えるようになります。

実際のコード例は下記のような形になるかと思います、これで設定を行ってテストすることができました🙋‍♀️

require 'rails_helper'

describe Api::V1::FoodsController, type: :request do
  include Committee::Rails::Test::Methods

  describe 'GET /api/v1/foods' do
    before do
      create(:food, id: 1, name: 'name1', memo: 'memo1')
      create(:food, id: 2, name: 'name2', memo: 'memo2')
    end

    it 'confirm json schema' do
      get '/api/v1/foods'
      assert_request_schema_confirm
      assert_response_schema_confirm
    end

    it 'successed request' do
      get '/api/v1/foods'
      expect(response.status).to eq 200
    end
  end
end

おまけ: Committeeのテストを扱いやすくする(アイデアレベル)

こんな感じでshared_exampleにしておいても、ちょっと良いかなと思いました。

require 'rails_helper'

shared_examples 'Committer Schema Check For GET request_url' do
  it 'confirm json schema' do
    get request_url
    assert_request_schema_confirm
    assert_response_schema_confirm
  end
end

describe Api::V1::FoodsController, type: :request do
  include Committee::Rails::Test::Methods

  describe 'GET /api/v1/foods' do
    let(:request_url) { '/api/v1/foods' }

    before do
      create(:food, id: 1, name: 'name1', memo: 'memo1')
      create(:food, id: 2, name: 'name2', memo: 'memo2')
    end

    include_examples 'Committer Schema Check For GET request_url'

    it 'successed request' do
      get request_url
      expect(response.status).to eq 200
    end
  end
end

それかRails.routesから特定のnamespace配下のrouteを取得して自動的にテストしてもいいかもですね👀

require 'rails_helper'

module RoutingTestHelper
  def routes(namespace: nil, actions: nil, ignore_paths: [])
    filtered_routes = all_routes
    filtered_routes.select! { |route| route.name.include?(namespace) }    if namespace
    filtered_routes.select! { |route| actions.include?(route.action) }    if actions
    filtered_routes.select! { |route| ignore_paths.exclude?(route.name) } if ignore_paths
    filtered_routes.reject { |route| route.url.nil? }
  end

  private

  def rails_routes
    Rails.application.routes
  end

  def all_routes
    rails_routes.routes.select(&:name).map do |route|
      Route.new(route, rails_routes.url_helpers)
    end
  end

  class Route
    def initialize(route, url_helpers)
      @name = route.name
      @action = route.requirements[:action]
      @controller = route.defaults[:controller]
      @keys = route.path.required_names
      @url_helpers = url_helpers
    end

    attr_reader :name, :action, :controller, :keys, :url_helpers

    def attributes
      attributes = { host: 'localhost', controller: controller, action: action }
      # NOTE: must be created record with key before execution.
      attributes.tap { keys.each { |key| attributes.merge!(key.to_sym => 1) } }
    end

    def url
      url_helpers.url_for attributes
    rescue ActionController::UrlGenerationError
      nil
    end
  end
end

include RoutingTestHelper
include Committee::Rails::Test::Methods

RSpec.describe '画面に正常に遷移できるか確認', type: :request do
  IGNORE_PATHS = []
  before do
    # seed等を実行して前提データを作る
  end

  routes(actions: ['show', 'index'], ignore_paths: IGNORE_PATHS).each do |route|
    it "#{route.name}(#{route.url})" do
      get route.url
      assert_request_schema_confirm
      assert_response_schema_confirm
      expect(response.status).to eq 200
    end
  end
end

参考

qiita.com

qiita.com

Nuxt.jsのプロジェクトにJestを導入するMEMO👢

最近Nuxt.jsで作成しているアプリケーションに後からjestを導入したので、その辺の手順をメモしておきます📝

Jestとは?

JestはJavaScriptのテスティングフレームワークです。

jestjs.io

基本的な構文は下記のような感じです、若干Rspecに近い感じがしますね👀

const sum = function sum(a, b) {
  return a + b;
}

describe('sample spec', () => {
  it('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
})

詳しい説明等は公式のドキュメントをご確認ください🙋

jestjs.io

Nuxt.jsにJestを導入する

前提

今回のNuxt.jsとTypeScriptとJestのバージョン情報は下記のとおりです。

  • nuxt: 2.10.2
  • typescript: 3.7.4
  • jest: 25.1.0

依存ライブラリのインストール

まず使用するためにJest本体と関連ライブラリをinstallします📦

// Jest本体とVue,TypeScriptのサポート用のライブラリをインストール
npm install --save-dev jest ts-jest vue-jest @vue/test-utils @types/jest 
// Jest内で最新のJS構文を使用するためにBabel関連のライブラリもインストール
npm install --save-dev npm install --save-dev babel-jest babel-core babel-preset-env

Jestの設定

Jestを使うにあたっての設定は特に必要なありません 🙆‍♂️

zero config Jest aims to work out of the box, config free, on most JavaScript projects. https://jestjs.io/ja/

※必要に応じてjest --initでデフォルトの設定ファイルを作成してカスタマイズ可能

なのでJestでBabelを使うための設定飲みを行います⚙

Babelの設定は下記のような.babelrcをプロジェクトのルートディレクトリに配置すればOKです👍

{
  // babel-preset-envを使用することを明示、変換先をfalse(何もしない)に指定
  "presets": [["env", { "modules": false }]], 
  "env": {
    "test": {
      // babel-preset-envを使用することを明示、対象環境をnodeに指定
      "presets": [["env", { "targets": { "node": "current" } }]] 
    }
  }
}

参考:

Jestの実行コマンドを追加

npm run testでJestが実行できるようにpackage.jsonscriptを追加します🏃

  "scripts": {
    "test": "jest --config jest.config.js spec/**/*.js"

テスト用のファイル.spec.jsを追加

今回は下記のようなVueコンポーネントをテストするSpecをサンプルで追加しました。

import Vue from 'vue'
import Vuetify from 'vuetify'
import { mount } from '@vue/test-utils'
import Favorite from '@/components/foods/Favorite.vue'
Vue.use(Vuetify)

describe('components/Favorite.vue', () => {
  it('is a Vue instance', () => {
    const wrapper = mount(Favorite)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })

  describe('toggeleFavorite', () => {
    describe('is not favorited.', () => {
      it('toggle favorited.', () => {
        const wrapper = mount(Favorite, { propsData: { favorited: false } })
        wrapper.vm.toggeleFavorite()
        expect(wrapper.vm.currentFavorited).toBe(true)
      })
    })

    describe('is favorited.', () => {
      it('toggle unfavorited.', () => {
        const wrapper = mount(Favorite, { propsData: { favorited: true } })
        wrapper.vm.toggeleFavorite()
        expect(wrapper.vm.currentFavorited).toBe(false)
      })
    })
  })
})

テストを実行

無事にテストが実行できました🙌

npm run test

> nuxt-app@1.0.0 test /nuxt-app
> jest --config jest.config.js spec/**/*.js

 PASS  spec/components/Favorite.spec.js (9.758s)
  components/Favorite.vue
    ✓ is a Vue instance (40ms)
    toggeleFavorite
      is not favorited.
        ✓ toggle favorited. (8ms)
      is favorited.
        ✓ toggle unfavorited. (5ms)
    favoritedColor
      is not favorited.
        ✓ return unfavorited color. (4ms)
      is favorited.
        ✓ return favorited color. (15ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        15.486s

CIでテストを実行する

さらに今回はCircleCIを使ってCI上でJestとESLintを実行するようにしてみました、設定ファイルは下記のような感じです⚙

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - persist_to_workspace:
          root: .
          paths:
            - .
  lint:
    docker:
      - image: circleci/node:12
    steps:
      - attach_workspace:
          at: .
      - run: npm run lint
  test:
    docker:
      - image: circleci/node:12
    steps:
      - attach_workspace:
          at: .
      - run: npm run test
workflows:
  version: 2
  build-test-lint:
    jobs:
      - build
      - lint:
          requires:
            - build
      - test:
          requires:
            - build

Workflowを使ってJestによるテストとESLintによる静的解析が並列で走るようになっています🤖

f:id:madogiwa0124:20200209134729p:plain

発生したエラーとTips

Vueのコンポーネントをテストしてるときにハマったことを色々メモしておきます📝

[Vue warn]: Unknown custom element: <v-icon> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

Vuetifyで定義されたコンポーネントが認識できていないことが原因、Vue.use(Vuetify)を実行してVuetifyを使うことを明示してあげたところ解決した。

import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)

github.com

Nuxtで定義されたコンポーネントnuxt-linkを認識できていないことが原因、RouterLinkStubを使ってstubして解決した。

import { mount, RouterLinkStub } from '@vue/test-utils'
const wrapper = mount(Component, { stubs: { NuxtLink: RouterLinkStub } })

onigra.github.io

[Vue warn]: Error in render: “TypeError: Cannot read property ‘resolve’ of undefined”

前回と違って今回はtoオプションを使ってリンク先をしていたため、RouterLinkStubを使ったStubがうまく使えなかったので別の方法を使う必要がありました。

    <v-btn value="recent" to="/foods">
      <span>Recent</span>
      <v-icon>mdi-history</v-icon>
    </v-btn>

実際に行った方法は、下記のような形でLocalVueを使ってRouterオブジェクトを作成して渡してあげることで解決できました。

import Vue from 'vue'
import VueRouter from 'vue-router'
import { mount, createLocalVue } from '@vue/test-utils'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

const wrapper = mount(Component, { localVue, router })

vue-test-utils.vuejs.org

Nuxt.jsに後からTypeSctiptを導入するときのMEMO📝

最近Nuxt.jsで作ったプロジェクトを作成後に後からTypeScriptを導入したときに色々とハマりどころがあったのでメモしておく。

前提

今回のNuxt.jsとTypeScriptのバージョン情報は下記のとおりです。

  • nuxt: 2.10.2
  • typescript: 3.7.4

また私が最初にNuxt.jsのプロジェクトを作成した際の設定は下記です。

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in .
? Project name foodolist-front
? Project description My delightful Nuxt.js project
? Author name Madogiwa
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)

基本的な手順

まずNuxt.jsにTypeScriptを導入する方法は公式のドキュメントがあるのですが、それに従って導入するのはセオリーだと思います。

https://typescript.nuxtjs.org/ja/

しかし私の場合だとうまく行かなったので追加で行った手順を記載しておきます😢

Module parse failed: Unexpected character '@'が発生する・・・

下記に従ってpackage.jsonnuxtではなくnuxt-tsで起動するように修正しました。

qiita.com

// before
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."

//after
"scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "generate": "nuxt-ts generate",
    "start": "nuxt-ts start",
    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."

公式のTypeScriptの導入ドキュメントではオプションとなっていたのですが実施しないと、 Module parse failed: Unexpected character '@'といったエラーが発生してうまく動きませんでした。。。

typescript.nuxtjs.org

Nuxt.jsのコミュニティでもRuntimeを使用しているので使用した方が良かったようです・・・!

typescript-template/package.json at master · nuxt-community/typescript-template · GitHub

eslint実行時にEnvironment key "jest/globals" is unknown + その他諸々が発生する・・・

eslint実行時にEnvironment key "jest/globals" is unknownや依存モジュールが認識されずコンソールに表示されるがまま色々なeslintまわりのプラグインを導入しました😇

  "devDependencies": {
    "eslint-plugin-jest": "^23.6.0",
    "eslint-plugin-node": "^11.0.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-plugin-unicorn": "^15.0.1",
    "eslint-plugin-vue": "^6.1.2"

この辺はtypescript-templateも記載されておらず正解がわからないのですが、一旦コンソールに表示されるがままにpackageをinstallしていくと動きました。。。 eslint-plugin-vueに関してはnode_modulesに依存により入っていたのですが、明示的にpackage.jsonに記載しないと認識してくれませんでした😢

typescript-template/package.json at master · nuxt-community/typescript-template · GitHub

おわりに

いろいろなネット上のの情報のおかげでなんとかNuxt.js + TypeScriptの環境を作れました😭
昨今のjs界隈ではTypeScriptがデファクトスタンダードになりつつあるようですが、あまりキャッチアップできていないので色々と試して理解を深めていきたい・・・!

参考

typescript.nuxtjs.org

github.com

qiita.com

qiita.com

rails 6の新機能`insert_all`を試してみたのでMEMO📝

railsでbulk insertするときには今まではactiverecord-importが使うことが多かったと思いますが、 rails 6からはActiveRecord::Persistence#insert_allが追加されたので標準機能を使ってbulk insertを行うことができるようになりました。

github.com

実際に使ってみたので使い方とかメモしておきます。

使い方

使い方は簡単でbulk insartを行うModelのclassを対象にinsert_allを呼び出してあげるだけです。

class Entry < ApplicationRecord
end

now = Time.current
Entry.insert_all([
  { title: 'foo1', body: 'bar1', created_at: now, updated_at: now },
  { title: 'foo2', body: 'bar3', created_at: now, updated_at: now },
])

#=> Entry Bulk Insert (7.3ms)  INSERT INTO "entries"("title","body","created_at","updated_at") VALUES ('foo1','bar1','2020-01-25 17:22:50.533182', '2020-01-25 17:22:50.533182'),('foo2','bar2','2020-01-25 17:22:50.533182', '2020-01-25 17:22:50.533182')

実行するとINSERT INTO table_name(column1, column2, ...) VALUES (value1, value2, ...)といったSQLが生成されて実行されます。 このように一つのINSERT文にまとめられるので、DBアクセスが少なくすみパフォーマンスがよくなりそうです👍(重複レコードがあった場合にはinsertがskipされます。)

created_atupdated_atを補完してくれないので自分で設定しないといけない点に注意です。実装をシンプルにしてパフォーマンスを担保するのとupdate_allの仕様に合わせているのが理由のようです🤔
https://github.com/rails/rails/issues/35493#issuecomment-470100313

ちなみにinsert_all!upsert_allというメソッドも用意されていて、insert_all!を使うとレコード重複が発生した際にActiveRecord::RecordNotUniqueをraiseします🚨upsert_allを使うとレコードが重複が発生した場合に上書きされます💾(ON DUPLICATE KEY UPDATE or ON CONFLICTが実行される)

おわりに

rails 6から追加されたinsert_allの使い方書いてみました。 activerecord-importの方がエラーハンドリング周りとか良い感じにやってくれそうですが、シンプルなbulk insart機能をrails標準機能でシンプルに実装できるのは良いですね🙌

参考

qiita.com

あなたの知ってるRubyGemsTipsというイベントでGemの依存関係まわりの話でLTしました💎

先日下記のイベントでLT登壇してきました。
(LTでイベント登壇するのは初めてだったので実績解除した🏆)

connpass.com

資料はこちら💎

speakerdeck.com

内容は、bundle gemでGemを作ったときに依存するgemをGemfilegemspecどっちに書くか的な話でした。

最終的な結論としては、下記のような形で発表しました。

  • RUNTIME依存性(実行時に依存するGem): 依存性解決時にGemfileが参照されないのでgemspecに書く
  • DEVELOP依存性(開発時に依存するGem): いろんな考え方があるけどGemfileに書いたほうが良さそう

RUNTIME依存性に関しては、一応事前にrubygemsのコードを読んでなんとなくinstall時の依存周りの解決方法を理解していた気になっていたのですが、最後までちゃんと理解できておらず自信なかったので概要だけさらっと話すだけになってしまいましたが、コードリーディングのメモはこちら📝

madogiwa0124.hatenablog.com

DEVELOP依存性については色々な考え方があるようですが、bundlerにbundle gem時の開発系のgemのデフォルトの記載先がgemspecadd_development_dependencyからGemfileに変わったようなので、今後はGemfile側に記載するのが主流になるのかなと思いました。

github.com

ですが、登壇中にタイムリーに投稿頂いた下記の記事にもある通り考え方次第だと思うので自分の使いやすいように使い分けるのが良さそうですね😅

結論という結論は特になく、現状だと Gemfile でしか指定できないことは Gemfile をもちいて行うしかなく、あとは考え方次第のスタイルという感じではないだろうかと理解している。

koic.hatenablog.com

今回、自分で普段Gemを作るときに軽く疑問に思ったことをテーマに決めたのですが、bundlerの中でそれに関するコミットが検討中にマージされたり、Rubyコミュニティで話題に上がってたテーマだったりと色々とタイムリーで昨今のRubyGems周りの流れも知れたので色々と勉強になってよかったなぁと思いました。

参加頂いた方、いろいろとアドバイスを頂いた方、ありがとうありがとうございました🙇‍♂️

OpenAPIとは一体何なのか調べたのでMEMO

最近、バックエンドはRails(APIモード)、フロントはNuxt.jsといったフロントエンドとバックエンドが別々で、APIでやり取りさせるような構成が多くなってきました。 そういう場合にバックエンドとフロントエンドのIF(スキーマ)を管理するのにOpenAPIというのがデファクトスタンダードになりつつあるようです。

自分自身、普通のモノリシックなRailsアプリケーションしか触ったことがなかったので、この辺キャッチアップできてなかったのですが、いろいろ環境とか作ったりしてなんとなく分かってきたので、メモしておきます📝

OpenAPIとは

OpenAPIとはもともとSwaggerと呼ばれるツールによって定義されたRESTfulなAPIの定義方法が標準化されてもののようです。

The OpenAPI Specification, formerly known as the Swagger Specification, is the world’s standard for defining RESTful interfaces. https://swagger.io/solutions/getting-started-with-oas/

いろいろ調べたところ下記のような周辺ツールも含めてOpenAPIと呼ばれているようです👀

tool memo link
Swagger Editor OpenAPIを使ったAPI定義を良い感じにプレビューしながら書けるエディター https://editor.swagger.io/
Swagger UI OpenAPIを使ったAPI定義(yaml or json)からWebで閲覧可能な良い感じのAPI定義書を生成する https://swagger.io/tools/swagger-ui/
OpenAPI Generator OpenAPIを使ったAPI定義(yaml or json)からStabを作成するツール https://openapi-generator.tech/

OpenAPI GeneratorSwagger Codegenでも代用可能だが、現状はOpenAPI Generatorが主流らしい。 どうやらSwagger Codegenのv2とv3で大きな変更が入りPythonと同様なことが起こるんじゃないかといった懸念等がありForkされOpenAPI Generatorが作られたようです。
Swagger Codegen Fork: Q&A · OpenAPI Generator

以前説明したNuxt.jsとRailsのdocker環境にSwagger UIとOpenAPI Generatorを追加したので試したい方はこちらから

github.com

※Swagger Editorは自分の好きなEditorで書いたほうが使いやすい + 使いたい場合はWebでも使えるので含めない方針としました。

OpenAPIの定義方法

OpenAPI定義は下記のような形です、今回の例ではyamlで定義していますがjsonでも定義可能です。

下記に詳しい説明が記載されています。

swagger.io

下記の例では、以下のAPIを定義したものです。

  • /foodsで食品の一覧を取得する
  • /foods/{id}で特定の食品を取得する
openapi: '3.0.2' # 使用するOpenAPIのバージョン

info: # API定義全体の説明
  title: 'FoodoList Api'
  version: '0.0.1'
servers: # APIを返すサーバーの情報
  - url: https://rails:3000
    description: Development server
paths: # ここにAPI定義のリストを書いていく
  '/foods':
    get: 
      tags:
        - foods
      summary: Get all foods.
      description: Returns an array of Food model
      parameters: []
      responses: 
        '200':
          description: A JSON array of Food model
          content:
            application/json:
              schema: 
                type: array
                items:
                  $ref: '#/components/schemas/Food' # componentsに定義したスキーマ情報をロード
                example: 
                  - id: 1
                    name: sample1
                    memo: sample memo
                    address: sample address
                    image_url: https://example.com/sample1.png
                  - id: 2
                    name: sample2
                    memo: sample memo
                    address: sample address
                    image_url: https://example.com/sample2.png
  '/foods/{id}':
    get:
      tags:
        - foods
      summary: find food.
      description: Returns an object of Food model
      parameters:
        - name: id
          in: path
          description: food id
          required: true
          schema:
            type: integer
      responses: 
        '200':
          description: A JSON of Food model
          content:
            application/json:
              schema: 
                type: object
                $ref: '#/components/schemas/Food'
                example: 
                  - id: 1
                    name: sample1
                    memo: sample memo
                    address: sample address
                    image_url: https://example.com/sample1.png
components: # ここに個別のスキーマ情報を書く
  schemas: 
    Food:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        name:
          type: string
        memo:
          type: string
        address:
          type: string
        image_url:
          type: string

Swagger UI

Swagger UIはOpenAPIの定義方法で記載したようなOpenAPIを使ったAPI定義から、きれいなAPIドキュメント生成するツールです。

swagger.io

下記のようなきれいなAPI定義書を生成することができます。

f:id:madogiwa0124:20200105143129p:plain

Docker環境も用意されていて、環境変数SWAGGER_JSONに定義ファイルのパスを指定することで生成するAPI定義書の元となるファイルを制御することができます。

https://hub.docker.com/r/swaggerapi/swagger-ui/

Swagger Editor

Swagger Editorは、OpenAPIの定義方法で記載したようなOpenAPIを使ったAPI定義をSwagegr UIでプレビューしながら書けるEditorです。Webでも使えるので、そちらから触ってもらったほうが早いと思います✍

editor.swagger.io

詳しい使い方とかはこちら

swagger.io

OpenAPI Generator

OpenAPIを使ったAPI定義からstab用のアプリケーションを作成することができるツールです。

コマンドラインから使用するcliとWebからGUIで使えるonlineの二種類があります。

stabを生成できるアプリケーションは、railssinatrarubypythonphpperl、elixirなど様々で詳しくは下記を参照してください。

openapi-generator.tech

使い方はnpmやhomebrew、Dockerで環境を構築するとopenapi-generator-cliコマンドが使えるようになります。

openapi-generator.tech

実際にstabを生成するコマンドの例は下記のような感じです。

$ openapi-generator generate -i /openapi-repo-name/openapi.yml -g stab-app -o /openapi-repo-name/stab-app-name

各オプションは下記のような感じ

  • -i: 定義ファイルのパス
  • -g: 生成するstabアプリケーションの種類(ruby-on-rails等)
  • -o: stabアプリケーションを生成するパス

実際に生成してみたのですが、私の定義が悪いのかもですがexampleをいい感じに返してくれるわけではないみたいですね(・・;)※生成するアプリケーションのフレームワークの種類にもよるのかもですが。。。

class FoodsController < ApplicationController

  def index
    # Your code here

    render json: {"message" => "yes, it worked"}
  end

  def show
    # Your code here

    render json: {"message" => "yes, it worked"}
  end

おわりに

OpenAPIや関連ツールについてまとめてみました。 実際の業務ではOpenAPIで定義して、Swagger UIでドキュメント化し、CIで定義ファイルが変更するたびにOpenAPI GeneratorでStabを生成してECR とかに上げて自動的にstabアプリケーションが更新されていくような仕組みを作ると良さそうですね👍

さくっと試したい人は環境を作っているので、下記から試してみてください。

github.com

参考

swagger.io

openapi-generator.tech

future-architect.github.io

qiita.com

girigiribauer.com