packages feed

wireform-yaml-0.1.0.0: src/YAML/Value.hs

{- | YAML 1.2 value representation.

The 'Value' ADT models the YAML 1.2 representation graph:

* Scalars corresponding to the YAML 1.2 /core schema/:
  'YNull', 'YBool', 'YInt', 'YFloat', and 'YString'.
* 'YSeq' for sequences (\"YAML lists\").
* 'YMap' for mappings (\"YAML dicts\"). Insertion order is preserved
  in the underlying 'Vector'; YAML mappings are unordered in the
  data model but downstream consumers (round-tripping in particular)
  typically want the source order.
* 'YAnchored' and 'YTagged' wrap sub-values with anchor identifiers and
  tag URIs so that anchor / alias relationships and explicit tags
  round-trip cleanly. An un-anchored, untagged scalar lives
  directly as 'YString' / 'YInt' / etc. without a wrapper.

A 'Document' carries the optional directives end marker and
optional document end marker so that emitters can faithfully
preserve multi-document streams.
-}
module YAML.Value (
  Value (..),
  Document (..),
  Stream (..),
  Tag (..),
  Anchor (..),

  -- * Construction helpers
  null_,
  bool,
  int,
  float,
  string,
  seq_,
  mapping,

  -- * Accessors
  unwrap,
  lookupKey,
) where

import Control.DeepSeq (NFData)
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Vector qualified as V
import GHC.Generics (Generic)


-- | An anchor identifier, e.g. the @ref@ in @&ref@.
newtype Anchor = Anchor {unAnchor :: Text}
  deriving stock (Show, Eq, Ord, Generic)
  deriving newtype (NFData)


{- | A YAML tag URI, e.g. @tag:yaml.org,2002:str@. Short-hand tags
(@!!str@) are resolved to their full URI form during decoding.
-}
newtype Tag = Tag {unTag :: Text}
  deriving stock (Show, Eq, Ord, Generic)
  deriving newtype (NFData)


{- | YAML representation node.

Integers have arbitrary precision. Presentation details such as quoting
and comments live in 'YAML.Annotated.AValue', rather than this data model.
-}
data Value
  = YNull
  | YBool !Bool
  | YInt !Integer
  | YFloat !Double
  | YString !Text
  | YSeq !(Vector Value)
  | YMap !(Vector (Value, Value))
  | -- | A node carrying an explicit YAML tag.
    YTagged !Tag !Value
  | {- | A node introduced with @&anchor@. Subsequent @*anchor@
    references in the source resolve to the same logical node;
    decoders expand aliases while retaining anchor metadata on the
    definition and on each resolved reference.
    -}
    YAnchored !Anchor !Value
  | -- | An alias reference. Decoders resolve these before returning a value.
    YAlias !Anchor
  deriving stock (Show, Eq, Generic)
  deriving anyclass (NFData)


-- | A single YAML document within a stream.
data Document = Document
  { docDirectivesEnd :: !Bool
  -- ^ @True@ when a @---@ marker preceded the body in the source.
  , docExplicitEnd :: !Bool
  -- ^ @True@ when a @...@ marker followed the body in the source.
  , docBody :: !Value
  }
  deriving stock (Show, Eq, Generic)
  deriving anyclass (NFData)


-- | A YAML stream is a sequence of documents.
newtype Stream = Stream {unStream :: Vector Document}
  deriving stock (Show, Eq, Generic)
  deriving newtype (NFData)


-- ---------------------------------------------------------------------------
-- Construction helpers
-- ---------------------------------------------------------------------------

null_ :: Value
null_ = YNull


bool :: Bool -> Value
bool = YBool


int :: Integer -> Value
int = YInt


float :: Double -> Value
float = YFloat


string :: Text -> Value
string = YString


seq_ :: [Value] -> Value
seq_ = YSeq . V.fromList


mapping :: [(Value, Value)] -> Value
mapping = YMap . V.fromList


-- ---------------------------------------------------------------------------
-- Accessors
-- ---------------------------------------------------------------------------

-- | Strip outer 'YAnchored' / 'YTagged' wrappers.
unwrap :: Value -> Value
unwrap = go
  where
    go (YAnchored _ v) = go v
    go (YTagged _ v) = go v
    go v = v


{- | Lookup by 'YString' key in a 'YMap'. Skips wrappers on both the
node we're searching and on its keys. Returns the first match in
source order.
-}
lookupKey :: Text -> Value -> Maybe Value
lookupKey k v = case unwrap v of
  YMap kvs -> goV 0
    where
      !len = V.length kvs
      goV !i
        | i >= len = Nothing
        | otherwise = case kvs V.! i of
            (kk, vv) -> case unwrap kk of
              YString k' | k' == k -> Just vv
              _ -> goV (i + 1)
  _ -> Nothing