{-|
Module      : Net.DNSBase.Extensible
Description : Extension classes plus a guide to adding RR types and EDNS options
Copyright   : (c) Viktor Dukhovni, 2026
License     : BSD-3-Clause
Maintainer  : ietf-dane@dukhovni.org
Stability   : unstable

The two classes 'TypeExtensible' and 'ValueExtensible' are the
hooks that an already-extensible RR-data or EDNS-option codec
uses to admit user-supplied additions at conf-build time.
'TypeExtensible' admits a new Haskell type as the extension;
'ValueExtensible' admits a runtime value.  Most callers never
write an instance of either: the common cases are
/registering/ a brand-new RR type or EDNS option that doesn't
itself need an extension hook, or /supplying/ an extension to
an already extensible codec.  The sections below walk through
each of these in turn, and finish with the steps for defining
a new extensible codec of your own.
-}

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Net.DNSBase.Extensible
    ( -- * Extension classes
      TypeExtensible(..)
    , ValueExtensible(..)

      -- * Extending the library
      -- $overview

      -- ** Adding a custom RR type
      -- $customRRtype

      -- ** Adding a custom EDNS option
      -- $customEDNS

      -- ** Adding a type-driven extension
      -- $typeExt

      -- ** Adding a value-driven extension
      -- $valueExt

      -- ** Writing a type-driven extensible codec
      -- $newTypeExt

      -- ** Writing a value-driven extensible codec
      -- $newValueExt
    ) where

import Data.Kind (Constraint, Type)

-- | RR-data or EDNS-option types whose codec admits a
-- /type/-driven extension.  See
-- [Adding a custom RR type]("Net.DNSBase.Extensible#typeExt")
-- for an example of this mechanism in use, and
-- [Writing a type-driven extensible codec]("Net.DNSBase.Extensible#newTypeExt")
-- for the steps to implement a type-driven extensible codec
-- of your own.
--
type TypeExtensible :: Type -> Type -> Constraint
class TypeExtensible a v where
    -- | Constraint a caller's extension type @b@ must satisfy.
    type TypeExtensionArg a b :: Constraint

    -- | Fold a caller's extension type @b@ into the existing
    -- codec context value of type @v@.
    extendByType :: forall t b -> (t ~ a, TypeExtensionArg t b)
                 => v -> v

-- | RR-data or EDNS-option types whose codec admits a
-- /value/-driven extension.  See
-- [Adding a valud-driven extension]("Net.DNSBase.Extensible#valueExt")
-- for an example of this mechanism in use, and
-- [Writing a value-driven extensible codec]("Net.DNSBase.Extensible#newValueExt")
-- for the steps to implement a value-driven extensible codec of
-- your own.
--
type ValueExtensible :: Type -> Type -> Constraint
class ValueExtensible a v where
    -- | Constraint a caller's extension value type @b@ must
    -- satisfy.
    type ValueExtensionArg a b :: Constraint

    -- | Fold a caller-supplied value of type @b@ into the
    -- existing codec context value of type @v@.
    extendByValue :: forall t -> (t ~ a)
                  => forall b. (ValueExtensionArg t b)
                  => b -> v -> v

