RubyでオレオレWebフレームワークを作っているのですが、だいぶ形になってきたので知識の整理がてら色々まとめてみる✍
今回は第二回目です✌️
👇第1回 Hello World 🌏はこちら https://madogiwa0124.hatenablog.com/entry/2019/08/25/181608
今回のゴール
今回は、GET
リクエストに対応したRouting機能を作ります👩💻
下記のように定義したら/hello
にアクセス時に'Hello My Framework.'
が表示されるように出来るような機能を作ります。
require_relative './lib/main.rb' router.get '/hello' do 'Hello My Framework.' end
処理のイメージはこんな感じです👀
- WEBrickからリクエスト(URL、METHOD)を取得
app
でmain
で定義したDSLを使って作成したrouter
で管理しているroutingをリクエストのURLとMETHODから探索- 見つかったroutingのProcオブジェクトを返却
application
でProcオブジェクトを実行- 結果からレスポンスを生成
- WEBrickにレスポンスを返却
まずはRoutingを行うClassを作る
Routingを管理するClassRouter
を作っていきます。
Router
の役割は2つです。
* get
メソッドでroutes
にroutingを追加する。
* bind!
メソッドでroutesをURLとMETHODから検索してRouteのオブジェクトを返す。
👇実装はこんな感じ
# frozen_string_literal: true require 'uri' class Router attr_accessor :routes def initialize @routes = [] end def get(path, &block) @routes << Route.new(path: path, process: block, method: 'GET') end def bind!(url:, method:) # urlのpathの部分だけ取得 ex. `http://localhost:8080/hello` -> `/hello` path = URI.parse(url).path routes.find { |route| route.path == path && route.method == method } end class Route def initialize(path:, process:, method:) @path = path @process = process @method = method end attr_reader :path, :process, :method end end
get
メソッドでblock引数をとっているので、下記のような形で呼び出されると、
router = Router.new router.get '/hello' do 'Hello My Framework.' end
👇下記のようなオブジェクトが返却されます。routerはこれの配列をroutes
に保持しているので、bind!
メソッドを使ってroutes
をmethodとurlで検索して該当のロジックを返却する仕組みです🕵️
#<Router::Route:0x00007ff3c68528e0 @method="GET", @path="/hello", @process=#<Proc:0x00007ff3c6852930@app.rb:10>>,
DSLをmainに定義してみる
Router
クラスを作りましたが今のままだと良い感じに使えないので、mainにメソッドを追加して良い感じ定義出来るようにします。
# frozen_string_literal: true require_relative './router.rb' require_relative './application.rb' def router @router ||= Router.new end # `Application`のオブジェクトを生成する際に`router`を渡すようにしています。 at_exit { Application.new(router: @router).run! }
実装はシンプルで、router
メソッドを定義して、その中でRouter
のオブジェクトを返しているだけです。
これでapp.rbでrouter.get
を使ってroutingを定義出来るようになりました🎉
require_relative './lib/main.rb' router.get '/hello' do 'Hello My Framework.' end
Requestからroutesを探索してResponseを返す
今回からちょっとロジックが増えてきたので、router
を引数で取りたいので、Proc
を使うのではなく、独自のApplication
クラスを作っていきます。
第一回で説明したとおり下記ルールを守れば何でもRack上に乗せられます 🙌
- callメソッドが定義されていること
- 環境(リクエスト)を受け取る引数を一つとること
- status、header、bodyの配列を返却すること
👇実際に実装したClassが下記です。
require 'rack' class Application def initialize(router:) @router = router end attr_reader :router def call(env) body = router.bind(url: env['REQUEST_URI'], method: env['REQUEST_METHOD']).process.call ['200', {'Content-Type' => 'text/html'}, [body]] end def run! Rack::Handler::WEBrick.run self end end
やっていることは、env
でWEBrick
から受け取ったリクエスト(env
)をもとにrouter.bind
でroutes
から該当するroute
を取得して、app.rb
で定義したblock内の処理を実行して、結果をレスポンスのbodyに詰めて返却してます 📦✨
これで/hello
でアクセスしたときにMy Framework.と表示されるようになりました🎊
おわりに
今回はGETとURLのみに対応したRouting機能を作ってみました!今回の実装は結構Sinatraのroutingを結構参考にしています 🎩
Sinatraでは今回のrouter
のようなDSLを提供するためにApplicationに定義したメソッド群をmainに委譲するDelegaterというmoduleを活用していますが、これは結構黒魔術感がある + まだメソッドの数も少ないので今回はmainにそのまま定義していく方法を採用しています 💦
sinatra/base.rb at 070d6db3ce3d064d113b44062b401a8d01bbe248 · sinatra/sinatra · GitHub
気になる人は、👆のコードを読んでみると良いかと思います👀
次回はroutingの仕組みをGETパラメーターに対応する等ブラッシュアップしていくような内容にしていこうかなと思っています(routing周りの話はあと2回ぐらい続く予定です)🛣