Skip to content
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

Encourage ExceptT (NonEmptyList ForeignError) directly #87

Merged
merged 3 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ New features:
Bugfixes:

Other improvements:
- Replace all usages of `F` and `FT` with `Except`/`ExceptT (NonEmptyList ForeignError)` (#87 by @JordanMartinez)

Often times, the `F` and `FT` aliases did more to hinder usage of this library than help. These aliases
haven't been deprecated, but usage of them is now discouraged. All code in the library now uses
the full type that is aliased by `F` and `FT`.

## [v6.0.1](https://github.com/purescript/purescript-foreign/releases/tag/v6.0.1) - 2021-04-20

Expand Down
7 changes: 4 additions & 3 deletions examples/Applicative.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ module Example.Applicative where

import Prelude

import Control.Monad.Except (runExcept)
import Control.Monad.Except (Except, runExcept)
import Data.List.NonEmpty (NonEmptyList)
import Effect (Effect)
import Effect.Console (logShow)
import Example.Util.Value (foreignValue)
import Foreign (F, Foreign, readNumber)
import Foreign (Foreign, ForeignError, readNumber)
import Foreign.Index ((!))

data Point = Point Number Number Number

instance showPoint :: Show Point where
show (Point x y z) = "(Point " <> show [x, y, z] <> ")"

readPoint :: Foreign -> F Point
readPoint :: Foreign -> Except (NonEmptyList ForeignError) Point
readPoint value = do
Point
<$> (value ! "x" >>= readNumber)
Expand Down
9 changes: 5 additions & 4 deletions examples/Complex.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ module Example.Complex where

import Prelude

import Control.Monad.Except (runExcept)
import Control.Monad.Except (Except, runExcept)
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe)
import Data.Traversable (traverse)
import Effect (Effect)
import Effect.Console (logShow)
import Example.Util.Value (foreignValue)
import Foreign (F, Foreign, readArray, readBoolean, readNumber, readString, readNullOrUndefined)
import Foreign (Foreign, ForeignError, readArray, readBoolean, readNumber, readString, readNullOrUndefined)
import Foreign.Index ((!))

type SomeObject =
Expand All @@ -18,7 +19,7 @@ type SomeObject =
, list :: Array ListItem
}

readSomeObject :: Foreign -> F SomeObject
readSomeObject :: Foreign -> Except (NonEmptyList ForeignError) SomeObject
readSomeObject value = do
foo <- value ! "foo" >>= readString
bar <- value ! "bar" >>= readBoolean
Expand All @@ -32,7 +33,7 @@ type ListItem =
, z :: Maybe Number
}

readListItem :: Foreign -> F ListItem
readListItem :: Foreign -> Except (NonEmptyList ForeignError) ListItem
readListItem value = do
x <- value ! "x" >>= readNumber
y <- value ! "y" >>= readNumber
Expand Down
7 changes: 4 additions & 3 deletions examples/Objects.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ module Example.Objects where

import Prelude

import Control.Monad.Except (runExcept)
import Control.Monad.Except (Except, runExcept)
import Data.List.NonEmpty (NonEmptyList)
import Effect (Effect)
import Effect.Console (logShow)
import Example.Util.Value (foreignValue)
import Foreign (F, Foreign, readNumber)
import Foreign (Foreign, ForeignError, readNumber)
import Foreign.Index ((!))

type Point = { x :: Number, y :: Number }

readPoint :: Foreign -> F Point
readPoint :: Foreign -> Except (NonEmptyList ForeignError) Point
readPoint value = do
x <- value ! "x" >>= readNumber
y <- value ! "y" >>= readNumber
Expand Down
7 changes: 4 additions & 3 deletions examples/ParseErrors.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ module Example.ParseErrors where

import Prelude

import Control.Monad.Except (runExcept)
import Control.Monad.Except (Except, runExcept)
import Data.List.NonEmpty (NonEmptyList)
import Data.Traversable (traverse)
import Effect (Effect)
import Effect.Console (logShow)
import Example.Util.Value (foreignValue)
import Foreign (F, Foreign, readArray, readBoolean, readNumber, readString)
import Foreign (Foreign, ForeignError, readArray, readBoolean, readNumber, readString)
import Foreign.Index ((!))

newtype Point = Point { x :: Number, y :: Number }

instance showPoint :: Show Point where
show (Point o) = "(Point { x: " <> show o.x <> ", y: " <> show o.y <> " })"

readPoint :: Foreign -> F Point
readPoint :: Foreign -> Except (NonEmptyList ForeignError) Point
readPoint value = do
x <- value ! "x" >>= readNumber
y <- value ! "y" >>= readNumber
Expand Down
7 changes: 4 additions & 3 deletions examples/Union.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ module Example.Union where

import Prelude

