2015-01-11 2 views
7

Предположим, у меня есть записи пользователя в моем PureScript код с следующего типа:Создание PureScript записей из несовместимых объектов JavaScript

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

CommonJS модуль выводится из кода PureScript. Экспортированные функции, связанные с пользователем, будут вызываться из внешнего кода JavaScript.

В коде JavaScript "пользователь" может быть представлен в виде:

var alice = {id: 123, username: 'alice', email: '[email protected]', isActive: true}; 

email может быть null:

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

email могут быть опущены:

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

isActive могут быть опущены, и в этом случае это как SUMED true:

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

id, к сожалению, иногда числовая строка:

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

Пяти JavaScript представления выше эквивалентны и должны производить эквивалентное PureScript записи.

Как мне написать функцию, которая принимает объект JavaScript и возвращает запись пользователя? Он использует значение по умолчанию для необязательного поля null/omitted, принуждает строку id к числу и бросает, если отсутствует требуемое поле или если значение имеет неправильный тип.

Два подхода, которые я вижу, - это использование FFI в модуле PureScript или определение функции преобразования во внешнем 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. Я еще не работал с эффектами.

Прошу прощения, что этот вопрос не очень ясен. У меня еще недостаточно понимания, чтобы точно знать, что я хочу знать.

ответ

7

Я собрал решение. Я уверен, что многое можно улучшить, например, изменить тип 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 
     } 

Update: Я усовершенствовал код выше на основе gist от Бен колера.

3

Вы имели в виду purescript-foreign (https://github.com/purescript/purescript-foreign)? Я думаю, это то, что вы ищете здесь.

+0

[примеры/Objects.purs] (https://github.com/purescript/purescript-foreign/blob/v0.3.0/examples/Objects.purs) кажется ближе всего к что я пытаюсь сделать. Как я могу изменить этот пример, чтобы позволить 'x' быть либо числом, либо числовой строкой? – davidchambers

+2

Одним из способов было бы создать такой тип, как 'data SoN = S String | N Number' , а затем записать 'IsForeign' экземпляр для типа' SoN' с помощью '' <|> оператора объединить две альтернативы: 'чтения F = S <$> ReadString F <|> N <$> readNumber f' –

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 
+0

Это хорошо см. чистую версию FFI для сравнения. – davidchambers

0

Как гб. написал, это именно то, для чего был создан тип данных 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 }