-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from phadej/ordhashmap
Add OrdHashMap
- Loading branch information
Showing
14 changed files
with
683 additions
and
195 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,333 @@ | ||
{-# LANGUAGE CPP #-} | ||
{-# LANGUAGE DataKinds #-} | ||
{-# LANGUAGE FlexibleContexts #-} | ||
{-# LANGUAGE GADTs #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
{-# LANGUAGE ExplicitForAll #-} | ||
{-# LANGUAGE TemplateHaskell #-} | ||
module Data.Swagger.Internal.AesonUtils ( | ||
-- * Generic functions | ||
AesonDefaultValue(..), | ||
sopSwaggerGenericToJSON, | ||
#if MIN_VERSION_aeson(0,10,0) | ||
sopSwaggerGenericToEncoding, | ||
#endif | ||
sopSwaggerGenericToJSONWithOpts, | ||
sopSwaggerGenericParseJSON, | ||
-- * Options | ||
HasSwaggerAesonOptions(..), | ||
SwaggerAesonOptions, | ||
mkSwaggerAesonOptions, | ||
saoPrefix, | ||
saoAdditionalPairs, | ||
saoSubObject, | ||
) where | ||
|
||
import Prelude () | ||
import Prelude.Compat | ||
|
||
import Control.Applicative ((<|>)) | ||
import Control.Lens (makeLenses, (^.)) | ||
import Control.Monad (unless) | ||
import Data.Aeson (ToJSON(..), FromJSON(..), Value(..), Object, object, (.:), (.:?), (.!=), withObject) | ||
import Data.Aeson.Types (Parser, Pair) | ||
import Data.Char (toLower, isUpper) | ||
import Data.Foldable (traverse_) | ||
import Data.Text (Text) | ||
|
||
import Generics.SOP | ||
|
||
import qualified Data.Text as T | ||
import qualified Data.HashMap.Strict as HM | ||
import qualified Data.Set as Set | ||
import qualified Data.HashMap.Strict.InsOrd as InsOrd | ||
|
||
#if MIN_VERSION_aeson(0,10,0) | ||
import Data.Aeson (Encoding, pairs, (.=), Series) | ||
import Data.Monoid ((<>)) | ||
#endif | ||
|
||
------------------------------------------------------------------------------- | ||
-- SwaggerAesonOptions | ||
------------------------------------------------------------------------------- | ||
|
||
data SwaggerAesonOptions = SwaggerAesonOptions | ||
{ _saoPrefix :: String | ||
, _saoAdditionalPairs :: [(Text, Value)] | ||
, _saoSubObject :: Maybe String | ||
} | ||
|
||
mkSwaggerAesonOptions | ||
:: String -- ^ prefix | ||
-> SwaggerAesonOptions | ||
mkSwaggerAesonOptions pfx = SwaggerAesonOptions pfx [] Nothing | ||
|
||
makeLenses ''SwaggerAesonOptions | ||
|
||
class (Generic a, All2 AesonDefaultValue (Code a)) => HasSwaggerAesonOptions a where | ||
swaggerAesonOptions :: proxy a -> SwaggerAesonOptions | ||
|
||
-- So far we use only default definitions | ||
aesonDefaults :: proxy a -> POP Maybe (Code a) | ||
aesonDefaults _ = hcpure (Proxy :: Proxy AesonDefaultValue) defaultValue | ||
|
||
------------------------------------------------------------------------------- | ||
-- Generics | ||
------------------------------------------------------------------------------- | ||
|
||
class AesonDefaultValue a where | ||
defaultValue :: Maybe a | ||
defaultValue = Nothing | ||
|
||
instance AesonDefaultValue Text where defaultValue = Nothing | ||
instance AesonDefaultValue (Maybe a) where defaultValue = Just Nothing | ||
instance AesonDefaultValue [a] where defaultValue = Just [] | ||
instance AesonDefaultValue (Set.Set a) where defaultValue = Just Set.empty | ||
instance AesonDefaultValue (InsOrd.InsOrdHashMap k v) where defaultValue = Just InsOrd.empty | ||
|
||
------------------------------------------------------------------------------- | ||
-- ToJSON | ||
------------------------------------------------------------------------------- | ||
|
||
-- | Generic serialisation for swagger records. | ||
-- | ||
-- Features | ||
-- | ||
-- * omits nulls, empty objects and empty arrays (configurable) | ||
-- * possible to add fields | ||
-- * possible to merge sub-object | ||
sopSwaggerGenericToJSON | ||
:: forall a xs. | ||
( Generic a | ||
, HasDatatypeInfo a | ||
, HasSwaggerAesonOptions a | ||
, All2 ToJSON (Code a) | ||
, All2 Eq (Code a) | ||
, Code a ~ '[xs] | ||
) | ||
=> a | ||
-> Value | ||
sopSwaggerGenericToJSON x = | ||
let ps = sopSwaggerGenericToJSON' opts (from x) (datatypeInfo proxy) (aesonDefaults proxy) | ||
in object (opts ^. saoAdditionalPairs ++ ps) | ||
where | ||
proxy = Proxy :: Proxy a | ||
opts = swaggerAesonOptions proxy | ||
|
||
-- | *TODO:* This is only used by ToJSON (ParamSchema SwaggerKindSchema) | ||
-- | ||
-- Also uses default `aesonDefaults` | ||
sopSwaggerGenericToJSONWithOpts | ||
:: forall a xs. | ||
( Generic a | ||
, All2 AesonDefaultValue (Code a) | ||
, HasDatatypeInfo a | ||
, All2 ToJSON (Code a) | ||
, All2 Eq (Code a) | ||
, Code a ~ '[xs] | ||
) | ||
=> SwaggerAesonOptions | ||
-> a | ||
-> Value | ||
sopSwaggerGenericToJSONWithOpts opts x = | ||
let ps = sopSwaggerGenericToJSON' opts (from x) (datatypeInfo proxy) defs | ||
in object (opts ^. saoAdditionalPairs ++ ps) | ||
where | ||
proxy = Proxy :: Proxy a | ||
defs = hcpure (Proxy :: Proxy AesonDefaultValue) defaultValue | ||
|
||
sopSwaggerGenericToJSON' | ||
:: (All2 ToJSON '[xs], All2 Eq '[xs]) | ||
=> SwaggerAesonOptions | ||
-> SOP I '[xs] | ||
-> DatatypeInfo '[xs] | ||
-> POP Maybe '[xs] | ||
-> [Pair] | ||
sopSwaggerGenericToJSON' opts (SOP (Z fields)) (ADT _ _ (Record _ fieldsInfo :* Nil)) (POP (defs :* Nil)) = | ||
sopSwaggerGenericToJSON'' opts fields fieldsInfo defs | ||
sopSwaggerGenericToJSON' _ _ _ _ = error "sopSwaggerGenericToJSON: unsupported type" | ||
|
||
sopSwaggerGenericToJSON'' | ||
:: (All ToJSON xs, All Eq xs) | ||
=> SwaggerAesonOptions | ||
-> NP I xs | ||
-> NP FieldInfo xs | ||
-> NP Maybe xs | ||
-> [Pair] | ||
sopSwaggerGenericToJSON'' (SwaggerAesonOptions prefix _ sub) = go | ||
where | ||
go :: (All ToJSON ys, All Eq ys) => NP I ys -> NP FieldInfo ys -> NP Maybe ys -> [Pair] | ||
go Nil Nil Nil = [] | ||
go (I x :* xs) (FieldInfo name :* names) (def :* defs) | ||
| Just name' == sub = case json of | ||
Object m -> HM.toList m ++ rest | ||
Null -> rest | ||
_ -> error $ "sopSwaggerGenericToJSON: subjson is not an object: " ++ show json | ||
-- If default value: omit it. | ||
| Just x == def = | ||
rest | ||
| otherwise = | ||
(T.pack name', json) : rest | ||
where | ||
json = toJSON x | ||
name' = fieldNameModifier name | ||
rest = go xs names defs | ||
#if __GLASGOW_HASKELL__ < 800 | ||
go _ _ _ = error "not empty" | ||
#endif | ||
|
||
fieldNameModifier = modifier . drop 1 | ||
modifier = lowerFirstUppers . drop (length prefix) | ||
lowerFirstUppers s = map toLower x ++ y | ||
where (x, y) = span isUpper s | ||
|
||
------------------------------------------------------------------------------- | ||
-- FromJSON | ||
------------------------------------------------------------------------------- | ||
|
||
sopSwaggerGenericParseJSON | ||
:: forall a xs. | ||
( Generic a | ||
, HasDatatypeInfo a | ||
, HasSwaggerAesonOptions a | ||
, All2 FromJSON (Code a) | ||
, All2 Eq (Code a) | ||
, Code a ~ '[xs] | ||
) | ||
=> Value | ||
-> Parser a | ||
sopSwaggerGenericParseJSON = withObject "Swagger Record Object" $ \obj -> | ||
let ps = sopSwaggerGenericParseJSON' opts obj (datatypeInfo proxy) (aesonDefaults proxy) | ||
in do | ||
traverse_ (parseAdditionalField obj) (opts ^. saoAdditionalPairs) | ||
to <$> ps | ||
where | ||
proxy = Proxy :: Proxy a | ||
opts = swaggerAesonOptions proxy | ||
|
||
parseAdditionalField :: Object -> (Text, Value) -> Parser () | ||
parseAdditionalField obj (k, v) = do | ||
v' <- obj .: k | ||
unless (v == v') $ fail $ | ||
"Additonal field don't match for key " ++ T.unpack k | ||
++ ": " ++ show v | ||
++ " /= " ++ show v' | ||
|
||
sopSwaggerGenericParseJSON' | ||
:: (All2 FromJSON '[xs], All2 Eq '[xs]) | ||
=> SwaggerAesonOptions | ||
-> Object | ||
-> DatatypeInfo '[xs] | ||
-> POP Maybe '[xs] | ||
-> Parser (SOP I '[xs]) | ||
sopSwaggerGenericParseJSON' opts obj (ADT _ _ (Record _ fieldsInfo :* Nil)) (POP (defs :* Nil)) = | ||
SOP . Z <$> sopSwaggerGenericParseJSON'' opts obj fieldsInfo defs | ||
sopSwaggerGenericParseJSON' _ _ _ _ = error "sopSwaggerGenericParseJSON: unsupported type" | ||
|
||
sopSwaggerGenericParseJSON'' | ||
:: (All FromJSON xs, All Eq xs) | ||
=> SwaggerAesonOptions | ||
-> Object | ||
-> NP FieldInfo xs | ||
-> NP Maybe xs | ||
-> Parser (NP I xs) | ||
sopSwaggerGenericParseJSON'' (SwaggerAesonOptions prefix _ sub) obj = go | ||
where | ||
go :: (All FromJSON ys, All Eq ys) => NP FieldInfo ys -> NP Maybe ys -> Parser (NP I ys) | ||
go Nil Nil = pure Nil | ||
go (FieldInfo name :* names) (def :* defs) | ||
| Just name' == sub = | ||
-- Note: we might strip fields of outer structure. | ||
cons <$> (withDef $ parseJSON $ Object obj) <*> rest | ||
| otherwise = case def of | ||
Just def' -> cons <$> obj .:? T.pack name' .!= def' <*> rest | ||
Nothing -> cons <$> obj .: T.pack name' <*> rest | ||
where | ||
cons h t = I h :* t | ||
name' = fieldNameModifier name | ||
rest = go names defs | ||
|
||
withDef = case def of | ||
Just def' -> (<|> pure def') | ||
Nothing -> id | ||
#if __GLASGOW_HASKELL__ < 800 | ||
go _ _ = error "not empty" | ||
#endif | ||
|
||
fieldNameModifier = modifier . drop 1 | ||
modifier = lowerFirstUppers . drop (length prefix) | ||
lowerFirstUppers s = map toLower x ++ y | ||
where (x, y) = span isUpper s | ||
|
||
------------------------------------------------------------------------------- | ||
-- ToEncoding | ||
------------------------------------------------------------------------------- | ||
|
||
#if MIN_VERSION_aeson(0,10,0) | ||
|
||
sopSwaggerGenericToEncoding | ||
:: forall a xs. | ||
( Generic a | ||
, HasDatatypeInfo a | ||
, HasSwaggerAesonOptions a | ||
, All2 ToJSON (Code a) | ||
, All2 Eq (Code a) | ||
, Code a ~ '[xs] | ||
) | ||
=> a | ||
-> Encoding | ||
sopSwaggerGenericToEncoding x = | ||
let ps = sopSwaggerGenericToEncoding' opts (from x) (datatypeInfo proxy) (aesonDefaults proxy) | ||
in pairs (pairsToSeries (opts ^. saoAdditionalPairs) <> ps) | ||
where | ||
proxy = Proxy :: Proxy a | ||
opts = swaggerAesonOptions proxy | ||
|
||
pairsToSeries :: [Pair] -> Series | ||
pairsToSeries = foldMap (\(k, v) -> (k .= v)) | ||
|
||
sopSwaggerGenericToEncoding' | ||
:: (All2 ToJSON '[xs], All2 Eq '[xs]) | ||
=> SwaggerAesonOptions | ||
-> SOP I '[xs] | ||
-> DatatypeInfo '[xs] | ||
-> POP Maybe '[xs] | ||
-> Series | ||
sopSwaggerGenericToEncoding' opts (SOP (Z fields)) (ADT _ _ (Record _ fieldsInfo :* Nil)) (POP (defs :* Nil)) = | ||
sopSwaggerGenericToEncoding'' opts fields fieldsInfo defs | ||
sopSwaggerGenericToEncoding' _ _ _ _ = error "sopSwaggerGenericToEncoding: unsupported type" | ||
|
||
sopSwaggerGenericToEncoding'' | ||
:: (All ToJSON xs, All Eq xs) | ||
=> SwaggerAesonOptions | ||
-> NP I xs | ||
-> NP FieldInfo xs | ||
-> NP Maybe xs | ||
-> Series | ||
sopSwaggerGenericToEncoding'' (SwaggerAesonOptions prefix _ sub) = go | ||
where | ||
go :: (All ToJSON ys, All Eq ys) => NP I ys -> NP FieldInfo ys -> NP Maybe ys -> Series | ||
go Nil Nil Nil = mempty | ||
go (I x :* xs) (FieldInfo name :* names) (def :* defs) | ||
| Just name' == sub = case toJSON x of | ||
Object m -> pairsToSeries (HM.toList m) <> rest | ||
Null -> rest | ||
_ -> error $ "sopSwaggerGenericToJSON: subjson is not an object: " ++ show (toJSON x) | ||
-- If default value: omit it. | ||
| Just x == def = | ||
rest | ||
| otherwise = | ||
(T.pack name' .= x) <> rest | ||
where | ||
name' = fieldNameModifier name | ||
rest = go xs names defs | ||
#if __GLASGOW_HASKELL__ < 800 | ||
go _ _ _ = error "not empty" | ||
#endif | ||
|
||
fieldNameModifier = modifier . drop 1 | ||
modifier = lowerFirstUppers . drop (length prefix) | ||
lowerFirstUppers s = map toLower x ++ y | ||
where (x, y) = span isUpper s | ||
|
||
#endif |
Oops, something went wrong.