packages feed

uri-templater-1.0.0: src/Network/URI/Template/Error.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Error types for URI template parsing and rendering.

This module provides structured error types that can be programmatically
inspected and pattern-matched, unlike the previous Doc AnsiStyle approach.
-}
module Network.URI.Template.Error (
  -- * Parse Errors
  ParseError (..),
  displayParseError,
  displayParseErrorSimple,

  -- * Render Errors
  RenderError (..),
  displayRenderError,
) where

import Control.Exception (Exception)
import qualified Data.Text as T
import GHC.Generics (Generic)


-- | Errors that can occur when parsing a URI template.
data ParseError
  = -- | The parser encountered an unexpected character at a specific position
    UnexpectedCharacter
      { errorPosition :: !Int
      -- ^ Position in the input string where the error occurred
      , errorChar :: !Char
      -- ^ The unexpected character
      , errorContext :: !String
      -- ^ Additional context about what was expected
      }
  | -- | The template expression was not properly terminated
    UnterminatedExpression
      { errorPosition :: !Int
      -- ^ Position where the expression started
      }
  | -- | An invalid modifier character was used
    InvalidModifier
      { errorPosition :: !Int
      -- ^ Position of the invalid modifier
      , errorChar :: !Char
      -- ^ The invalid modifier character
      }
  | -- | A variable name was malformed
    MalformedVariable
      { errorPosition :: !Int
      -- ^ Position of the malformed variable
      , errorVariable :: !String
      -- ^ The malformed variable name
      }
  | -- | An invalid max length specification
    InvalidMaxLength
      { errorPosition :: !Int
      -- ^ Position of the max length specification
      , errorLengthSpec :: !String
      -- ^ The invalid length specification
      }
  | -- | Generic parse error (fallback for non-specific errors)
    GenericParseError
      { errorMessage :: !String
      -- ^ Error message
      }
  deriving (Show, Eq, Generic)


instance Exception ParseError


-- | Display a parse error with full context, including source location
-- and surrounding text for better error messages.
displayParseError :: String -> ParseError -> T.Text
displayParseError source err = case err of
  UnexpectedCharacter pos ch ctx ->
    T.pack $
      unlines
        [ "Parse error at position " ++ show pos ++ ":"
        , showSourceContext source pos
        , "Unexpected character '" ++ [ch] ++ "'"
        , ctx
        ]
  UnterminatedExpression pos ->
    T.pack $
      unlines
        [ "Parse error at position " ++ show pos ++ ":"
        , showSourceContext source pos
        , "Unterminated expression: expected '}'"
        ]
  InvalidModifier pos ch ->
    T.pack $
      unlines
        [ "Parse error at position " ++ show pos ++ ":"
        , showSourceContext source pos
        , "Invalid modifier character '" ++ [ch] ++ "'"
        , "Valid modifiers are: + # . / ; ? & ="
        ]
  MalformedVariable pos var ->
    T.pack $
      unlines
        [ "Parse error at position " ++ show pos ++ ":"
        , showSourceContext source pos
        , "Malformed variable name: " ++ var
        ]
  InvalidMaxLength pos spec ->
    T.pack $
      unlines
        [ "Parse error at position " ++ show pos ++ ":"
        , showSourceContext source pos
        , "Invalid max length specification: " ++ spec
        , "Expected a positive integer"
        ]
  GenericParseError msg ->
    T.pack $ "Parse error: " ++ msg


-- | Display a parse error in a simple format without source context.
displayParseErrorSimple :: ParseError -> T.Text
displayParseErrorSimple err = case err of
  UnexpectedCharacter pos ch ctx ->
    T.pack $ "Unexpected character '" ++ [ch] ++ "' at position " ++ show pos ++ ": " ++ ctx
  UnterminatedExpression pos ->
    T.pack $ "Unterminated expression at position " ++ show pos
  InvalidModifier pos ch ->
    T.pack $ "Invalid modifier '" ++ [ch] ++ "' at position " ++ show pos
  MalformedVariable pos var ->
    T.pack $ "Malformed variable '" ++ var ++ "' at position " ++ show pos
  InvalidMaxLength pos spec ->
    T.pack $ "Invalid max length '" ++ spec ++ "' at position " ++ show pos
  GenericParseError msg ->
    T.pack msg


-- | Show source context around an error position
showSourceContext :: String -> Int -> String
showSourceContext source pos =
  let
    contextSize = 40
    start = max 0 (pos - contextSize)
    end = min (length source) (pos + contextSize)
    before = take (pos - start) $ drop start source
    after = take (end - pos) $ drop pos source
    caretPos = length before
  in
    unlines
      [ "  " ++ before ++ after
      , "  " ++ replicate caretPos ' ' ++ "^"
      ]


-- | Errors that can occur when rendering a URI template.
data RenderError
  = -- | A required variable was not provided in the bindings
    MissingVariable
      { renderVarName :: !T.Text
      -- ^ The name of the missing variable
      }
  | -- | A variable had an incompatible value type for the template context
    InvalidValueType
      { renderVarName :: !T.Text
      -- ^ The name of the variable
      , renderTypeError :: !String
      -- ^ Description of the type mismatch
      }
  deriving (Show, Eq, Generic)


instance Exception RenderError


-- | Display a render error in a human-readable format.
displayRenderError :: RenderError -> T.Text
displayRenderError err = case err of
  MissingVariable var ->
    "Missing required variable: " <> var
  InvalidValueType var typeErr ->
    "Invalid value type for variable '" <> var <> "': " <> T.pack typeErr