Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating PureScript records from inconsistent JavaScript objects

Tags:

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: '[email protected]', 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.

like image 587
davidchambers Avatar asked Jan 11 '15 08:01

davidchambers


2 Answers

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.

like image 156
davidchambers Avatar answered Nov 09 '22 00:11

davidchambers


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

like image 32
gb. Avatar answered Nov 09 '22 02:11

gb.