2020年9月8日火曜日

Ruby prettier 入門

概要

Ruby prettier は Ruby のソースコードを整形してくれるツールです
今回は簡単な使い方を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • prettier 0.20.1

インストール

  • bundle init
  • vim Gemfile
gem "prettier"
  • bundle install

とりあえず使ってみる

とりあえずどんな感じで整形してくれるか確認してみます
以下のようなコードがあった場合に prettier を使ってみます

  • vim app.rb
puts "hoge"; puts "hoge"; puts "hoge"; puts "hoge"; puts "hoge"
puts "hoge"; puts "hoge"; puts "hoge"; puts "hoge"
puts "hoge"; puts "hoge"; puts "hoge"
puts "hoge"; puts "hoge"
puts "hoge"

prettier は rbprettier というコマンドを使います
--write を使うと指定のファイルをそのまま上書きして整形します

  • bundle exec rbprettier --write app.rb

以下のように整形してくれます

puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'
puts 'hoge'

1 行でブロックを書いた場合にどうなるか

Ruby では map などを使って配列などを別の配列に加工するなどの処理を良くすると思います
またそういった場合にブラケットを使ってブロックを 1 行で書くこともあると思います
例えば以下のようなコードだとどうなるか確認します

puts (1..5).map { |v| v if v % 2 == 0 }.compact
  • bundle exec rbprettier --write app.rb

結果は特に整形されることなくそのままでした
1 行ブロックはちゃんと考慮してくれているようです
ただ do ... end を使ったブロックの場合は 1 行で書いても整形します

puts (1..5).map do |v| v if v % 2 == 0 end.compact

これは以下のようになりました

puts (1..5).map do |v|
  if v % 2 == 0vend
end.compact

複数行のブロックは do ... end を使うのが Ruby 的にもルールなので、これは当然といえば当然だと思います

オプション

.prettierrc というファイルに整形時のオプションを指定することができます
例えば Ruby の場合ダブルクォートは変数展開が使えます
しかし変数展開する必要がないのにダブルクォートを使っている場合に自動的にシングルクォートに変換してくれる機能があります

  • vim .prettierrc
{
  "preferSingleQuotes": true
}
  • vim app.rb
puts "hoge"
  • bundle exec rbprettier --write app.rb

これを以下のように整形してくれます

puts 'hoge'

他にも printWidth で 1 行の最大文字数を設定したり tabWidth でインデント時のタブの数を設定できます
Ruby で使えるすべてのオプションはこちらが参考になります

関数間の改行やクラス間の改行挿入

試してみたのですがどうやら改行挿入はやってくれない感じでした
例えば以下のようなコードは prettier してもそのままでした
オプションも見つからなかったのでこのあたりは手動で頑張るしかなさそうです

def hoge
  puts 'hoge'
end
def hoge2
  puts 'hoge2'
end

class Hoge
  def hoge
    puts 'hoge'
  end
end
class Fuga
  def fuga
    puts 'fuga'
  end
end

最後に

Ruby のコードを自動整形することができる prettier を使ってみました
シンプルなフォーマッタなので使いやすい反面細かいところは自力で頑張るしかなさそうです
もう少し詳細に整形したい場合などは rubocop などを使ってチェックすると良いかなと思います
(※rubocop 自体はフォーマッタではなくフォーマットチェックツールなので注意)

個人的にはフォーマッタはシンプルな方が好きです
理由はフォーマッタのルールが厳しすぎてビルド時にいろいろな警告やエラーが発生しそれの対応をするほうが面倒な場面が多いからです

参考サイト

2020年9月7日月曜日

Ruby thor で CLI ツールの開発に入門してみる

概要

Thor は Ruby で CLI ツールを作成することができるライブラリです
今回は簡単な使い方を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • thor 1.0.1

インストール

  • bundle init
  • vim Gemfile
gem "thor"
  • bundle config path vendor
  • bundle install

Hello thor

とりあえず動作させてみましょう
Thor クラスを継承したクラス内でメソッドを定義するだけでそれを CLI から呼び出すことができます
また help コマンド時の説明も desc などの DSL を使うことで定義できます

require "thor"

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  def hello(name)
    puts "Hello #{name}"
  end
end

MyCLI.start(ARGV)

これで help を表示すると以下のように表示されます

  • bundle exec ruby app.rb help
Commands:
  app.rb hello NAME                 # say hello to NAME
  app.rb help [COMMAND]             # Describe available commands or one specific command

