acolyte-core-0.1.0.0: src/Acolyte/Core/Path.hs
-- | Type-level path encoding using promoted lists and GHC.TypeLits.
--
-- Paths are type-level lists of 'PathSegment's. Literal segments use
-- 'Symbol' from GHC.TypeLits directly — no marker types, no TH, no
-- proc macros. This is one of the main advantages over typeway's Rust
-- implementation, which needs proc macros to work around the absence
-- of const generic strings.
--
-- @
-- type UsersPath = '[ Lit "users" ]
-- type UserByIdPath = '[ Lit "users", Capture Int ]
-- type PostsPath = '[ Lit "users", Capture Int, Lit "posts", Capture Int ]
-- @
module Acolyte.Core.Path
( -- * Path segment kind
PathSegment (..)
-- * Type families
, Captures
, CapturesTuple
, CountCaptures
-- * Re-exports for convenience
-- * Path construction helpers
, At
, Param
, At2
, Param2
, ParamNamed
-- * Re-exports for convenience
, Symbol
, Nat
, Type
) where
import Data.Kind (Type)
import Data.Text (Text)
import GHC.TypeLits (Symbol, Nat, type (+))
-- | A segment in a URL path, used at the type level.
--
-- * @Lit s@ matches the literal string @s@ (e.g., @Lit "users"@).
-- * @Capture t@ captures a path segment parsed as type @t@ (e.g., @Capture Int@).
-- * @CaptureRest@ captures all remaining segments as @[Text]@.
data PathSegment
= Lit Symbol -- ^ Literal path segment
| Capture Type -- ^ Captured path parameter
| CaptureNamed Symbol Type -- ^ Captured path parameter with a name
| CaptureRest -- ^ Wildcard tail capture
-- | Extract the list of captured types from a path.
--
-- This is a closed type family — evaluated top-to-bottom by GHC with
-- no backtracking. O(n) in path length, no recursive instance resolution.
--
-- @
-- Captures '[] ~ '[]
-- Captures '[ Lit "users" ] ~ '[]
-- Captures '[ Lit "users", Capture Int ] ~ '[Int]
-- Captures '[ Capture Int, Capture String ] ~ '[Int, String]
-- @
type Captures :: [PathSegment] -> [Type]
type family Captures (path :: [PathSegment]) :: [Type] where
Captures '[] = '[]
Captures (Lit _ ': rest) = Captures rest
Captures (Capture t ': rest) = t ': Captures rest
Captures (CaptureNamed _ t ': rest) = t ': Captures rest
Captures (CaptureRest ': _) = '[[Text]]
-- | Count the number of captures in a path (for arity checking).
type CountCaptures :: [PathSegment] -> Nat
type family CountCaptures (path :: [PathSegment]) :: Nat where
CountCaptures '[] = 0
CountCaptures (Lit _ ': rest) = CountCaptures rest
CountCaptures (Capture _ ': rest) = 1 + CountCaptures rest
CountCaptures (CaptureNamed _ _ ': rest) = 1 + CountCaptures rest
CountCaptures (CaptureRest ': _) = 1
-- | Convert a type-level list of capture types to a tuple.
--
-- Single captures are unwrapped for ergonomics: @Capture Int@ gives the
-- handler an @Int@, not a @(Int,)@.
--
-- @
-- CapturesTuple '[] ~ ()
-- CapturesTuple '[Int] ~ Int
-- CapturesTuple '[Int, String] ~ (Int, String)
-- CapturesTuple '[Int, String, Bool] ~ (Int, String, Bool)
-- @
type CapturesTuple :: [Type] -> Type
type family CapturesTuple (cs :: [Type]) :: Type where
CapturesTuple '[] = ()
CapturesTuple '[a] = a
CapturesTuple '[a, b] = (a, b)
CapturesTuple '[a, b, c] = (a, b, c)
CapturesTuple '[a, b, c, d] = (a, b, c, d)
CapturesTuple '[a, b, c, d, e] = (a, b, c, d, e)
CapturesTuple '[a, b, c, d, e, f] = (a, b, c, d, e, f)
CapturesTuple '[a, b, c, d, e, f, g] = (a, b, c, d, e, f, g)
CapturesTuple '[a, b, c, d, e, f, g, h] = (a, b, c, d, e, f, g, h)
-- | A single literal path segment. The most common path pattern.
--
-- @
-- type API = '[ Get (At "health") Text ]
-- -- equivalent to: Get '[ 'Lit "health" ] Text
-- @
type At (s :: Symbol) = '[ 'Lit s ]
-- | A literal segment followed by a typed capture.
--
-- @
-- type API = '[ Get (Param "users" Int) (Json User) ]
-- -- equivalent to: Get '[ 'Lit "users", 'Capture Int ] (Json User)
-- @
type Param (s :: Symbol) (t :: Type) = '[ 'Lit s, 'Capture t ]
-- | Two literal segments.
--
-- @
-- type API = '[ Get (At2 "api" "health") Text ]
-- -- equivalent to: Get '[ 'Lit "api", 'Lit "health" ] Text
-- @
type At2 (s1 :: Symbol) (s2 :: Symbol) = '[ 'Lit s1, 'Lit s2 ]
-- | Two literal segments followed by a typed capture.
--
-- @
-- type API = '[ Get (Param2 "api" "users" Int) (Json User) ]
-- @
type Param2 (s1 :: Symbol) (s2 :: Symbol) (t :: Type) = '[ 'Lit s1, 'Lit s2, 'Capture t ]
-- | A literal segment followed by a named typed capture.
--
-- @
-- type API = '[ Get (ParamNamed "userId" "users" Int) (Json User) ]
-- -- equivalent to: Get '[ 'Lit "users", 'CaptureNamed "userId" Int ] (Json User)
-- @
type ParamNamed (name :: Symbol) (s :: Symbol) (t :: Type) = '[ 'Lit s, 'CaptureNamed name t ]