Creating PureScript records from inconsistent JavaScript objects - purescript

Assume I have User records in my PureScript code with the following type:
{ id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
A CommonJS module is derived from the PureScript code. Exported User-related functions will be called from external JavaScript code.
In the JavaScript code, a "user" may be represented as:
var alice = {id: 123, username: 'alice', email: 'alice#example.com', isActive: true};
email may be null:
var alice = {id: 123, username: 'alice', email: null, isActive: true};
email may be omitted:
var alice = {id: 123, username: 'alice', isActive: true};
isActive may be omitted, in which case it is assumed true:
var alice = {id: 123, username: 'alice'};
id is unfortunately sometimes a numeric string:
var alice = {id: '123', username: 'alice'};
The five JavaScript representations above are equivalent and should produce equivalent PureScript records.
How do I go about writing a function which takes a JavaScript object and returns a User record? It would use the default value for a null/omitted optional field, coerce a string id to a number, and throw if a required field is missing or if a value is of the wrong type.
The two approaches I can see are to use the FFI in the PureScript module or to define the conversion function in the external JavaScript code. The latter seems hairy:
function convert(user) {
var rec = {};
if (user.email == null) {
rec.email = PS.Data_Maybe.Nothing.value;
} else if (typeof user.email == 'string') {
rec.email = PS.Data_Maybe.Just.create(user.email);
} else {
throw new TypeError('"email" must be a string or null');
}
// ...
}
I'm not sure how the FFI version would work. I haven't yet worked with effects.
I'm sorry that this question is not very clear. I don't yet have enough understanding to know exactly what it is that I want to know.

I've put together a solution. I'm sure much can be improved, such as changing the type of toUser to Json -> Either String User and preserving error information. Please leave a comment if you can see any ways this code could be improved. :)
This solution uses PureScript-Argonaut in addition to a few core modules.
module Main
( User()
, toEmail
, toId
, toIsActive
, toUser
, toUsername
) where
import Control.Alt ((<|>))
import Data.Argonaut ((.?), toObject)
import Data.Argonaut.Core (JNumber(), JObject(), Json())
import Data.Either (Either(..), either)
import Data.Maybe (Maybe(..))
import Global (isNaN, readFloat)
type User = { id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
hush :: forall a b. Either a b -> Maybe b
hush = either (const Nothing) Just
toId :: JObject -> Maybe Number
toId obj = fromNumber <|> fromString
where
fromNumber = (hush $ obj .? "id")
fromString = (hush $ obj .? "id") >>= \s ->
let id = readFloat s in if isNaN id then Nothing else Just id
toUsername :: JObject -> Maybe String
toUsername obj = hush $ obj .? "username"
toEmail :: JObject -> Maybe String
toEmail obj = hush $ obj .? "email"
toIsActive :: JObject -> Maybe Boolean
toIsActive obj = (hush $ obj .? "isActive") <|> Just true
toUser :: Json -> Maybe User
toUser json = do
obj <- toObject json
id <- toId obj
username <- toUsername obj
isActive <- toIsActive obj
return { id: id
, username: username
, email: toEmail obj
, isActive: isActive
}
Update: I've made improvements to the code above based on a gist from Ben Kolera.

Have you had a look at purescript-foreign (https://github.com/purescript/purescript-foreign)? I think that's what you're looking for here.

As gb. wrote, that is exactly what the Foreign data type was built for. Off the top of my head:
convert :: Foreign -> F User
convert f = do
id <- f ! "id" >>= readNumber
name <- f ! "name" >>= readString
email <- (f ! "email" >>= readNull >>= traverse readString) <|> pure Nothing
isActive <- (f ! "isActive" >>= readBoolean) <|> pure true
return { id, name, email, isActive }

Just a little more ffi
module User where
import Data.Maybe
import Data.Function
foreign import data UserExternal :: *
type User =
{
id :: Number,
username :: String,
email :: Maybe String,
isActive :: Boolean
}
type MbUser =
{
id :: Maybe Number,
username :: Maybe String,
email :: Maybe String,
isActive :: Maybe Boolean
}
foreign import toMbUserImpl """
function toMbUserImpl(nothing, just, user) {
var result = {},
properties = ['username', 'email', 'isActive'];
var i, prop;
for (i = 0; i < properties.length; i++) {
prop = properties[i];
if (user.hasOwnProperty(prop)) {
result[prop] = just(user[prop]);
} else {
result[prop] = nothing;
}
}
if (!user.hasOwnProperty('id') || isNaN(parseInt(user.id))) {
result.id = nothing;
} else {
result.id = just(user.id);
}
return result;
}
""" :: forall a. Fn3 (Maybe a) (a -> Maybe a) UserExternal MbUser
toMbUser :: UserExternal -> MbUser
toMbUser ext = runFn3 toMbUserImpl Nothing Just ext
defaultId = 0
defaultName = "anonymous"
defaultActive = false
userFromMbUser :: MbUser -> User
userFromMbUser mbUser =
{
id: fromMaybe defaultId mbUser.id,
username: fromMaybe defaultName mbUser.username,
email: mbUser.email,
isActive: fromMaybe defaultActive mbUser.isActive
}
userFromExternal :: UserExternal -> User
userFromExternal ext = userFromMbUser $ toMbUser ext

Related

PureScript - Access Properties of a NewType

Consider the following code sample, which creates a new type to represent a customer model:
module Main where
import Effect (Effect)
import Effect.Console ( logShow )
import Prelude (Unit,(+),(/),(*),(-), (<>),discard)
newtype Customer
= Customer
{ firstname :: String
}
sample :: Customer
sample = Customer
{ firstname : "Average"
}
first :: Customer -> String
first a = _.firstname a
main = do
logShow ( first sample )
The expected output would be the value Average, which is equal to sample.name, but instead an error is produced:
Could not match type
{ firstname :: t0
| t1
}
with type
Customer
while checking that type Customer
is at least as general as type { firstname :: t0
| t1
}
while checking that expression a
has type { firstname :: t0
| t1
}
in value declaration first
where t0 is an unknown type
t1 is an unknown type
This is a good error, but doesn't explain how to actually access this value.
How do you access the value of an object created as a newType?
You have to do
first :: Customer -> String
first (Customer a) = _.firstname a
Since newtype is really, a new type.
One another way is to derive Newtype instance for that particular newtype, which exposes certain functions that will let you work on the data wrapped by the newtype.
derive instance newtypeCustomer :: Newtype Customer _
first :: Customer -> String
first = (_.firstname <<< unwrap)

Is there better way to unwrap record from sum type?

I have a sum type with record param, records have the same prop of the same type (tag :: String), and I need to get its value from passed T type value. So I do with case pattern matching:
data T = T1 { tag :: String, ... } | T2 { tag :: String, ...} | T3 {tag :: String, ...}
fun :: T -> String
fun t = case t of
T1 { tag } -> tag
T2 { tag } -> tag
T3 { tag } -> tag
I wonder if there is a more simple, less verbose way to do this?
If all your cases always have this field, and its semantics is the same in all cases (otherwise why would you have a function that conflates them?), then a cleaner design would be to bring it out of the cases:
type T = { tag :: String, theCase :: TCase }
data TCase = T1 { ... } | T2 { ... } | T3 { ... }
fun :: T -> String
fun = _.tag

Validation that returns more than 1 error

I'm doing a few validations using an older Scala version (2.0) but for each record I am currently getting only 1 error, assuming the record has 2 or more error -- I want to receive everything wrong with that record/item
case class Response(request: JsValue,
success: Boolean,
column: Option[String] = None,
message: String = "")
Here is validator, that return all errors in a json object
def validator(asJson: JsValue): Response = {
if (name == "")
return Response(asJson, false, Some("name"), "Name can't be blank")
if (age == -1 || age == "N/A")
return Response(asJson, false, Some("age"), "Age can't be blank")
if (DOB == -1 || DOB == "N/A" )
return Response(asJson, false, Some("DOB"), "DOB cannot be blank")
else
Response(asJson, true)
}
Currently if record1 doesn't have a name + age + DOB ---> I'm only getting "Name can't be blank"
How do I get (multiple errors per item instead of just 1 error per item):
Name can't be blank, Age can't be blank, DOB cannot be blank
This is just an outline of how it might work:
def validator(asJson: JsValue): Response = {
val errors = List(
if (name == "" ) Some("Name can't be blank") else None,
if (age == -1 || age == "N/A") Some("Age can't be blank") else None,
if (DOB == -1 || DOB == "N/A" ) Some("DOB cannot be blank") else None,
).flatten
if (errors.isEmpty) {
Response(asJson, true)
} else {
Response(asJson, false, errors.mkString(", "))
}
}
The errors value is created by making a List of Option values, one for each validation check. The flatten method extracts the contents of all the Some values and removes any None values. The result is a list of error strings.
The rest of the code formats the Response based on the list of errors.
If you are using Scala 2.13 then Option.when makes the tests shorter and clearer:
Option.when(name == "")("Name can't be blank"),
Option.when(age == -1 || age == "N/A")("Age can't be blank"),
Option.when(DOB == -1 || DOB == "N/A")("DOB cannot be blank"),
Here is an alternative using Validated that you might consider at some point
import play.api.libs.json._
import cats.data.ValidatedNec
import cats.implicits._
case class User(name: String, age: Int, dob: String)
case class UserDTO(name: Option[String], age: Option[Int], dob: Option[String])
implicit val userDtoFormat = Json.format[UserDTO]
val raw =
"""
|{
| "name": "Picard"
|}
|""".stripMargin
val userDto = Json.parse(raw).as[UserDTO]
def validateUser(userDto: UserDTO): ValidatedNec[String, User] = {
def validateName(user: UserDTO): ValidatedNec[String, String] =
user.name.map(_.validNec).getOrElse("Name is empty".invalidNec)
def validateAge(user: UserDTO): ValidatedNec[String, Int] =
user.age.map(_.validNec).getOrElse("Age is empty".invalidNec)
def validateDob(user: UserDTO): ValidatedNec[String, String] =
user.dob.map(_.validNec).getOrElse("DOB is empty".invalidNec)
(validateName(userDto), validateAge(userDto), validateDob(userDto)).mapN(User)
}
validateUser(userDto)
// res0: cats.data.ValidatedNec[String,User] = Invalid(Chain(Age is empty, DOB is empty))
Note the distinction between UserDTO ("data transfer object") which models whatever payload was sent over the wire, and User which models the actual data we require for our business logic. We separate the concern of user validation into its own method validateUser. Now we can work with valid user or errors like so
validateUserDTO(userDto) match {
case Valid(user) =>
// do something with valid user
case Invalid(errors) =>
// do something with errors
}
First of all, in scala you do not have the return statement, the last statement is returned (in fact it exists but its use is not recomended).
Secondly, in your code, I suppose you want to pass through several if statements. But if you return after each if statement you exit your function at the first condition that is true, and you will never go to the rest of the code.
If you want to gather several instances of Response, just gather them in a collection, and return the collection at the end of the function. A collection might be a List, or a Seq, or whatever.
In my code I've simple Validator that does exactly that:
abstract class Validator[T, ERR >: Null] {
def validate(a:T):Seq[ERR]
protected def is(errorCase:Boolean, err: => ERR) = if (errorCase) err else null
protected def check(checks:ERR*) = checks.collectFirst{ case x if x != null => x }.getOrElse(null)
protected def checkAll(checks:ERR*) = checks.filter(_ != null)
}
//implement checks on some types
val passwordValidator = new Validator[String, String] {
val universalPasswordInstruction = "Password needs to have at least one small letter, big letter, number and other sign."
def validate(a: String) = checkAll( //here we check all cases
is(a.length < 10, "Password should be longer than 10"),
check( //here we check only first one.
is(a.forall(_.isLetter), s"Passowrd has only letters. $universalPasswordInstruction"),
is(a.forall(_.isLetterOrDigit), s"Use at least one ascii character like '#' or '*'. $universalPasswordInstruction"),
),
is(a.contains("poop"), "Really!")
)
}
val userValidator = new Validator[(String, String), (Int, String)] {
override def validate(a: (String, String)): Seq[(Int, String)] = checkAll(
is(a._1.length > 0, (0, "Username is empty!")),
) ++ passwordValidator.validate(a._2).map(x => (1, x))
}
//use
userValidator.validate("SomeUserName" -> "SomePassword") match {
case Seq() => //OK case
case errors => //deal with it
}
Maybe not so fancy but works for me :). Code is simple and rather self explanatory. Nulls here are just implementation detail (they doesn't show up in usercode, and can be switched to Options).

Haskell API client pagination

I'm trying to build a Haskell client to consume a RESTful JSON API. I want to fetch a page, then take the "next_page" key from the response, and feed it into the query params of another api request. I want to continue doing this until I have paged through all the items. What is the cleanest way of doing this in Haskell? I'm using explicit recursion now, but I feel there must be a better way, maybe with the writer or state monad.
EDIT: Added my code. I'm aware I'm misusing fromJust.
data Post = Post {
title :: !Text,
domain :: !Text,
score :: Int,
url :: !Text
} deriving (Show)
instance FromJSON Post where
parseJSON (Object v) = do
objectData <- v .: "data"
title <- objectData .: "title"
domain <- objectData .: "domain"
score <- objectData .: "score"
url <- objectData .: "url"
return $ Post title domain score url
data GetPostsResult = GetPostsResult {
posts :: [Post],
after :: Maybe Text
} deriving (Show)
instance FromJSON GetPostsResult where
parseJSON (Object v) = do
rootData <- v .: "data"
posts <- rootData .: "children"
afterCode <- rootData .:? "after"
return $ GetPostsResult posts afterCode
fetchPage:: Text -> IO (Maybe ([Post],Maybe Text))
fetchPage afterCode = do
let url = "https://www.reddit.com/r/videos/top/.json?sort=top&t=day&after=" ++ unpack afterCode
b <- get url
let jsonBody = b ^. responseBody
let postResponse = decode jsonBody :: Maybe GetPostsResult
let pagePosts = posts <$> postResponse
let nextAfterCode = after $ fromJust postResponse
if isNothing pagePosts then return Nothing else return (Just (fromJust pagePosts,nextAfterCode))
getPosts :: Text -> [Post] -> IO [Post]
getPosts x y = do
p <- liftIO $ fetchPage x
let posts = fst (fromJust p)
let afterParam = snd (fromJust p)
case afterParam of
Nothing -> return []
Just aff -> getPosts aff (posts ++ y)
main = do
a <- getPosts "" []
print a
Your approach is certainly appealing due to it's simplicity, however it has the disadvantage, that the list of Posts will only be available after the whole pagination chain has ended.
I would suggest to use a streaming library like Pipe, Conduit and friends. The advantage is that you can stream the results and also limit the number of posts to retrieve, by using functions provided by the respective streaming library. Here is an example with Pipe, I added the necessary imports and added postsP:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Data.Text (Text)
import qualified Data.Text as T
import Network.Wreq
import Data.Maybe
import Control.Lens
import Control.Monad.IO.Class
import qualified Pipes as P
import Data.Foldable (for_)
data Post = Post {
title :: !Text,
domain :: !Text,
score :: Int,
url :: !Text
} deriving (Show)
instance FromJSON Post where
parseJSON (Object v) = do
objectData <- v .: "data"
title <- objectData .: "title"
domain <- objectData .: "domain"
score <- objectData .: "score"
url <- objectData .: "url"
return $ Post title domain score url
data GetPostsResult = GetPostsResult {
posts :: [Post],
after :: Maybe Text
} deriving (Show)
instance FromJSON GetPostsResult where
parseJSON (Object v) = do
rootData <- v .: "data"
posts <- rootData .: "children"
afterCode <- rootData .:? "after"
return $ GetPostsResult posts afterCode
fetchPage:: Text -> IO (Maybe ([Post],Maybe Text))
fetchPage afterCode = do
let url = "https://www.reddit.com/r/videos/top/.json?sort=top&t=day&after=" ++ T.unpack afterCode
b <- get url
let jsonBody = b ^. responseBody
let postResponse = decode jsonBody :: Maybe GetPostsResult
let pagePosts = posts <$> postResponse
let nextAfterCode = after $ fromJust postResponse
if isNothing pagePosts then return Nothing else return (Just (fromJust pagePosts,nextAfterCode))
getPosts :: Text -> [Post] -> IO [Post]
getPosts x y = do
p <- liftIO $ fetchPage x
let posts = fst (fromJust p)
let afterParam = snd (fromJust p)
case afterParam of
Nothing -> return []
Just aff -> getPosts aff (posts ++ y)
postsP :: Text -> P.Producer Post IO ()
postsP x = do
p <- liftIO (fetchPage x)
for_ p $ \(posts,afterParam) -> do
P.each posts
for_ afterParam postsP
main = P.runEffect $ P.for (postsP "") (liftIO . print)
I would suggest you need a continuation monad. Within the continuation you can have the logic to assemble a page, yield it to the caller of the continuation, and then repeat. When you call your "get_pages" function you will get back the first page and a new function that takes your new parameters.
It sounds like you need this answer, which shows how to create such a monad. Make it a monad transformer if you need any other monadic state inside your function.

scala : case class & copy() returning Any

could you help me concerning a basic question?
I have a list of "Rdv" (meetings) where Rdv is a case class having 3 fields storing phone numbers as Strings: telBureau, telPortable & TelPrivé.
I get this list from slick, via a native SQL query; this query fills the 3 phone number fields with either a String or "null"(a null object, not the "null" string).
I would like to remove these null fields, so I wrote this:
var l2:List[Rdv] = liste.list()
l2=l2.map( (w:Rdv) =>{
if ( w.telPrivé==null ) w.copy( telPrivé = "" )
})
but I get this error:
found:List[Any], required:List[Rdv]
so after the map I added ".asInstanceOf[List[Rdv]]" but then I get this error:
java.lang.ClassCastException: scala.runtime.BoxedUnit cannot be cast to metier.Objets$Rdv
It seems to ba a basic question, but I can't do that.
olivier.
Try this:
var l2: List[Rdv] = liste.list()
l2 = l2 map ((w: Rdv => if (w.telPrivé == null) w.copy( telPrivé = "" ) else w)
Could you try doing something like this?
val l2: List[Rdv] = liste list ()
val l3 = ls map{
case x # Rdv(_, null, _) => x.copy(telPrive = "")
case x => x
}
Honestly, should should make that field, if it's nullable to be an Option and then have a member function which you call defined such that:
case class Rdv(a: String, b: Option[String], c: String){
def realC = b getOrElse ""
}