packages feed

settei-0.1.0.0: src/Settei/Key.hs

-- |
-- Module: Settei.Key
-- Description: Validated structural keys with canonical dotted rendering.
module Settei.Key
  ( Key,
    KeyError (..),
    mkKey,
    parseKey,
    renderKey,
    keySegments,
  )
where

import Data.Generics.Labels ()
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Settei.Prelude

-- | A validated, hierarchical configuration key.
newtype Key = ValidatedKey
  { segments :: NonEmpty Text
  }
  deriving stock (Generic, Eq, Ord, Show)

-- | Why textual input could not be represented as a 'Key'.
data KeyError
  = KeyIsEmpty
  | KeySegmentIsEmpty
  | KeySegmentContainsDot !Text
  deriving stock (Generic, Eq, Show)

-- | Validate structural key segments.
mkKey :: NonEmpty Text -> Either KeyError Key
mkKey value
  | any Text.null segmentList = Left KeySegmentIsEmpty
  | Just part <- findWithDot segmentList = Left (KeySegmentContainsDot part)
  | otherwise = Right (ValidatedKey value)
  where
    segmentList = NonEmpty.toList value
    findWithDot = find (Text.any (== '.'))

-- | Parse dot-separated text into a non-empty structural key.
parseKey :: Text -> Either KeyError Key
parseKey value
  | Text.null value = Left KeyIsEmpty
  | otherwise =
      maybe
        (Left KeyIsEmpty)
        mkKey
        (NonEmpty.nonEmpty (Text.splitOn "." value))

-- | Render a key using its canonical dot-separated spelling.
renderKey :: Key -> Text
renderKey value = Text.intercalate "." (NonEmpty.toList (value ^. #segments))

-- | Return the structural segments of a key.
keySegments :: Key -> NonEmpty Text
keySegments value = value ^. #segments

find :: (a -> Bool) -> [a] -> Maybe a
find predicate = \case
  [] -> Nothing
  value : rest
    | predicate value -> Just value
    | otherwise -> find predicate rest