import Control.Monad.Except (runExcept)
import Control.Monad.Except (Except, runExcept)
import Data.List.NonEmpty (NonEmptyList)
import Effect (Effect)
import Effect.Console (logShow)
import Example.Util.Value (foreignValue)
import Foreign (F, Foreign, readBoolean, readString)
import Foreign (Foreign, ForeignError, readBoolean, readString)
import Foreign.Index ((!))

data StringList = Nil | Cons String StringList
Expand All @@ -15,7 +16,7 @@ instance showStringList :: Show StringList where
show Nil = "Nil"
show (Cons s l) = "(Cons " <> show s <> " " <> show l <> ")"

readStringList :: Foreign -> F StringList
readStringList :: Foreign -> Except (NonEmptyList ForeignError) StringList
readStringList value =
value ! "nil" >>=
readBoolean >>=
Expand Down
6 changes: 4 additions & 2 deletions examples/Util/Value.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ module Example.Util.Value where

import Prelude

import Control.Monad.Except (Except)
import Data.Function.Uncurried (Fn3, runFn3)
import Data.List.NonEmpty (NonEmptyList)

import Foreign (F, Foreign, ForeignError(..), fail)
import Foreign (Foreign, ForeignError(..), fail)

foreign import foreignValueImpl :: forall r. Fn3 (String -> r) (Foreign -> r) String r

foreignValue :: String -> F Foreign
foreignValue :: String -> Except (NonEmptyList ForeignError) Foreign
foreignValue json = runFn3 foreignValueImpl (fail <<< ForeignError) pure json
39 changes: 26 additions & 13 deletions src/Foreign.purs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
-- | This module defines types and functions for working with _foreign_
-- | data.

