-- This file is part of Hoppy. -- -- Copyright 2015-2020 Bryan Gardiner -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . -- | Support for specifying overrides of values based on parameters. -- -- For example, an entity may have a name that you want to override on a -- per-language basis. A single value like this may be represented as a -- @'WithOverrides' Language Name@ value. Such a value will have a default -- name, as well as zero or more overrides, keyed by @Language@. -- -- A 'MapWithOverrides' type is also provided for ease of overriding values -- inside of a map. module Foreign.Hoppy.Generator.Override ( WithOverrides, plain, overridden, unoverriddenValue, overriddenValues, MapWithOverrides, plainMap, mapWithOverrides, addOverrideMap, addOverrideMaps, applyOverrideMaps, insertMapOverride, overriddenMapLookup, ) where import qualified Data.Map as M import Data.Maybe (fromMaybe) -- | Represents a default value of type @v@ with optional overrides keyed by -- parameter type @p@. The type @p@ must have an 'Ord' instance. data WithOverrides p v = WithOverrides { unoverriddenValue :: v -- ^ The default, unoverriden value for the 'WithOverrides'. Lookups on the -- override will return this value a given parameter doesn't have an -- override. , overriddenValues :: M.Map p v -- ^ Any overridden values that have been added to the 'WithOverrides'. } -- | Creates a 'WithOverrides' with the given default value @v@, and no -- overridden values. plain :: v -> WithOverrides p v plain x = WithOverrides x M.empty -- | Creates a 'WithOverrides' with the given default value @v@, and overridden -- values in the map. overridden :: v -> M.Map p v -> WithOverrides p v overridden = WithOverrides -- | Extracts a value, possibly overridden based on a parameter. getOverride :: Ord p => p -> WithOverrides p v -> v getOverride p o = fromMaybe (unoverriddenValue o) $ M.lookup p $ overriddenValues o addOverride :: Ord p => p -> v -> WithOverrides p v -> WithOverrides p v addOverride p v o = o { overriddenValues = M.insert p v $ overriddenValues o } -- | Represents a map from @k@ values to @v@ values, where each entry can be -- overridden based on parameter @p@. A key is either present with a default -- value and possibly some overridden values, or it is completely absent -- it -- is not possible for a key to have overridden values but no default value. newtype MapWithOverrides p k v = MapWithOverrides { fromMapWithOverrides :: M.Map k (WithOverrides p v) } -- | Converts a plain map to a 'MapWithOverrides' without any overrides. plainMap :: M.Map k v -> MapWithOverrides p k v plainMap = MapWithOverrides . M.map plain -- | Direct constructor for 'MapWithOverrides'. mapWithOverrides :: M.Map k (WithOverrides p v) -> MapWithOverrides p k v mapWithOverrides = MapWithOverrides -- | Adds an override @v@ for key @k@ under parameter @p@ to a -- 'MapWithOverrides'. -- -- It is an error for a parameter to override a key that is not present in the -- defaults map. insertMapOverride :: (Ord p, Ord k, Show p, Show k) => p -> k -> v -> MapWithOverrides p k v -> MapWithOverrides p k v insertMapOverride p k v (MapWithOverrides m) = -- We could do this whole operation as an 'alter' rather than a -- 'lookup'/'adjust', but this way, if an insertion is invalid then we return -- an error directly rather than hiding it inside the resulting structure. case M.lookup k m of Just _ -> MapWithOverrides $ M.adjust (addOverride p v) k m Nothing -> error $ "insertMapOverride: Can't add override for parameter " ++ show p ++ " under key " ++ show k ++ " that has no default value." -- | Adds a collection of overrides @v@ for multiple keys @k@, all under a single -- parameter @p@, to a 'MapWithOverrides'. -- -- It is an error for a parameter to override a key that is not present in the -- defaults map. addOverrideMap :: (Ord p, Ord k, Show p, Show k) => p -> M.Map k v -> MapWithOverrides p k v -> MapWithOverrides p k v addOverrideMap p pOverrides (MapWithOverrides m) = MapWithOverrides $ M.foldrWithKey (\k vOverride acc -> M.alter (\kOverrides -> case kOverrides of Just overrides -> Just $ addOverride p vOverride overrides Nothing -> error $ "addOverrideMap: Parameter " ++ show p ++ " supplies override for key " ++ show k ++ " that is not in the map of unoverridden values.") k acc) m pOverrides -- | Adds overrides @v@ for multiple keys @k@ under multiple parameters @p@ to a -- 'MapWithOverrides'. -- -- It is an error for a parameter to override a key that is not present in the -- defaults map. addOverrideMaps :: (Ord p, Ord k, Show p, Show k) => M.Map p (M.Map k v) -> MapWithOverrides p k v -> MapWithOverrides p k v addOverrideMaps overrideMaps (MapWithOverrides m) = MapWithOverrides $ M.foldrWithKey (\p pOverrides -> fromMapWithOverrides . addOverrideMap p pOverrides . MapWithOverrides) m overrideMaps -- | Constructs a 'MapWithOverrides' from a map of default values and a bunch of -- parameter-specific maps overlaid on top of it. -- -- It is an error for a parameter to override a key that is not present in the -- defaults map. applyOverrideMaps :: (Ord p, Ord k, Show p, Show k) => M.Map p (M.Map k v) -> M.Map k v -> MapWithOverrides p k v applyOverrideMaps overrideMaps baseMap = MapWithOverrides $ M.foldrWithKey (\p pOverrides acc -> M.foldrWithKey (\k vOverride acc' -> M.alter (\kOverrides -> case kOverrides of Just overrides -> Just $ addOverride p vOverride overrides Nothing -> error $ "applyOverrideMaps: Parameter " ++ show p ++ " supplies override for key " ++ show k ++ " that is not in the map of unoverridden values.") k acc') acc pOverrides) (M.map plain baseMap) overrideMaps -- | Looks up a value for @k@ in the given 'MapWithOverrides', with the -- possibility that the value is overridden by the parameter @p@. overriddenMapLookup :: (Ord p, Ord k) => p -> k -> MapWithOverrides p k v -> Maybe v overriddenMapLookup p k (MapWithOverrides x) = getOverride p <$> M.lookup k x