packages feed

openapi3-3.2.5: src/Data/HashMap/Strict/InsOrd/Compat.hs

-- Ported from GetShopTV/swagger2 (pull request #262) to apply the same
-- insert-ordered-containers-0.3 compatibility fix to openapi3.
-- Credit for the design and implementation belongs to the swagger2 authors.
{-# LANGUAGE CPP #-}
-- |
-- Module:      Data.HashMap.Strict.InsOrd.Compat
-- Maintainer:  Nickolay Kudasov <nickolay@getshoptv.com>
-- Stability:   experimental
--
-- Compatibility wrapper around @insert-ordered-containers@ to mitigate the
-- breaking changes introduced in @insert-ordered-containers-0.3.0@:
-- <https://github.com/erikd/insert-ordered-containers/pull/8>.
--
-- That change fixed 'Eq' and Aeson instances in the upstream package, but it is
-- a behavioral break for @swagger2@ where we need stable Swagger Schema
-- generation and JSON object-like encoding.
--
-- This module keeps the old @swagger2@ expectations:
--
-- * 'InsOrdHashMap' values are encoded as JSON objects (not arrays of key/value
--   tuples), so field names remain first-class object keys.
-- * Equality intentionally ignores insertion order (compares as plain hash
--   maps), which matches how many tests currently assert JSON equality.
--
-- Simple encoding examples:
--
-- >>> import Data.Aeson (encode, eitherDecode)
-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL8
-- >>> import qualified Data.HashMap.Strict as HM
-- >>> import qualified Data.HashMap.Strict.InsOrd.Compat as IOHM
-- >>> let decodeIOHM s = either error id (eitherDecode (BSL8.pack s) :: Either String (IOHM.InsOrdHashMap String Int))
--
-- A regular hash map has no insertion order guarantee:
--
-- >>> encode (HM.fromList [("a", 1 :: Int), ("b", 2)])
-- "{\"a\":1,\"b\":2}"
-- >>> encode (HM.fromList [("b", 1 :: Int), ("a", 2)])
-- "{\"a\":2,\"b\":1}"
--
-- Our compat 'InsOrdHashMap' encodes to a JSON object as well, but preserves insertion order:
--
-- >>> encode (IOHM.fromList [("a", 1 :: Int), ("b", 2)])
-- "{\"a\":1,\"b\":2}"
-- >>> encode (IOHM.fromList [("b", 1 :: Int), ("a", 2)])
-- "{\"b\":1,\"a\":2}"
--
-- Round-tripping through decode/encode demonstrates the caveat: encoding
-- preserves insertion order, but decoded object key order is not guaranteed.
--
-- >>> encode (decodeIOHM "{\"a\":1,\"b\":2}")
-- "{\"a\":1,\"b\":2}"
-- >>> encode (decodeIOHM "{\"b\":1,\"a\":2}")
-- "{\"a\":2,\"b\":1}"
--
-- This object encoding is what @swagger2@ wants for generated Swagger
-- definitions/properties because it keeps emitted schemas easy to consume and
-- stable in practice.
--
-- Important caveat: decoding cannot be fully stable with respect to insertion
-- order due to @aeson@ limitations. In particular, object parsing goes through
-- structures that do not preserve all ordering guarantees end-to-end. We accept
-- this trade-off for now because the primary requirement is deterministic
-- /encoding/ for generated Swagger Schema artifacts.
--
-- Many tests rely on @aesonQQ@-style JSON equality, where semantic object
-- equality matters more than insertion order. Comparing via plain hash maps
-- makes those tests robust under benign key-order variation. This is a weaker
-- notion of equality and hopefully will be revisited later.
module Data.HashMap.Strict.InsOrd.Compat (
  InsOrdHashMap,
  -- * Construction
  empty,
  singleton,
  -- * Basic interface
  null,
  size,
  member,
  lookup,
  lookupDefault,
  insert,
  insertWith,
  delete,
  adjust,
  update,
  alter,
  -- * Combine
  union,
  unionWith,
  unionWithKey,
  unions,
  -- * Transformations
  map,
  mapKeys,
  traverseKeys,
  mapWithKey,
  traverseWithKey,
  -- ** Unordered
  unorderedTraverse,
  unorderedTraverseWithKey,
  -- * Difference and intersection
  difference,
  intersection,
  intersectionWith,
  intersectionWithKey,
  -- * Folds
  foldl',
  foldlWithKey',
  foldr,
  foldrWithKey,
  foldMapWithKey,
  -- ** Unordered
  unorderedFoldMap,
  unorderedFoldMapWithKey,
  -- * Filter
  filter,
  filterWithKey,
  mapMaybe,
  mapMaybeWithKey,
  -- * Conversions
  keys,
  elems,
  toList,
  toRevList,
  fromList,
  toHashMap,
  fromHashMap,
  -- * Lenses
  hashMap,
  unorderedTraversal,
  -- * Debugging
  valid,
  ) where

#if !MIN_VERSION_insert_ordered_containers(0,3,0)
import Prelude hiding (null, lookup, map, foldl', foldr, filter)
import Data.HashMap.Strict.InsOrd
#else
import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap

import Prelude hiding (null, size, member, lookup, lookupDefault, map, foldl', filter)
import qualified Prelude

import qualified Data.Aeson as A
import qualified Data.Aeson.Encoding as E
import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap

import qualified GHC.Exts as Exts

import Data.Data                       (Data)
import Data.Foldable                   (Foldable (foldMap))
import Data.Hashable                   (Hashable (..))

import qualified Control.Lens        as Lens
import Control.Lens
       (At (..), Index, Iso, IxValue, Ixed (..), Traversal, _1, _2, iso, (<&>))

import qualified Optics.Core         as Optics

newtype InsOrdHashMap k v = InsOrdHashMap { unCompatInsOrdHashMap :: InsOrdHashMap.InsOrdHashMap k v }
  deriving (Show, Read, Data, Functor, Foldable, Traversable, Semigroup, Monoid)

instance (Eq k, Eq v) => Eq (InsOrdHashMap k v) where
    a == b = toHashMap a == toHashMap b

instance (Eq k, Hashable k) => Exts.IsList (InsOrdHashMap k v) where
    type Item (InsOrdHashMap k v) = Exts.Item (InsOrdHashMap.InsOrdHashMap k v)
    fromList = InsOrdHashMap . InsOrdHashMap.fromList
    toList   = InsOrdHashMap.toList . unCompatInsOrdHashMap

-------------------------------------------------------------------------------
-- Aeson
-------------------------------------------------------------------------------

instance (A.ToJSONKey k) => A.ToJSON1 (InsOrdHashMap k) where
    liftToJSON _ t _ = case A.toJSONKey :: A.ToJSONKeyFunction k of
      A.ToJSONKeyText f _ -> A.object . fmap (\(k, v) -> (f k, t v)) . toList
      A.ToJSONKeyValue f _ -> A.toJSON . fmap (\(k,v) -> A.toJSON (f k, t v)) . toList

    liftToEncoding o t _ = case A.toJSONKey :: A.ToJSONKeyFunction k of
      A.ToJSONKeyText _ f ->  E.dict f t foldrWithKey
      A.ToJSONKeyValue _ f -> E.list (A.liftToEncoding2 (const False) f (E.list f) o t (E.list t)) . toList

instance (A.ToJSONKey k, A.ToJSON v) => A.ToJSON (InsOrdHashMap k v) where
    toJSON = A.toJSON1
    toEncoding = A.toEncoding1

-------------------------------------------------------------------------------

instance (Eq k, Hashable k, A.FromJSONKey k) => A.FromJSON1 (InsOrdHashMap k) where
    liftParseJSON o p pl v = fromList . HashMap.toList <$> A.liftParseJSON o p pl v

instance (Eq k, Hashable k, A.FromJSONKey k, A.FromJSON v) => A.FromJSON (InsOrdHashMap k v) where
    parseJSON = A.parseJSON1

-------------------------------------------------------------------------------
-- indexed-traversals
-------------------------------------------------------------------------------

instance (Eq k, Hashable k) => Optics.FunctorWithIndex k (InsOrdHashMap k) where
    imap = mapWithKey
instance (Eq k, Hashable k) => Optics.FoldableWithIndex k (InsOrdHashMap k) where
    ifoldMap = foldMapWithKey
    ifoldr   = foldrWithKey
instance (Eq k, Hashable k) => Optics.TraversableWithIndex k (InsOrdHashMap k) where
    itraverse = traverseWithKey

-------------------------------------------------------------------------------
-- Lens
-------------------------------------------------------------------------------

type instance Index (InsOrdHashMap k v) = k
type instance IxValue (InsOrdHashMap k v) = v

instance (Eq k, Hashable k) => Ixed (InsOrdHashMap k v) where
    ix k f m = ixImpl k pure f m
    {-# INLINABLE ix #-}

ixImpl
  :: (Eq k, Hashable k, Functor f)
  => k
  -> (InsOrdHashMap k v -> f (InsOrdHashMap k v))
  -> (v -> f v)
  -> InsOrdHashMap k v
  -> f (InsOrdHashMap k v)
ixImpl k point f m = case lookup k m of
    Just v  -> f v <&> \v' -> insert k v' m
    Nothing -> point m
{-# INLINE ixImpl #-}

instance (Eq k, Hashable k) => At (InsOrdHashMap k a) where
    at k f m = f mv <&> \r -> case r of
        Nothing -> maybe m (const (delete k m)) mv
        Just v' -> insert k v' m
      where mv = lookup k m
    {-# INLINABLE at #-}

-------------------------------------------------------------------------------
-- Optics
-------------------------------------------------------------------------------

type instance Optics.Index (InsOrdHashMap k v) = k
type instance Optics.IxValue (InsOrdHashMap k v) = v

instance (Eq k, Hashable k) => Optics.Ixed (InsOrdHashMap k v) where
    ix k = Optics.atraversalVL $ \point f m -> ixImpl k point f m
    {-# INLINE ix #-}

instance (Eq k, Hashable k) => Optics.At (InsOrdHashMap k a) where
    at k = Optics.lensVL $ \f m -> Lens.at k f m
    {-# INLINE at #-}

-------------------------------------------------------------------------------

-- | This is a slight lie, as roundtrip doesn't preserve ordering.
hashMap :: Iso (InsOrdHashMap k a) (InsOrdHashMap k b) (HashMap k a) (HashMap k b)
hashMap = iso toHashMap fromHashMap

unorderedTraversal :: Traversal (InsOrdHashMap k a) (InsOrdHashMap k b) a b
unorderedTraversal = hashMap . traverse

-------------------------------------------------------------------------------
-- * Construction
-------------------------------------------------------------------------------

empty :: InsOrdHashMap k v
empty = InsOrdHashMap InsOrdHashMap.empty

singleton :: Hashable k => k -> v -> InsOrdHashMap k v
singleton k v = InsOrdHashMap (InsOrdHashMap.singleton k v)

-------------------------------------------------------------------------------
-- * Basic interface
-------------------------------------------------------------------------------

null :: InsOrdHashMap k v -> Bool
null = InsOrdHashMap.null . unCompatInsOrdHashMap

size :: InsOrdHashMap k v -> Int
size = InsOrdHashMap.size . unCompatInsOrdHashMap

insert :: Hashable k => k -> v -> InsOrdHashMap k v -> InsOrdHashMap k v
insert k v = InsOrdHashMap . InsOrdHashMap.insert k v . unCompatInsOrdHashMap

insertWith :: Hashable k => (v -> v -> v) -> k -> v -> InsOrdHashMap k v -> InsOrdHashMap k v
insertWith f k v = InsOrdHashMap . InsOrdHashMap.insertWith f k v . unCompatInsOrdHashMap

delete :: Hashable k => k -> InsOrdHashMap k v -> InsOrdHashMap k v
delete k = InsOrdHashMap . InsOrdHashMap.delete k . unCompatInsOrdHashMap

adjust :: Hashable k => (v -> v) -> k -> InsOrdHashMap k v -> InsOrdHashMap k v
adjust f k = InsOrdHashMap . InsOrdHashMap.adjust f k . unCompatInsOrdHashMap

update :: Hashable k => (v -> Maybe v) -> k -> InsOrdHashMap k v -> InsOrdHashMap k v
update f k = InsOrdHashMap . InsOrdHashMap.update f k . unCompatInsOrdHashMap

alter :: Hashable k => (Maybe v -> Maybe v) -> k -> InsOrdHashMap k v -> InsOrdHashMap k v
alter f k = InsOrdHashMap . InsOrdHashMap.alter f k . unCompatInsOrdHashMap

member :: Hashable k => k -> InsOrdHashMap k v -> Bool
member k = InsOrdHashMap.member k . unCompatInsOrdHashMap

lookup :: Hashable k => k -> InsOrdHashMap k v -> Maybe v
lookup k = InsOrdHashMap.lookup k . unCompatInsOrdHashMap

lookupDefault :: Hashable k => v -> k -> InsOrdHashMap k v -> v
lookupDefault k def = InsOrdHashMap.lookupDefault k def . unCompatInsOrdHashMap

-- * Combine

union :: Hashable k => InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
union m1 m2 = InsOrdHashMap (InsOrdHashMap.union (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

unionWith :: Hashable k => (v -> v -> v) -> InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
unionWith f m1 m2 = InsOrdHashMap (InsOrdHashMap.unionWith f (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

unionWithKey :: Hashable k => (k -> v -> v -> v) -> InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
unionWithKey f m1 m2 = InsOrdHashMap (InsOrdHashMap.unionWithKey f (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

unions :: Hashable k => [InsOrdHashMap k v] -> InsOrdHashMap k v
unions = InsOrdHashMap . InsOrdHashMap.unions . Prelude.map unCompatInsOrdHashMap

-------------------------------------------------------------------------------
-- * Transformations
-------------------------------------------------------------------------------

map :: (v -> v) -> InsOrdHashMap k v -> InsOrdHashMap k v
map f = InsOrdHashMap . InsOrdHashMap.map f . unCompatInsOrdHashMap

mapKeys :: Hashable k => (k -> k) -> InsOrdHashMap k v -> InsOrdHashMap k v
mapKeys f = InsOrdHashMap . InsOrdHashMap.mapKeys f . unCompatInsOrdHashMap

traverseKeys :: (Applicative f, Hashable k) => (k -> f k) -> InsOrdHashMap k v -> f (InsOrdHashMap k v)
traverseKeys f = fmap InsOrdHashMap . InsOrdHashMap.traverseKeys f . unCompatInsOrdHashMap

mapWithKey :: (k -> v1 -> v2) -> InsOrdHashMap k v1 -> InsOrdHashMap k v2
mapWithKey f = InsOrdHashMap . InsOrdHashMap.mapWithKey f . unCompatInsOrdHashMap

traverseWithKey :: (Applicative f, Hashable k) => (k -> v1 -> f v2) -> InsOrdHashMap k v1 -> f (InsOrdHashMap k v2)
traverseWithKey f = fmap InsOrdHashMap . InsOrdHashMap.traverseWithKey f . unCompatInsOrdHashMap

-- ** Unordered

unorderedTraverse :: (Applicative f, Hashable k) => (v -> f v) -> InsOrdHashMap k v -> f (InsOrdHashMap k v)
unorderedTraverse f = fmap InsOrdHashMap . InsOrdHashMap.unorderedTraverse f . unCompatInsOrdHashMap

unorderedTraverseWithKey :: (Applicative f, Hashable k) => (k -> v -> f v) -> InsOrdHashMap k v -> f (InsOrdHashMap k v)
unorderedTraverseWithKey f = fmap InsOrdHashMap . InsOrdHashMap.unorderedTraverseWithKey f . unCompatInsOrdHashMap

-------------------------------------------------------------------------------
-- * Difference and intersection
-------------------------------------------------------------------------------

difference :: Hashable k => InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
difference m1 m2 = InsOrdHashMap (InsOrdHashMap.difference (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

intersection :: Hashable k => InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
intersection m1 m2 = InsOrdHashMap (InsOrdHashMap.intersection (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

intersectionWith :: Hashable k => (v -> v -> v) -> InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
intersectionWith f m1 m2 = InsOrdHashMap (InsOrdHashMap.intersectionWith f (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

intersectionWithKey :: Hashable k => (k -> v -> v -> v) -> InsOrdHashMap k v -> InsOrdHashMap k v -> InsOrdHashMap k v
intersectionWithKey f m1 m2 = InsOrdHashMap (InsOrdHashMap.intersectionWithKey f (unCompatInsOrdHashMap m1) (unCompatInsOrdHashMap m2))

-------------------------------------------------------------------------------
-- * Folds
-------------------------------------------------------------------------------

foldl' :: (a -> v -> a) -> a -> InsOrdHashMap k v -> a
foldl' f z = InsOrdHashMap.foldl' f z . unCompatInsOrdHashMap

foldlWithKey' :: (a -> k -> v -> a) -> a -> InsOrdHashMap k v -> a
foldlWithKey' f z = InsOrdHashMap.foldlWithKey' f z . unCompatInsOrdHashMap

foldMapWithKey :: Monoid m => (k -> v -> m) -> InsOrdHashMap k v -> m
foldMapWithKey f = InsOrdHashMap.foldMapWithKey f . unCompatInsOrdHashMap

foldrWithKey :: (k -> v -> a -> a) -> a -> InsOrdHashMap k v -> a
foldrWithKey f z = InsOrdHashMap.foldrWithKey f z . unCompatInsOrdHashMap

-- ** Unordered

unorderedFoldMap :: Monoid m => (v -> m) -> InsOrdHashMap k v -> m
unorderedFoldMap f = InsOrdHashMap.unorderedFoldMap f . unCompatInsOrdHashMap

unorderedFoldMapWithKey :: Monoid m => (k -> v -> m) -> InsOrdHashMap k v -> m
unorderedFoldMapWithKey f = InsOrdHashMap.unorderedFoldMapWithKey f . unCompatInsOrdHashMap

-------------------------------------------------------------------------------
-- * Filter
-------------------------------------------------------------------------------

filter :: (v -> Bool) -> InsOrdHashMap k v -> InsOrdHashMap k v
filter f = InsOrdHashMap . InsOrdHashMap.filter f . unCompatInsOrdHashMap

filterWithKey :: (k -> v -> Bool) -> InsOrdHashMap k v -> InsOrdHashMap k v
filterWithKey f = InsOrdHashMap . InsOrdHashMap.filterWithKey f . unCompatInsOrdHashMap

mapMaybe :: (v -> Maybe v) -> InsOrdHashMap k v -> InsOrdHashMap k v
mapMaybe f = InsOrdHashMap . InsOrdHashMap.mapMaybe f . unCompatInsOrdHashMap

mapMaybeWithKey :: (k -> v -> Maybe v) -> InsOrdHashMap k v -> InsOrdHashMap k v
mapMaybeWithKey f = InsOrdHashMap . InsOrdHashMap.mapMaybeWithKey f . unCompatInsOrdHashMap

-------------------------------------------------------------------------------
-- * Conversions
-------------------------------------------------------------------------------

keys :: InsOrdHashMap k v -> [k]
keys = InsOrdHashMap.keys . unCompatInsOrdHashMap

elems :: InsOrdHashMap k v -> [v]
elems = InsOrdHashMap.elems . unCompatInsOrdHashMap

toRevList :: InsOrdHashMap k v -> [(k, v)]
toRevList = InsOrdHashMap.toRevList . unCompatInsOrdHashMap

fromList :: Hashable k => [(k, v)] -> InsOrdHashMap k v
fromList = InsOrdHashMap . InsOrdHashMap.fromList

toList :: InsOrdHashMap k v -> [(k, v)]
toList = InsOrdHashMap.toList . unCompatInsOrdHashMap

toHashMap :: InsOrdHashMap k v -> HashMap k v
toHashMap = InsOrdHashMap.toHashMap . unCompatInsOrdHashMap

fromHashMap :: HashMap k v -> InsOrdHashMap k v
fromHashMap = InsOrdHashMap . InsOrdHashMap.fromHashMap

-------------------------------------------------------------------------------
-- * Debugging
-------------------------------------------------------------------------------

valid :: (Eq k, Hashable k) => InsOrdHashMap k v -> Bool
valid = InsOrdHashMap.valid . unCompatInsOrdHashMap

#endif