packages feed

figment-0.2.0.0: src/Figment/Merge.hs

-- | Combining values: the four strategies of Rust figment, with the
-- same semantics (see @coalesce.rs@ upstream).
--
-- * 'Merge'   — on conflict the incoming value wins
-- * 'Join'    — on conflict the existing value wins
-- * 'Adjoin'  — like 'Join', but arrays are concatenated
-- * 'Admerge' — like 'Merge', but arrays are concatenated
--
-- Dictionaries are always merged key by key, recursively. A @null@ is a
-- value like any other: merging @null@ over a key replaces it.
--
-- Laws (property-tested in @test/Figment/MergeLaws.hs@):
--
-- * @coalesce Join a b == coalesce Merge b a@ (duality, tags included)
-- * @coalesce o v v@ has the same data as @v@ for 'Merge' and 'Join'
-- * the empty dictionary is a two-sided identity on dictionaries
-- * for a fixed order, 'coalesce' is associative on trees of identical
--   shape. It is /not/ associative in general (a scalar between two
--   dictionaries), which is why t'Value' has no 'Semigroup' instance; the
--   lawful monoid lives one level up, on 'Figment.Figment'.
module Figment.Merge
  ( Order (..)
  , coalesce
  , coalesceDict
  , coalesceData
  , coalesceProfile
  ) where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map

import Figment.Profile
import Figment.Value

-- | How a provider's values combine with the values already present.
data Order = Merge | Join | Adjoin | Admerge
  deriving stock (Eq, Ord, Show, Enum, Bounded)

leftWins :: Order -> Bool
leftWins Join = True
leftWins Adjoin = True
leftWins _ = False

concatenates :: Order -> Bool
concatenates Adjoin = True
concatenates Admerge = True
concatenates _ = False

-- | Combine an existing value (left) with an incoming one (right).
coalesce :: Order -> Value -> Value -> Value
coalesce o a@(Value ta na) b@(Value tb nb) = case (na, nb) of
  (Dict da, Dict db) ->
    Value (if leftWins o then ta else tb) (Dict (coalesceDict o da db))
  (Array xa, Array xb)
    | concatenates o -> Value ta (Array (xa <> xb))
  _ -> if leftWins o then a else b

-- | Combine two dictionaries key by key.
coalesceDict :: Order -> Dict -> Dict -> Dict
coalesceDict o = Map.unionWith (coalesce o)

-- | Combine per-profile data.
coalesceData :: Order -> Map Profile Dict -> Map Profile Dict -> Map Profile Dict
coalesceData o = Map.unionWith (coalesceDict o)

-- | Combine selected-profile hints.
coalesceProfile :: Order -> Profile -> Profile -> Profile
coalesceProfile o a b = if leftWins o then a else b