从不一致的 JavaScript 个对象创建 PureScript 记录
Creating PureScript records from inconsistent JavaScript objects
假设我的 PureScript 代码中有以下类型的用户记录:
{ id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
CommonJS 模块源自 PureScript 代码。导出的用户相关函数将从外部 JavaScript 代码调用。
在JavaScript代码中,一个"user"可以表示为:
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 对象和 returns 用户记录的函数? 它会使用默认值null/omitted 可选字段,将字符串 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 版本如何工作。我还没有使用过效果器。
不好意思这个问题不是很清楚。我还没有足够的了解来确切地知道我想知道的是什么。
您看过 purescript-foreign
(https://github.com/purescript/purescript-foreign) 吗?我想这就是您要找的。
我已经制定了一个解决方案。我确信可以改进很多,例如将 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 对上面的代码进行了改进。
再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
作为国标。写道,这正是构建 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 }
假设我的 PureScript 代码中有以下类型的用户记录:
{ id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
CommonJS 模块源自 PureScript 代码。导出的用户相关函数将从外部 JavaScript 代码调用。
在JavaScript代码中,一个"user"可以表示为:
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 对象和 returns 用户记录的函数? 它会使用默认值null/omitted 可选字段,将字符串 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 版本如何工作。我还没有使用过效果器。
不好意思这个问题不是很清楚。我还没有足够的了解来确切地知道我想知道的是什么。
您看过 purescript-foreign
(https://github.com/purescript/purescript-foreign) 吗?我想这就是您要找的。
我已经制定了一个解决方案。我确信可以改进很多,例如将 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 对上面的代码进行了改进。
再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
作为国标。写道,这正是构建 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 }