packages feed

acolyte-core-0.1.0.0: src/Acolyte/Core/API.hs

-- | API-level combinators: the completeness check and handler binding.
--
-- The key design decision: APIs are type-level lists of endpoints.
-- The 'Serves' constraint is a type synonym that uses 'CheckArity'
-- to compare the API length against the handler tuple arity via
-- closed type families. No class, no instances, no recursive
-- constraint resolution — O(1) for the compiler.
module Acolyte.Core.API
  ( -- * API specification
    type API
    -- * Completeness check
  , Serves
    -- * Length checking
  , Length
  , TupleArity
  , type (==)
    -- * Arity checking
  , CheckArity
    -- * Type-level list append
  , type (++)
    -- * Custom type errors
  , HandlerMismatchMsg
  ) where

import Data.Kind (Type, Constraint)
import GHC.TypeLits (Nat, type (+), TypeError, ErrorMessage (..))
import Data.Type.Equality (type (==))
import Data.Type.Bool (If)


-- | An API is a type-level list of endpoint types.
--
-- @
-- type UsersAPI =
--   '[ Get  '[ Lit "users" ]                (Json [User])
--    , Get  '[ Lit "users", Capture Int ]    (Json User)
--    , Post '[ Lit "users" ] (Json CreateUser) (Json User)
--    ]
-- @
type API = [Type]


-- | Compute the length of a type-level list.
type Length :: [k] -> Nat
type family Length (xs :: [k]) :: Nat where
  Length '[]       = 0
  Length (_ ': xs) = 1 + Length xs


-- | Compute the arity of a handler tuple.
-- Covers arities 0–25 (matching 'BuildServer' coverage).
type TupleArity :: Type -> Nat
type family TupleArity h :: Nat where
  TupleArity ()                 = 0
  TupleArity (a, b)             = 2
  TupleArity (a, b, c)          = 3
  TupleArity (a, b, c, d)       = 4
  TupleArity (a, b, c, d, e)    = 5
  TupleArity (a, b, c, d, e, f) = 6
  TupleArity (a, b, c, d, e, f, g) = 7
  TupleArity (a, b, c, d, e, f, g, h) = 8
  TupleArity (a, b, c, d, e, f, g, h, i) = 9
  TupleArity (a, b, c, d, e, f, g, h, i, j) = 10
  TupleArity (a, b, c, d, e, f, g, h, i, j, k) = 11
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l) = 12
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m) = 13
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n) = 14
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) = 15
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) = 16
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) = 17
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) = 18
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) = 19
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) = 20
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) = 21
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) = 22
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w) = 23
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x) = 24
  TupleArity (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y) = 25
  TupleArity _                  = 1  -- bare value = single handler


-- | Type-level list append.
--
-- Used to combine sub-APIs into a larger API:
--
-- @
-- type FullAPI = UsersAPI ++ ArticlesAPI ++ CommentsAPI
-- @
type family (xs :: [k]) ++ (ys :: [k]) :: [k] where
  '[]       ++ ys = ys
  (x ': xs) ++ ys = x ': (xs ++ ys)
infixr 5 ++


-- | Custom error message for handler/endpoint count mismatch.
type HandlerMismatchMsg :: Nat -> Nat -> ErrorMessage
type family HandlerMismatchMsg (nEndpoints :: Nat) (nHandlers :: Nat) :: ErrorMessage where
  HandlerMismatchMsg n m =
    'Text "API has " ':<>: 'ShowType n ':<>: 'Text " endpoint(s) but "
    ':<>: 'ShowType m ':<>: 'Text " handler(s) were provided."
    ':$$: 'Text "Every endpoint must have exactly one handler."


-- | Verify that the API endpoint count matches the handler count.
-- Produces a clear 'TypeError' on mismatch instead of a confusing
-- GHC constraint error.
type CheckArity :: Nat -> Nat -> Constraint
type family CheckArity (apiLen :: Nat) (handlerCount :: Nat) :: Constraint where
  CheckArity n n = ()
  CheckArity n m = TypeError (HandlerMismatchMsg n m)


-- | The API completeness check.
--
-- @Serves api handlers@ holds when the handler tuple arity matches
-- the number of endpoints in the API list. If they don't match,
-- produces a clear compile error:
--
-- @
-- API has 4 endpoint(s) but 3 handler(s) were provided.
-- Every endpoint must have exactly one handler.
-- @
--
-- This is a constraint synonym — no class, no instances, no
-- overlapping pragmas. 'CheckArity' does the work via a closed
-- type family that reduces to @()@ on match or 'TypeError' on
-- mismatch.
type Serves (api :: [Type]) (handlers :: Type) =
  CheckArity (Length api) (TupleArity handlers)