purescript-routing-duplex
Unified parsing and printing for routes in PureScript
Simple bidirectional parser/printers for your routing data types.
README見ればだいたい分かる
例
code:purs(hs)
data Route
= Root
| Profile Username
| Post Username PostId
| Feed { search :: Maybe String, sorting :: Maybe Sort }
route :: RouteDuplex' Route
route = root $ G.sum
{ "Root" : G.noArgs -- /
, "Profile": "user" / username -- e.g. /user/mrsekut
, "Post" : "user" / username / "post" / postId -- e.g. /user/mrsekut/post/1
, "Feed" : "feed" ? { search : optional <<< string -- e.g. /feed?search=purescript&sorting=asc
, sorting: optional <<< sort
}
}
実際はusernameの定義など20行ぐらい他に書いているがcoreとなる部分はこんな感じ
/や?などの演算子をうまく使ってめちゃくちゃ良い感じに定義できるのが楽しい
with Halogen
haskell cake実装
realworldのminimal実装
Why?のところいまいち何を言っているかわからん
2つの問題と解決する
pathの文字列を解析して、PursのData型に変換する
これがparse
Pursデータ型を、path文字列として出力する
これがprint
bidirectionalってそういう意味かmrsekut.icon
イメージ的にはAesonの、Jsonとの相互変換と同じ感じかなmrsekut.icon
Routingを定義する
Recordで定義する
同義の書き換えがいくつかあって若干ややこしいがreadme見ればわかる
基本的にはもっとも洗練したやつで書くだろうからさほど問題にはならないだろう
code:purs(hs)
module Router where
import Prelude
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
import Routing.Duplex (RouteDuplex', path, root, segment, string, parse, print)
import Routing.Duplex.Generic as G
data Route = Home | Profile String
derive instance Generic Route _
instance Show Route where
show = genericShow
route :: RouteDuplex' Route
route = root $ G.sum
{ "Home": G.noArgs -- "/"
, "Profile": path "profile" (string segment) -- "/profile/jake-delhome"
}
a = parse route "/profile/jake-delhomme" -- Right (Profile "jake-delhomme")
b = print route $ Profile "jake-delhomme"-- "/profile/jake-delhomme"
Recordのfield名の箇所("Profile"のところ)が、Route型に対応する
見た目は文字列だが、ここもしっかり型安全になっている
気持ちとしては、parser combinatorと同じ
rootは、pathの最初の/にmatchする
文字列をparseしても良い感じに型変換できる
/user/1ならUser Int型だよ、的な
例を見ればほぼ説明がなくても理解できるmrsekut.icon
recordにすることでtype-level演算子ではなくなっっているのだなmrsekut.icon
/user/mrsekut/post/1/hoge/2みたいに連鎖するときはどう書くの?
productをネストするの?
productは良い感じに書き換えられる
普通に書いた場合
code:purs(hs)
, "Post": G.product -- /user/mrsekut/post/1
(path "user" (string segment))
(path "post" (int segment))
こう書ける
code:purs(hs)
import Routing.Duplex.Generic.Syntax ((/))
..
, "Post": "user" / segment / "post" / int segment -- /user/mrsekut/post/1
/が使える
string segmentのstringは省略できる
pathも省略できる
paramsも良い感じに書き換えられる
code:purs(hs)
route = root $ sum
{ ...
, "Feed": path "feed" (record # _search := optional (param "search"))
}
where
_search = Proxy :: Proxy "search"
最終的にこう書ける
code:purs(hs)
, "Feed": "feed" ? { search: optional <<< string }
paramsの取りうる型も型安全にできる
e.g. ?sort=ascと?sort=descのいずれかしか許さん
ここは独自に相互変換する関数を用意しておく必要がある
code:purs(hs)
data Sort = Asc | Desc
derive instance genericSort :: Generic Sort _
sortToString :: Sort -> String
sortToString = case _ of
Asc -> "asc"
Desc -> "desc"
sortFromString :: String -> Either String Sort
sortFromString = case _ of
"asc" -> Right Asc
"desc" -> Right Desc
val -> Left $ "Not a sort: " <> val
CRUD用のpathも作っておけば上のやつに対して同一ルールでpatuの定義ができる
例えば/userに対して使えば、/user/editならupdateみたいな
hashとpush Stateのやつ
ここの説明だけ雑すぎるmrsekut.icon
型
code:purs(hs)
data RouteDuplex i o = RouteDuplex (i -> RoutePrinter) (RouteParser o)
type RouteDuplex' a = RouteDuplex a a
型を見れば分かる通り、input→RoutePrinterとRouteParser→outputの
input, outputの部分を引数に取るが、
普通はこれは同じ型なので、エイリアスとして'付きの型が提供されている
基本的には'の方を使う