candid 0.3.2.1 → 0.4
raw patch · 17 files changed
+813/−637 lines, 17 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Codec.Candid: FutureT :: Type a
+ Codec.Candid: FutureV :: Value
+ Codec.Candid: isSubtypeOf :: (Pretty k1, Pretty k2, Ord k1, Ord k2) => Type (Ref k1 Type) -> Type (Ref k2 Type) -> Either String ()
+ Codec.Candid.TestExports: typeGraph :: forall a. Candid a => Type (Ref TypeRep Type)
+ Codec.Candid.TestExports: unrollTypeTable :: SeqDesc -> (forall k. (Pretty k, Ord k) => [Type (Ref k Type)] -> r) -> r
- Codec.Candid.TestExports: generateCandidDefs :: [DidDef TypeName] -> Q ([Dec], TypeName -> Q Name)
+ Codec.Candid.TestExports: generateCandidDefs :: Text -> [DidDef TypeName] -> Q ([Dec], TypeName -> Q Name)
Files
- CHANGELOG.md +6/−0
- candid.cabal +3/−1
- src/Codec/Candid.hs +23/−7
- src/Codec/Candid/Class.hs +5/−3
- src/Codec/Candid/Coerce.hs +91/−177
- src/Codec/Candid/Decode.hs +9/−1
- src/Codec/Candid/Encode.hs +2/−0
- src/Codec/Candid/Infer.hs +1/−0
- src/Codec/Candid/Parse.hs +1/−1
- src/Codec/Candid/Subtype.hs +185/−0
- src/Codec/Candid/TH.hs +6/−5
- src/Codec/Candid/TestExports.hs +10/−0
- src/Codec/Candid/TypTable.hs +2/−1
- src/Codec/Candid/Types.hs +6/−0
- test/SpecTests.hs +6/−3
- test/Tests.hs +454/−0
- test/test.hs +3/−438
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for haskell-candid +## 0.4-- 2022-11-05++* Fix did file parsing bug: Allow underscores in unicode escapes+* Implement the new subtyping rules from spec version 0.1.4+ https://github.com/dfinity/candid/pull/311+ ## 0.3.2.1 -- 2022-12-01 * GHC-9.2 compatibility
candid.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: candid-version: 0.3.2.1+version: 0.4 license: Apache license-file: LICENSE maintainer: mail@joachim-breitner.de@@ -41,6 +41,7 @@ Codec.Candid.EncodeTextual Codec.Candid.Encode Codec.Candid.Infer+ Codec.Candid.Subtype Codec.Candid.Coerce default-language: Haskell2010@@ -89,6 +90,7 @@ hs-source-dirs: test other-modules: SpecTests+ Tests THTests default-language: Haskell2010
src/Codec/Candid.hs view
@@ -1,8 +1,6 @@ {-| -This module provides preliminary Haskell supprot for decoding and encoding the __Candid__ data format. See <https://github.com/dfinity/candid/blob/master/spec/Candid.md> for the official Candid specification.--__Warning:__ The interface of this library is still in flux, as we are yet learning the best idioms around Candid and Haskell.+This module provides Haskell support for decoding and encoding the __Candid__ data format. See <https://github.com/dfinity/candid/blob/master/spec/Candid.md> for the official Candid specification. -} @@ -49,7 +47,6 @@ {- | * Generating interface descriptions (.did files) from Haskell functions-* Future types * Parsing the textual representation dynamically against an expected type -}@@ -118,6 +115,7 @@ , unescapeFieldName , candidHash , Value(..)+ , isSubtypeOf -- ** Dynamic use @@ -150,15 +148,18 @@ import Codec.Candid.Decode import Codec.Candid.Encode import Codec.Candid.EncodeTextual+import Codec.Candid.Subtype -- $setup -- >>> :set -dppr-cols=200 -- >>> import Data.Text (Text) -- >>> import qualified Data.Text as T--- >>> import Data.Void (Void)+-- >>> import Data.Void (Void, vacuous) -- >>> import Prettyprinter (pretty) -- >>> import qualified Data.ByteString.Lazy.Char8 as BS+-- >>> import Numeric.Natural -- >>> :set -XScopedTypeVariables+-- >>> :set -XTypeApplications {- $haskell_types @@ -169,7 +170,7 @@ >>> decode (encode ([True, False], Just 100)) == Right ([True, False], Just 100) True -Here, no type annotations are needed, the library can infer them from the types of the Haskell values. You can see the Candid types used using `typeDesc` and `seqDesc`:+Here, no type annotations are needed, the library can infer them from the types of the Haskell values. You can see the Candid types used using `seqDesc` (with `tieKnot`) for an argument sequence, or `typeDesc` for a single type: >>> :type +d ([True, False], Just 100) ([True, False], Just 100) :: ([Bool], Maybe Integer)@@ -188,6 +189,8 @@ >>> pretty (typeDesc @(Rec ("bar" .== Maybe Integer .+ "foo" .== [Bool]))) record {bar : opt int; foo : vec bool} +NB: `typeDesc` cannot work with recursive types, but see `seqDesc` together with `tieKnot`.+ -} {- $own_type@@ -375,7 +378,7 @@ Sometimes one needs to interact with Candid in a dynamic way, without static type information. -This library allows the parsing and pretty-printing of candid values. The binary value was copied from above:+This library allows the parsing and pretty-printing of candid values: >>> import Data.Row >>> :set -XDataKinds -XTypeOperators@@ -401,6 +404,19 @@ Right (#bar .== Just 100 .+ #foo .== [True,False]) This function does not support the full textual format yet; in particular type annotations can only be used around number literals.++Related to dynamic use is the ability to perform a subtype check, using 'isSubtypeOf' (but you have to set up the arguments correctly first):++>>> isSubtypeOf (vacuous $ typeDesc @Natural) (vacuous $ typeDesc @Integer)+Right ()+>>> isSubtypeOf (vacuous $ typeDesc @Integer) (vacuous $ typeDesc @Natural)+Left "Type int is not a subtype of nat"+>>> isSubtypeOf (vacuous $ typeDesc @(Rec ("foo" .== [Bool]))) (vacuous $ typeDesc @(Rec ("bar" .== Maybe Integer .+ "foo" .== Maybe [Bool])))+Right ()+>>> isSubtypeOf (vacuous $ typeDesc @(Rec ("bar" .== Maybe Integer .+ "foo" .== Maybe [Bool]))) (vacuous $ typeDesc @(Rec ("foo" .== [Bool])))+Left "Type opt vec bool is not a subtype of vec bool"+>>> isSubtypeOf (vacuous $ typeDesc @(Rec ("bar" .== Integer))) (vacuous $ typeDesc @(Rec ("foo" .== Integer)))+Left "Missing record field foo of type int" -}
src/Codec/Candid/Class.hs view
@@ -73,8 +73,7 @@ -- Decode (ts, vs) <- decodeVals b -- Coerce to expected type- c <- coerceSeqDesc ts (buildSeqDesc (asTypes @(AsTuple a)))- vs' <- c vs+ vs' <- coerceSeqDesc vs ts (buildSeqDesc (asTypes @(AsTuple a))) fromCandidVals vs' -- | Decode (dynamic) values to Haskell type@@ -106,9 +105,12 @@ seqDesc :: forall a. CandidArg a => SeqDesc seqDesc = buildSeqDesc (asTypes @(AsTuple a)) +typeGraph :: forall a. Candid a => Type (Ref TypeRep Type)+typeGraph = asType @(AsCandid a)+ -- | NB: This will loop with recursive types! typeDesc :: forall a. Candid a => Type Void-typeDesc = asType @(AsCandid a) >>= go+typeDesc = typeGraph @a >>= go where go (Ref _ t) = t >>= go instance Pretty TypeRep where
src/Codec/Candid/Coerce.hs view
@@ -5,9 +5,7 @@ {-# LANGUAGE FlexibleContexts #-} module Codec.Candid.Coerce ( coerceSeqDesc- , SeqCoercion , coerce- , Coercion ) where @@ -15,243 +13,159 @@ import qualified Data.Vector as V import qualified Data.ByteString.Lazy as BS import qualified Data.Map as M-import Data.Bifunctor-import Data.List-import Data.Tuple import Control.Monad.State.Lazy import Control.Monad.Except import Codec.Candid.FieldName import Codec.Candid.Types import Codec.Candid.TypTable--type SeqCoercion = [Value] -> Either String [Value]-type Coercion = Value -> Either String Value+import Codec.Candid.Subtype -coerceSeqDesc :: SeqDesc -> SeqDesc -> Either String SeqCoercion-coerceSeqDesc sd1 sd2 =+coerceSeqDesc :: [Value] -> SeqDesc -> SeqDesc -> Either String [Value]+coerceSeqDesc vs sd1 sd2 = unrollTypeTable sd1 $ \ts1 -> unrollTypeTable sd2 $ \ts2 ->- coerceSeq ts1 ts2+ coerceSeq vs ts1 ts2 coerceSeq :: (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ [Value] -> [Type (Ref k1 Type)] -> [Type (Ref k2 Type)] ->- Either String SeqCoercion-coerceSeq t1 t2 = runM $ goSeq t1 t2+ Either String [Value]+coerceSeq vs t1 t2 = runSubTypeM $ goSeq vs t1 t2 --- | This function implements the `C[<t> <: <t>]` coercion function from the--- spec. It returns `Left` if no subtyping relation holds, or `Right c` if it--- holds, together with a coercion function.+-- | This function implements the @V : T ~> V' : T'@ relation from the Candid spec. ----- The coercion function itself is not total because the intput value isn’t--- typed, so we have to cater for errors there. It should not fail if the--- passed value really is inherently of the input type.+-- Because values in this library are untyped, we have to pass what we know about+-- their type down, so that we can do the subtype check upon a reference.+-- The given type must match the value closely (as it is the case when decoding+-- from the wire) and this function may behave oddly if @v@ and @t1@ are not related. ----- In a dependently typed language we’d maybe have something like--- `coerce :: foreach t1 -> foreach t2 -> Either String (t1 -> t2)`--- instead, and thus return a total function+-- Morally, this function looks only at @v@ and @t2@. It only needs @t1@ for+-- refences, and hence needs to take @t2@ apart for the recursive calls.+-- Practically, it's sometimes more concise to look at t2 instead of v. coerce :: (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ Value -> Type (Ref k1 Type) -> Type (Ref k2 Type) ->- Either String Coercion-coerce t1 t2 = runM $ memo t1 t2--type Memo k1 k2 =- (M.Map (Type (Ref k1 Type), Type (Ref k2 Type)) Coercion,- M.Map (Type (Ref k2 Type), Type (Ref k1 Type)) Coercion)-type M k1 k2 = ExceptT String (State (Memo k1 k2))--runM :: (Ord k1, Ord k2) => M k1 k2 a -> Either String a-runM act = evalState (runExceptT act) (mempty, mempty)--flipM :: M k1 k2 a -> M k2 k1 a-flipM (ExceptT (StateT f)) = ExceptT (StateT f')- where- f' (m1,m2) = second swap <$> f (m2,m1) -- f (m2,m1) >>= \case (r, (m2',m1')) -> pure (r, (m1', m2'))+ Either String Value+coerce v t1 t2 = runSubTypeM $ go v t1 t2 -memo, go ::+go :: (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ Value -> Type (Ref k1 Type) -> Type (Ref k2 Type) ->- M k1 k2 Coercion+ SubTypeM k1 k2 Value goSeq :: (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ [Value] -> [Type (Ref k1 Type)] -> [Type (Ref k2 Type)] ->- M k1 k2 SeqCoercion----- Memoization uses lazyiness: When we see a pair for the first time,--- we optimistically put the resulting coercion into the map.--- Either the following recursive call will fail (but then this optimistic--- value was never used), or it will succeed, but then the guess was correct.-memo t1 t2 = do- gets (M.lookup (t1,t2) . fst) >>= \case- Just c -> pure c- Nothing -> mdo- modify (first (M.insert (t1,t2) c))- c <- go t1 t2- return c+ SubTypeM k1 k2 [Value] -- Look through refs-go (RefT (Ref _ t1)) t2 = memo t1 t2-go t1 (RefT (Ref _ t2)) = memo t1 t2+go v (RefT (Ref _ t1)) t2 = go v t1 t2+go v t1 (RefT (Ref _ t2)) = go v t1 t2 -- Identity coercion for primitive values-go NatT NatT = pure pure-go Nat8T Nat8T = pure pure-go Nat16T Nat16T = pure pure-go Nat32T Nat32T = pure pure-go Nat64T Nat64T = pure pure-go IntT IntT = pure pure-go Int8T Int8T = pure pure-go Int16T Int16T = pure pure-go Int32T Int32T = pure pure-go Int64T Int64T = pure pure-go Float32T Float32T = pure pure-go Float64T Float64T = pure pure-go BoolT BoolT = pure pure-go TextT TextT = pure pure-go NullT NullT = pure pure-go PrincipalT PrincipalT = pure pure+go v NatT NatT = pure v+go v Nat8T Nat8T = pure v+go v Nat16T Nat16T = pure v+go v Nat32T Nat32T = pure v+go v Nat64T Nat64T = pure v+go v IntT IntT = pure v+go v Int8T Int8T = pure v+go v Int16T Int16T = pure v+go v Int32T Int32T = pure v+go v Int64T Int64T = pure v+go v Float32T Float32T = pure v+go v Float64T Float64T = pure v+go v BoolT BoolT = pure v+go v TextT TextT = pure v+go v NullT NullT = pure v+go v PrincipalT PrincipalT = pure v -- Nat <: Int-go NatT IntT = pure $ \case- NatV n -> pure $ IntV (fromIntegral n)- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing nat <: int"+go (NatV n) NatT IntT = pure $ IntV (fromIntegral n) -- t <: reserved-go _ ReservedT = pure (const (pure ReservedV))+go _ _ ReservedT = pure ReservedV --- empty <: t-go EmptyT _ = pure $ \v ->- throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing empty"+-- empty <: t (actually just a special case of `v :/ t`)+go v EmptyT _ = throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing empty" -- vec t1 <: vec t2-go (VecT t1) (VecT t2) = do- c <- memo t1 t2- pure $ \case- VecV vs -> VecV <$> mapM c vs- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing vector"+go (VecV vs) (VecT t1) (VecT t2) = VecV <$> mapM (\v -> go v t1 t2) vs -- Option: The normal rule-go (OptT t1) (OptT t2) = lift (runExceptT (memo t1 t2)) >>= \case- Right c -> pure $ \case- OptV Nothing -> pure (OptV Nothing)- OptV (Just v) -> OptV . Just <$> c v- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing option"- Left _ -> pure (const (pure (OptV Nothing)))+go (OptV Nothing) (OptT _) (OptT _) = pure NullV+go (OptV (Just v)) (OptT t1) (OptT t2) =+ lift (runExceptT (go v t1 t2)) >>= \case+ Right v' -> pure (OptV (Just v'))+ Left _ -> pure (OptV Nothing) -- Option: The constituent rule-go t (OptT t2) | not (isOptLike t2) = lift (runExceptT (memo t t2)) >>= \case- Right c -> pure $ \v -> OptV . Just <$> c v- Left _ -> pure (const (pure (OptV Nothing)))+go v t1 (OptT t2) | not (isOptLike t2) =+ lift (runExceptT (go v t1 t2)) >>= \case+ Right v' -> pure (OptV (Just v'))+ Left _ -> pure (OptV Nothing)+ -- Option: The fallback rule-go _ (OptT _) = pure (const (pure (OptV Nothing)))+go _ _ (OptT _) = pure (OptV Nothing) -- Records-go (RecT fs1) (RecT fs2) = do- let m1 = M.fromList fs1- let m2 = M.fromList fs2- new_fields <- sequence- [ case unRef t of- OptT _ -> pure (fn, OptV Nothing)- ReservedT -> pure (fn, ReservedV)- t -> throwError $ show $ "Missing record field" <+> pretty fn <+> "of type" <+> pretty t- | (fn, t) <- M.toList $ m2 M.\\ m1- ]- field_coercions <- sequence- [ do c <- memo t1 t2- pure $ \vm -> case M.lookup fn vm of- Nothing -> throwError $ show $ "Record value lacks field" <+> pretty fn <+> "of type" <+> pretty t1- Just v -> (fn, ) <$> c v- | (fn, (t1, t2)) <- M.toList $ M.intersectionWith (,) m1 m2- ]- pure $ \case- TupV ts -> do- let vm = M.fromList $ zip [hashedField n | n <- [0..]] ts- coerced_fields <- mapM ($ vm) field_coercions- return $ RecV $ sortOn fst $ coerced_fields <> new_fields- RecV fvs -> do- let vm = M.fromList fvs- coerced_fields <- mapM ($ vm) field_coercions- return $ RecV $ sortOn fst $ coerced_fields <> new_fields+go rv (RecT fs1) (RecT fs2) = do+ vm <- case rv of+ TupV ts -> pure $ M.fromList $ zip [hashedField n | n <- [0..]] ts+ RecV fvs -> pure $ M.fromList fvs v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing record" + let m1 = M.fromList fs1+ fmap RecV $ forM fs2 $ \(fn, t2) -> (fn,) <$>+ case (M.lookup fn vm, M.lookup fn m1) of+ (Just v, Just t1) -> go v t1 t2+ _ -> case unRef t2 of+ OptT _ -> pure (OptV Nothing)+ ReservedT -> pure ReservedV+ t -> throwError $ show $ "Missing record field" <+> pretty fn <+> "of type" <+> pretty t+ -- Variants-go (VariantT fs1) (VariantT fs2) = do+go (VariantV fn v) (VariantT fs1) (VariantT fs2) = do let m1 = M.fromList fs1 let m2 = M.fromList fs2- cm <- M.traverseWithKey (\fn t1 ->- case M.lookup fn m2 of- Just t2 -> memo t1 t2- Nothing -> throwError $ show $ "Missing variant field" <+> pretty fn <+> "of type" <+> pretty t1- ) m1- pure $ \case- VariantV fn v | Just c <- M.lookup fn cm -> VariantV fn <$> c v- | otherwise -> throwError $ show $ "Unexpected variant field" <+> pretty fn- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing variant"+ case (M.lookup fn m1, M.lookup fn m2) of+ (Just t1, Just t2) -> VariantV fn <$> go v t1 t2+ (Nothing, _) -> throwError $ show $ "Wrongly typed variant missing field " <+> pretty fn+ (_, Nothing) -> throwError $ show $ "Unexpected variant field" <+> pretty fn -- Reference types-go (FuncT mt1) (FuncT mt2) = goMethodType mt1 mt2 >> pure pure-go (ServiceT meths1) (ServiceT meths2) = do- let m1 = M.fromList meths1- forM_ meths2 $ \(m, mt2) -> case M.lookup m m1 of- Just mt1 -> goMethodType mt1 mt2- Nothing -> throwError $ show $ "Missing service method" <+> pretty m <+> "of type" <+> pretty mt2- pure pure+go v t1@(FuncT _) t2@(FuncT _) = isSubtypeOfM t1 t2 >> pure v+go v t1@(ServiceT _) t2@(ServiceT _) = isSubtypeOfM t1 t2 >> pure v -- BlobT-go BlobT BlobT = pure pure-go (VecT t) BlobT | isNat8 t = pure $ \case- VecV vs -> BlobV . BS.pack . V.toList <$> mapM goNat8 vs- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing vec nat8 to blob"+go v BlobT BlobT = pure v+go (VecV vs) (VecT t) BlobT | isNat8 t = BlobV . BS.pack . V.toList <$> mapM goNat8 vs where goNat8 (Nat8V n) = pure n goNat8 v = throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing vec nat8 to blob"-go BlobT (VecT t) | isNat8 t = pure $ \case- BlobV b -> return $ VecV $ V.fromList $ map (Nat8V . fromIntegral) $ BS.unpack b- v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing blob to vec nat8"--go t1 t2 = throwError $ show $ "Type" <+> pretty t1 <+> "is not a subtype of" <+> pretty t2+go (BlobV b) BlobT (VecT t) | isNat8 t = pure $ VecV $ V.fromList $ map (Nat8V . fromIntegral) $ BS.unpack b -goMethodType ::- (Pretty k2, Pretty k1, Ord k2, Ord k1) =>- MethodType (Ref k1 Type) ->- MethodType (Ref k2 Type) ->- M k1 k2 ()-goMethodType (MethodType ta1 tr1 q1 o1) (MethodType ta2 tr2 q2 o2) = do- unless (q1 == q2) $ throwError "Methods differ in query annotation"- unless (o1 == o2) $ throwError "Methods differ in oneway annotation"- void $ flipM $ goSeq ta2 ta1- void $ goSeq tr1 tr2+go v t1 t2 = throwError $ show $ "Cannot coerce " <+> pretty v <+> ":" <+> pretty t1 <+> "to type " <+> pretty t2 -goSeq _ [] = pure (const (return []))-goSeq ts1 (RefT (Ref _ t) : ts) = goSeq ts1 (t:ts)-goSeq ts1@[] (NullT : ts) = do- cs2 <- goSeq ts1 ts- pure $ \_vs -> (NullV :) <$> cs2 []-goSeq ts1@[] (OptT _ : ts) = do- cs2 <- goSeq ts1 ts- pure $ \_vs -> (OptV Nothing :) <$> cs2 []-goSeq ts1@[] (ReservedT : ts) = do- cs2 <- goSeq ts1 ts- pure $ \_vs -> (ReservedV :) <$> cs2 []-goSeq [] ts =- throwError $ show $ "Argument type list too short, expecting types" <+> pretty ts-goSeq (t1:ts1) (t2:ts2) = do- c1 <- memo t1 t2- cs2 <- goSeq ts1 ts2- pure $ \case- [] -> throwError $ show $ "Expecting value of type:" <+> pretty t1- (v:vs) -> do- v' <- c1 v- vs' <- cs2 vs- return (v':vs')+goSeq _ _ [] = pure []+goSeq vs ts1 (RefT (Ref _ t) : ts) = goSeq vs ts1 (t:ts)+goSeq vs@[] ts1@[] (OptT _ : ts) = (OptV Nothing :) <$> goSeq vs ts1 ts+goSeq vs@[] ts1@[] (ReservedT : ts) = (ReservedV :) <$> goSeq vs ts1 ts+goSeq [] [] ts = throwError $ show $ "Argument type list too short, expecting types" <+> pretty ts+goSeq (v:vs) (t1:ts1) (t2:ts2) = do+ v' <- go v t1 t2+ vs' <- goSeq vs ts1 ts2+ pure $ v' : vs'+goSeq _ _ _ = throwError $ "Illtyped input to goSeq" unRef :: Type (Ref a Type) -> Type (Ref a Type) unRef (RefT (Ref _ t)) = unRef t
src/Codec/Candid/Decode.hs view
@@ -88,6 +88,12 @@ PrincipalV <$> decodePrincipal decodeVal EmptyT = fail "Empty value"+decodeVal FutureT = do+ m <- getLEB128Int+ _n <- getLEB128Int @Natural+ _ <- G.getLazyByteString m+ pure FutureV+ decodeVal (RefT v) = absurd v referenceByte :: G.Get ()@@ -168,7 +174,9 @@ unless (isOrdered (map fst m)) $ fail "Service methods not in strict order" return (Right m)- _ -> fail "Unknown structural type"+ _ -> do+ _ <- getLEB128Int >>= G.getLazyByteString+ return (Left FutureT) decodeTypRef :: Natural -> G.Get (Type Int) decodeTypRef max = do
src/Codec/Candid/Encode.hs view
@@ -185,6 +185,8 @@ PrincipalT -> return $ -24 + FutureT -> error "Cannot encode a future type"+ -- Short-hands BlobT -> addCon t $ -- blob = vec nat8
src/Codec/Candid/Infer.hs view
@@ -42,6 +42,7 @@ inferTyp (FuncV _ _) = return (FuncT (MethodType [] [] False False)) -- no principal type inferTyp (ServiceV _) = return (ServiceT []) -- no principal type inferTyp (PrincipalV _) = return PrincipalT+inferTyp FutureV = return FutureT inferTyp (BlobV _) = return BlobT inferTyp (AnnV _ t) = return t -- Maybe do type checking?
src/Codec/Candid/Parse.hs view
@@ -137,7 +137,7 @@ _ -> fail $ "Invalid hex string " ++ show raw hexdigit :: Parser Char-hexdigit = oneOf "0123456789ABCDEFabcdef"+hexdigit = oneOf "0123456789ABCDEFabcdef" <|> char '_' *> hexdigit -- slightly too liberal: allows leading _ seqP :: Parser [Type TypeName] seqP = parenComma argTypeP
+ src/Codec/Candid/Subtype.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE FlexibleContexts #-}+module Codec.Candid.Subtype+ ( isSubtypeOf+ , SubTypeM+ , runSubTypeM+ , isSubtypeOfM+ )+ where++import Prettyprinter+import qualified Data.Map as M+import Data.Bifunctor+import Data.Tuple+import Control.Monad.State.Lazy+import Control.Monad.Except+import Control.Monad.Trans.Except++import Codec.Candid.Types+import Codec.Candid.TypTable++type Memo k1 k2 =+ (M.Map (Type (Ref k1 Type), Type (Ref k2 Type)) (Either String ()),+ M.Map (Type (Ref k2 Type), Type (Ref k1 Type)) (Either String ()))++type SubTypeM k1 k2 = ExceptT String (State (Memo k1 k2))++runSubTypeM :: (Ord k1, Ord k2) => SubTypeM k1 k2 a -> Either String a+runSubTypeM act = evalState (runExceptT act) (mempty, mempty)++-- | Returns 'Right' if the first argument is a subtype of the second, or+-- returns 'Left' with an explanation if not+isSubtypeOf ::+ (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ Type (Ref k1 Type) ->+ Type (Ref k2 Type) ->+ Either String ()+isSubtypeOf t1 t2 = runSubTypeM $ isSubtypeOfM t1 t2++isSubtypeOfM ::+ (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ Type (Ref k1 Type) ->+ Type (Ref k2 Type) ->+ SubTypeM k1 k2 ()+isSubtypeOfM t1 t2 = memo t1 t2++flipM :: SubTypeM k1 k2 a -> SubTypeM k2 k1 a+flipM (ExceptT (StateT f)) = ExceptT (StateT f')+ where+ f' (m1,m2) = second swap <$> f (m2,m1) -- f (m2,m1) >>= \case (r, (m2',m1')) -> pure (r, (m1', m2'))++memo, go ::+ (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ Type (Ref k1 Type) ->+ Type (Ref k2 Type) ->+ SubTypeM k1 k2 ()++goSeq ::+ (Pretty k1, Pretty k2, Ord k1, Ord k2) =>+ [Type (Ref k1 Type)] ->+ [Type (Ref k2 Type)] ->+ SubTypeM k1 k2 ()++-- Memoization: When we see a pair for the first time,+-- we optimistically put 'True' into the map.+-- Either the following recursive call will fail (but then this optimistic+-- value wasn't a problem), or it will succeed, but then the guess was correct.+-- If it fails we put 'False' into it, to as a caching optimization+memo t1 t2 = do+ gets (M.lookup (t1,t2) . fst) >>= \case+ Just r -> except r+ Nothing -> assume_ok >> (go t1 t2 `catchE` remember_failure)+ where+ remember r = modify (first (M.insert (t1,t2) r))+ assume_ok = remember (Right ())+ remember_failure e = remember (Left e) >> throwError e++-- Look through refs+go (RefT (Ref _ t1)) t2 = memo t1 t2+go t1 (RefT (Ref _ t2)) = memo t1 t2++-- Identity coercion for primitive values+go NatT NatT = pure ()+go Nat8T Nat8T = pure ()+go Nat16T Nat16T = pure ()+go Nat32T Nat32T = pure ()+go Nat64T Nat64T = pure ()+go IntT IntT = pure ()+go Int8T Int8T = pure ()+go Int16T Int16T = pure ()+go Int32T Int32T = pure ()+go Int64T Int64T = pure ()+go Float32T Float32T = pure ()+go Float64T Float64T = pure ()+go BoolT BoolT = pure ()+go TextT TextT = pure ()+go NullT NullT = pure ()+go PrincipalT PrincipalT = pure ()++-- Nat <: Int+go NatT IntT = pure ()++-- t <: reserved+go _ ReservedT = pure ()++-- empty <: t+go EmptyT _ = pure ()++-- vec t1 <: vec t2+go (VecT t1) (VecT t2) = memo t1 t2++-- Option: very simple+go _ (OptT _) = pure ()++-- Records+go (RecT fs1) (RecT fs2) = do+ let m1 = M.fromList fs1+ let m2 = M.fromList fs2+ -- Check missing fields+ sequence_+ [ case unRef t of+ OptT _ -> pure ()+ ReservedT -> pure ()+ t -> throwError $ show $ "Missing record field" <+> pretty fn <+> "of type" <+> pretty t+ | (fn, t) <- M.toList $ m2 M.\\ m1+ ]+ -- Check existing fields+ sequence_ [ memo t1 t2 | (_fn, (t1, t2)) <- M.toList $ M.intersectionWith (,) m1 m2 ]++-- Variants+go (VariantT fs1) (VariantT fs2) = do+ let m1 = M.fromList fs1+ let m2 = M.fromList fs2+ sequence_+ [ case M.lookup fn m2 of+ Just t2 -> memo t1 t2+ Nothing -> throwError $ show $ "Missing variant field" <+> pretty fn <+> "of type" <+> pretty t1+ | (fn, t1) <- M.toList m1+ ]++-- Reference types+go (FuncT mt1) (FuncT mt2) = goMethodType mt1 mt2+go (ServiceT meths1) (ServiceT meths2) = do+ let m1 = M.fromList meths1+ forM_ meths2 $ \(m, mt2) -> case M.lookup m m1 of+ Just mt1 -> goMethodType mt1 mt2+ Nothing -> throwError $ show $ "Missing service method" <+> pretty m <+> "of type" <+> pretty mt2++-- BlobT+go BlobT BlobT = pure ()+go (VecT t) BlobT | isNat8 t = pure ()+go BlobT (VecT t) | isNat8 t = pure ()++-- Final catch-all+go t1 t2 = throwError $ show $ "Type" <+> pretty t1 <+> "is not a subtype of" <+> pretty t2++goMethodType ::+ (Pretty k2, Pretty k1, Ord k2, Ord k1) =>+ MethodType (Ref k1 Type) ->+ MethodType (Ref k2 Type) ->+ SubTypeM k1 k2 ()+goMethodType (MethodType ta1 tr1 q1 o1) (MethodType ta2 tr2 q2 o2) = do+ unless (q1 == q2) $ throwError "Methods differ in query annotation"+ unless (o1 == o2) $ throwError "Methods differ in oneway annotation"+ flipM $ goSeq ta2 ta1+ goSeq tr1 tr2++goSeq _ [] = pure ()+goSeq ts1 (RefT (Ref _ t) : ts) = goSeq ts1 (t:ts)+-- Missing optional arguments are ok+goSeq ts1@[] (OptT _ : ts) = goSeq ts1 ts+goSeq ts1@[] (ReservedT : ts) = goSeq ts1 ts+goSeq [] ts = throwError $ show $ "Argument type list too short, expecting types" <+> pretty ts+goSeq (t1:ts1) (t2:ts2) = memo t1 t2 >> goSeq ts1 ts2++unRef :: Type (Ref a Type) -> Type (Ref a Type)+unRef (RefT (Ref _ t)) = unRef t+unRef t = t++isNat8 :: Type (Ref a Type) -> Bool+isNat8 (RefT (Ref _ t)) = isNat8 t+isNat8 Nat8T = True+isNat8 _ = False
src/Codec/Candid/TH.hs view
@@ -49,7 +49,7 @@ candidFile :: QuasiQuoter candidFile = quoteFile candid --- | This quasi-quoter turns all type definitions of a Canddi file into Haskell types, as one 'Row'. The `service` of the candid file is ignored.+-- | This quasi-quoter turns all type definitions of a Canddi file into Haskell types, as one 'Row'. The @service@ of the candid file is ignored. -- -- Recursive types are not supported. -- @@ -89,10 +89,10 @@ -- | Turns all candid type definitions into newtypes -- Used, so far, only in the Candid test suite runner-generateCandidDefs :: [DidDef TypeName] -> Q ([Dec], TypeName -> Q TH.Name)-generateCandidDefs defs = do+generateCandidDefs :: T.Text -> [DidDef TypeName] -> Q ([Dec], TypeName -> Q TH.Name)+generateCandidDefs prefix defs = do assocs <- for defs $ \(tn, _) -> do- thn <- newName ("Candid_" ++ T.unpack tn)+ thn <- newName ("Candid_" ++ T.unpack prefix ++ T.unpack tn) return (tn, thn) let m = M.fromList assocs@@ -102,7 +102,7 @@ decls <- for defs $ \(tn, t) -> do t' <- traverse resolve t n <- resolve tn- dn <- newName ("Candid_" ++ T.unpack tn)+ dn <- newName ("Candid_" ++ T.unpack prefix ++ T.unpack tn) newtypeD (cxt []) n [] Nothing (normalC dn [bangType (bang noSourceUnpackedness noSourceStrictness) (typ t')]) [derivClause Nothing [conT ''Candid, conT ''Eq, conT ''Show]]@@ -233,6 +233,7 @@ typ (VariantT fs) = [t| V.Var $(row [t| (V..==) |] [t| (V..+) |] [t| V.Empty |] fs) |] typ (FuncT mt) = [t| FuncRef $(methodType mt) |] typ (ServiceT ms) = [t| ServiceRef $(mrow [t| (R..==) |] [t| (R..+) |] [t| R.Empty |] ms) |]+typ FutureT = fail "Cannot represent a future Candid type as a Haskell type" typ (RefT v) = conT v isTuple :: [(FieldName, b)] -> Bool
src/Codec/Candid/TestExports.hs view
@@ -4,6 +4,8 @@ ( module Codec.Candid.Parse , module Codec.Candid.TH , module Codec.Candid.FieldName+ , module Codec.Candid.TypTable+ , module Codec.Candid.Class ) where import Codec.Candid.Parse@@ -23,4 +25,12 @@ import Codec.Candid.FieldName ( invertHash+ )++import Codec.Candid.TypTable+ ( unrollTypeTable+ )++import Codec.Candid.Class+ ( typeGraph )
src/Codec/Candid/TypTable.hs view
@@ -81,7 +81,8 @@ -- | This takes a type description and replaces all named types with their definition. ----- This produces an infinite type! Only use this in sufficiently lazy contexts.+-- This can produce an infinite type! Only use this in sufficiently lazy contexts, or when the+-- type is known to be not recursive. tieKnot :: SeqDesc -> [Type Void] tieKnot (SeqDesc m (ts :: [Type k])) = ts' where
src/Codec/Candid/Types.hs view
@@ -42,6 +42,8 @@ -- short-hands | BlobT -- ^ a short-hand for 'VecT' 'Nat8T'+ -- future types+ | FutureT -- for recursive types | RefT a -- ^ A reference to a named type deriving (Show, Eq, Ord, Functor, Foldable, Traversable)@@ -73,6 +75,7 @@ ReservedT >>= _ = ReservedT EmptyT >>= _ = EmptyT BlobT >>= _ = BlobT+ FutureT >>= _ = FutureT PrincipalT >>= _ = PrincipalT OptT t >>= f = OptT (t >>= f) VecT t >>= f = VecT (t >>= f)@@ -119,6 +122,7 @@ pretty (ServiceT s) = "service" <+> ":" <+> braces (group (align (vsep $ prettyMeth <$> s))) pretty PrincipalT = "principal"+ pretty FutureT = "future" prettyList = encloseSep lparen rparen (comma <> space) . map pretty @@ -160,6 +164,7 @@ | PrincipalV Principal | BlobV BS.ByteString | AnnV Value (Type Void)+ | FutureV -- ^ An opaque value of a future type deriving (Eq, Ord, Show) instance Pretty Value where@@ -195,6 +200,7 @@ pretty (VariantV f NullV) = "variant" <+> braces (pretty f) pretty (VariantV f v) = "variant" <+> braces (pretty f <+> "=" <+> pretty v) pretty (AnnV v t) = prettyAnn v t+ pretty FutureV = "future" prettyList = encloseSep lparen rparen (comma <> space) . map pretty
test/SpecTests.hs view
@@ -48,13 +48,12 @@ | basename <- files , let file = dir </> basename , Just name <- pure $ T.stripSuffix ".test.did" (T.pack basename)- -- , name /= "construct" -- for now ] (decls, testGroups) <- fmap unzip $ for candid_tests $ \(name, testfile) -> do- (decls, resolve) <- generateCandidDefs (testDefs testfile)+ (decls, resolve) <- generateCandidDefs name (testDefs testfile) tests <- traverse (traverse resolve) (testTests testfile) testGroup <-- [| testGroup ("File " ++ $(liftString (T.unpack name))) $(listE+ [| testGroup ("Candid spec test file " ++ $(liftString (T.unpack name))) $(listE [ [| testCase name $( case testAssertion of CanParse i1 -> [| case $(parseInput i1) of@@ -98,3 +97,7 @@ |]) [] return $ concat decls ++ [d1, d2] )++++
+ test/Tests.hs view
@@ -0,0 +1,454 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedLabels #-}++{-# OPTIONS_GHC -Wno-orphans #-}++module Tests (tests) where++import qualified Data.Text as T+import qualified Data.ByteString.Lazy as BS+import qualified Data.ByteString.Lazy.Char8 as B+import qualified Data.Vector as V hiding (singleton)+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.SmallCheck+import qualified Test.Tasty.QuickCheck as QC+import Test.SmallCheck.Series+import Data.Void+import Data.Either+import GHC.Int+import GHC.Word+import Numeric.Natural+import GHC.Generics (Generic)+import Prettyprinter+import Data.Row+import Data.Proxy+import qualified Data.Row.Records as R+import qualified Data.Row.Variants as V++import Codec.Candid+import Codec.Candid.TestExports++newtype Peano = Peano (Maybe Peano)+ deriving (Show, Eq)+ deriving Candid via (Maybe Peano)++peano :: Peano+peano = Peano $ Just $ Peano $ Just $ Peano $ Just $ Peano Nothing++newtype LinkedList a = LinkedList (Maybe (a, LinkedList a))+ deriving (Show, Eq)+ deriving newtype Candid++cons :: a -> LinkedList a -> LinkedList a+cons x y = LinkedList $ Just (x, y)+nil :: LinkedList a+nil = LinkedList Nothing++natList :: LinkedList Natural+natList = cons 1 (cons 2 (cons 3 (cons 4 nil)))++stringList :: [T.Text]+stringList = [T.pack "HI", T.pack "Ho"]++newtype ARecord a = ARecord { foo :: a }+ deriving (Eq, Show, Generic)+ deriving anyclass (Serial m)++deriving via (AsRecord (ARecord a))+ instance Candid a => Candid (ARecord a)++data EmptyRecord = EmptyRecord+ deriving (Eq, Show, Generic, Serial m)+ deriving Candid via (AsRecord EmptyRecord)++newtype MiddleField a = MiddleField a+ deriving (Eq, Show)++instance Candid a => Candid (MiddleField a) where+ type AsCandid (MiddleField a) = Rec ("_1_" .== a)+ toCandid (MiddleField x) = #_1_ .== x+ fromCandid r = MiddleField (r .! #_1_)++newtype JustRight a = JustRight a+ deriving (Eq, Show)++instance Candid a => Candid (JustRight a) where+ type AsCandid (JustRight a) = Var ("Right" .== a)+ toCandid (JustRight x) = V.singleton (Label @"Right") x+ fromCandid = JustRight . snd . V.unSingleton++data SimpleRecord = SimpleRecord { foo :: Bool, bar :: Word8 }+ deriving (Generic, Eq, Show)+ deriving (Serial m)+ deriving Candid via (AsRecord SimpleRecord)++roundTripTest :: forall a. (CandidArg a, Eq a, Show a) => a -> Assertion+roundTripTest v1 = do+ let bytes1 = encode v1+ v2 <- case decode @a bytes1 of+ Left err -> assertFailure err+ Right v -> return v+ assertEqual "values" v1 v2++subTypeRoundTripProp :: forall a b. (CandidArg a, Serial IO a, Show a, CandidArg b) => TestTree+subTypeRoundTripProp = testProperty desc $ \v ->+ isRight $ decode @b (encode @a v)+ where+ desc = show $ pretty (tieKnot (seqDesc @a)) <+> "<:" <+> pretty (tieKnot (seqDesc @b))++subTypeRoundTripTest' :: forall a b.+ (CandidArg a, Eq a, Show a) =>+ (CandidArg b, Eq b, Show b) =>+ a -> b -> Assertion+subTypeRoundTripTest' v1 v2 = do+ let bytes1 = encode v1+ v2' <- case decode @b bytes1 of+ Left err -> assertFailure err+ Right v -> return v+ v2 @=? v2'++subTypeRoundTripTest :: forall a b.+ (CandidArg a, Eq a, Show a) =>+ (CandidArg b, Eq b, Show b) =>+ a -> b -> Assertion+subTypeRoundTripTest v1 v2 = do+ subTypeRoundTripTest' v1 v2+ -- now try the other direction+ let bytes2 = encode v2+ case decode @a bytes2 of+ Left _err -> return ()+ Right _ -> assertFailure "converse subtype test succeeded"++instance Monad m => Serial m T.Text where+ series = T.pack <$> series++instance (Monad m, Serial m a) => Serial m (V.Vector a) where+ series = V.fromList <$> series++parseTest :: HasCallStack => String -> DidFile -> TestTree+parseTest c e = testCase c $+ case parseDid c of+ Left err -> assertFailure err+ Right s -> s @?= e++printTestType :: forall a. (Candid a, HasCallStack) => String -> TestTree+printTestType e = testCase e $+ show (pretty (typeDesc @a)) @?= e++printTestSeq :: forall a. (CandidArg a, HasCallStack) => String -> TestTree+printTestSeq e = testCase e $+ show (pretty (tieKnot (seqDesc @a))) @?= e++roundTripTestGroup :: String ->+ (forall a. (CandidArg a, Serial IO a, Show a, Eq a) => a -> Either String a) ->+ TestTree+roundTripTestGroup group_desc roundtrip =+ testGroup ("roundtrip (" <> group_desc <> ")") $ withSomeTypes $ \(Proxy :: Proxy a) ->+ let desc = show $ pretty (tieKnot (seqDesc @a)) in+ testProperty desc $ \v ->+ case roundtrip @a v of+ Right y | y == v -> Right ("all good" :: String)+ Right y -> Left $+ show v ++ " round-tripped to " ++ show y+ Left err -> Left $+ show v ++ " failed to decode:\n" ++ err++withSomeTypes ::+ (forall a. (CandidArg a, Serial IO a, Show a, Eq a) => Proxy a -> b) -> [b]+withSomeTypes mkTest =+ [ mkTest (Proxy @Bool)+ , mkTest (Proxy @Natural)+ , mkTest (Proxy @Word8)+ , mkTest (Proxy @Word16)+ , mkTest (Proxy @Word32)+ , mkTest (Proxy @Word64)+ , mkTest (Proxy @Integer)+ , mkTest (Proxy @Int8)+ , mkTest (Proxy @Int16)+ , mkTest (Proxy @Int32)+ , mkTest (Proxy @Int64)+ , mkTest (Proxy @Float)+ , mkTest (Proxy @Double)+ , mkTest (Proxy @T.Text)+ , mkTest (Proxy @())+ , mkTest (Proxy @Reserved)+ , mkTest (Proxy @Principal)+ , mkTest (Proxy @BS.ByteString)+ , mkTest (Proxy @(Maybe T.Text))+ , mkTest (Proxy @(V.Vector T.Text))+ , mkTest (Proxy @EmptyRecord)+ , mkTest (Proxy @(ARecord T.Text))+ , mkTest (Proxy @(Either Bool T.Text))+ , mkTest (Proxy @SimpleRecord)+ , mkTest (Proxy @(Rec ("a" .== Bool .+ "b" .== Bool .+ "c" .== Bool)))+ , mkTest (Proxy @(V.Var ("upgrade" .== () .+ "reinstall" .== () .+ "install" .== ())))+ , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnFalse, AnnFalse)))+ , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnTrue, AnnFalse)))+ , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnFalse, AnnTrue)))+ , mkTest (Proxy @(ServiceRef Empty))+ ]++tests :: [TestTree]+tests =+ [ testGroup "encode tests"+ [ testCase "empty" $ encode () @?= B.pack "DIDL\0\0"+ , testCase "bool" $ encode (Unary True) @?= B.pack "DIDL\0\1\x7e\1"+ ]+ , testGroup "decode error message"+ [ testCase "simple mismatch" $ fromCandidVals @(Unary ()) (toCandidVals True) @?= Left "Cannot coerce true into null"+ , testCase "missing variant" $ fromCandidVals @(Either () ()) (toCandidVals (V.singleton #foo ())) @?= Left "Unexpected tag foo"+ , testCase "error in variant" $ fromCandidVals @(Either () ()) (toCandidVals (Left @Bool @() True)) @?= Left "Cannot coerce true into null"+ ]+ , testGroup "roundtrip"+ [ testCase "empty" $ roundTripTest ()+ , testCase "bool" $ roundTripTest $ Unary True+ , testCase "simple record 1" $ roundTripTest (ARecord True, False)+ , testCase "simple record 2" $ roundTripTest (ARecord (100000 :: Natural), False)+ , testCase "simple variant 1" $ roundTripTest $ Unary (Left True :: Either Bool Bool)+ , testCase "simple variant 2" $ roundTripTest $ Unary (Right False :: Either Bool Bool)+ , testCase "nested record 2" $ roundTripTest (ARecord (True,False), False)+ , testCase "peano" $ roundTripTest $ Unary peano+ , testCase "lists" $ roundTripTest (natList, stringList)+ , testCase "custom record" $ roundTripTest $ Unary (SimpleRecord True 42)+ ]+ , testGroup "subtype roundtrips"+ [ testCase "nat/int" $ subTypeRoundTripTest (Unary (42 :: Natural)) (Unary (42 :: Integer))+ , testCase "null/opt" $ subTypeRoundTripTest (Unary ()) (Unary (Nothing @Integer))+ , testCase "rec" $ subTypeRoundTripTest (ARecord True, True) (EmptyRecord, True)+ , testCase "tuple" $ subTypeRoundTripTest ((42::Integer,-42::Integer), 100::Integer) (EmptyRecord, 100::Integer)+ , testCase "variant" $ subTypeRoundTripTest' (JustRight (42 :: Natural), True) (Right 42 :: Either Bool Natural, True)+ , testCase "rec/any" $ subTypeRoundTripTest (ARecord True, True) (Reserved, True)+ , testCase "tuple/any" $ subTypeRoundTripTest ((42::Integer, 42::Natural), True) (Reserved, True)+ , testCase "tuple/tuple" $ subTypeRoundTripTest ((42::Integer,-42::Integer,True), 100::Integer) ((42::Integer, -42::Integer), 100::Integer)+ , testCase "tuple/middle" $ subTypeRoundTripTest ((42::Integer,-42::Integer,True), 100::Integer) (MiddleField (-42) :: MiddleField Integer, 100::Integer)+ , testCase "records" $ subTypeRoundTripTest (Unary (SimpleRecord True 42)) (Unary (ARecord True))+ ]++ , roundTripTestGroup "Haskell → Candid → Haskell" $ \(v :: a) ->+ decode @a (encode @a v)+ , roundTripTestGroup "Haskell → [Value] → Haskell" $ \(v :: a) ->+ fromCandidVals (toCandidVals @a v)+ , roundTripTestGroup "Haskell → [Value] → Textual → [Value] → Haskell" $ \(v :: a) ->+ parseValues (show (pretty (toCandidVals @a v))) >>= fromCandidVals @a++ , testGroup "subtype round trip smallchecks"+ [ subTypeRoundTripProp @Natural @Natural+ , subTypeRoundTripProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @Reserved+ , subTypeRoundTripProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("Hi" .== Reserved))+ , subTypeRoundTripProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("Hi" .== Word8))+ , subTypeRoundTripProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("_1_" .== Word8))+ , subTypeRoundTripProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8 .+ "_2_" .== Bool)) @(Rec ("_1_" .== Word8))+ , subTypeRoundTripProp @(Maybe (Rec ("Hi" .== Word8 .+ "_1_" .== Word8 .+ "_0_" .== Bool))) @(Maybe (Bool,Word8))+ , subTypeRoundTripProp @(Var ("Hi" .== Word8)) @(Var ("Hi" .== Word8 .+ "Ho" .== T.Text))+ , subTypeRoundTripProp @(Var ("Ho" .== T.Text)) @(Var ("Hi" .== Word8 .+ "Ho" .== T.Text))+ , subTypeRoundTripProp @Natural @Reserved+ , subTypeRoundTripProp @BS.ByteString @Reserved+ , subTypeRoundTripProp @BS.ByteString @(V.Vector Word8)+ , subTypeRoundTripProp @(V.Vector Word8) @BS.ByteString+ , subTypeRoundTripProp @Principal @Reserved+ ]++ , testGroup "subtype test" $+ [ testGroup "reflexivity" $ concat $ withSomeTypes $ \(Proxy :: Proxy a) ->+ let td = seqDesc @a in+ unrollTypeTable td $ \ts ->+ [ testCase (show (pretty t)) $ assertRight $ t `isSubtypeOf` t | t <- ts ]+ , testGroup "negative tests"+ [ let t1 = typeGraph @Integer+ t2 = typeGraph @Natural+ in testCase (show (pretty t1) ++ " </: " ++ show (pretty t2)) $+ assertLeft $ t1 `isSubtypeOf` t2+ ]+ ]+ , testGroup "candid type printing" $+ [ printTestType @Bool "bool"+ , printTestType @Integer "int"+ , printTestType @Natural "nat"+ , printTestType @Int8 "int8"+ , printTestType @Word8 "nat8"+ , printTestType @SimpleRecord "record {bar : nat8; foo : bool}"+ , printTestType @(JustRight T.Text) "variant {Right : text}"+ , printTestType @(FuncRef (Bool, Unary (), AnnTrue, AnnFalse)) "func (bool) -> (null) query"+ , printTestType @(FuncRef (Bool, T.Text, AnnFalse, AnnTrue)) "func (bool) -> (text) oneway"+ , printTestType @(ServiceRef Empty) "service : {}"+ , printTestType @(ServiceRef ("foo" .== (Bool, T.Text, AnnFalse, AnnTrue) .+ "bar" .== ((),(),AnnFalse, AnnFalse)))+ "service : {bar : () -> (); foo : (bool) -> (text) oneway;}"+ , printTestSeq @() "()"+ , printTestSeq @(Unary ()) "(null)"+ , printTestSeq @(Unary (Bool, Bool)) "(record {0 : bool; 1 : bool})"+ , printTestSeq @((),()) "(null, null)"+ , printTestSeq @(Bool,Bool) "(bool, bool)"+ , printTestSeq @(Bool,(Bool, Bool)) "(bool, record {0 : bool; 1 : bool})"+ , printTestSeq @Bool "(bool)"+ ]+ , testGroup "candid value printing" $+ let t :: Value -> String -> TestTree+ t v e = testCase e $ show (pretty v) @?= e+ in+ [ t (BoolV True) "true"+ , t (BoolV False) "false"+ , t (NatV 1) "1"+ , t (IntV 1) "+1"+ , t (IntV 0) "+0"+ , t (IntV (-1)) "-1"+ , t (Nat8V 1) "(1 : nat8)"+ , t (RecV [("bar", TextV "baz")]) "record {bar = \"baz\"}"+ , t (FuncV (Principal "\xde\xad\xbe\xef") "foo") "func \"psokg-ww6vw-7o6\".\"foo\""+ , t (ServiceV (Principal "\xde\xad\xbe\xef")) "service \"psokg-ww6vw-7o6\""+ , t (PrincipalV (Principal "")) "principal \"aaaaa-aa\""+ , t (PrincipalV (Principal "\xab\xcd\x01")) "principal \"em77e-bvlzu-aq\""+ , t (PrincipalV (Principal "\xde\xad\xbe\xef")) "principal \"psokg-ww6vw-7o6\""+ ]+ , testGroup "candid value printing (via binary) " $+ let t :: forall a. (HasCallStack, CandidArg a) => a -> String -> TestTree+ t v e = testCase e $ do+ let bytes = encode v+ (_, vs) <- assertRight $ decodeVals bytes+ show (pretty vs) @?= e+ in+ [ t True "(true)"+ , t (SimpleRecord False 42) "(record {bar = (42 : nat8); foo = false})"+ , t (JustRight (Just (3 :: Natural))) "(variant {Right = opt 3})"+ , t (JustRight (3 :: Word8)) "(variant {Right = (3 : nat8)})"+ , t () "()"+ , t (Unary ()) "(null)"+ , t (Unary (True, False)) "(record {true; false})"+ , t (Unary (True, (True, False))) "(record {true; record {true; false}})"+ , t (#_0_ .== True .+ #_1_ .== False) "(record {true; false})"+ ]++ , testGroup "dynamic values (AST)" $+ let t :: forall a. (HasCallStack, CandidArg a, Eq a, Show a) => String -> a -> TestTree+ t s e = testCase s $ do+ bytes <- either assertFailure return $ encodeTextual s+ x <- either assertFailure return $ decode @a bytes+ x @?= e++ t' :: HasCallStack => String -> TestTree+ t' s = testCase ("Bad: " <> s) $ do+ vs <- either assertFailure return $ parseValues s+ case encodeDynValues vs of+ Left _err -> return ()+ Right _ -> assertFailure "Ill-typed value encoded?"+ in+ [ t "true" True+ , t "false" False+ , t "1" (1 :: Natural)+ , t "1 : nat8" (1 :: Word8)+ , t "record { bar = \"baz\" }" (#bar .== ("baz":: T.Text))+ , t "vec {}" (V.fromList [] :: V.Vector Void)+ , t "vec {4; +4}" (V.fromList [4 :: Integer,4])+ , t "vec {4; null : reserved}" (V.fromList [Reserved, Reserved])+ , t "vec {record {}; record {0 = true}}" (V.fromList [R.empty, R.empty])+ , t "vec {variant {a = true}; variant {b = null}}"+ (V.fromList [IsJust #a True, IsJust #b () :: V.Var ("a" V..== Bool V..+ "b" V..== ())])+ , t "\"hello\"" ("hello" :: T.Text)+ , t "blob \"hello\"" ("hello" :: BS.ByteString)+ , t "blob \"\\00\\ff\"" ("\x00\xff" :: BS.ByteString)+ , t "func \"psokg-ww6vw-7o6\".\"foo\""+ (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "foo")+ , t "func \"psokg-ww6vw-7o6\".foo"+ (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "foo")+ , t "func \"psokg-ww6vw-7o6\".\"\""+ (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "")+ , t "service \"psokg-ww6vw-7o6\""+ (ServiceRef @Empty (Principal "\xde\xad\xbe\xef"))+ , t "principal \"psokg-ww6vw-7o6\""+ (Principal "\xde\xad\xbe\xef")++ , t' "vec {true; 4}"+ ]++ , testGroup "candid type parsing"+ [ parseTest "service : {}" $+ DidFile [] []+ , parseTest "service : { foo : (text) -> (text) }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] False False)]+ , parseTest "service : { foo : (text,) -> (text,); }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] False False)]+ , parseTest "service : { foo : (x : text,) -> (y : text,); }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] False False)]+ , parseTest "service : { foo : (opt text) -> () }" $+ DidFile [] [("foo", MethodType [OptT TextT] [] False False) ]+ , parseTest "service : { foo : (record { text; blob }) -> () }" $+ DidFile [] [("foo", MethodType [RecT [(hashedField 0, TextT), (hashedField 1, BlobT)]] [] False False) ]+ , parseTest "service : { foo : (record { x_ : null; 5 : nat8 }) -> () }" $+ DidFile [] [("foo", MethodType [RecT [("x_", NullT), (hashedField 5, Nat8T)]] [] False False) ]+ , parseTest "service : { foo : (record { x : null; 5 : nat8 }) -> () }" $+ DidFile [] [("foo", MethodType [RecT [("x", NullT), (hashedField 5, Nat8T)]] [] False False) ]+ , parseTest "service : { foo : (text) -> (text) query }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] True False)]+ , parseTest "service : { foo : (text) -> (text) oneway }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] False True)]+ , parseTest "service : { foo : (text) -> (text) query oneway }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] True True)]+ , parseTest "service : { foo : (text) -> (text) oneway query }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] True True)]+ , parseTest "service : (opt SomeInit) -> { foo : (text) -> (text) oneway query }" $+ DidFile [] [("foo", MethodType [TextT] [TextT] True True)]+ , parseTest "type t = int; service : { foo : (t) -> (t) }" $+ DidFile [("t", IntT)] [("foo", MethodType [RefT "t"] [RefT "t"] False False)]+ ]+ , testProperty "field name escaping round-tripping" $ \e ->+ let f = either labledField hashedField e in+ let f' = unescapeFieldName (escapeFieldName f) in+ f' == f+ , testGroup "candid hash inversion"+ [ QC.testProperty "long dictionary name" $+ let s = "precriticized" in+ invertHash (candidHash s) QC.=== Just s+ , QC.testProperty "long capitalized dictionary name" $+ let s = "Precriticized" in+ invertHash (candidHash s) QC.=== Just s+ , QC.testProperty "all hashes find something" $+ QC.forAll QC.arbitraryBoundedIntegral $ \w ->+ w >= 32 QC.==> case invertHash w of+ Nothing -> False+ Just s -> candidHash s == w+ ]+ ]++assertRight :: Either String a -> IO a+assertRight = either assertFailure pure++assertLeft :: Either String () -> Assertion+assertLeft = either (const (pure ())) (\() -> assertFailure "unexpected success")++instance Monad m => Serial m BS.ByteString where+ series = BS.pack <$> series++instance Monad m => Serial m Principal where+ series = Principal <$> series++instance Monad m => Serial m Reserved where+ series = Reserved <$ series @m @()++instance Monad m => Serial m (FuncRef mt) where+ series = FuncRef <$> series <*> series++instance Monad m => Serial m (ServiceRef r) where+ series = ServiceRef <$> series++instance (Monad m, Forall r (Serial m), AllUniqueLabels r) => Serial m (Rec r) where+ series = R.fromLabelsA @(Serial m) (\_l -> series)++instance (Monad m, Forall r (Serial m), AllUniqueLabels r) => Serial m (Var r) where+ series = V.fromLabels @(Serial m) (\_l -> series)
test/test.hs view
@@ -1,445 +1,10 @@-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingVia #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE OverloadedLabels #-}--{-# OPTIONS_GHC -Wno-orphans #-}--import qualified Data.Text as T-import qualified Data.ByteString.Lazy as BS-import qualified Data.ByteString.Lazy.Char8 as B-import qualified Data.Vector as V hiding (singleton) import Test.Tasty import Test.Tasty.Ingredients.Rerun-import Test.Tasty.HUnit-import Test.Tasty.SmallCheck-import qualified Test.Tasty.QuickCheck as QC-import Test.SmallCheck.Series-import Data.Void-import Data.Either-import GHC.Int-import GHC.Word-import Numeric.Natural-import GHC.Generics (Generic)-import Prettyprinter-import Data.Row-import Data.Proxy-import qualified Data.Row.Records as R-import qualified Data.Row.Variants as V -import Codec.Candid-import Codec.Candid.TestExports- import THTests (thTests) import SpecTests (specTests)+import Tests (tests) main :: IO ()-main = defaultMainWithRerun tests--newtype Peano = Peano (Maybe Peano)- deriving (Show, Eq)- deriving Candid via (Maybe Peano)--peano :: Peano-peano = Peano $ Just $ Peano $ Just $ Peano $ Just $ Peano Nothing--newtype LinkedList a = LinkedList (Maybe (a, LinkedList a))- deriving (Show, Eq)- deriving newtype Candid--cons :: a -> LinkedList a -> LinkedList a-cons x y = LinkedList $ Just (x, y)-nil :: LinkedList a-nil = LinkedList Nothing--natList :: LinkedList Natural-natList = cons 1 (cons 2 (cons 3 (cons 4 nil)))--stringList :: [T.Text]-stringList = [T.pack "HI", T.pack "Ho"]--newtype ARecord a = ARecord { foo :: a }- deriving (Eq, Show, Generic)- deriving anyclass (Serial m)--deriving via (AsRecord (ARecord a))- instance Candid a => Candid (ARecord a)--data EmptyRecord = EmptyRecord- deriving (Eq, Show, Generic, Serial m)- deriving Candid via (AsRecord EmptyRecord)--newtype MiddleField a = MiddleField a- deriving (Eq, Show)--instance Candid a => Candid (MiddleField a) where- type AsCandid (MiddleField a) = Rec ("_1_" .== a)- toCandid (MiddleField x) = #_1_ .== x- fromCandid r = MiddleField (r .! #_1_)--newtype JustRight a = JustRight a- deriving (Eq, Show)--instance Candid a => Candid (JustRight a) where- type AsCandid (JustRight a) = Var ("Right" .== a)- toCandid (JustRight x) = V.singleton (Label @"Right") x- fromCandid = JustRight . snd . V.unSingleton--data SimpleRecord = SimpleRecord { foo :: Bool, bar :: Word8 }- deriving (Generic, Eq, Show)- deriving (Serial m)- deriving Candid via (AsRecord SimpleRecord)--roundTripTest :: forall a. (CandidArg a, Eq a, Show a) => a -> Assertion-roundTripTest v1 = do- let bytes1 = encode v1- v2 <- case decode @a bytes1 of- Left err -> assertFailure err- Right v -> return v- assertEqual "values" v1 v2--subTypProp :: forall a b. (CandidArg a, Serial IO a, Show a, CandidArg b) => TestTree-subTypProp = testProperty desc $ \v ->- isRight $ decode @b (encode @a v)- where- desc = show $ pretty (tieKnot (seqDesc @a)) <+> "<:" <+> pretty (tieKnot (seqDesc @b))--subTypeTest' :: forall a b.- (CandidArg a, Eq a, Show a) =>- (CandidArg b, Eq b, Show b) =>- a -> b -> Assertion-subTypeTest' v1 v2 = do- let bytes1 = encode v1- v2' <- case decode @b bytes1 of- Left err -> assertFailure err- Right v -> return v- v2 @=? v2'--subTypeTest :: forall a b.- (CandidArg a, Eq a, Show a) =>- (CandidArg b, Eq b, Show b) =>- a -> b -> Assertion-subTypeTest v1 v2 = do- subTypeTest' v1 v2- -- now try the other direction- let bytes2 = encode v2- case decode @a bytes2 of- Left _err -> return ()- Right _ -> assertFailure "converse subtype test succeeded"--instance Monad m => Serial m T.Text where- series = T.pack <$> series--instance (Monad m, Serial m a) => Serial m (V.Vector a) where- series = V.fromList <$> series--parseTest :: HasCallStack => String -> DidFile -> TestTree-parseTest c e = testCase c $- case parseDid c of- Left err -> assertFailure err- Right s -> s @?= e--printTestType :: forall a. (Candid a, HasCallStack) => String -> TestTree-printTestType e = testCase e $- show (pretty (typeDesc @a)) @?= e--printTestSeq :: forall a. (CandidArg a, HasCallStack) => String -> TestTree-printTestSeq e = testCase e $- show (pretty (tieKnot (seqDesc @a))) @?= e--roundTripTestGroup :: String ->- (forall a. (CandidArg a, Serial IO a, Show a, Eq a) => a -> Either String a) ->- TestTree-roundTripTestGroup group_desc roundtrip =- withSomeTypes ("roundtrip (" <> group_desc <> ")") $ \(Proxy :: Proxy a) ->- let desc = show $ pretty (tieKnot (seqDesc @a)) in- testProperty desc $ \v ->- case roundtrip @a v of- Right y | y == v -> Right ("all good" :: String)- Right y -> Left $- show v ++ " round-tripped to " ++ show y- Left err -> Left $- show v ++ " failed to decode:\n" ++ err--withSomeTypes ::- String ->- (forall a. (CandidArg a, Serial IO a, Show a, Eq a) => Proxy a -> TestTree) ->- TestTree-withSomeTypes groupName mkTest =- testGroup groupName- [ mkTest (Proxy @Bool)- , mkTest (Proxy @Natural)- , mkTest (Proxy @Word8)- , mkTest (Proxy @Word16)- , mkTest (Proxy @Word32)- , mkTest (Proxy @Word64)- , mkTest (Proxy @Integer)- , mkTest (Proxy @Int8)- , mkTest (Proxy @Int16)- , mkTest (Proxy @Int32)- , mkTest (Proxy @Int64)- , mkTest (Proxy @Float)- , mkTest (Proxy @Double)- , mkTest (Proxy @T.Text)- , mkTest (Proxy @())- , mkTest (Proxy @Reserved)- , mkTest (Proxy @Principal)- , mkTest (Proxy @BS.ByteString)- , mkTest (Proxy @(Maybe T.Text))- , mkTest (Proxy @(V.Vector T.Text))- , mkTest (Proxy @EmptyRecord)- , mkTest (Proxy @(ARecord T.Text))- , mkTest (Proxy @(Either Bool T.Text))- , mkTest (Proxy @SimpleRecord)- , mkTest (Proxy @(Rec ("a" .== Bool .+ "b" .== Bool .+ "c" .== Bool)))- , mkTest (Proxy @(V.Var ("upgrade" .== () .+ "reinstall" .== () .+ "install" .== ())))- , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnFalse, AnnFalse)))- , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnTrue, AnnFalse)))- , mkTest (Proxy @(FuncRef (Bool, T.Text, AnnFalse, AnnTrue)))- , mkTest (Proxy @(ServiceRef Empty))- ]--tests :: TestTree-tests = testGroup "tests"- [ specTests- , testGroup "encode tests"- [ testCase "empty" $ encode () @?= B.pack "DIDL\0\0"- , testCase "bool" $ encode (Unary True) @?= B.pack "DIDL\0\1\x7e\1"- ]- , testGroup "decode error message"- [ testCase "simple mismatch" $ fromCandidVals @(Unary ()) (toCandidVals True) @?= Left "Cannot coerce true into null"- , testCase "missing variant" $ fromCandidVals @(Either () ()) (toCandidVals (V.singleton #foo ())) @?= Left "Unexpected tag foo"- , testCase "error in variant" $ fromCandidVals @(Either () ()) (toCandidVals (Left @Bool @() True)) @?= Left "Cannot coerce true into null"- ]- , testGroup "roundtrip"- [ testCase "empty" $ roundTripTest ()- , testCase "bool" $ roundTripTest $ Unary True- , testCase "simple record 1" $ roundTripTest (ARecord True, False)- , testCase "simple record 2" $ roundTripTest (ARecord (100000 :: Natural), False)- , testCase "simple variant 1" $ roundTripTest $ Unary (Left True :: Either Bool Bool)- , testCase "simple variant 2" $ roundTripTest $ Unary (Right False :: Either Bool Bool)- , testCase "nested record 2" $ roundTripTest (ARecord (True,False), False)- , testCase "peano" $ roundTripTest $ Unary peano- , testCase "lists" $ roundTripTest (natList, stringList)- , testCase "custom record" $ roundTripTest $ Unary (SimpleRecord True 42)- ]- , testGroup "subtypes"- [ testCase "nat/int" $ subTypeTest (Unary (42 :: Natural)) (Unary (42 :: Integer))- , testCase "null/opt" $ subTypeTest (Unary ()) (Unary (Nothing @Integer))- , testCase "rec" $ subTypeTest (ARecord True, True) (EmptyRecord, True)- , testCase "tuple" $ subTypeTest ((42::Integer,-42::Integer), 100::Integer) (EmptyRecord, 100::Integer)- , testCase "variant" $ subTypeTest' (JustRight (42 :: Natural), True) (Right 42 :: Either Bool Natural, True)- , testCase "rec/any" $ subTypeTest (ARecord True, True) (Reserved, True)- , testCase "tuple/any" $ subTypeTest ((42::Integer, 42::Natural), True) (Reserved, True)- , testCase "tuple/tuple" $ subTypeTest ((42::Integer,-42::Integer,True), 100::Integer) ((42::Integer, -42::Integer), 100::Integer)- , testCase "tuple/middle" $ subTypeTest ((42::Integer,-42::Integer,True), 100::Integer) (MiddleField (-42) :: MiddleField Integer, 100::Integer)- , testCase "records" $ subTypeTest (Unary (SimpleRecord True 42)) (Unary (ARecord True))- ]-- , roundTripTestGroup "Haskell → Candid → Haskell" $ \(v :: a) ->- decode @a (encode @a v)- , roundTripTestGroup "Haskell → [Value] → Haskell" $ \(v :: a) ->- fromCandidVals (toCandidVals @a v)- , roundTripTestGroup "Haskell → [Value] → Textual → [Value] → Haskell" $ \(v :: a) ->- parseValues (show (pretty (toCandidVals @a v))) >>= fromCandidVals @a-- , testGroup "subtype smallchecks"- [ subTypProp @Natural @Natural- , subTypProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @Reserved- , subTypProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("Hi" .== Reserved))- , subTypProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("Hi" .== Word8))- , subTypProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8)) @(Rec ("_1_" .== Word8))- , subTypProp @(Rec ("Hi" .== Word8 .+ "_1_" .== Word8 .+ "_2_" .== Bool)) @(Rec ("_1_" .== Word8))- , subTypProp @(Maybe (Rec ("Hi" .== Word8 .+ "_1_" .== Word8 .+ "_0_" .== Bool))) @(Maybe (Bool,Word8))- , subTypProp @(Var ("Hi" .== Word8)) @(Var ("Hi" .== Word8 .+ "Ho" .== T.Text))- , subTypProp @(Var ("Ho" .== T.Text)) @(Var ("Hi" .== Word8 .+ "Ho" .== T.Text))- , subTypProp @Natural @Reserved- , subTypProp @BS.ByteString @Reserved- , subTypProp @BS.ByteString @(V.Vector Word8)- , subTypProp @(V.Vector Word8) @BS.ByteString- , subTypProp @Principal @Reserved- ]- , testGroup "candid type printing" $- [ printTestType @Bool "bool"- , printTestType @Integer "int"- , printTestType @Natural "nat"- , printTestType @Int8 "int8"- , printTestType @Word8 "nat8"- , printTestType @SimpleRecord "record {bar : nat8; foo : bool}"- , printTestType @(JustRight T.Text) "variant {Right : text}"- , printTestType @(FuncRef (Bool, Unary (), AnnTrue, AnnFalse)) "func (bool) -> (null) query"- , printTestType @(FuncRef (Bool, T.Text, AnnFalse, AnnTrue)) "func (bool) -> (text) oneway"- , printTestType @(ServiceRef Empty) "service : {}"- , printTestType @(ServiceRef ("foo" .== (Bool, T.Text, AnnFalse, AnnTrue) .+ "bar" .== ((),(),AnnFalse, AnnFalse)))- "service : {bar : () -> (); foo : (bool) -> (text) oneway;}"- , printTestSeq @() "()"- , printTestSeq @(Unary ()) "(null)"- , printTestSeq @(Unary (Bool, Bool)) "(record {0 : bool; 1 : bool})"- , printTestSeq @((),()) "(null, null)"- , printTestSeq @(Bool,Bool) "(bool, bool)"- , printTestSeq @(Bool,(Bool, Bool)) "(bool, record {0 : bool; 1 : bool})"- , printTestSeq @Bool "(bool)"- ]- , testGroup "candid value printing" $- let t :: Value -> String -> TestTree- t v e = testCase e $ show (pretty v) @?= e- in- [ t (BoolV True) "true"- , t (BoolV False) "false"- , t (NatV 1) "1"- , t (IntV 1) "+1"- , t (IntV 0) "+0"- , t (IntV (-1)) "-1"- , t (Nat8V 1) "(1 : nat8)"- , t (RecV [("bar", TextV "baz")]) "record {bar = \"baz\"}"- , t (FuncV (Principal "\xde\xad\xbe\xef") "foo") "func \"psokg-ww6vw-7o6\".\"foo\""- , t (ServiceV (Principal "\xde\xad\xbe\xef")) "service \"psokg-ww6vw-7o6\""- , t (PrincipalV (Principal "")) "principal \"aaaaa-aa\""- , t (PrincipalV (Principal "\xab\xcd\x01")) "principal \"em77e-bvlzu-aq\""- , t (PrincipalV (Principal "\xde\xad\xbe\xef")) "principal \"psokg-ww6vw-7o6\""- ]- , testGroup "candid value printing (via binary) " $- let t :: forall a. (HasCallStack, CandidArg a) => a -> String -> TestTree- t v e = testCase e $ do- let bytes = encode v- (_, vs) <- either assertFailure return $ decodeVals bytes- show (pretty vs) @?= e- in- [ t True "(true)"- , t (SimpleRecord False 42) "(record {bar = (42 : nat8); foo = false})"- , t (JustRight (Just (3 :: Natural))) "(variant {Right = opt 3})"- , t (JustRight (3 :: Word8)) "(variant {Right = (3 : nat8)})"- , t () "()"- , t (Unary ()) "(null)"- , t (Unary (True, False)) "(record {true; false})"- , t (Unary (True, (True, False))) "(record {true; record {true; false}})"- , t (#_0_ .== True .+ #_1_ .== False) "(record {true; false})"- ]-- , testGroup "dynamic values (AST)" $- let t :: forall a. (HasCallStack, CandidArg a, Eq a, Show a) => String -> a -> TestTree- t s e = testCase s $ do- bytes <- either assertFailure return $ encodeTextual s- x <- either assertFailure return $ decode @a bytes- x @?= e-- t' :: HasCallStack => String -> TestTree- t' s = testCase ("Bad: " <> s) $ do- vs <- either assertFailure return $ parseValues s- case encodeDynValues vs of- Left _err -> return ()- Right _ -> assertFailure "Ill-typed value encoded?"- in- [ t "true" True- , t "false" False- , t "1" (1 :: Natural)- , t "1 : nat8" (1 :: Word8)- , t "record { bar = \"baz\" }" (#bar .== ("baz":: T.Text))- , t "vec {}" (V.fromList [] :: V.Vector Void)- , t "vec {4; +4}" (V.fromList [4 :: Integer,4])- , t "vec {4; null : reserved}" (V.fromList [Reserved, Reserved])- , t "vec {record {}; record {0 = true}}" (V.fromList [R.empty, R.empty])- , t "vec {variant {a = true}; variant {b = null}}"- (V.fromList [IsJust #a True, IsJust #b () :: V.Var ("a" V..== Bool V..+ "b" V..== ())])- , t "\"hello\"" ("hello" :: T.Text)- , t "blob \"hello\"" ("hello" :: BS.ByteString)- , t "blob \"\\00\\ff\"" ("\x00\xff" :: BS.ByteString)- , t "func \"psokg-ww6vw-7o6\".\"foo\""- (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "foo")- , t "func \"psokg-ww6vw-7o6\".foo"- (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "foo")- , t "func \"psokg-ww6vw-7o6\".\"\""- (FuncRef @((), (), AnnFalse, AnnFalse) (Principal "\xde\xad\xbe\xef") "")- , t "service \"psokg-ww6vw-7o6\""- (ServiceRef @Empty (Principal "\xde\xad\xbe\xef"))- , t "principal \"psokg-ww6vw-7o6\""- (Principal "\xde\xad\xbe\xef")-- , t' "vec {true; 4}"- ]-- , testGroup "candid type parsing"- [ parseTest "service : {}" $- DidFile [] []- , parseTest "service : { foo : (text) -> (text) }" $- DidFile [] [("foo", MethodType [TextT] [TextT] False False)]- , parseTest "service : { foo : (text,) -> (text,); }" $- DidFile [] [("foo", MethodType [TextT] [TextT] False False)]- , parseTest "service : { foo : (x : text,) -> (y : text,); }" $- DidFile [] [("foo", MethodType [TextT] [TextT] False False)]- , parseTest "service : { foo : (opt text) -> () }" $- DidFile [] [("foo", MethodType [OptT TextT] [] False False) ]- , parseTest "service : { foo : (record { text; blob }) -> () }" $- DidFile [] [("foo", MethodType [RecT [(hashedField 0, TextT), (hashedField 1, BlobT)]] [] False False) ]- , parseTest "service : { foo : (record { x_ : null; 5 : nat8 }) -> () }" $- DidFile [] [("foo", MethodType [RecT [("x_", NullT), (hashedField 5, Nat8T)]] [] False False) ]- , parseTest "service : { foo : (record { x : null; 5 : nat8 }) -> () }" $- DidFile [] [("foo", MethodType [RecT [("x", NullT), (hashedField 5, Nat8T)]] [] False False) ]- , parseTest "service : { foo : (text) -> (text) query }" $- DidFile [] [("foo", MethodType [TextT] [TextT] True False)]- , parseTest "service : { foo : (text) -> (text) oneway }" $- DidFile [] [("foo", MethodType [TextT] [TextT] False True)]- , parseTest "service : { foo : (text) -> (text) query oneway }" $- DidFile [] [("foo", MethodType [TextT] [TextT] True True)]- , parseTest "service : { foo : (text) -> (text) oneway query }" $- DidFile [] [("foo", MethodType [TextT] [TextT] True True)]- , parseTest "service : (opt SomeInit) -> { foo : (text) -> (text) oneway query }" $- DidFile [] [("foo", MethodType [TextT] [TextT] True True)]- , parseTest "type t = int; service : { foo : (t) -> (t) }" $- DidFile [("t", IntT)] [("foo", MethodType [RefT "t"] [RefT "t"] False False)]- ]- , thTests- , testProperty "field name escaping round-tripping" $ \e ->- let f = either labledField hashedField e in- let f' = unescapeFieldName (escapeFieldName f) in- f' == f- , testGroup "candid hash inversion"- [ QC.testProperty "long dictionary name" $- let s = "precriticized" in- invertHash (candidHash s) QC.=== Just s- , QC.testProperty "long capitalized dictionary name" $- let s = "Precriticized" in- invertHash (candidHash s) QC.=== Just s- , QC.testProperty "all hashes find something" $- QC.forAll QC.arbitraryBoundedIntegral $ \w ->- w >= 32 QC.==> case invertHash w of- Nothing -> False- Just s -> candidHash s == w- ]- ]--instance Monad m => Serial m BS.ByteString where- series = BS.pack <$> series--instance Monad m => Serial m Principal where- series = Principal <$> series--instance Monad m => Serial m Reserved where- series = Reserved <$ series @m @()--instance Monad m => Serial m (FuncRef mt) where- series = FuncRef <$> series <*> series--instance Monad m => Serial m (ServiceRef r) where- series = ServiceRef <$> series--instance (Monad m, Forall r (Serial m), AllUniqueLabels r) => Serial m (Rec r) where- series = R.fromLabelsA @(Serial m) (\_l -> series)--instance (Monad m, Forall r (Serial m), AllUniqueLabels r) => Serial m (Var r) where- series = V.fromLabels @(Serial m) (\_l -> series)+main = defaultMainWithRerun $+ testGroup "tests" $ tests ++ [specTests, thTests]