module Data.API.JSONToCBOR
( serialiseJSONWithSchema
, jsonToCBORWithSchema
, deserialiseJSONWithSchema
, postprocessJSON
) where
import Data.API.Changes
import Data.API.JSON
import Data.API.Types
import Data.API.Utils
import Control.Applicative
import Data.Aeson hiding (encode)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as HMap
import qualified Data.Map as Map
import Data.Traversable
import qualified Data.Vector as Vec
import Codec.Serialise as CBOR
import Data.Binary.Serialise.CBOR.JSON (cborToJson, jsonToCbor)
import Codec.CBOR.Term
import Data.Fixed (Pico)
import Data.Maybe (fromMaybe)
import Data.Scientific
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time.Clock.POSIX
import Data.Time (UTCTime(UTCTime))
import Prelude
serialiseJSONWithSchema :: API -> TypeName -> Value -> LBS.ByteString
serialiseJSONWithSchema api tn v = serialise $ jsonToCBORWithSchema api tn v
jsonToCBORWithSchema :: API -> TypeName -> Value -> Term
jsonToCBORWithSchema = jsonToCBORTypeName . apiNormalForm
jsonToCBORTypeName :: NormAPI -> TypeName -> Value -> Term
jsonToCBORTypeName napi tn v =
case Map.lookup tn napi of
Just (NRecordType nrt) -> jsonToCBORRecord napi nrt v
Just (NUnionType nut) -> jsonToCBORUnion napi nut v
Just (NEnumType net) -> jsonToCBOREnum napi net v
Just (NTypeSynonym ty) -> jsonToCBORType napi ty v
Just (NNewtype bt) -> jsonToCBORBasic bt v
Nothing -> error $ "serialiseJSONWithSchema: missing definition for type "
++ T.unpack (_TypeName tn)
jsonToCBORType :: NormAPI -> APIType -> Value -> Term
jsonToCBORType napi ty0 v = case (ty0, v) of
(TyList ty, Array arr) | Vec.null arr -> TList []
| otherwise -> TListI $ map (jsonToCBORType napi ty) (Vec.toList arr)
(TyList _ , _) -> error "serialiseJSONWithSchema: expected array"
(TyMaybe _ , Null) -> TList []
(TyMaybe ty, _) -> TList [jsonToCBORType napi ty v]
(TyName tn, _) -> jsonToCBORTypeName napi tn v
(TyBasic bt, _) -> jsonToCBORBasic bt v
(TyJSON , _) -> jsonToCbor v
jsonToCBORRecord :: NormAPI -> NormRecordType -> Value -> Term
jsonToCBORRecord napi nrt v = case v of
Object hm -> TMap $ map (f hm) $ Map.toAscList nrt
_ -> error "serialiseJSONWithSchema: expected object"
where
f hm (fn, ty) = case HMap.lookup (_FieldName fn) hm of
Nothing -> error $ "serialiseJSONWithSchema: missing field " ++ T.unpack (_FieldName fn)
Just v' -> (TString (_FieldName fn), jsonToCBORType napi ty v')
jsonToCBORUnion :: NormAPI -> NormUnionType -> Value -> Term
jsonToCBORUnion napi nut v = case v of
Object hm | [(k, r)] <- HMap.toList hm -> case Map.lookup (FieldName k) nut of
Just ty -> TMap [(TString k, jsonToCBORType napi ty r)]
Nothing -> error "serialiseJSONWithSchema: unexpected alternative in union"
_ -> error "serialiseJSONWithSchema: expected single-field object"
jsonToCBOREnum :: NormAPI -> NormEnumType -> Value -> Term
jsonToCBOREnum _ _ v = case v of
String t -> TString t
_ -> error "serialiseJSONWithSchema: expected string"
jsonToCBORBasic :: BasicType -> Value -> Term
jsonToCBORBasic bt v = case (bt, v) of
(BTstring, String t) -> TString t
(BTstring, _) -> error "serialiseJSONWithSchema: expected string"
(BTbinary, String t) -> case B64.decode $ TE.encodeUtf8 t of
Left err-> error $ "serialiseJSONWithSchema: base64-decoding failed: " ++ err
Right bs -> TBytes bs
(BTbinary, _) -> error "serialiseJSONWithSchema: expected string"
(BTbool , Bool b) -> TBool b
(BTbool , _) -> error "serialiseJSONWithSchema: expected bool"
(BTint , Number n) | Right i <- (floatingOrInteger n :: Either Double Int) -> TInt i
(BTint , _) -> error "serialiseJSONWithSchema: expected integer"
(BTutc , String t) ->
TTagged 1000 (TMap [ (TInt 1, TInt secs)
, (TInt (12), TInt psecs) ])
where
(secs, frac) = case properFraction $ utcTimeToPOSIXSeconds utc of
(secs', frac')
| frac' < 0 -> (secs' 1, frac' + 1)
| otherwise -> (secs', frac')
psecs = round $ frac * 1000000000000
utc = fromMaybe (error $ "jsonToCBORBasic: " ++ T.unpack t) $
parseUTC' t
(BTutc , _) -> error "serialiseJSONWithSchema: expected string"
deserialiseJSONWithSchema :: API -> TypeName -> LBS.ByteString -> Value
deserialiseJSONWithSchema api tn bs = case postprocessJSON api tn (cborToJson (deserialise bs)) of
Right v -> v
Left err -> error $ "deserialiseJSONWithSchema could not post-process: " ++ prettyValueError err
postprocessJSON :: API -> TypeName -> Value -> Either ValueError Value
postprocessJSON api = postprocessJSONTypeName (apiNormalForm api)
postprocessJSONTypeName :: NormAPI -> TypeName -> Value -> Either ValueError Value
postprocessJSONTypeName napi tn v = do
t <- Map.lookup tn napi ?! InvalidAPI (TypeDoesNotExist tn)
case t of
NRecordType nrt -> postprocessJSONRecord napi nrt v
NUnionType nut -> postprocessJSONUnion napi nut v
NEnumType _ -> pure v
NTypeSynonym ty -> postprocessJSONType napi ty v
NNewtype bt -> postprocessJSONType napi (TyBasic bt) v
postprocessJSONType :: NormAPI -> APIType -> Value -> Either ValueError Value
postprocessJSONType napi ty0 v = case ty0 of
TyList ty -> case v of
Array arr -> Array <$> traverse (postprocessJSONType napi ty) arr
_ -> Left $ JSONError $ expectedArray v
TyMaybe ty -> case v of
Array arr -> case Vec.toList arr of
[] -> pure Null
[v1] -> postprocessJSONType napi ty v1
_:_:_ -> Left $ JSONError $ SyntaxError "over-long array when converting Maybe value"
_ -> Left $ JSONError $ expectedArray v
TyName tn -> postprocessJSONTypeName napi tn v
TyBasic BTutc -> case v of
Object obj -> case HMap.toList obj of
[(k1, Number v0), (km12, Number v1)]
| T.unpack k1 == "1" && T.unpack km12 == "-12" ->
let psecs :: Pico
psecs = realToFrac v1 / 1000000000000
dt :: POSIXTime
dt = realToFrac v0 + realToFrac psecs
in pure $! String $! mkUTC' $! forceUTCTime (posixSecondsToUTCTime dt)
_ -> Left $ JSONError UnexpectedField
String t -> case parseUTC' t of
Nothing -> Left $ JSONError $ SyntaxError $
"UTC time in wrong format: " ++ T.unpack t
Just utcTime -> pure $! String $! mkUTC' $! forceUTCTime utcTime
_ -> Left $ JSONError $ expectedObject v
TyBasic _ -> pure v
TyJSON -> pure v
forceUTCTime :: UTCTime -> UTCTime
forceUTCTime t@(UTCTime !_day !_daytime) = t
postprocessJSONRecord :: NormAPI -> NormRecordType -> Value -> Either ValueError Value
postprocessJSONRecord napi nrt v = case v of
Object hm -> Object <$> HMap.traverseWithKey f hm
_ -> Left $ JSONError $ expectedObject v
where
f t v' = do ty <- Map.lookup (FieldName t) nrt ?! JSONError UnexpectedField
postprocessJSONType napi ty v'
postprocessJSONUnion :: NormAPI -> NormUnionType -> Value -> Either ValueError Value
postprocessJSONUnion napi nut v = case v of
Object hm | [(k, r)] <- HMap.toList hm
, Just ty <- Map.lookup (FieldName k) nut
-> Object . HMap.singleton k <$> postprocessJSONType napi ty r
_ -> Left $ JSONError $ expectedObject v