-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds mechanism for converting Fleece schemas to Aeson Values
This allows for the use of Fleece schemas in defining `ToJSON` instances, like we do using `toParser` for `FromJSON` instances.
- Loading branch information
Showing
4 changed files
with
191 additions
and
3 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
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
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
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,186 @@ | ||
{-# LANGUAGE DataKinds #-} | ||
{-# LANGUAGE InstanceSigs #-} | ||
{-# LANGUAGE RankNTypes #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
{-# LANGUAGE TypeApplications #-} | ||
{-# LANGUAGE TypeFamilies #-} | ||
{-# LANGUAGE TypeOperators #-} | ||
|
||
module Fleece.Aeson.ToValue | ||
( ToValue (..) | ||
, toValue | ||
) where | ||
|
||
import qualified Data.Aeson as Aeson | ||
import qualified Data.Aeson.Key as AesonKey | ||
import qualified Data.Aeson.Types as AesonTypes | ||
import qualified Data.ByteString.Lazy as LBS | ||
import Data.Coerce (coerce) | ||
import qualified Data.Map.Strict as Map | ||
import qualified Data.Text as T | ||
import qualified Data.Text.Encoding as Enc | ||
import GHC.TypeLits (KnownSymbol, symbolVal) | ||
import Shrubbery (type (@=)) | ||
import qualified Shrubbery | ||
|
||
import qualified Fleece.Core as FC | ||
|
||
toValue :: ToValue a -> a -> Aeson.Value | ||
toValue (ToValue _name f) = | ||
f | ||
|
||
data ToValue a | ||
= ToValue FC.Name (a -> Aeson.Value) | ||
|
||
instance FC.Fleece ToValue where | ||
newtype Object ToValue object _constructor | ||
= Object (object -> [AesonTypes.Pair]) | ||
|
||
newtype Field ToValue object _a | ||
= Field (object -> [AesonTypes.Pair]) | ||
|
||
newtype AdditionalFields ToValue object _a | ||
= AdditionalFields (object -> [AesonTypes.Pair]) | ||
|
||
newtype UnionMembers ToValue _allTypes handledTypes | ||
= UnionMembers (Shrubbery.BranchBuilder handledTypes Aeson.Value) | ||
|
||
newtype TaggedUnionMembers ToValue _allTypes handledTypes | ||
= TaggedUnionMembers (Shrubbery.TaggedBranchBuilder handledTypes (T.Text, [AesonTypes.Pair])) | ||
|
||
schemaName (ToValue name _toJSON) = | ||
name | ||
|
||
number = | ||
ToValue (FC.unqualifiedName "number") Aeson.toJSON | ||
|
||
text = | ||
ToValue (FC.unqualifiedName "text") Aeson.toJSON | ||
|
||
boolean = | ||
ToValue (FC.unqualifiedName "boolean") Aeson.toJSON | ||
|
||
null = | ||
ToValue | ||
(FC.unqualifiedName "null") | ||
(\FC.Null -> Aeson.Null) | ||
|
||
array (ToValue name itemToJSON) = | ||
ToValue | ||
(FC.annotateName name "array") | ||
(Aeson.Array . fmap itemToJSON) | ||
|
||
nullable (ToValue name toJSON) = | ||
ToValue (FC.annotateName name "nullable") $ \mbValue -> | ||
case mbValue of | ||
Left FC.Null -> Aeson.Null | ||
Right value -> toJSON value | ||
|
||
required name accessor (ToValue _name toJSON) = | ||
let | ||
key = AesonKey.fromString name | ||
in | ||
Field $ \object -> | ||
[(key, toJSON (accessor object))] | ||
|
||
optional name accessor (ToValue _name toJSON) = | ||
let | ||
key = AesonKey.fromString name | ||
in | ||
Field $ \object -> | ||
case accessor object of | ||
Just value -> | ||
[(key, toJSON value)] | ||
Nothing -> | ||
[] | ||
|
||
additionalFields accessor (ToValue _name toJSON) = | ||
AdditionalFields $ \object -> | ||
map (\(key, value) -> (AesonKey.fromText key, toJSON value)) | ||
. Map.toList | ||
. accessor | ||
$ object | ||
|
||
mapField _f encoder = | ||
coerce encoder | ||
|
||
constructor _f = | ||
Object (\_ -> mempty) | ||
|
||
field (Object mkStart) (Field mkNext) = | ||
Object $ \object -> | ||
mkStart object <> mkNext object | ||
|
||
additional (Object mkStart) (AdditionalFields mkRest) = | ||
Object $ \object -> | ||
mkStart object <> mkRest object | ||
|
||
objectNamed name (Object toObject) = | ||
ToValue name (Aeson.object . toObject) | ||
|
||
boundedEnumNamed name toText = | ||
ToValue name (Aeson.toJSON . toText) | ||
|
||
validateNamed name uncheck _check (ToValue _unvalidatedName toJSON) = | ||
ToValue name (toJSON . uncheck) | ||
|
||
unionNamed name (UnionMembers builder) = | ||
let | ||
branches = | ||
Shrubbery.branchBuild builder | ||
in | ||
ToValue name (Shrubbery.dissectUnion branches) | ||
|
||
unionMemberWithIndex _index encoder = | ||
UnionMembers $ | ||
let | ||
-- It's important that this let is _inside_ the 'UnionMembers' | ||
-- constructor so that it lazy enough to allow the recursive reference | ||
-- of 'anyJSON' to itself within arrays. | ||
ToValue _name toJSON = encoder | ||
in | ||
Shrubbery.singleBranch toJSON | ||
|
||
unionCombine (UnionMembers left) (UnionMembers right) = | ||
UnionMembers (Shrubbery.appendBranches left right) | ||
|
||
taggedUnionNamed name tagProperty (TaggedUnionMembers builder) = | ||
let | ||
branches = | ||
Shrubbery.taggedBranchBuild builder | ||
|
||
tagKey = | ||
AesonKey.fromString tagProperty | ||
in | ||
ToValue name $ \value -> | ||
let | ||
(tagValue, fields) = | ||
Shrubbery.dissectTaggedUnion branches value | ||
in | ||
Aeson.object ((tagKey, Aeson.toJSON tagValue) : fields) | ||
|
||
taggedUnionMemberWithTag :: | ||
forall tag allTags a proxy. | ||
KnownSymbol tag => | ||
proxy tag -> | ||
FC.Object ToValue a a -> | ||
FC.TaggedUnionMembers ToValue allTags '[tag @= a] | ||
taggedUnionMemberWithTag tag (Object mkFields) = | ||
let | ||
tagValue = | ||
T.pack (symbolVal tag) | ||
in | ||
TaggedUnionMembers (Shrubbery.taggedSingleBranch @tag (\a -> (tagValue, mkFields a))) | ||
|
||
taggedUnionCombine (TaggedUnionMembers left) (TaggedUnionMembers right) = | ||
TaggedUnionMembers (Shrubbery.appendTaggedBranches left right) | ||
|
||
jsonString (ToValue name toJSON) = | ||
ToValue | ||
name | ||
( Aeson.String | ||
. Enc.decodeUtf8 | ||
. LBS.toStrict | ||
. Aeson.encode | ||
. toJSON | ||
) |