Madogiwa Blog

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

自作フレームワークで動的URLに対応したときの方針とか考えたこと📝

github.com

/hoge/1/fuga/2にGETでアクセスしたときに下記のロジックを実行させたいときにどうするかちょっと悩んだので考えたこととかメモしておく。

router.get '/hoge/:hoge_id/fuga/:fuga_id' do
  request.params['id']
end

今までは完全一致でパスの検証をしていたので、idみたいな動的に変更されるURLだとid単位にroutingを用意しないと行けない感じでした😭

動的項目を表すプレフィックスをどうするか

今回はsinatrarailsに倣って:を動的項目を表すプレフィックスにしました。

get '/hello/:name' do
  # "GET /hello/foo" と "GET /hello/bar" にマッチ
  # params['name'] は 'foo' か 'bar'
  "Hello #{params['name']}!"
end

http://sinatrarb.com/intro-ja.html

get '/patients/:id', to: 'patients#show'

Rails のルーティング - Railsガイド

URLの中の動的項目をどう判定するか

今回は/でsplitして動的項目だけリクエストのときにきたURLに書き換えて一致しているか判断するような方針をとりました。

例えば/hoge/1/fuga/2/hoge/:hoge_id/fuga/:fuga_idをチェックするさいにhoge, 1, fuga, 2hoge, :hoge_id, fuga, :fuga_idで動的項目の値を書き換えて一致していることを確認するようにしました。(これでいけるはずと思ってますけど、なんか漏れとかありそう・・・)

      def dynamic_indx
        @dynamic_indx ||= path.split('/').map.with_index do |val, i|
          i if val.include?(DYNAMIC_PREFIX)
        end.compact
      end

      attr_reader :path, :process, :method
      def path_match?(path)
        request_path, route_path = [path, self.path].map(&->(array) { array.split('/') })
        request_path.each.with_index do |val, i|
          route_path[i] = val if dynamic_indx.include?(i)
        end
        route_path == request_path
      end

※今は出来てないけど、この辺ちょっと動的項目の一意性を担保(hoge/:hoge_id/fuga/:fuga_idはNG)するのと、2回連続で動的項目を設定不可(hoge/:hoge_id/:fuga_id)とかはチェックするようにしたほうが良いかもですね・・・!

ちなみにsinatraだとMustermannという正規表現プレフィックスを元に一致チェックと行えるやつを使ってるみたいですね👀

github.com

if '/foo/bar' =~ Mustermann.new('/foo/*')
  puts 'it works!'
end

case 'something.png'
when Mustermann.new('foo/*') then puts "prefixed with foo"
when Mustermann.new('*.pdf') then puts "it's a PDF"
when Mustermann.new('*.png') then puts "it's an image"
end

pattern = Mustermann.new('/:prefix/*.*')
pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }

railsはどうやってるのかは見てないですが、後で時間があるときに見てみようかと思います。

動的項目をrequestのparamとして受け取る

これはrouteにurl_argsというインスタンス変数を追加して、{ 動的項目 => 値 }の要はhashを設定するようにしました。 そしてそれをrequestparamsにmergeするようにしました。

module Makanai
  class Application
    def execute_route
      route = router.bind!(url: request.url, method: request.method)
      # NOTE: merge dynamic url params (ex. /resources/1 -> { 'id' => '1' })
      @request.params.merge!(route.url_args)
      route.process.call(request)


module Makanai
  class Router
      def build_url_args(path:)
        request_path, route_path = [path, self.path].map(&->(array) { array.split('/') })
        dynamic_indx.each do |i|
          @url_args.merge!({ route_path[i].gsub(DYNAMIC_PREFIX, '') => request_path[i] })
        end
      end

でもrequestのオブジェクトをrouteのインスタンス変数で上書きするのは、ちょっとイマイチな気もしていたりするけど、railsとかsinatraに合わせておいたほうがいいかなと。。。

おわりに

今回はオレオレWebフレームワークを動的URLに対応させるときに個人的に考えていたことをメモしました。 DSL周りの考え方とか、paramsのような跨って使うようなobjectをどこで修正してどうやって処理に引き継がせるかとか、判断基準とか難しいけど、勉強になりますね💪

オレオレフレームワークの全体像が気になる方はこちらからコードをすべて読むことが出来ます。

github.com