Skip to content

Support for Tuple #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions src/Simple/JSON.purs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ module Simple.JSON (
import Prelude

import Control.Monad.Except (runExcept, withExcept)
import Data.Array (length)
import Data.Either (Either)
import Data.Foreign (F, Foreign, ForeignError(..), MultipleErrors, readArray, readBoolean, readChar, readInt, readNull, readNumber, readString, toForeign)
import Data.Foreign (F, Foreign, ForeignError(..), MultipleErrors, fail, readArray, readBoolean, readChar, readInt, readNull, readNumber, readString, toForeign)
import Data.Foreign.Index (readProp)
import Data.Foreign.Internal (readStrMap)
import Data.Foreign.JSON (parseJSON)
Expand All @@ -35,6 +36,7 @@ import Data.Record.Builder as Builder
import Data.StrMap as StrMap
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
import Data.Traversable (sequence, traverse)
import Data.Tuple (Tuple(..))
import Global.Unsafe (unsafeStringify)
import Type.Row (class RowLacks, class RowToList, Cons, Nil, RLProxy(RLProxy), kind RowList)

Expand Down Expand Up @@ -76,47 +78,57 @@ read = readImpl
class ReadForeign a where
readImpl :: Foreign -> F a

instance readForeign :: ReadForeign Foreign where
instance readForeignForeign :: ReadForeign Foreign where
readImpl = pure

instance readChar :: ReadForeign Char where
instance readForeignChar :: ReadForeign Char where
readImpl = readChar

instance readNumber :: ReadForeign Number where
instance readForeignNumber :: ReadForeign Number where
readImpl = readNumber

instance readInt :: ReadForeign Int where
instance readForeignInt :: ReadForeign Int where
readImpl = readInt

instance readString :: ReadForeign String where
instance readForeignString :: ReadForeign String where
readImpl = readString

instance readBoolean :: ReadForeign Boolean where
instance readForeignBoolean :: ReadForeign Boolean where
readImpl = readBoolean

instance readArray :: ReadForeign a => ReadForeign (Array a) where
instance readForeignArray :: ReadForeign a => ReadForeign (Array a) where
readImpl = readElements <=< readArray
where
readElements xs = sequence $ readImpl <$> xs

instance readNullOrUndefined :: ReadForeign a => ReadForeign (NullOrUndefined a) where
instance readForeignNullOrUndefined :: ReadForeign a => ReadForeign (NullOrUndefined a) where
readImpl = readNullOrUndefined readImpl

instance readMaybe :: ReadForeign a => ReadForeign (Maybe a) where
instance readForeignMaybe :: ReadForeign a => ReadForeign (Maybe a) where
readImpl = map unNullOrUndefined <<< readImpl

instance readNullable :: ReadForeign a => ReadForeign (Nullable a) where
instance readForeignNullable :: ReadForeign a => ReadForeign (Nullable a) where
readImpl o = withExcept (map reformat) $
map toNullable <$> traverse readImpl =<< readNull o
where
reformat error = case error of
TypeMismatch inner other -> TypeMismatch ("Nullable " <> inner) other
_ -> error

instance readStrMap :: ReadForeign a => ReadForeign (StrMap.StrMap a) where
instance readForeignStrMap :: ReadForeign a => ReadForeign (StrMap.StrMap a) where
readImpl = sequence <<< StrMap.mapWithKey (const readImpl) <=< readStrMap

instance readRecord ::
instance readForeignTuple :: (ReadForeign a, ReadForeign b) => ReadForeign (Tuple a b) where
readImpl = asTuple <=< readArray
where asTuple :: Array Foreign -> F (Tuple a b)
asTuple = case _ of
[a, b] -> do
ra <- readImpl a
rb <- readImpl b
pure $ Tuple ra rb
l -> fail $ TypeMismatch "2 values" (show (length l) <> " values")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I just realized now that you have a single array decoding. For reading, is there not a way we could somehow maybe read in the whole array and then map them back to the tuples? Might require some generics-rep code?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like (a /\ b /\ c /\ d) should be [a,b,c,d]?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Separate PR?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized that some actually do encode nested array pairs though. I think probably it'd be best to go with whatever you need with this for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nested tuples don't serialise correctly, I'll try to fix it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understood what you mean... I need ReadForeign Unit for encoding Tuple3 & friends and used a broken readImpl for now, but I guess Data.Foreign could provide that.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should never be using the value of Unit anywhere for reading or writing, it's only passed around like an opaque foreign import data declared type. You should always use an empty record for this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit's foreign representation should be null or undefined or something anyway, the value is absolutely meaningless because it's never supposed to be inspected or used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that what you mean? How would that be better?
In any case, a generic n-tuple serialisation as array probably won't need unit anyway, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, as in no one will give you a stop element anyway. People expect (1 /\ 2 /\ 3) to become [1,2,3]


