packages feed

HaXPath-0.3.0.1: src/HaXPath.hs

{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}

-- | The core module of the XPath-generating DSL. This module should be used as a qualified import.
module HaXPath(
  -- * Basic data types
  IsExpression,
  Showed,
  Bool',
  Bool,
  false,
  true,
  Number',
  Number,
  Text',
  Text,
  text,
  -- * Nodes
  Node',
  Node,
  node,
  namedNode,
  DocumentRoot',
  root',
  DocumentRoot,
  root,
  at,
  -- * Basic combinators
  not,
  (&&.),
  (||.),
  contains,
  doesNotContain,
  Eq,
  (=.),
  (/=.),
  Ord,
  (<.),
  (<=.),
  (>.),
  (>=.),
  position,
  -- * Paths
  CurrentContext,
  RootContext,
  IsContext,
  Context,
  Path',
  Path,
  AbsolutePath',
  AbsolutePath,
  RelativePath',
  RelativePath,
  PathLike,
  show',
  show,
  -- * Axes
  ancestor,
  child,
  descendant,
  descendantOrSelf,
  following,
  followingSibling,
  parent,
  self,
  -- * Path combinators
  SlashOperator(..),
  DoubleSlashOperator(..),
  Filterable(..),
  count,
  (|.)
) where

import           Data.List (intercalate)
import           Data.List.NonEmpty (NonEmpty((:|)))
import           Data.Proxy  (Proxy (Proxy))
import Data.Semigroup (sconcat)
import qualified Data.String as S
import           Prelude     (($), (*), (+), (-), (.), (<$>), (<>), (==))
import qualified Prelude     as P

-- | XPath textual (string) data type, which can be showed as the string type @s@.
newtype Text' s = Text { unText :: Expression s }

-- | 'Text'' specialised so it can be shown as 'P.String'.
type Text = Text' P.String

-- | XPath numeric data type, which can be showed as the string type @s@.
newtype Number' s = Number { unNumber :: Expression s }

-- | 'Number'' specialised so it can be shown as 'P.String'.
type Number = Number' P.String

-- | XPath boolean data type, which can be showed as the string type @s@.
newtype Bool' s = Bool { unBool :: Expression s }

-- | 'Bool'' specialised so it can be shown as 'P.String'.
type Bool = Bool' P.String

-- | XPath @true()@ value.
true :: S.IsString s => Bool' s
true = Bool $ Function "true" []

-- | XPath @false()@ value.
false :: S.IsString s => Bool' s
false = Bool $ Function "false" []

data PathBegin = FromRootContext | FromCurrentContext deriving (P.Eq)

-- Internal data type to represent an XPath expression using the string-like type s.
data Expression s = Function s [Expression s] |
                    -- Apply the named function to zero or more arguments.
                    Operator s (Expression s) (Expression s) |
                    -- Apply a binary operator to the two operands.
                    Attribute s |
                    -- Access the given attribute of the node (@).
                    TextLiteral s |
                    -- Text value in quotes.
                    IntegerLiteral P.Integer |
                    -- Literal integer (XPath number).
                    NamedNode s |
                    -- Select node with the provided name.
                    FilteredNode (Expression s) [Expression s] |
                    LocationStep Axis (Expression s) |
                    -- From current context move along the given axis and select nodes matching the expression.
                    PathFrom PathBegin (Expression s) (P.Maybe (Expression s)) [Expression s]
                    -- From the starting point, take the first path (expression), then follow the next path (expression)
                    -- (if present) and finally filter by zero or more boolean (expressions).

-- | Class of types which can be used to form a valid XPath expression. Library users should not create instances of
-- this class.
class IsExpression a where
  toExpression :: a -> Expression (Showed a)

