tagtree-0.1.0.0: src/Data/TagTree.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
module Data.TagTree
( -- * Types
Tag (..),
TagPattern (unTagPattern),
TagNode (..),
-- * Create Tags
constructTag,
deconstructTag,
-- * Creating Tag Trees
tagTree,
-- * Searching Tags
mkTagPattern,
mkTagPatternFromTag,
tagMatch,
-- * Working with Tag Trees
foldTagTree,
)
where
import Control.Monad.Combinators.NonEmpty (sepBy1)
import Data.Aeson (FromJSON, ToJSON, ToJSONKey)
import Data.Default (Default (def))
import qualified Data.Map.Strict as Map
import Data.TagTree.PathTree (annotatePathsWith, foldSingleParentsWith, mkTreeFromPaths)
import qualified Data.Text as T
import Data.Tree (Forest)
import System.FilePattern (FilePattern, (?==))
import qualified Text.Megaparsec as M
import qualified Text.Megaparsec.Char as M
-- | A hierarchical tag
--
-- Tag nodes are separated by @/@
newtype Tag = Tag {unTag :: Text}
deriving (Eq, Ord, Show, Generic)
deriving newtype
( ToJSON,
FromJSON,
ToJSONKey
)
--------------
-- Tag Pattern
---------------
-- | A glob-based pattern to match hierarchical tags
--
-- For example, the pattern
--
-- > foo/**
--
-- matches both the following
--
-- > foo/bar/baz
-- > foo/baz
newtype TagPattern = TagPattern {unTagPattern :: FilePattern}
deriving
( Eq,
Ord,
Show,
Generic
)
deriving newtype
( ToJSON,
FromJSON
)
mkTagPattern :: Text -> TagPattern
mkTagPattern =
TagPattern . toString
mkTagPatternFromTag :: Tag -> TagPattern
mkTagPatternFromTag (Tag t) =
TagPattern $ toString t
tagMatch :: TagPattern -> Tag -> Bool
tagMatch (TagPattern pat) (Tag tag) =
pat ?== toString tag
-----------
-- Tag Tree
-----------
-- | An individual component of a hierarchical tag
--
-- The following hierarchical tag,
--
-- > foo/bar/baz
--
-- has three tag nodes: @foo@, @bar@ and @baz@
newtype TagNode = TagNode {unTagNode :: Text}
deriving (Eq, Show, Ord, Generic)
deriving newtype (ToJSON)
deconstructTag :: HasCallStack => Tag -> NonEmpty TagNode
deconstructTag (Tag s) =
either error id $ parse tagParser (toString s) s
where
tagParser :: Parser (NonEmpty TagNode)
tagParser =
nodeParser `sepBy1` M.char '/'
nodeParser :: Parser TagNode
nodeParser =
TagNode . toText <$> M.some (M.anySingleBut '/')
constructTag :: NonEmpty TagNode -> Tag
constructTag (fmap unTagNode . toList -> nodes) =
Tag $ T.intercalate "/" nodes
-- | Construct the tree from a list of hierarchical tags
tagTree :: (Eq a, Default a) => Map Tag a -> Forest (TagNode, a)
tagTree tags =
fmap (annotatePathsWith $ countFor tags) $
mkTreeFromPaths $
toList . deconstructTag
<$> Map.keys tags
where
countFor tags' path =
fromMaybe def $ Map.lookup (constructTag path) tags'
foldTagTree :: (Eq a, Default a) => Forest (TagNode, a) -> Forest (NonEmpty TagNode, a)
foldTagTree tree =
foldSingleParentsWith foldNodes <$> fmap (fmap (first (:| []))) tree
where
foldNodes (parent, x) (child, y) = do
guard (x == def)
Just (parent <> child, y)
type Parser a = M.Parsec Void Text a
parse :: Parser a -> String -> Text -> Either Text a
parse p fn s =
first (toText . M.errorBundlePretty) $
M.parse (p <* M.eof) fn s