-- |
-- | `ExceptT (NonEmptyList ForeignError) m` is used in this library
-- | to encode possible failures when dealing with foreign data.
-- |
-- | The `Alt` instance for `ExceptT` allows us to accumulate errors,
-- | unlike `Either`, which preserves only the last error.
module Foreign
( Foreign
, ForeignError(..)
Expand Down Expand Up @@ -76,12 +81,20 @@ renderForeignError (ErrorAtIndex i e) = "Error at array index " <> show i <> ":
renderForeignError (ErrorAtProperty prop e) = "Error at property " <> show prop <> ": " <> renderForeignError e
renderForeignError (TypeMismatch exp act) = "Type mismatch: expected " <> exp <> ", found " <> act

-- | While this alias is not deprecated, it is recommended
-- | that one use `Except (NonEmptyList ForeignError)` directly
-- | for all future usages rather than this type alias.
-- |
-- | An error monad, used in this library to encode possible failures when
-- | dealing with foreign data.
-- |
-- | The `Alt` instance for `Except` allows us to accumulate errors,
-- | unlike `Either`, which preserves only the last error.
type F = Except MultipleErrors

-- | While this alias is not deprecated, it is recommended
-- | that one use `ExceptT (NonEmptyList ForeignError)` directly
-- | for all future usages rather than this type alias.
type FT = ExceptT MultipleErrors

-- | Coerce any value to the a `Foreign` value.
Expand All @@ -107,7 +120,7 @@ foreign import tagOf :: Foreign -> String

-- | Unsafely coerce a `Foreign` value when the value has a particular `tagOf`
-- | value.
unsafeReadTagged :: forall m a. Monad m => String -> Foreign -> FT m a
unsafeReadTagged :: forall m a. Monad m => String -> Foreign -> ExceptT (NonEmptyList ForeignError) m a
unsafeReadTagged tag value
| tagOf value == tag = pure (unsafeFromForeign value)
| otherwise = fail $ TypeMismatch tag (tagOf value)
Expand All @@ -122,52 +135,52 @@ foreign import isUndefined :: Foreign -> Boolean
foreign import isArray :: Foreign -> Boolean

-- | Attempt to coerce a foreign value to a `String`.
readString :: forall m. Monad m => Foreign -> FT m String
readString :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m String
readString = unsafeReadTagged "String"

-- | Attempt to coerce a foreign value to a `Char`.
readChar :: forall m. Monad m => Foreign -> FT m Char
readChar :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m Char
readChar value = mapExceptT (map $ either (const error) fromString) (readString value)
where
fromString = maybe error pure <<< toChar
error = Left $ NEL.singleton $ TypeMismatch "Char" (tagOf value)

-- | Attempt to coerce a foreign value to a `Boolean`.
readBoolean :: forall m. Monad m => Foreign -> FT m Boolean
readBoolean :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m Boolean
readBoolean = unsafeReadTagged "Boolean"

-- | Attempt to coerce a foreign value to a `Number`.
readNumber :: forall m. Monad m => Foreign -> FT m Number
readNumber :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m Number
readNumber = unsafeReadTagged "Number"

-- | Attempt to coerce a foreign value to an `Int`.
readInt :: forall m. Monad m => Foreign -> FT m Int
readInt :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m Int
readInt value = mapExceptT (map $ either (const error) fromNumber) (readNumber value)
where
fromNumber = maybe error pure <<< Int.fromNumber
error = Left $ NEL.singleton $ TypeMismatch "Int" (tagOf value)

-- | Attempt to coerce a foreign value to an array.
readArray :: forall m. Monad m => Foreign -> FT m (Array Foreign)
readArray :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m (Array Foreign)
readArray value
| isArray value = pure $ unsafeFromForeign value
| otherwise = fail $ TypeMismatch "array" (tagOf value)

readNull :: forall m. Monad m => Foreign -> FT m (Maybe Foreign)
readNull :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m (Maybe Foreign)
readNull value
| isNull value = pure Nothing
| otherwise = pure (Just value)

readUndefined :: forall m. Monad m => Foreign -> FT m (Maybe Foreign)
readUndefined :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m (Maybe Foreign)
readUndefined value
| isUndefined value = pure Nothing
| otherwise = pure (Just value)

readNullOrUndefined :: forall m. Monad m => Foreign -> FT m (Maybe Foreign)
readNullOrUndefined :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m (Maybe Foreign)
readNullOrUndefined value
| isNull value || isUndefined value = pure Nothing
| otherwise = pure (Just value)

-- | Throws a failure error in `FT`.
fail :: forall m a. Monad m => ForeignError -> FT m a
-- | Throws a failure error in `ExceptT (NonEmptyList ForeignError) m`.
fail :: forall m a. Monad m => ForeignError -> ExceptT (NonEmptyList ForeignError) m a
fail = throwError <<< NEL.singleton
12 changes: 6 additions & 6 deletions src/Foreign/Index.purs
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,36 @@ import Prelude

import Control.Monad.Except.Trans (ExceptT)

import Foreign (Foreign, FT, ForeignError(..), typeOf, isUndefined, isNull, fail)
import Foreign (Foreign, ForeignError(..), typeOf, isUndefined, isNull, fail)
import Data.Function.Uncurried (Fn2, runFn2, Fn4, runFn4)
import Data.List.NonEmpty (NonEmptyList)

-- | This type class identifies types that act like _property indices_.
-- |
-- | The canonical instances are for `String`s and `Int`s.
class Index i m | i -> m where
index :: Foreign -> i -> FT m Foreign
index :: Foreign -> i -> ExceptT (NonEmptyList ForeignError) m Foreign
hasProperty :: i -> Foreign -> Boolean
hasOwnProperty :: i -> Foreign -> Boolean
errorAt :: i -> ForeignError -> ForeignError

class Indexable a m | a -> m where
ix :: forall i. Index i m => a -> i -> FT m Foreign
ix :: forall i. Index i m => a -> i -> ExceptT (NonEmptyList ForeignError) m Foreign

infixl 9 ix as !

foreign import unsafeReadPropImpl :: forall r k. Fn4 r (Foreign -> r) k Foreign r

unsafeReadProp :: forall k m. Monad m => k -> Foreign -> FT m Foreign
unsafeReadProp :: forall k m. Monad m => k -> Foreign -> ExceptT (NonEmptyList ForeignError) m Foreign
unsafeReadProp k value =
runFn4 unsafeReadPropImpl (fail (TypeMismatch "object" (typeOf value))) pure k value

-- | Attempt to read a value from a foreign value property
readProp :: forall m. Monad m => String -> Foreign -> FT m Foreign
readProp :: forall m. Monad m => String -> Foreign -> ExceptT (NonEmptyList ForeignError) m Foreign
readProp = unsafeReadProp

-- | Attempt to read a value from a foreign value at the specified numeric index
readIndex :: forall m. Monad m => Int -> Foreign -> FT m Foreign
readIndex :: forall m. Monad m => Int -> Foreign -> ExceptT (NonEmptyList ForeignError) m Foreign
readIndex = unsafeReadProp

foreign import unsafeHasOwnProperty :: forall k. Fn2 k Foreign Boolean
Expand Down
6 changes: 4 additions & 2 deletions src/Foreign/Keys.purs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ module Foreign.Keys

import Prelude

import Foreign (FT, Foreign, ForeignError(..), typeOf, isUndefined, isNull, fail)
import Foreign (Foreign, ForeignError(..), typeOf, isUndefined, isNull, fail)
import Control.Monad.Except (ExceptT)
import Data.List.NonEmpty (NonEmptyList)

foreign import unsafeKeys :: Foreign -> Array String

-- | Get an array of the properties defined on a foreign value
keys :: forall m. Monad m => Foreign -> FT m (Array String)
keys :: forall m. Monad m => Foreign -> ExceptT (NonEmptyList ForeignError) m (Array String)
keys value
| isNull value = fail $ TypeMismatch "object" "null"
| isUndefined value = fail $ TypeMismatch "object" "undefined"
Expand Down