packages feed

servant-0.20.3.0: src/Servant/API/Range.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Servant.API.Range (Range (unRange), unsafeRange, mkRange) where

import           Data.Aeson
import           Data.Aeson.Types (modifyFailure)
import           Data.Bifunctor   (first)
import           Data.Ix
import           Data.Proxy       (Proxy (Proxy))
import qualified Data.Text        as T
import           GHC.Generics     (Generic)
import           GHC.TypeLits
import           Servant.API

-- | A newtype wrapper around 'Natural' that ensures the value is within a given range.
--
-- Example:
--
-- >>> :{
--   let validRange = mkRange 5 :: Maybe (Range 1 10)
--   in case validRange of
--        Just r  -> "Valid range: " ++ show (unRange r)
--        Nothing -> "Invalid range"
-- :}
-- "Valid range: 5"
--
-- >>> :{
--   let invalidRange = mkRange 15 :: Maybe (Range 1 10)
--   in case invalidRange of
--        Just r  -> "Valid range: " ++ show (unRange r)
--        Nothing -> "Invalid range"
-- :}
-- "Invalid range"
--
-- >>> decode "5" :: Maybe (Range 1 10)
-- Just (MkRange {unRange = 5})
--
-- >>> decode "15" :: Maybe (Range 1 10)
-- Nothing
newtype Range (min :: Nat) (max :: Nat) = MkRange {unRange :: Natural}
    deriving stock (Eq, Ord, Show, Generic)
    deriving newtype (Ix, ToJSON, ToHttpApiData)

unsafeRange :: Natural -> Range min max
unsafeRange = MkRange

instance (KnownNat min, KnownNat max) => Bounded (Range min max) where
    minBound = MkRange . fromInteger $ natVal (Proxy @min)
    maxBound = MkRange . fromInteger $ natVal (Proxy @max)

parseErrorMsg :: forall min max. (KnownNat min, KnownNat max) => Proxy (Range min max) -> String
parseErrorMsg _ =
    "Expecting a natural number between " <> show (natVal (Proxy @min)) <> " and " <> show (natVal (Proxy @max)) <> "."

mkRange :: forall min max. (KnownNat min, KnownNat max) => Natural -> Maybe (Range min max)
mkRange n
    | inRange (minBound :: Range min max, maxBound :: Range min max) (MkRange n) = Just (MkRange n)
    | otherwise = Nothing

instance (KnownNat min, KnownNat max) => FromJSON (Range min max) where
    parseJSON v = do
        n <- modifyFailure (const $ parseErrorMsg @min @max Proxy) $ parseJSON v
        maybe (fail $ parseErrorMsg @min @max Proxy) pure $ mkRange n

instance (KnownNat min, KnownNat max) => FromHttpApiData (Range min max) where
    parseQueryParam v = do
        n <- first (const . T.pack $ parseErrorMsg @min @max Proxy) $ parseQueryParam v
        maybe (Left . T.pack $ parseErrorMsg @min @max Proxy) Right $ mkRange n