hello メソッドを実行してみるとちゃんと実行されているのが確認できると思います

  • bundle exec ruby app.rb hello hawksnowlog

=> Hello hawksnowlog

可変長引数を受け取る

先程の例だと引数を 1 つ以上指定するとエラーになります
可変長にすることで引数をいくつでも受け取ることができるようになります

require "thor"

class MyCLI < Thor
  desc "team NAME member1 member2", "show team NAME and members"
  def team(name, *members)
    puts "Team #{name}"
    puts "Members #{members.join(',')}"
  end
end

MyCLI.start(ARGV)

こんな感じで実行することができます

  • ruby app.rb team number 1 2 3 4
Team number
Members 1,2,3,4`

オプションを指定する

--hoge のような感じでオプションを指定することができます

require "thor"

class MyCLI < Thor
  desc "print NAME", "print NAME and optional age"
  option :age
  def print(name)
    puts "Name #{name}"
    puts "Age #{options[:age]}"
  end
end

MyCLI.start(ARGV)
  • bundle exec ruby app.rb print hawksnowlog --age 10

クラスオプション

クラス内で共通のオプションを持たせたい場合は class_option を使います
定義したすべてのメソッドでオプションを使うことができます

require "thor"

class MyCLI < Thor
  class_option :verbose, :type => :boolean

  desc "hello NAME", "say hello to NAME"
  def hello(name)
    puts "Hello #{name}"
    puts "Bye!" if options[:verbose]
  end
end

MyCLI.start(ARGV)
  • bundle exec ruby app.rb hello hawk --verbose

Long Description

help commad とすることで詳細を表示することができます
long_desc を使います

require "thor"

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  long_desc <<-LONGDESC
      \u001b[31maaaaaaaaaaaaaaaaaa\u001b[0m

      bbbbbbbbbbbbbbbb  

      > $ bundle exec ruby app.rb hello "hawksnowlog"
  LONGDESC
  def hello(name)
    puts "Hello #{name}"
  end
end

MyCLI.start(ARGV)

ANSI カラーコードも使えました

  • bundle exec ruby app.rb help hello

サブコマンド

git remote add や git remote remove のようにサブコマンドを定義することもできます
例えば name というコマンドに対するサブコマンドを定義したい場合は以下のようにします
subcommand を使って更にサブコマンド用のクラスを定義する感じになります

require "thor"

class Show < Thor
  desc "print", "Print my name"
  def print
    puts "hawksnowlog"
  end
end

class MyCLI < Thor
  desc "name SUBCOMMAND", "Operations for your name"
  subcommand "name", Show
end

MyCLI.start(ARGV)

Thorfile

thor はタスクランナーとして使うこともできます
Thorfile というファイルに同じように実行したいコマンドを定義しておくと rake のようにタスクとして実行することができます

  • vim Thorfile
require "thor"

class Task < Thor
  class << self
    def exit_on_failure?
      true
    end
  end

  desc "hello NAME", "say hello to NAME"
  def hello(name)
    puts "Hello #{name}"
  end
end

exit_on_failure? はおまじないだと思ってください
これがないと Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define exit_on_failure? in Thor::Sandbox::Task という警告が表示されてしまいます
タスクの一覧を確認する場合は thor -T を使います

  • bundle exec thor -T
task
----
thor task:hello NAME  # say hello to NAME

あとはタスクを指定して実行するだけです
こちらの使い方のほうが thor っぽいというか rake っぽくも使えるので良いかなと思います

  • bundle exec thor task:hello hawksnowlog

最後に

Ruby の thor を使って CLI ツールを作成する方法を紹介しました
引数の制御やオプション、サブコマンドなど必要な機能は一通り揃っているかなと思います
よくタスクランナーの使い方が紹介されているイメージですが元は CLI ビルドツールになります

参考サイト

2020年9月4日金曜日

Sinatra を Passenger 上で動作させてみた

概要

Passenger と Sinatra を連携させてみました
Sinatra アプリに来る前段で何か管理や処理をしたいことがある場合に使えるテクニックかなと思います

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • passenger 6.0.6
    • sinatra 2.0.8.1

Sinatra アプリの準備

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install
  • vim app.rb
require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/' do
    'ok'
  end
end
  • vim config.ru
require './app'
run MyApp
  • bundle exec rackup config.ru

これで localhost:9292 で Webrick が動作してアプリにアクセスできます

Passenger 上で動作させる

厳密に言うと Passenger 上で動作させるわけではなく Passenger 経由で Webrick のアプリにアクセスするようになります

  • vim Gemfile
gem "passenger"
  • bundle install
  • vim config.ru
require './app'
require "phusion_passenger/rack_handler"
run MyApp
  • bundle exec passenger start

と実行すると localhost:3000 が起動してこれを経由して localhost:9292 にアクセスするようになります

Passenger を経由することで pid ファイルや log ファイルがカレントディレクトリに生成されるようになります
ちなみにログファイルの名前は --log-file オプションでポート番号は --port で変更できます

Passenger のカスタマイズ

Passenger をカスタマイズするには Passengerfile.json というファイルを作成しそこに設定を記載することでカスタマイズできます

  • vim Passengerfile.json
{
  "port": 4000,
 "log_file": "awesome.log",
  "environment": "staging"
}

例えば上記の場合だとログファイル名が awesome.log になり Passenger の動作ポートが 4000 番になります
すべての設定は先程紹介したようにオプションでも設定できます

他にも多くの設定があります
すべての設定は公式の Configuration reference
が参考になります

最後に

Sinatra と Passenger を連携させてみました
所謂 Web サーバやリバースプロキシ的な役割になるので Web アプリの前段で何か処理をさせたい場合などに使えると思います
またリクエストのロギングなどを更に詳細にしたい場合などにも使えると思います
Passenger 自体にはローリングアップデートやスロットリングの機能もあるのでそういったことをアプリでしないで Passenger に任すこともできると思います

参考サイト

2020年9月3日木曜日

ohai で macOS 上のデータを参照してみた

概要

ohai は chef の内部で使われているライブラリです
実行中のマシンの情報を取得することができます
今回は ohai 単体で実行してみました

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • ohai 16.4.12

インストール

  • bundle init
  • vim Gemfile
gem "ohai"
  • bundle config path vendor
  • bundle install

システム全体の情報を取得

とりあえず ohai で取得できる情報をすべて取得してみます
かなり長い JSON が表示されるのが確認できると思います

require 'ohai'

system_info = Ohai::System.new
system_info.all_plugins
puts system_info.json_pretty_print

特定のプラグインの情報だけ取得

ohai はプラグインを読み込みそのプラグインによって取得するシステムの情報を決定します
先程紹介した all_plugins はすべてのプラグインを読み込むので結構重たい処理になります
必要なプラグインの情報だけ取得したい場合はプラグイン名を引数に指定することで処理も軽くなります
データ取得後は data からハッシュとして参照することができます

system_info = Ohai::System.new
system_info.all_plugins(['cpu', 'os'])
puts system_info.json_pretty_print
puts system_info.data['os']

プラグインの一覧を表示

ではプラグインの一覧はどうやって参照するかというと一度すべてのプラグインを読み込んだあとはで provides_map を参照します
ここで表示されたプラグイン名を指定することで必要なプラグインだけを使用することができます

require 'ohai'

system_info = Ohai::System.new
system_info.all_plugins
system_info.provides_map.all_plugins.each do |plugin|
  puts plugin.name
end

実行中の OS の表示

特定の情報はプラグインを使用しないでも取得することができます
OS の情報を取得したいだけであれば以下のようにして取得することができます

require 'ohai'

puts Ohai::Mixin::OS.collect_os

最後に

ohai を単体で使ってみました
リソースの使用量も取得できるので簡単な監視ツールとして使うことができると思います
chef だけを使っているとあまり ohai のことを気にすることはありませんが ohai の仕組みを知ることで chef も更に使えるようになると思います

参考サイト

2020年9月2日水曜日

Flask の POST メソッド時のリクエスト JSON に含まれるキーをキャメルケースでもスネークケースでも受け取れるようにする方法

概要

デコレータと正規表現を使って実現しています
正規表現はキャメルケースをスネークケースに変更することを想定しています
一応変換後のスネークケースのキーが存在しない場合にだけ追加するようにしています
また request.json を shallow copy することで request.json の値をデコレータで動的に変更しています

環境

  • macOS 10.15.6
  • Python 3.8.5
    • flask 1.1.2

サンプルコード

import re
import copy
from flask import Flask, request, jsonify

app = Flask(__name__)

def _convert(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

def convert(func):
    def wrapper(*args, **kwargs):
        req = copy.copy(request.json)
        for key, value in req.items():
            new_key = _convert(key)
            if (new_key in req.keys()) is False:
                request.json[new_key] = value
        return func(*args, **kwargs)
    return wrapper

@app.route('/', methods=["POST"])
@convert
def hello_world():
    req = request.json
    print(req)
    return req["user_name"]

# curl -XPOST -H 'content-type: application/json' -d '{"user_name":"hawksnowlog"}' localhost:5000
# curl -XPOST -H 'content-type: application/json' -d '{"userName":"hawksnowlog"}' localhost:5000

request.json にユーザのリクエストと異なるデータが入るので別の変数で管理したほうが良いかもしれないです
また Python 側では必ずスネークケースで受け取るようにしてください

参考サイト

2020年9月1日火曜日

ruby grape 超入門

概要

grape は RESTful な API を構築するためのフレームワークです
DSL を使うことで簡単に RESTful API を構築することができます
今回は基本的に GET/POST/DELETE/PUT の API を grape で構築してみたので紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • grape 1.4.0

インストール

  • bundle init
  • vim Gemfile
gem "grape"
  • bundle config path vendor
  • bundle install

Getting Started

とりあえず超簡単なサンプルを作成してみます

  • vim app.rb
require 'grape'

module Test
  class API < Grape::API
    format :json
    prefix :api

    resource :blog do
      desc 'Return an article.'
      get :article do
        {
          "title" => "title",
          "body" => "body"
        }
      end
    end
  end
end
  • vim config.ru
require './app'

Test::API.compile!
run Test::API
  • bundle exec rackup config.ru

記事を取得するリクエストは以下のようになります

  • localhost:9292/api/blog/article

とりあえずは単純な JSON が返ってくるだけです
ここから POST や DELETE, PUT の API を追加していきます

POST の追加

まずは記事を追加できる機能を作ってみます
本来ならデータベースで管理したほうが良いですが今回はテストなのでメモリ上で管理します

require 'grape'

module Test
  class API < Grape::API
    format :json
    prefix :api
    articles = []

    resource :blog do
      desc 'Return an article.'
      get :article do
        articles
      end

      desc 'Create new article.'
      params do
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      post do
        articles.append({
          title: params[:title],
          body: params[:body]
        })
      end
    end
  end
end

articles という配列を変数として管理し POST 時にその配列に追加していきます
必須パラメータは requires を使います
必要なパラメータ分 requires しタイプとパラメータの説明を付与します
実際に処理をするブロック内でパラメータを参照する場合は params[:title] という感じで取得します

記事を登録するリクエストは以下のようになります

  • curl -XPOST -H 'Content-Type: application/json' -d '{"title":"First article","body":"Today is sunny"}' localhost:9292/api/blog

また記事を GET する API も articles をそのまま返却するようにします

DELETE の追加

次に記事を削除する API を追加します
今回は配列のインデックスで削除できるようにしてみます

require 'grape'

module Test
  class API < Grape::API
    format :json
    prefix :api
    articles = []

    resource :blog do
      desc 'Return an article.'
      get :article do
        articles
      end

      desc 'Create new article.'
      params do
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      post do
        articles.append({
          title: params[:title],
          body: params[:body]
        })
      end

      desc 'Delete an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
      end
      delete ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles.delete_at(params[:index])
      end
    end
  end
end

記事を削除するリクエストは以下のようになります

  • curl -XDELETE localhost:9292/api/blog/0

もし指定のインデックスが存在しない場合には {"error":"Does not found index"} が返ってきます
:index のように URI からパラメータを受け取る場合には delete ':index' という感じで引数に指定します
こうすることでブロック内で params[:index] として値を受け取ることができるようになります

PUT の追加

最後に更新の API を作ってみましょう
これまで実装してきた POST や DELETE の機能を組み合わせる感じになります

require 'grape'

module Test
  class API < Grape::API
    format :json
    prefix :api
    articles = []

    resource :blog do
      desc 'Return an article.'
      get :article do
        articles
      end

      desc 'Create new article.'
      params do
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      post do
        articles.append({
          title: params[:title],
          body: params[:body]
        })
      end

      desc 'Delete an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
      end
      delete ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles.delete_at(params[:index])
      end

      desc 'Update an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      put ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles[params[:index]] = {
          title: params[:title],
          body: params[:body]
        }
      end
    end
  end
end

PUT のリクエストは以下のようになります

  • curl -XPUT -H 'Content-Type: application/json' -d '{"title":"Third article","body":"It is cloudy"}' localhost:9292/api/blog/0

DELETE と同様で index がない場合にはエラーにしています

helpers メソッドを使う

helpers メソッドはブロック内に定義したメソッドを各 API で使うことができる機能です
各 API での共通処理などを記述しておきます
例えば認証機能などを各 API に付与したい場合には以下のように使えます

require 'grape'

module Test
  class API < Grape::API
    format :json
    prefix :api
    articles = []
    @username = ""

    http_basic do |username, password|
      @username = username
    end

    helpers do
      def authenticate
        error!('Unauthorized', 401) unless @username == "admin"
      end
    end

    resource :blog do
      desc 'Return an article.'
      get :article do
        authenticate
        articles
      end
    end
  end
end

ベーシック認証で特定のユーザ名やパスワードでないとアクセスできないようになります
helpers 内で変数を参照する場合はインスタンス変数にしておきましょう

最後に

grape を使って簡単な RESTful API を構築してみました
DSL なので初めは少し戸惑うかもしれませんが慣れれば簡単に使えると思います
RESTful API に必要な基本的なヘッダ処理 (Content-Type や Method) などのハンドリングがすでに実装されているのは嬉しい点かなと思います

まだ使い込んでないので何とも言えませんが REST 専用のフレームワークっぽいので HTML などを返却する場合には Rails や Sinatra などを使ったほうが良いかもしれません
ただ grape には swagger と連携したりRails と連携して REST を構築したりできる拡張機能も充実しているのでサードパーティ系のエコシステムが充実していそうな感じもします

参考サイト

2020年8月31日月曜日

Ruby で macOS アプリが作れる Shoes を試してみた

概要

Ruby で macOS アプリが開発できる shoes を試してみました
かなり簡単に GUI アプリが作成できます

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • shoes 3.3

インストール

公式だとインストーラを使う方法が推奨されています
macOS の場合はここからダウンロードできます

執筆時点では shoes-3.3.7-osx-10.10.tgz というファイルがダウンロードできました
ダウンロードした圧縮ファイルを解凍すると Shoes.app というファイルが解凍されるのでそれを Applications 配下に移動すればインストールは完了です

起動すると以下のような管理 GUI が立ち上がります

サンプルアプリを作成してみる

とりあえず動作するアプリを作成してみます

  • vim app.rb
Shoes.app do
  background "#DFA"
  para "Welcome to Shoes"
end

あとは Shoes.app から「Run on App」で app.rb を選択すれば OK です

ボタンを設置する

トライ&エラーできる環境が整ったら開発していきます
まずはボタンを設置してみましょう
先程のサンプルに button を追加するだけです

Shoes.app do
  background "#DFA"
  para "Welcome to Shoes"
  button "Push me"
end

これで起動するとボタンが追加されているのが確認できます

ボタンが押されたらテキストを表示する

次にボタンのイベントハンドリングをしてみます
やり方は簡単で button によって作成されたオブジェクトに対して .click メソッドをコールするだけです

Shoes.app do
  background "#DFA"
  @t1 = para "Welcome to Shoes"
  b1 = button "Push me"
  b1.click {
    @t1.replace "Aha! Click!"
  }
end

また para で作成したテキストフィールドも @t1 でインスタンス変数化しておきそれを click のブロック内で .replace することで文字を変更しています

テキストボックスを置いてみる

テキストボックスは edit_line を使います

Shoes.app do
  background "#DFA"
  stack do
    @t1 = para "Welcome to Shoes"
    @el1 = edit_line
    b1 = button "Push me"
    b1.click {
      @t1.replace @el1.text
    }
  end
end

edlt_line に対して text メソッドを使うことで入力中のテキストを取得できます

その他コンポーネントも充実

他にも図形を描いたり図を挿入できたりリンクを使えたりといろいろと機能があります
興味があればチュートリアルの続きをやれば使い方の基本はだいたい学べるかなと思います

配布用のアプリとしてビルドするには

Shoes.app のトップに「Package an App with Shoes」があるので選択します
そして作成した Ruby ファイルを選択しビルドするプラットフォームを選択すれば OK です
「Select Architecture」をクリックするとプラットフォームの一覧が表示されます
「I want advanced install options」を選択すると配布時のアプリのアイコンなども設定できます

ビルドが完了すると tgz の圧縮ファイルが作成されるのでそれを解凍すると .app ファイルがあるのでこれをクリックするだけでアプリが起動するようになります

最後に

macOS アプリを Ruby で開発できる Shoes を使ってみました
かなり簡単に開発できるので Xcode や Swift の使い方を知らない人でも簡単に macOS アプリが開発できるかなと思います
ドキュメントもかなり見やすく充実しているのでわからないことがあったらドキュメントを検索すれば解決するかなと思います

参考サイト