packages feed

dovetail-aeson (empty) → 0.1.0.0

raw patch · 3 files changed

+389/−0 lines, 3 filesdep +aesondep +basedep +dovetail

Dependencies added: aeson, base, dovetail, purescript, purescript-cst, reflection, text, unordered-containers, vector

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Phil Freeman (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ dovetail-aeson.cabal view
@@ -0,0 +1,44 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 7d4dce6b707b74a3b9317eae0c627e812de98f8e6a533535f74e233434a40bf2++name:           dovetail-aeson+version:        0.1.0.0+synopsis:       Use Aeson from your Dovetail programs+category:       Language+homepage:       https://github.com/paf31/dovetail#readme+bug-reports:    https://github.com/paf31/dovetail/issues+author:         Phil Freeman+maintainer:     freeman.phil@gmail.com+copyright:      2021 Phil Freeman+license:        BSD3+license-file:   LICENSE+build-type:     Simple++source-repository head+  type: git+  location: https://github.com/paf31/dovetail++library+  exposed-modules:+      Dovetail.Aeson+  other-modules:+      Paths_dovetail_aeson+  hs-source-dirs:+      src+  ghc-options: -Wall -fwarn-unused-imports+  build-depends:+      aeson+    , base >=4.7 && <5+    , dovetail+    , purescript+    , purescript-cst+    , reflection+    , text+    , unordered-containers+    , vector+  default-language: Haskell2010
+ src/Dovetail/Aeson.hs view
@@ -0,0 +1,315 @@+{-# LANGUAGE BlockArguments        #-}+{-# LANGUAGE ConstraintKinds       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DerivingVia           #-}+{-# LANGUAGE ImportQualifiedPost   #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE KindSignatures        #-}+{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PatternSynonyms       #-}+{-# LANGUAGE RankNTypes            #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TupleSections         #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE UndecidableInstances  #-}++-- | +-- This module provides support for using "Dovetail" with @aeson@.+--+-- For certain appliations, it is useful to use JSON as an input or output format,+-- but to let the user decide the exact schema for that JSON data. The simplest way+-- to let the user have control over this from Dovetail is to use PureScript's+-- types to define the serialization functions. That is, just like with generic+-- deriving, our serializers will be inferred from our types. But with Dovetail,+-- we will use the inferred *PureScript* type to synthesize a serializer, not the+-- Haskell types.+--+-- The @query-json@ example in the repository is a good example. The user's program+-- defines a function from JSON inputs to JSON outputs, and the types and format +-- of the input and output data are determined by the type of the user's program,+-- which is allowed to be a function between any two *serializable* PureScript types.+--+-- Serializable types include primitives (strings, chars, ints, numbers and booleans),+-- records and arrays of other serializable types, and the special 'Nullable' type+-- which is provided for handling serialization of nullable JSON substructures.+--+-- Note: you do not need to use this module if you are working with JSON whose+-- structure is known ahead of time. In that case, you can simply use Dovetail +-- directly to marshall Haskell data back and forth over the FFI boundary, and+-- Aeson for serialization. This module should be used when the structure is+-- not known ahead of time, because it is controlled by the user.+--+-- The user's program may have a polymorphic type signature. This can happen+-- easily: for example, if the user's program in the @query-json@ example is a+-- record accessor such as @_.foo@, then it will have a polymorphic (indeed, also+-- row-polymorphic) type. We cannot know what JSON data the user will pass as an+-- input to a polymorphic program, and we can't synthesize a specific type for +-- that input data. So, this module also provides the 'UnknownJSON' type for+-- handling these cases, which is simply a wrapper around Aeson's 'Aeson.Value'+-- type. Since a polymorphic program cannot inspect a value with a polymorphic+-- type (thanks to parametricity), it is safe to make this data accessible to+-- the program in this way. However, this also means that such data will not+-- be visible in the debugger (and instead, will appear as an abstract @<foreign>@ +-- value).++module Dovetail.Aeson +  ( +  -- * Serializable types    +  -- ** Evaluation+    evalJSON+  +  -- ** Type reification+  , Serializable+  , reify +  +  -- ** Supporting code+  , stdlib+  , Nullable(..)+  , UnknownJSON(..)+  ) where++import Control.Monad.Fix (MonadFix)  +import Data.Aeson qualified as Aeson+import Data.Aeson.Types qualified as Aeson+import Data.Dynamic qualified as Dynamic+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.Proxy (Proxy(..))+import Data.Reflection (reifySymbol)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Vector (Vector)+import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)+import Dovetail+import Dovetail.Evaluate qualified as Evaluate+import Language.PureScript qualified as P+import Language.PureScript.Names qualified as Names+import Language.PureScript.Label qualified as Label++-- | Evaluate a PureScript expression from source, returning its value as +-- encoded JSON.+--+-- This function is a convenient counterpart to 'eval' which can be useful +-- for applications whose output format is JSON.+evalJSON +  :: MonadFix m+  => Maybe ModuleName +  -> Text+  -> InterpretT m Aeson.Value+evalJSON defaultModuleName expr = do+  (val, ty) <- eval defaultModuleName expr+  liftEvalT $ reify ty \(_ :: Proxy a) -> +    Aeson.toJSON <$> (fromValue @_ @a =<< val)++-- | A constraint synonym for the constraint our reified types will satisfy:+-- serialization via 'Aeson.ToJSON', deserialization via 'Aeson.FromJSON', and+-- transport via the FFI, via 'Evaluate.ToValue'.+--+-- This synonym is provided just for the convenience of tidying up the type+-- signatures.+type Serializable m a =+  ( Aeson.FromJSON a+  , Aeson.ToJSON a+  , Evaluate.ToValue m a+  )++-- | Reify a PureScript 'P.SourceType' as a Haskell type which supports+-- transport via the FFI using 'Evaluate.ToValue', and JSON serialization using+-- 'Aeson.ToJSON' and 'Aeson.FromJSON'.+--+-- Just as @DeriveGeneric@ allows us to derive a type-directed serialization+-- method based on the Haskell type of our values, this function allows us to+-- derive a serialization method based on the *PureScript* type of a value.+--+-- This can be useful in more advanced use cases where 'evalJSON' won't suffice.+-- For example, if we want to take data as input from a JSON structure,+-- then we can reify the PureScript type of the domain of a PureScript function.+reify +  :: forall m r+   . MonadFix m+  => P.SourceType +  -- ^ The PureScript type we wish to reify, for example, from the return value of 'eval'.+  -> (forall a. Serializable m a => Proxy a -> EvalT m r)+  -- ^ The continuation, which will receive a 'Proxy' for the type which has been+  -- reified.+  -> EvalT m r+reify = go where+  go :: P.SourceType +     -> (forall a. Serializable m a => Proxy a -> EvalT m r)+     -> EvalT m r+  go (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Int"))) f =+    f (Proxy :: Proxy Integer)+  go (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Number"))) f =+    f (Proxy :: Proxy Double)+  go (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "String"))) f =+    f (Proxy :: Proxy Text)+  go (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Char"))) f =+    f (Proxy :: Proxy Char)+  go (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Boolean"))) f =+    f (Proxy :: Proxy Bool)+  go P.TypeVar{} f =+    f (Proxy :: Proxy UnknownJSON) +  go (P.TypeApp _ (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Record"))) ty) f = do+    let (knownFields, unknownFields) = P.rowToSortedList ty+    case unknownFields of+      P.KindApp _ P.REmpty{} _ ->+        goRecord knownFields (\(Proxy :: Proxy xs) -> f (Proxy :: Proxy (Record xs)))+      P.TypeVar{} ->+        goRecord knownFields (\(Proxy :: Proxy xs) -> f (Proxy :: Proxy (OpenRecord xs)))+      _ ->+        Evaluate.throwErrorWithContext (Evaluate.OtherError "record type is not serializable")+  go (P.TypeApp _ (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "Prim")) (P.ProperName "Array"))) ty) f =+    go ty (\(Proxy :: Proxy a) -> f (Proxy :: Proxy (Vector a)))+  go (P.TypeApp _ (P.TypeConstructor _ (P.Qualified (Just (P.ModuleName "JSON")) (P.ProperName "Nullable"))) ty) f =+    go ty (\(Proxy :: Proxy a) -> f (Proxy :: Proxy (Nullable a))) +  go _  _ =+    Evaluate.throwErrorWithContext (Evaluate.OtherError "type is not serializable")++  goRecord+    :: [P.RowListItem P.SourceAnn]+    -> (forall a. (ToJSONObject a, FromJSONObject a, ToObject m a) => Proxy a -> EvalT m r)+    -> EvalT m r+  goRecord [] f = +    f (Proxy :: Proxy Nil)+  goRecord (P.RowListItem _ (Label.Label k) x : xs) f = do+    t <- Evaluate.evalPSString k+    reifySymbol (Text.unpack t) \(Proxy :: Proxy k) ->+      go x \(Proxy :: Proxy x) ->+        goRecord xs \(Proxy :: Proxy xs) ->+          f (Proxy :: Proxy (Cons k x xs))++data OpenRecord xs = OpenRecord+  { _knownFields :: xs+  , _allFields :: HashMap Text UnknownJSON+  }++instance FromJSONObject xs => Aeson.FromJSON (OpenRecord xs) where+  parseJSON = Aeson.withObject "object" \o -> +    OpenRecord <$> parseJSONObject o <*> pure (fmap UnknownJSON o)+  +instance ToJSONObject xs => Aeson.ToJSON (OpenRecord xs) where+  toJSON (OpenRecord xs o) = Aeson.Object (toJSONObject xs <> fmap getUnknownJSON o)++instance (MonadFix m, ToObject m xs) => ToValue m (OpenRecord xs) where+  toValue (OpenRecord xs o) = Evaluate.Object (toObject xs <> fmap toValue o)+  +  fromValue (Evaluate.Object o) = +    let isUnknownJSON (Evaluate.Foreign dyn)+          | Just{} <- Dynamic.fromDynamic @Aeson.Value dyn = True+        isUnknownJSON _ = False+     in OpenRecord <$> fromObject o <*> traverse fromValue (HashMap.filter isUnknownJSON o)+  fromValue other = +    Evaluate.throwErrorWithContext (Evaluate.TypeMismatch "object" other)++newtype Record xs = Record { getRecord :: xs }++instance FromJSONObject xs => Aeson.FromJSON (Record xs) where+  parseJSON = fmap Record . Aeson.withObject "object" parseJSONObject+  +instance ToJSONObject xs => Aeson.ToJSON (Record xs) where+  toJSON (Record xs) = Aeson.Object (toJSONObject xs)++instance (MonadFix m, ToObject m xs) => ToValue m (Record xs) where+  toValue = Evaluate.Object . toObject . getRecord+  +  fromValue (Evaluate.Object o) = +    Record <$> fromObject o +  fromValue other = +    Evaluate.throwErrorWithContext (Evaluate.TypeMismatch "object" other)++class FromJSONObject a where+  parseJSONObject :: Aeson.Object -> Aeson.Parser a+  +class ToJSONObject a where+  toJSONObject :: a -> Aeson.Object++class ToObject m a where+  toObject :: a -> HashMap Text (Value m)+  fromObject :: HashMap Text (Value m) -> EvalT m a++data Nil = Nil++instance FromJSONObject Nil where+  parseJSONObject _ = pure Nil+  +instance ToJSONObject Nil where +  toJSONObject _ = HashMap.empty++instance Monad m => ToObject m Nil where+  toObject _ = HashMap.empty+  fromObject _ = pure Nil++data Cons (k :: Symbol) x xs = Cons x xs+  +instance forall k x xs. (KnownSymbol k, Aeson.FromJSON x, FromJSONObject xs) => FromJSONObject (Cons k x xs) where+  parseJSONObject o =+    let k = symbolVal (Proxy :: Proxy k)+     in Cons <$> o Aeson..: (Text.pack k) <*> parseJSONObject o+  +instance forall k x xs. (KnownSymbol k, Aeson.ToJSON x, ToJSONObject xs) => ToJSONObject (Cons k x xs) where+  toJSONObject (Cons x xs) =+    let k = symbolVal (Proxy :: Proxy k)+     in HashMap.insert (Text.pack k) (Aeson.toJSON x) (toJSONObject xs)++instance forall m k x xs. (KnownSymbol k, ToValue m x, ToObject m xs) => ToObject m (Cons k x xs) where+  toObject (Cons x xs) = do+    let k = symbolVal (Proxy :: Proxy k)+    HashMap.insert (Text.pack k) (toValue x) (toObject xs)+        +  fromObject m = do+    let k = symbolVal (Proxy :: Proxy k)+    case HashMap.lookup (Text.pack k) m of+      Nothing -> +        Evaluate.throwErrorWithContext (Evaluate.FieldNotFound (Text.pack k) (Evaluate.Object m))+      Just v -> +        Cons <$> fromValue v <*> fromObject m++-- | A representation of nullable values for use in derived serializers.+--+-- See 'reify' and 'stdlib'.+newtype Nullable a = Nullable (Maybe a)+  deriving (Aeson.FromJSON, Aeson.ToJSON) via Maybe a++instance ToValue m a => ToValue m (Nullable a) where+  toValue (Nullable Nothing) = +    Evaluate.Constructor (Names.ProperName "Null") []+  toValue (Nullable (Just a)) = +    Evaluate.Constructor (Names.ProperName "NotNull") [toValue a]+  +  fromValue (Evaluate.Constructor (Names.ProperName "Null") []) =+    pure (Nullable Nothing)+  fromValue (Evaluate.Constructor (Names.ProperName "NotNull") [val]) =+    Nullable . Just <$> fromValue val+  fromValue other =+    Evaluate.throwErrorWithContext (Evaluate.TypeMismatch "Nullable" other)+    +-- | A representation of arbitrary JSON values for use in derived serializers.+--+-- This type is reified to stand in for any polymorphic type variables in a +-- PureScript type, since we cannot know the structure of values of those types+-- ahead of time.+--+-- See 'reify' and 'stdlib'.+newtype UnknownJSON = UnknownJSON { getUnknownJSON :: Aeson.Value }+  deriving (Aeson.ToJSON, Aeson.FromJSON) via Aeson.Value+  +instance MonadFix m => ToValue m UnknownJSON where+  toValue = toValue . Evaluate.ForeignType . getUnknownJSON+  fromValue = fmap (UnknownJSON . Evaluate.getForeignType) . fromValue+  +-- | This action makes a module named @JSON@ available to your PureScript code.+--+-- It defines the PureScript counterpart of the 'Nullable' type, which is used+-- to serialize nullable types when deriving serializers using 'reify'.+--+-- Any PureScript code which needs to support type-directed serialization for+-- values which may involve @null@ should import this module.+stdlib :: MonadFix m => InterpretT m (Module Ann)+stdlib = build . Text.unlines $+  [ "module JSON where"+  , ""+  , "data Nullable a = Null | NotNull a"+  ]