packages feed

dojang-0.1.0: src/Dojang/Types/FilePathExpression.hs

{-# LANGUAGE OverloadedStrings #-}

module Dojang.Types.FilePathExpression
  ( EnvironmentVariable
  , FilePathExpression (..)
  , toPathText
  , (++)
  , (+/+)
  ) where

import Data.Char (isAlphaNum, isAscii)
import Data.String (IsString (..))
import Prelude hiding (head, null, (++))

import Data.Hashable (Hashable (hashWithSalt))
import Data.Text (Text, head, null, pack)


-- | An environment variable.
type EnvironmentVariable = Text


-- | An expression that can be used to build a file path.
data FilePathExpression
  = -- | A bare component of a file path.  E.g., @foo@ and @bar@ in @/foo/bar@.
    BareComponent Text
  | -- | The root of a file path.  E.g., @/@ on POSIX systems or @C:\@ on
    -- Windows.  The 'Maybe' 'Char' is the drive letter on Windows, if present.
    -- On POSIX systems, this is always 'Nothing'.
    Root (Maybe Char)
  | -- | A concatenation of two 'FilePathExpression's.  E.g., @${FOO}bar@ is
    -- a concatenation of @${FOO}@ and @bar@.
    Concatenation FilePathExpression FilePathExpression
  | -- | A path separator which combines two 'FilePathExpression's.  E.g.,
    -- @/@ on POSIX systems or @\@ on Windows.
    PathSeparator FilePathExpression FilePathExpression
  | -- | A simple substitution of an environment variable.  E.g., @$FOO@ or
    -- @${BAR}@.
    Substitution EnvironmentVariable
  | -- | A substitution of an environment variable with a default value for when
    -- the environment variable is not set.  E.g., @${FOO:-this value is used if
    -- FOO is not set or is empty}@.
    SubstitutionWithDefault EnvironmentVariable FilePathExpression
  | -- | A conditional substitution of an environment variable with an expression
    -- to be expanded if the environment variable is set.  E.g., @${FOO:+this
    -- value is used if FOO is set and non-empty}@.
    ConditionalSubstitution EnvironmentVariable FilePathExpression
  -- TODO: Add support for SubstringSubstitution (e.g., ${FOO:0:3})
  -- See also https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
  deriving (Eq, Show)


instance Hashable FilePathExpression where
  hashWithSalt salt (BareComponent component) =
    salt `hashWithSalt` (0 :: Int) `hashWithSalt` component
  hashWithSalt salt (Root driveLetter) =
    salt `hashWithSalt` (1 :: Int) `hashWithSalt` driveLetter
  hashWithSalt salt (Concatenation expr1 expr2) =
    salt `hashWithSalt` (2 :: Int) `hashWithSalt` expr1 `hashWithSalt` expr2
  hashWithSalt salt (PathSeparator expr1 expr2) =
    salt `hashWithSalt` (3 :: Int) `hashWithSalt` expr1 `hashWithSalt` expr2
  hashWithSalt salt (Substitution envVar) =
    salt `hashWithSalt` (4 :: Int) `hashWithSalt` envVar
  hashWithSalt salt (SubstitutionWithDefault envVar expr) =
    salt `hashWithSalt` (5 :: Int) `hashWithSalt` envVar `hashWithSalt` expr
  hashWithSalt salt (ConditionalSubstitution envVar expr) =
    salt `hashWithSalt` (6 :: Int) `hashWithSalt` envVar `hashWithSalt` expr


infixl 5 ++, +/+


-- | Concatenates two 'FilePathExpression's.  A synonym for 'Concatenation'.
(++) :: FilePathExpression -> FilePathExpression -> FilePathExpression
(++) = Concatenation
{-# INLINE (++) #-}


-- | Combines two 'FilePathExpression's with a path separator.  A synonym for
-- 'PathSeparator'.
(+/+) :: FilePathExpression -> FilePathExpression -> FilePathExpression
(+/+) = PathSeparator
{-# INLINE (+/+) #-}


-- | Converts a 'FilePathExpression' to a textual representation.
toPathText :: FilePathExpression -> Text
toPathText (Root (Just driveLetter)) = pack [driveLetter, ':', '/']
toPathText expr = toPathText' False expr


-- | Internal implementation of 'toPathText'.
toPathText'
  :: Bool
  -- ^ Whether it is followed by a 'BareComponent'.
  -> FilePathExpression
  -- ^ The 'FilePathExpression' to render.
  -> Text
  -- ^ The rendered 'FilePathExpression'.
toPathText' _ (BareComponent component) = component
toPathText' _ (Root Nothing) = "/"
toPathText' _ (Root (Just driveLetter)) = pack [driveLetter, ':']
toPathText' followedByBare (Concatenation a b) =
  toPathText' doesBStartWithBare a <> renderedB
 where
  renderedB :: Text
  renderedB = toPathText' followedByBare b
  doesBStartWithBare :: Bool
  doesBStartWithBare =
    not (null renderedB)
      && let c = head renderedB in isAscii c && isAlphaNum c || c == '_'
toPathText' followedByBare (PathSeparator a@(Root Nothing) b) =
  toPathText' False a <> toPathText' followedByBare b
toPathText' followedByBare (PathSeparator a b) =
  toPathText' False a <> "/" <> toPathText' followedByBare b
toPathText' False (Substitution envVar) = "$" <> envVar
toPathText' True (Substitution envVar) = "${" <> envVar <> "}"
toPathText' _ (SubstitutionWithDefault envVar expr) =
  "${" <> envVar <> ":-" <> toPathText' False expr <> "}"
toPathText' _ (ConditionalSubstitution envVar expr) =
  "${" <> envVar <> ":+" <> toPathText' False expr <> "}"


instance IsString FilePathExpression where
  fromString = BareComponent . pack