-- $overview
--
-- The sections below detail the available extension mechanisms,
-- showing workable code fragments, and even complete programs.
--
-- $customRRtype
-- #customRRtype#
--
-- The library decodes recognised RR types into matching
-- 'Net.DNSBase.RData.KnownRData' values and falls back to
-- 'Net.DNSBase.RData.OpaqueRData' for unrecognised codepoints.
--
-- To teach the resolver about an RR type the library does not
-- natively handle, declare a new 'Net.DNSBase.RData.KnownRData'
-- instance for your data type and pass the type to
-- 'Net.DNSBase.Resolver.registerRRtype' when building a
-- 'Net.DNSBase.Resolver.ResolverConf'.
--
-- /1.  Declare the RRTYPE and the associated data type./
--
-- The example below adds an alternative defintion of the usual
-- DNS @A@ RRtype.  Stored as 'Data.Word.Word32' value, and presented in
-- hexadecimal.  First define the required 'Net.DNSBase.RRTYPE.RRTYPE':
--
-- > pattern EXT_HEXA :: RRTYPE
-- > pattern EXT_HEXA = RRTYPE 1
--
-- Next a type to carry the decoded form:
--
-- > data T_ext_hexa = T_EXT_HEXA Word32 deriving (Eq, Ord, Show)
--
-- /2.  Give it a presentation form./
--
-- 'Net.DNSBase.Present.Presentable' objects print values in DNS (zone file)
-- presentation form.
--
-- > -- | CPS Presentation form is 8 hex nibbles
-- > instance Presentable T_ext_hexa where
-- >     present (T_EXT_HEXA w) = \k -> B.word32HexFixed w <> k
--
-- /3.  Implement the 'Net.DNSBase.RData.KnownRData' instance./
--
-- The instance carries the RR-type code, an optional presentation override for
-- the type name, the wire encoder, and the wire decoder.  Plain RR types —
-- those with no per-type extension state — leave
-- 'Net.DNSBase.RData.RDataExtensionVal' at its @()@ default; the three leading
-- @_@ patterns on 'Net.DNSBase.RData.rdDecode' discard the /required type/
-- argument, the (trivial) extension value and the fixed data length.  The
-- @get32@ parser checks that at least 4 bytes are available, and the message
-- decoder calling @rdDecode@ checks that the entire payload was consumed.
--
-- > instance KnownRData T_ext_hexa where
-- >     rdType _ = EXT_HEXA
-- >     -- Specify the RRTYPE name
-- >     rdTypePres _ = present @String "HEXA"
-- >     -- Implement the fixed size encoder
-- >     rdEncode (T_EXT_HEXA w) = putSizedBuilder $! mbWord32 w
-- >     -- The decoder wraps the payload inside 'Net.DNSBase.RData.RData'
-- >     rdDecode _ _ _ = RData . T_EXT_HEXA <$> get32
--
-- /4.  Plug it into the resolver./
--
-- 'Net.DNSBase.Resolver.registerRRtype' installs the new
-- 'Net.DNSBase.RData.RData' type.  Application-registed RR types take
-- precedence over any built-in types with the same
-- 'Net.DNSBase.RRTYPE.RRTYPE', except for a small set of RFC-reserved slots
-- (e.g., the OPT pseudo-RR) where the library entry always wins.
--
-- > withHEXA :: ResolverConf -> ResolverConf
-- > withHEXA = registerRRtype T_ext_hexa
--
-- Applying the above to 'Net.DNSBase.Resolver.defaultResolvConf'
-- you get a configuration that associates the custom data type
-- with its 'Net.DNSBase.RRTYPE.RRTYPE'.  This can be passed to
-- 'Net.DNSBase.Resolver.makeResolvSeed', whose output can in
-- turn be used to spawn one or more resolver instances via
-- 'Net.DNSBase.Resolver.withResolver'.
--
-- You can find a complete
-- [demo program](https://github.com/dnsbase/dnsbase/blob/main/demos/demoextrr.hs)
-- on Github. The expected output with @google.com@ as the command-line argument, should be
-- similar to:
--
-- > www.google.com. 208 IN HEXA 8efb9977
-- > www.google.com. 208 IN HEXA 8efb9c77
-- > www.google.com. 208 IN HEXA 8efb9677
-- > www.google.com. 208 IN HEXA 8efb9a77
-- > www.google.com. 208 IN HEXA 8efb9d77
-- > www.google.com. 208 IN HEXA 8efb9b77
-- > www.google.com. 208 IN HEXA 8efb9777
-- > www.google.com. 208 IN HEXA 8efb9877
--
-- $customEDNS
-- #customEDNS#
--
-- The library decodes recognised EDNS options into matching
-- 'Net.DNSBase.RData.KnownEdnsOption' values and falls back to
-- 'Net.DNSBase.RData.OpaqueOption' for unrecognised codepoints.
--
-- To teach the resolver about an EDNS option the library does
-- not natively handle, declare a 'Net.DNSBase.RData.KnownEdnsOption'
-- instance for your data type and pass the type to
-- 'Net.DNSBase.Resolver.registerEdnsOption' when building a
-- 'Net.DNSBase.Resolver.ResolverConf'.
--
-- /1.  Declare the 'OptNum' and the associated data type./
--
-- The example below adds support for sending and receiving the
-- DNS @COOKIE@ option (applications would need to decide when
-- to send cookies, what to put in them, and what to do with
-- cookies received from servers).
--
-- > pattern EXT_COOKIE :: OptNum
-- > pattern EXT_COOKIE = OptNum 10
--
-- Next a type to carry the decoded form:
--
-- > import Data.ByteString.Builder as BS
-- > import Data.ByteString.Short as SBS
-- >
-- > data T_ext_cookie = T_EXT_COOKIE
-- >     { clientCookie :: Word64              -- ^ Fixed length
-- >     , serverCookie :: SBS.ShortByteString -- ^ Possibly empty
-- >     } deriving (Eq, Ord, Show)
--
-- /2.  Give it a presentation form./
--
-- 'Net.DNSBase.Present.Presentable' prints DNS object values in ASCII
-- text presentation form.
--
-- > instance Presentable T_ext_cookie where
-- >     present T_EXT_COOKIE {..} k =
-- >          B.word64HexFixed clientCookie
-- >          <> present @Bytes16 (coerce serverCookie) k
--
-- /3.  Implement the 'Net.DNSBase.RData.KnownEdnsOption' instance./
--
-- The instance carries the EDNS option code, an optional presentation override
-- for the type name, the wire encoder, and the wire decoder.  Plain EDNS
-- option types — those with no per-type extension state — leave
-- 'Net.DNSBase.RData.RDataExtensionVal' at its @()@ default; the three leading
-- @_@ patterns on 'Net.DNSBase.RData.optDecode' discard the /required type/
-- argument, the (trivial) extension value and the fixed data length.  The
-- @get64@ parser checks that at least 8 bytes are available, and the message
-- decoder calling @optDecode@ checks that the entire payload was consumed.
--
-- > instance KnownEdnsOption T_ext_cookie where
-- >     optNum _ = EXT_COOKIE
-- >
-- >     optPres _ = present @String "COOKIE"
-- >
-- >     optEncode T_EXT_COOKIE {..} = putSizedBuilder $!
-- >         mbWord64 clientCookie
-- >         <> mbShortByteString serverCookie
-- >
-- >     optDecode _ _ len = do
-- >          clientCookie <- get64
-- >          serverCookie <-
-- >              if | len == 8 -> pure mempty
-- >                 | len > 40 -> failSGet "Server cookie too long"
-- >                 | len < 16 -> failSGet "Server cookie too short"
-- >                 | otherwise -> getShortNByteString (len - 8)
-- >          pure $ EdnsOption $ T_EXT_COOKIE {..}
--
-- /4.  Plug it into the resolver./
--
-- 'Net.DNSBase.Resolver.registerEdnsOption' installs the new
-- 'Net.DNSBase.EDNS.KnownEdnsOption' type.  Application-registed EDNS
-- option types take precedence over any built-in types with the
-- same 'Net.DNSBase.EDNS.OptNum'.
--
-- > withCookie :: ResolverConf -> ResolverConf
-- > withCookie = registerEdnsOption T_ext_cookie
--
-- Applying the above to 'Net.DNSBase.Resolver.defaultResolvConf'
-- you get a configuration that associates the custom data type
-- with its 'Net.DNSBase.EDNS.OptNum'.  This can be passed to
-- 'Net.DNSBase.Resolver.makeResolvSeed', whose output can in
-- turn be used to spawn one or more resolver instances via
-- 'Net.DNSBase.Resolver.withResolver'.
--
-- Below is a complete program with more detailed comments,
-- if compiled and executed it prints a cookie obtained from
-- the (current at time of writing) authoritative name server
-- for the @isc.org@ domain.
--
-- You can find a complete
-- [demo program](https://github.com/dnsbase/dnsbase/blob/main/demos/demoextopt.hs)
-- on Github.  The expected output should be similar to:
--
-- > -- ; COOKIE:  <16 client hex nibbles + 32 server hex nibbles>
-- > -- isc.org. 7200 IN NS ns1.isc.org.
-- > -- isc.org. 7200 IN NS ns2.isc.org.
-- > -- isc.org. 7200 IN NS ns3.isc.org.
-- > -- isc.org. 7200 IN NS ns.isc.afilias-nst.info.
-- > -- isc.org. 7200 IN NS nsp.dnsnode.net.
--
-- $typeExt
-- #typeExt#
--
-- Type-driven extensions let a caller extend an already
-- implemented codec by supplying a new Haskell /type/ that
-- meets the codec's extension configuration constraint
-- ('TypeExtensionArg').  The built-in data types that support
-- this extensionn mechanism are 'Net.DNSBase.RData.SVCB.T_svcb'
-- and 'Net.DNSBase.RData.SVCB.T_https'.
--
-- Each 'Net.DNSBase.RData.SVCB.SVCParamValue' is decoded by a
-- 'Net.DNSBase.RData.SVCB.KnownSVCParamValue' instance registered
-- in the codec's set of known types.  Unknown keys fall through
-- to decoding via 'Net.DNSBase.RData.SVCB.OpaqueSPV'.
--
-- To add or override a parameter value, implement a
-- 'Net.DNSBase.RData.SVCB.KnownSVCParamValue' instance for your
-- data type and configure it in either or both ot
-- 'Net.DNSBase.RData.SVCB.T_svcb' and 'Net.DNSBase.RData.SVCB.T_https'
-- by calling 'Net.DNSBase.Resolver.extendRRwithType'.
--
-- An EDNS-option codec that supports type-driven extension
-- would be customised analogously via
-- 'Net.DNSBase.Resolver.extendEdnsOptionWithType'.
--
-- The example below adds the boolean @ohttp@ parameter (RFC 9540,
-- key 8), which carries no payload — its presence or absence in a
-- 'Net.DNSBase.RRTYPE.SVCB' or 'Net.DNSBase.RRTYPE.HTTPS' record
-- is the entire signal.  If the type were missing from the library,
-- this would introduce it, otherwise replaces the underlying @SVCB@
-- key slot with the specified type.
--
-- > pattern EXT_OHTTP :: SVCParamKey
-- > pattern EXT_OHTTP = SVCParamKey 8
-- >
-- > data SPV_EXT_ohttp = SPV_EXT_OHTTP deriving (Eq, Ord, Show)
-- >
-- > instance Presentable SPV_EXT_ohttp where
-- >     present _ = present "ohttp" -- key[=value], no value to add
-- >
-- > instance KnownSVCParamValue SPV_EXT_ohttp where
-- >     spvKey _    = EXT_OHTTP
-- >     spvKeyPres _ = present "ohttp" -- For key-only contexts
-- >     encodeSPV _ = pure ()
-- >     decodeSPV _ _ = pure $ SVCParamValue SPV_EXT_OHTTP
--
-- 'Net.DNSBase.Resolver.extendRRwithType' takes an existing
-- type to be extended and the new extension type.  It adds
-- the extension type to the set of types known to the extended
-- type.  Next, once again build a custom resolver configuration
-- that supports or prefers the new 'Net.DNSBase.RData.SVCB.SVCParamValue':
--
-- > withOHTTP :: ResolverConf -> ResolverConf
-- > withOHTTP = extendRRwithType T_svcb  SPV_EXT_ohttp
-- >           . extendRRwithType T_https SPV_EXT_ohttp
--
-- And with the associated resolver seed you're ready to
-- spin up resolvers that use the new data type for this
-- SVCB/HTTPS key value.
--
-- > makeResolvSeed $ withOHTTP defaultResolvConf >>= \case
-- >     Left why -> ... handle error ...
-- >     Right seed -> withResolver seed \rslv -> ...
--
-- As with custom 'Net.DNSBase.RRTYPE.RRTYPE' registrations,
-- application-registered 'Net.DNSBase.RData.SVCB.SVCParamValue'
-- types take precedence over any built-in type for the same
-- SVCB key with the exception of the reserved @mandatory@ key
-- (codepoint 0, RFC 9460 -- section 8), for which the library's
-- implementation is always used.
--
-- You can find a complete
-- [demo program](https://github.com/dnsbase/dnsbase/blob/main/demos/demoextspv.hs)
-- on Github.  Rather than the value-less @ohttp@ key shown above,
-- the demo shadows the standard @ipv4hint@ key (codepoint 4) with
-- a representation that holds each hint as a raw 'Data.Word.Word32'
-- and presents each address as eight hexadecimal nibbles under a
-- made-up name @IPV4HEX@, so that the override is visible in the
-- output of an @HTTPS@ lookup against a domain whose records carry
-- @ipv4hint@ values (e.g. @cloudflare.com@).  Other SvcParam entries
-- in the same answer continue to use their built-in decoders.
--
-- $valueExt
-- #valueExt#
--
-- Value-driven extensions let a caller extend an already
-- implemented codec by supplying a runtime /value/ rather than
-- a Haskell /type/.  An example of is the data type that models
-- EDNS Extended DNS Errors: 'Net.DNSBase.EDNS.Option.EDE.O_ede'
-- which on the wire carries a numeric code with an optional
-- text string.
--
-- Each EDE numeric code is associated with a descriptive name
-- as part of IANA registration.  New codes that are not known
-- to the library can be registered at runtime, and are stored
-- as part of any received EDE options when a message is decoded
-- by the resolver.
--
-- 'Net.DNSBase.Resolver.extendEdnsOptionWithValue' can be used
-- to register an new EDE code to name association, or to
-- override a built-in name, by passing a @(code, name)@ pair.
-- For a given info-code a new registration takes precedence
-- over the library's built-in value (if any).
--
-- In this case, no new Haskell type need be defined — the
-- registration is just data, because decoding of just needs
-- to map each number to a string, without any additional
-- custom behaviours.
--
-- Any @RData@ codec that supported value-driven extensions
-- would be customised analogously with the use of
-- 'Net.DNSBase.Resolver.extendRRwithValue'.
--
-- ==== __Code outline__
--
-- > withCustomEdeNames :: ResolverConf -> ResolverConf
-- > withCustomEdeNames =
-- >     extendEdnsOptionWithValue O_ede (33, "Frobnicated")
-- >   . extendEdnsOptionWithValue O_ede (34, "Bogosity")
-- >
-- > let customConf :: ResolverConf
-- >     customConf = withCustomEdeNames defaultResolverConf
-- > ... seed <- makeResolvSeed customConf ...
-- > ... withResolver seed ...
--
-- $newTypeExt
-- #newTypeExt#
--
-- For library authors designing an RR-data or EDNS-option
-- codec that needs to accept user-supplied typed extensions,
-- the steps below wire up 'TypeExtensible' for use with the
-- existing 'Net.DNSBase.Resolver.extendRRwithType' /
-- 'Net.DNSBase.Resolver.extendEdnsOptionWithType' API.
--
-- The 'Net.DNSBase.RData.SVCB.X_svcb' data type illustrates
-- the approach.  Its extension state is an 'Data.IntMap' table
-- mapping key numbers to 'Net.DNSBase.RData.SVCB.SVCParamValue' decoders.  Its
-- 'TypeExtensible' instance inserts new entries at their declared
-- 'Net.DNSBase.RData.SVCB.spvKey' slots.
--
-- The recipe is analogous on the RData and EDNS-option sides;
-- they differ only in the type class constraints on the extended
-- types:
--
-- * 'Net.DNSBase.RData.KnownRData', vs.
-- * 'Net.DNSBase.EDNS.Option.KnownEdnsOption'
--
-- and the names of the associated extension value types, and their
-- default (initial) value fields:
--
-- * Extension state types:
--     * 'Net.DNSBase.RData.RDataExtensionVal', vs.
--     * 'Net.DNSBase.EDNS.Option.OptionExtensionVal'
--
-- * Extension state default values:
--     * 'Net.DNSBase.RData.rdataExtensionVal'
--     * 'Net.DNSBase.EDNS.Option.optionExtensionVal'
--
-- /1.  Choose the codec's extension value type./
--
-- This is the state the decoder needs to make sense of known and
-- new DNS data and what the 'TypeExtensible' instance folds new
-- entries into.  For @SVCB@ it is a map from a
-- 'Net.DNSBase.RData.SVCB.SVCParamKey' codepoint to a decoder for
-- the associated 'Net.DNSBase.RData.SVCB.KnownSVCParamValue'
-- type.
--
-- /2.  Declare the state as part of the type class instance./
--
-- Override the associated extension-value type and its
-- default.  On the @RData@ side:
--
-- > instance ... => KnownRData MyType where
-- >     type RDataExtensionVal MyType = SomeState
-- >     rdataExtensionVal _ = baselineState -- the initial value
-- >     ...
-- >     rdDecode _ state len = ... decode using state + len ...
--
-- On the EDNS option side:
--
-- > instance ... => KnownEdnsOption MyType where
-- >     type OptionExtensionVal MyType = SomeState
-- >     optionExtensionVal _ = baselineState -- the initial value
-- >     ...
-- >     optDecode _ state len = ... decode using state + len ...
--
-- Plugins are only needed for decoding, with a typed value already
-- in hand the encoder can just use the value's encode method.
--
-- /3.  Define the 'TypeExtensible' instance./
--
-- Since the application provided type is not fixed in advance,
-- to be usable it must satisfy some superclass constraint that
-- makes it possible for the extension to provide actual input
-- to the customised decoder.  Therefore, the 'TypeExtensible'
-- instance needs to define a 'Data.Kind.Constraint' that
-- ensures that the newly registered type supports the requisite
-- methods.
--
-- The 'extendByType' method defined as part of the instance is
-- responsible for extracting from the provided type all the
-- information needed to extend its decoder to support the
-- novel type.  In the case of @SVCB@ the type is added to
-- the key codepoint number to decoder function map.
--
-- > instance TypeExtensible MyType SomeState where
-- >     type TypeExtensionArg MyType b = (MyExtensionClass b)
-- >     extendByType _ b state = ... -- update state with b
--
-- 'Net.DNSBase.Resolver.extendRRwithType' (or
-- 'Net.DNSBase.Resolver.extendEdnsOptionWithType' for an EDNS
-- option) passes each caller-supplied extension to
-- 'extendByType', which uses to update the state using the
-- methods of @MyExtensionClass@.
--
-- $newValueExt
-- #newValueExt#
--
-- For library authors designing an RR-data or EDNS-option
-- codec that needs to accept user-supplied value-driven
-- extensions, the recipe parallels the
-- [Type extensible case](#newTypeExt), with 'ValueExtensible'
-- in place of 'TypeExtensible' doing the work:
--
-- * 'Net.DNSBase.Resolver.extendRRwithValue'
-- * 'Net.DNSBase.Resolver.extendEdnsOptionWithValue'
--
-- The reference built-in example is
-- 'Net.DNSBase.EDNS.Option.EDE.O_ede', whose extension state is
-- an @IntMap@ of info-code to friendly-name entries and whose
-- 'ValueExtensible' instance inserts new entries into that map.
--
-- /1.  Choose the codec's extension state type/, and
--
-- /2.  Adjust the parent-class instance accordinly/.
--
-- For the EDE EDNS option, this is:
--
-- > instance KnownEdnsOption O_ede where
-- >     type OptionExtensionVal O_ede = IntMap ShortByteString
-- >     optionExtensionVal _ = baseEdeNames -- initial table
-- >     ...
-- >     optDecode _ state len = ... decode using @state@
--
-- /3.  Implement the 'ValueExtensible' instance./  In this case,
--
-- the constraint 'ValueExtensionArg' fixes the caller's value
-- type, and 'extendByValue' folds the value in.
--
-- > instance ValueExtensible O_ede (IntMap ShortByteString) where
-- >     type ValueExtensionArg O_ede b = b ~ (Word16, ShortByteString)
-- >     extendByValue _ (code, name) m =
-- >         IM.insert (fromIntegral code) name m
--
-- 'Net.DNSBase.Resolver.extendEdnsOptionWithValue' (or
-- 'Net.DNSBase.Resolver.extendRRwithValue' for an RR type)
-- passes each caller-supplied value to 'extendByValue'.