从不一致的JavaScript对象中创建PureScript记录

16
假设我有PureScript代码中的用户记录,类型如下:
{ id        :: Number
, username  :: String
, email     :: Maybe String
, isActive  :: Boolean
}

CommonJS模块源于PureScript代码。导出的与用户相关的函数将从外部JavaScript代码中调用。

在JavaScript代码中,“用户”可能表示为:

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

email 可能为 null

var alice = {id: 123, username: 'alice', email: null, isActive: true};

email 可以省略:

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

isActive 可以省略,如果省略,则默认为 true


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

id有时候是一个数字字符串:

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

以上五种 JavaScript 表示形式是等价的,应该产生等价的 PureScript 记录。

我该如何编写一个函数,它接受一个 JavaScript 对象并返回一个用户记录? 它将使用空值或省略的可选字段的默认值,将字符串 id 强制转换为数字,并在缺少必需字段或值类型错误时抛出异常。

我看到两种方法可以使用:一种是在 PureScript 模块中使用 FFI,另一种是在外部 JavaScript 代码中定义转换函数。后者似乎有些麻烦:

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');
  }
  // ...
}

我不确定FFI版本会如何工作。我还没有处理过effects。

很抱歉这个问题不是非常清楚。我还没有足够的理解来知道我想要知道什么。

4个回答

10

我已经整理了一个解决方案。我相信还有很多可以改善的地方,比如将toUser的类型更改为Json -> Either String User并保留错误信息。如果您发现任何可以改善这段代码的方法,请留下评论。:)

此解决方案除了使用一些核心模块外,还使用了PureScript-Argonaut

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
         }

更新: 我根据Ben Kolera的gist改进了上面的代码。


6

examples/Objects.purs 看起来最接近我想要做的。我怎样修改这个例子以允许 x 成为数字或数字字符串? - davidchambers
2
一种方法是创建一个类型,如下所示:data SoN = S String | N Number然后为类型 SoN 编写一个 IsForeign 实例,使用 <|> 运算符来组合这两个选项:read f = S <$> readString f <|> N <$> readNumber f - Phil Freeman

2

正如gb.所写的那样,Foreign数据类型就是为此而构建的。以下是我脑海中浮现出来的:

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 }

1
只是更多的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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接