packages feed

dnsbase-1.0.0.0: src/Net/DNSBase/RData/SVCB/SPVSet.hs

{-|
Module      : Net.DNSBase.RData.SVCB.SPVSet
Description : Indexed collection of SVCB service-parameter values
Copyright   : (c) Viktor Dukhovni, 2026
License     : BSD-3-Clause
Maintainer  : ietf-dane@dukhovni.org
Stability   : unstable

The collection of service parameters carried in a single
@SVCB@ or @HTTPS@ record, keyed by 'Net.DNSBase.RData.SVCB.SVCParamKey' (one value
per key code, per RFC 9460).  Construction is via the 'IsList'
instance — @fromList [SVCParamValue p1, ...]@; enumeration via
'toList' returns the parameters in ascending key order, which
is also the canonical on-the-wire order.

Each key code is associated with a specific 'KnownSVCParamValue'
type; the values are held inside the existential 'SVCParamValue'
wrapper so a single set can mix heterogeneous parameter types.
'spvLookup' takes the parameter /type/ as a type application
and returns the typed value when the matching key is present:

> ghci> import qualified GHC.IsList as IL (fromList)
> ghci> spvset = IL.fromList @SPVSet [SVCParamValue (SPV_PORT 80), SVCParamValue SPV_NDALPN]
> ghci> spvset
> fromList @SPVSet [SVCParamValue SPV_NDALPN,SVCParamValue 80]
> ghci> spvLookup @SPV_ndalpn spvset
> Just SPV_NDALPN

The type application can be elided when a pattern match
constrains the result type:

> ghci> case spvLookup spvset of { Just (SPV_PORT p) -> p; _ -> 0 }
> 80
-}

module Net.DNSBase.RData.SVCB.SPVSet
    ( SPVSet(SPVMap)
    , spvLookup
    , spvSetFromMonoList
    ) where

import qualified Data.IntMap.Strict as IM
import GHC.IsList(IsList(..))

import Net.DNSBase.Internal.Util

import Net.DNSBase.Present
import Net.DNSBase.RData.SVCB.SVCParamValue

-- | The set of service parameters in an @SVCB@ or @HTTPS@ record,
-- with at most one value per 'Net.DNSBase.RData.SVCB.SVCParamKey'.  The 'Monoid'
-- instance provides the empty set; the 'Semigroup' instance is
-- left-biased on key collisions.
newtype SPVSet = SPVSet (IM.IntMap SVCParamValue)
    deriving (Eq, Semigroup, Monoid)

instance Show SPVSet where
    showsPrec p (toList -> pvs) = showsP p $
        showString "fromList @SPVSet " . shows' pvs

instance Presentable SPVSet where
    present vs k = case toList vs of
        v:rest -> present v $ foldr presentSp k rest
        []     -> k

-- | One-sided pattern that exposes the underlying 'IM.IntMap' for
-- traversal or low-level inspection.  The values are kept in the
-- existential 'SVCParamValue' wrapper, so any concrete parameter
-- may be either typed (a 'KnownSVCParamValue' instance) or
-- 'Net.DNSBase.RData.SVCB.OpaqueSPV'.  For typed lookups by parameter type use
-- 'spvLookup' instead.
pattern SPVMap :: IM.IntMap SVCParamValue -- ^ Values as an 'IM.IntMap'
               -> SPVSet
pattern SPVMap m <- SPVSet m

-- | Look up the parameter at the key associated with type @a@,
-- returning a 'Just' only when the stored value really is of type
-- @a@.  A value of the same key code but held as 'Net.DNSBase.RData.SVCB.OpaqueSPV'
-- (because the typed instance was not registered when the record
-- was decoded) does /not/ match — opaque values must be
-- retrieved through 'SPVMap'.
spvLookup :: forall a. KnownSVCParamValue a
          => SPVSet -> Maybe a
spvLookup = (>>= fromSPV @a) . IM.lookup key . coerce
  where
    key = fromIntegral $ spvKey a

-- | Construction is via 'fromList', and enumeration is via 'toList'.
instance IsList SPVSet where
    type Item SPVSet = SVCParamValue

    -- | Construct a parameter Map from an unordered list.
    fromList vs =
        coerce $ IM.fromList
               [ (key, v) | v <- vs , let key = fromIntegral $ serviceParamKey v ]

    -- | Return the map as a list in ascending parameter order.
    toList = IM.elems . coerce

-- | Build an 'SPVSet' from a list of 'SVCParamValue's whose keys
-- are in /strict monotone-increasing/ order (so also no
-- duplicates) — the precondition imposed by the underlying
-- 'IM.fromDistinctAscList' used to build the set.  The \"Mono\"
-- is intended as a reminder of the precondition.
--
-- Used internally by the wire-form decoder, which validates
-- ascending keys during parsing; application code that cannot
-- guarantee the precondition should use 'fromList' from the
-- 'IsList' instance instead.
spvSetFromMonoList :: [SVCParamValue] -> SPVSet
spvSetFromMonoList = coerce $ IM.fromDistinctAscList . map kv
  where
    kv !v = let !k = fromIntegral $ serviceParamKey v in (k, v)