instance IsExpression (Text' s) where
  toExpression = unText

instance IsExpression (Number' s) where
  toExpression = unNumber

instance IsExpression (Bool' s) where
  toExpression = unBool

showExpression :: (S.IsString s, P.Show s) => Expression s -> [s]
showExpression (Function f es) = [f, "("] <> args <> [")"]
  where
    args = intercalate [", "] $ showExpression <$> es
showExpression (Operator o a b) =
  showOperand a <> [" ", o, " "] <> showOperand b
  where
    showOperand e@(TextLiteral _)    = showExpression e
    showOperand e@(IntegerLiteral _) = showExpression e
    showOperand e@(Function _ _)     = showExpression e
    showOperand e@(Attribute _)      = showExpression e
    showOperand e                    = "(" : showExpression e <> [")"]

showExpression (Attribute a) = ["@", a]
showExpression (TextLiteral t) = [S.fromString $ P.show t]
showExpression (IntegerLiteral i) = [S.fromString $ P.show i]
showExpression (PathFrom begin p pNextMay preds) =
  let prefix = case begin of
        FromRootContext    -> "/"
        FromCurrentContext -> ""
  in
  let showPath x = case x of
        LocationStep _ _ -> showExpression x
        _                -> "(" : showExpression x <> [")"]
  in
  let fullPShowed = prefix : showPath p <> case pNextMay of
        P.Nothing    -> []
        P.Just pNext -> "/" : showPath pNext
  in
  showWithPredicates fullPShowed preds
showExpression (LocationStep axis n) = showAxis axis : ["::"] <> showExpression n
showExpression (NamedNode n) = [n]
showExpression (FilteredNode n preds) = showExpression n <> showPredicates preds

showPredicates :: (S.IsString s, P.Show s) => [Expression s] -> [s]
showPredicates preds =  "[" : intercalate ["]["] (showExpression <$> preds) <> ["]"]

showWithPredicates :: (S.IsString s, P.Show s) => [s] -> [Expression s] -> [s]
showWithPredicates s es
  | P.not (P.null es) = "(" : s <> [")"] <> showPredicates es
  | P.otherwise = s

-- | Display an XPath expression. This is useful to sending the XPath expression to a separate XPath evaluator e.g.
-- a web browser.
show' :: (PathLike p,
          IsExpression p,
          P.Monoid (Showed p),
          S.IsString (Showed p),
          P.Show (Showed p)) =>
          p -> Showed p
show' = sconcat . (P.mempty :|) . showExpression . toExpression

-- | Specialisation of 'show'' to only generate 'P.String's.
show :: (PathLike p, IsExpression p, Showed p ~ P.String) => p -> P.String
show = show'

instance S.IsString s => S.IsString (Text' s) where
  fromString = Text . TextLiteral . S.fromString

boolToInt :: Bool' s -> Number' s
boolToInt (Bool b) = Number b

-- | Access the value of a node's attribute in text form (equivalent to XPath's @\@@).
at :: s -> Text' s
at = Text . Attribute

-- | Type class of XPath types that can be compared for equality. Library users should not create instances of this
-- class.
class IsExpression t => Eq t

instance Eq (Text' s)
instance Eq (Number' s)
instance Eq (Bool' s)

-- | The XPath @=@ operator.
(=.) :: (Eq a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x =. y = Bool $ Operator "=" (toExpression x) (toExpression y)
infix 4 =.

-- | The XPath @!=@ operator.
(/=.) :: (Eq a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x /=. y = Bool $ Operator "!=" (toExpression x) (toExpression y)
infix 4 /=.

-- | Type class of XPath types that can be ordered. Library users should not create instances of this class.
class Eq t => Ord t

instance Ord (Text' s)
instance Ord (Number' s)
instance Ord (Bool' s)

-- | The XPath @<@ operator.
(<.) :: (Ord a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x <. y = Bool $ Operator "<" (toExpression x) (toExpression y)
infix 4 <.

-- | The XPath @<=@ operator.
(<=.) :: (Ord a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x <=. y = Bool $ Operator "<=" (toExpression x) (toExpression y)
infix 4 <=.

-- | The XPath @>@ operator.
(>.) :: (Ord a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x >. y = Bool $ Operator ">" (toExpression x) (toExpression y)
infix 4 >.

-- | The XPath @>=@ operator.
(>=.) :: (Ord a, S.IsString (Showed a)) => a -> a -> Bool' (Showed a)
x >=. y = Bool $ Operator ">=" (toExpression x) (toExpression y)
infix 4 >=.

instance  S.IsString s => P.Num (Number' s) where
  Number x + Number y = Number $ Operator "+" x y

  Number x - Number y = Number $ Operator "-" x y

  Number x * Number y = Number $ Operator "*" x y

  abs x = x * P.signum x

  signum x = boolToInt (x >. 0) - boolToInt (x <. 0)

  fromInteger = Number . IntegerLiteral

-- | The XPath @position()@ function.
position :: S.IsString s => Number' s
position = Number $ Function "position" []

-- | The XPath @text()@ function.
text :: S.IsString s => Text' s
text = Text $ Function "text" []

-- | The XPath @contains()@ function.
contains :: S.IsString s => Text' s -> Text' s -> Bool' s
contains x y = Bool . Function "contains" $ [toExpression x, toExpression y]

-- | The opposite of 'contains'.
doesNotContain :: S.IsString s => Text' s -> Text' s -> Bool' s
doesNotContain x y = not $ contains x y

-- | The XPath @count()@ function.
count :: (IsContext c, S.IsString s) => Path' c s -> Number' s
count p = Number $ Function "count" [toExpression p]

-- | The XPath @and@ operator.
(&&.) :: S.IsString s => Bool' s -> Bool' s -> Bool' s
x &&. y = Bool $ Operator "and" (toExpression x) (toExpression y)
infixr 3 &&.

-- | The XPath @or@ operator.
(||.) :: S.IsString s => Bool' s -> Bool' s -> Bool' s
x ||. y = Bool $ Operator "or" (toExpression x) (toExpression y)
infixr 2 ||.

-- | The XPath @not(.)@ function.
not :: S.IsString s => Bool' s -> Bool' s
not x = Bool $ Function "not" [toExpression x]

data Axis = Ancestor |
            Child |
            Descendant |
            DescendantOrSelf |
            Following |
            FollowingSibling |
            Parent |
            Self

showAxis :: S.IsString s => Axis -> s
showAxis axis = case axis of
  Ancestor         -> "ancestor"
  Child            -> "child"
  Descendant       -> "descendant"
  DescendantOrSelf -> "descendant-or-self"
  Following        -> "following"
  FollowingSibling -> "following-sibling"
  Parent           -> "parent"
  Self             -> "self"

-- | An XPath node which can be showed as the string type @s@.
newtype Node' s = Node { unNode :: Expression s }

-- | 'Node'' specialised so it can be shown as 'P.String'.
type Node = Node' P.String

instance IsExpression (Node' s) where
  toExpression = unNode

-- | An XPath beginning from some context @c@ (either the root context or the current context).
newtype Path' c s = Path { unPath :: Expression s }

-- | 'Path'' specialised so it can be shown as 'P.String'.
type Path c = Path' c P.String

-- | An XPath relative to the current context.
type RelativePath' = Path' CurrentContext

-- | 'RelativePath'' specialised so it can be shown as 'P.String'.
type RelativePath = RelativePath' P.String

-- | An XPath beginning from the document root.
type AbsolutePath' = Path' RootContext

-- | 'AbsolutePath'' specialised so it can be shown as 'P.String'.
type AbsolutePath = AbsolutePath' P.String

-- | Type to indicate the XPath begins from the current context.
data CurrentContext

-- | Type to indicate the XPath begins from the document root.
data RootContext

-- | Class of valid types for the type parameter @c@ in 'Path''. Library users should not create instances of this
-- class.
class IsContext c where
  toPathBegin :: proxy c -> PathBegin

instance IsContext RootContext where
  toPathBegin _ = FromRootContext

instance IsContext CurrentContext where
  toPathBegin _ = FromCurrentContext

instance IsContext c => IsExpression (Path' c s) where
  toExpression = unPath

-- | The XPath @node()@ function.
node :: S.IsString s => Node' s
node = Node $ Function "node" []

-- | Create a node with the given name.
namedNode :: S.IsString s => s -> Node' s
namedNode = Node . NamedNode

-- | Type to represent the root of the document. Useful in forming an XPaths which must begin from the root.
data DocumentRoot' s = DocumentRoot

-- | 'DocumentRoot'' specialised so it can be used in paths to be shown as 'P.String'.
type DocumentRoot = DocumentRoot' P.String

-- | The root of the document. There is no corresponding XPath expression for 'root' but it can be used to indicate that
-- an XPath must be begin from the root by using this as the first step in the path.
root' :: DocumentRoot' s
root' = DocumentRoot

-- | Specialisation of 'root'' so it can be used in paths to be shown as 'P.String'.
root :: DocumentRoot
root = root'

-- | Type family which allows a context to be inferred. This allows for support of abbreviated syntax.
type family Context p where
  Context (Path' c s) = c
  Context (Node' s) = CurrentContext
  Context (DocumentRoot' s) = RootContext

-- | Type family which associates an expression type with the type that will be returned by 'show'' when it is dislayed
-- in XPath syntax. This allows flexiblity to use different string-like types, such as 'P.String', @Text@, @ByteString@
-- or even builders for these types.
type family (Showed p) where
  Showed (Number' s) = s
  Showed (Text' s) = s
  Showed (Bool' s) = s
  Showed (Path' c s) = s
  Showed (Node' s) = s
  Showed (DocumentRoot' s) = s

-- | Constraint for path-like types - i.e. either a 'Path'' or otherwise a type that can be converted to one using
-- abbreviated syntax rules.
type PathLike p = IsContext (Context p)

-- | Type class for the XPath @/@ operator. It can operate on multiple types as the axes can be inferred based on
-- XPath's abbreviated syntax. Library users should not create instances of this class.
class (PathLike p, PathLike q, Showed p ~ Showed q) => SlashOperator p q where
  -- | The XPath @/@ operator.
  (/.) :: p -> q -> Path' (Context p) (Showed q)
  infixl 8 /.

instance IsContext c => SlashOperator (Path' c s) (Path' CurrentContext s) where
  pa /. nextPa = Path $ case toExpression pa of
    PathFrom begin fstPath P.Nothing preds -> PathFrom begin fstPath (P.Just $ toExpression nextPa) preds
    _ -> PathFrom
      (toPathBegin (Proxy :: Proxy c))
      (toExpression $ fromCurrentContext pa)
      (P.Just $ toExpression nextPa)
      []

instance IsContext c => SlashOperator (Path' c s) (Node' s) where
  pa /. n = pa /. child n

instance SlashOperator (Node' s) (Path' CurrentContext s) where
  n /. pa = child n /. pa

instance SlashOperator (Node' s) (Node' s) where
  n /. nextNode = child n /. child nextNode

instance SlashOperator (DocumentRoot' s) (Path' CurrentContext s) where
  DocumentRoot /. p = fromRootContext p

instance SlashOperator (DocumentRoot' s) (Node' s) where
  DocumentRoot /. n = fromRootContext (child n)

-- | Type class for the XPath @//@ operator. It can operate on multiple types as the axes can be inferred based on
-- XPath's abbreviated syntax. Library users should not create instances of this class.
class (PathLike p, PathLike q, Showed p ~ Showed q) => DoubleSlashOperator p q where
  -- | The XPath @//@ operator.
  (//.) :: p -> q -> Path' (Context p) (Showed q)
  infixl 8 //.

instance (IsContext c, S.IsString s) => DoubleSlashOperator (Path' c s) (Path' CurrentContext s) where
  pa //. nextPa = Path $ case toExpression pa of
    PathFrom begin fstPath P.Nothing preds -> PathFrom begin fstPath nextPa' preds
    _ -> PathFrom (toPathBegin (Proxy :: Proxy c)) (toExpression $ fromCurrentContext pa) nextPa' []

    where
      nextPa' = P.Just . toExpression $ descendantOrSelf node /. nextPa

instance (IsContext c, S.IsString s) => DoubleSlashOperator (Path' c s) (Node' s) where
  pa //. n = pa /. descendantOrSelf node /. n

instance S.IsString s => DoubleSlashOperator (Node' s) (Path' CurrentContext s) where
  n //. pa = child n //. pa

instance S.IsString s => DoubleSlashOperator (Node' s) (Node' s) where
  n //. nextNode = child n //. child nextNode

instance S.IsString s => DoubleSlashOperator (DocumentRoot' s) (Path' CurrentContext s) where
  DocumentRoot //. p = fromRootContext (descendantOrSelf node) /. p

instance S.IsString s => DoubleSlashOperator (DocumentRoot' s) (Node' s) where
  DocumentRoot //. n = fromRootContext (descendantOrSelf node /. n)

locationStep :: Axis -> Node' s -> Path' c s
locationStep axis n = Path $ LocationStep axis (toExpression n)

-- | The XPath @ancestor::@ axis.
ancestor :: Node' s -> Path' CurrentContext s
ancestor = locationStep Ancestor

-- | The XPath @child::@ axis.
child :: Node' s -> Path' CurrentContext s
child = locationStep Child

-- | The XPath @descendant::@ axis.
descendant :: Node' s -> Path' CurrentContext s
descendant = locationStep Descendant

-- | The XPath @descendant-or-self::@ axis.
descendantOrSelf :: Node' s -> Path' CurrentContext s
descendantOrSelf = locationStep DescendantOrSelf

-- | The XPath @following::@ axis.
following :: Node' s -> Path' CurrentContext s
following = locationStep Following

-- | The XPath @following-sibling::@ axis.
followingSibling :: Node' s -> Path' CurrentContext s
followingSibling = locationStep FollowingSibling

-- | The XPath @parent::@ axis.
parent :: Node' s -> Path' CurrentContext s
parent = locationStep Parent

-- | The XPath @self::@ axis.
self :: Node' s -> Path' CurrentContext s
self = locationStep Self

changeContext :: PathBegin -> Path' c s -> Path' c' s
changeContext begin (Path p) = Path $ case p of
  PathFrom _ fstPath sndPath preds -> PathFrom begin fstPath sndPath preds
  LocationStep _ _                 -> if begin == FromRootContext then PathFrom begin p P.Nothing [] else p
  other                            -> PathFrom begin other P.Nothing []

fromCurrentContext :: Path' c s -> Path' CurrentContext s
fromCurrentContext = changeContext FromCurrentContext

fromRootContext :: Path' CurrentContext s -> Path' RootContext s
fromRootContext = changeContext FromRootContext

-- | The union of two node-sets.
(|.) :: (PathLike p,
         PathLike q,
         IsExpression p,
         IsExpression q,
         Context p ~ Context q,
         Showed p ~ Showed q,
         S.IsString (Showed q)) =>
         p -> q-> Path' (Context p) (Showed q)
x |. y = Path $ Operator "|" (toExpression x) (toExpression y)
infix 7 |.

-- | Type class to allow filtering of node sets. Library users should not create instances of this class.
class (IsExpression p, PathLike p) => Filterable p where
  -- | Filter the nodes returned by @p@ such that they match the list of predicates.
  (#) :: Showed p ~ s => p -> [Bool' s] -> p
  infixl 9 #

instance IsContext c => Filterable (Path' c s) where
  xp # preds =
    let predExps = toExpression <$> preds in
    Path $ case toExpression xp of
      LocationStep axis (FilteredNode n ps)  -> LocationStep axis (FilteredNode n (ps <> predExps))
      LocationStep axis e                    -> LocationStep axis (FilteredNode e predExps)
      PathFrom begin firstSteps nextSteps ps -> PathFrom begin firstSteps nextSteps (ps <> predExps)
      otherExp                               -> PathFrom (toPathBegin (Proxy :: Proxy c)) otherExp P.Nothing predExps

instance Filterable (Node' s) where
  n # preds =
    let predExps = toExpression <$> preds in
    Node $ case toExpression n of
      FilteredNode nExp ps -> FilteredNode nExp (ps <> predExps)
      otherExp             -> FilteredNode otherExp predExps