instance readForeignRecord ::
( RowToList fields fieldList
, ReadForeignFields fieldList () fields
) => ReadForeign (Record fields) where
Expand All @@ -133,7 +145,7 @@ class ReadForeignFields (xs :: RowList) (from :: # Type) (to :: # Type)
-> Foreign
-> F (Builder (Record from) (Record to))

instance readFieldsCons ::
instance readForeignFieldsCons ::
( IsSymbol name
, ReadForeign ty
, ReadForeignFields tail from from'
Expand All @@ -153,7 +165,7 @@ instance readFieldsCons ::
name = reflectSymbol nameP
withExcept' = withExcept <<< map $ ErrorAtProperty name

instance readFieldsNil ::
instance readForeignFieldsNil ::
ReadForeignFields Nil () () where
getFields _ _ =
pure id
Expand Down Expand Up @@ -197,6 +209,9 @@ instance writeForeignNullable :: WriteForeign a => WriteForeign (Nullable a) whe
instance writeForeignStrMap :: WriteForeign a => WriteForeign (StrMap.StrMap a) where
writeImpl = toForeign <<< StrMap.mapWithKey (const writeImpl)

instance writeForeignTuple :: (WriteForeign a, WriteForeign b) => WriteForeign (Tuple a b) where
writeImpl (Tuple a b) = writeImpl [toForeign a, toForeign b]

instance recordWriteForeign ::
( RowToList row rl
, WriteForeignFields rl row () to
Expand Down
24 changes: 23 additions & 1 deletion test/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Prelude

import Control.Monad.Aff (Aff)
import Control.Monad.Eff (Eff)
import Control.Monad.Except (runExcept)
import Data.Argonaut.Core (Json)
import Data.Argonaut.Parser (jsonParser)
import Data.Either (Either(..), either, fromLeft, isRight)
Expand All @@ -16,6 +15,7 @@ import Data.Maybe (Maybe)
import Data.NonEmpty (NonEmpty(..))
import Data.Nullable (Nullable)
import Data.StrMap (StrMap)
import Data.Tuple (Tuple)
import Partial.Unsafe (unsafePartial)
import Simple.JSON (class ReadForeign, class WriteForeign, readJSON, writeJSON)
import Test.Spec (describe, it)
Expand Down Expand Up @@ -50,6 +50,10 @@ type MyTestMaybe =
{ a :: Maybe String
}

type MyTestTuple =
{ a :: Tuple Int String
}

type MyTestManyMaybe =
{ a :: Maybe String
, aNull :: Maybe String
Expand Down Expand Up @@ -106,6 +110,21 @@ main = run [consoleReporter] do
(unsafePartial $ fromLeft result) `shouldEqual`
(NonEmptyList (NonEmpty (ErrorAtProperty "b" (TypeMismatch "Nullable String" "Undefined")) Nil))
isRight (result :: E MyTestNullable) `shouldEqual` false
it "fails with invalid length Tuple" do
let result = readJSON """
{ "a": [1, "foo", 4] }
"""
(unsafePartial $ fromLeft result) `shouldEqual`
(NonEmptyList (NonEmpty (ErrorAtProperty "a" (TypeMismatch "2 values" "3 values")) Nil))
isRight (result :: E MyTestTuple) `shouldEqual` false
it "fails with invalid Tuple" do
let result = readJSON """
{ "a": [1, 4] }
"""
(unsafePartial $ fromLeft result) `shouldEqual`
(NonEmptyList (NonEmpty (ErrorAtProperty "a" (TypeMismatch "String" "Number")) Nil))
isRight (result :: E MyTestTuple) `shouldEqual` false


describe "roundtrips" do
it "works with proper JSON" $ roundtrips (Proxy :: Proxy MyTest) """
Expand All @@ -132,3 +151,6 @@ main = run [consoleReporter] do
it "works with Nullable" $ roundtrips (Proxy :: Proxy MyTestNullable) """
{ "a": null, "b": "a" }
"""
it "works with Tuple" $ roundtrips (Proxy :: Proxy MyTestTuple) """
{ "a": [1, "foo"] }
"""