Creating PureScript Records from Inconsistent JavaScript Objects

Suppose I have user entries in my PureScript code with the following type:

{ id :: Number , username :: String , email :: Maybe String , isActive :: Boolean } 

CommonJS module derived from PureScript code. Exported user-related functions will be called from external JavaScript code.

In JavaScript code, a β€œuser” can be represented as:

 var alice = {id: 123, username: 'alice', email: ' alice@example.com ', isActive: true}; 

email can 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 true assumed:

 var alice = {id: 123, username: 'alice'}; 

id , unfortunately, is sometimes a numeric string:

 var alice = {id: '123', username: 'alice'}; 

The five JavaScript representations above are equivalent and must contain equivalent PureScript entries.

How do I write a function that takes a JavaScript object and returns a user record? . It would use the default value for the optional null / omitted field, force the id string to a number, and throw if the required field is missing or if the value is of the wrong type.

Two approaches that I see are using FFI in a PureScript module or defining a transform function in 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 will work. I have not worked with effects yet.

I'm sorry this question is not very clear. I still do not have enough understanding to know exactly what exactly I want to know.

+7
purescript
source share
4 answers

I put together a solution. I am sure that much can be improved, for example, changing the toUser type to Json -> Either String User and saving the error information. Please leave a comment if you see how this code can be improved. :)

This solution uses PureScript-Argonaut in addition to several 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 ab -> 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 improved the code above based on gist from Ben Kolera.

+7
source share

Have you seen purescript-foreign ( https://github.com/purescript/purescript-foreign )? I think you are looking here.

+3
source share

Some 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 
+1
source share

Like gb. wrote, this is exactly what the Foreign data type was created for. Above 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 } 
0
source share

All Articles