purescript-routing-duplex
Unified parsing and printing for routes in PureScript
Simple bidirectional parser/printers for your routing data types.
README見ればだいたい分かる
例
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ってそういう意味か

イメージ的にはAesonの、Jsonとの相互変換と同じ感じかな

Routingを定義する
Recordで定義する
同義の書き換えがいくつかあって若干ややこしいがreadme見ればわかる
基本的にはもっとも洗練したやつで書くだろうからさほど問題にはならないだろう
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
型だよ、的な
例を見ればほぼ説明がなくても理解できる

recordにすることでtype-level演算子ではなくなっっているのだな

/user/mrsekut/post/1/hoge/2
みたいに連鎖するときはどう書くの?
productをネストするの?
productは良い感じに書き換えられる
普通に書いた場合
purs(hs) , "Post": G.product -- /user/mrsekut/post/1
(path "user" (string segment))
(path "post" (int segment))
こう書ける
purs(hs)import Routing.Duplex.Generic.Syntax ((/))
..
, "Post": "user" / segment / "post" / int segment -- /user/mrsekut/post/1
/
が使える
string segment
の string
は省略できる
path
も省略できる
paramsも良い感じに書き換えられる
purs(hs)route = root $ sum
{ ...
, "Feed": path "feed" (record # _search := optional (param "search"))
}
where
_search = Proxy :: Proxy "search"
最終的にこう書ける
purs(hs) , "Feed": "feed" ? { search: optional <<< string }
paramsの取りうる型も型安全にできる
e.g. ?sort=asc
と ?sort=desc
のいずれかしか許さん
ここは独自に相互変換する関数を用意しておく必要がある
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のやつ
ここの説明だけ雑すぎる

型
purs(hs)data RouteDuplex i o = RouteDuplex (i -> RoutePrinter) (RouteParser o)
type RouteDuplex' a = RouteDuplex a a
型を見れば分かる通り、input→ RoutePrinter
と RouteParser
→outputの
input, outputの部分を引数に取るが、
普通はこれは同じ型なので、エイリアスとして '
付きの型が提供されている
基本的には '
の方を使う