2015-01-11 11 views
7

Si supponga che ho record utente nel mio codice PureScript con il seguente tipo:creazione di record PureScript da incoerente JavaScript oggetti

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

modulo A CommonJS è derivato dal codice PureScript. Le funzioni relative agli utenti esportate saranno chiamate dal codice JavaScript esterno.

Nel codice JavaScript, un "utente" può essere rappresentata come:

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

email possono essere null:

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

email possono essere omesse:

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

isActive può essere omesso, nel qual caso è come Sumed true:

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

id purtroppo a volte è una stringa numerica:

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

I cinque rappresentazioni JavaScript di cui sopra sono equivalenti e devono produrre dischi PureScript equivalenti.

Come posso scrivere una funzione che accetta un oggetto JavaScript e restituisce un record Utente? Utilizzerebbe il valore predefinito per un campo facoltativo nullo/omesso, costringerebbe una stringa id a un numero e genererà se manca un campo obbligatorio o se un valore è del tipo errato.

I due approcci che riesco a vedere sono quelli di utilizzare l'FFI nel modulo PureScript o di definire la funzione di conversione nel codice JavaScript esterno. Quest'ultimo sembra peloso:

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

Non sono sicuro di come funzionerebbe la versione FFI. Non ho ancora lavorato con effetti.

Mi dispiace che questa domanda non sia molto chiara. Non ho ancora abbastanza comprensione per sapere esattamente cosa voglio sapere.

risposta

7

Ho messo insieme una soluzione. Sono sicuro che molte cose possono essere migliorate, come cambiare il tipo di toUser a Json -> Either String User e conservare le informazioni sugli errori. Si prega di lasciare un commento se è possibile vedere in che modo questo codice potrebbe essere migliorato. :)

Questa soluzione utilizza PureScript-Argonaut oltre a pochi moduli di base.

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: ho fatto miglioramenti al codice di cui sopra sulla base di una gist da Ben Kolera.

3

Hai dato un'occhiata a purescript-foreign (https://github.com/purescript/purescript-foreign)? Penso che sia quello che stai cercando qui.

+0

[esempi/Objects.purs] (https://github.com/purescript/purescript-foreign/blob/v0.3.0/examples/Objects.purs) sembra più vicino al quello che sto cercando di fare. Come posso modificare quell'esempio per consentire a 'x' di essere un numero o una stringa numerica? – davidchambers

+2

Un modo sarebbe creare un tipo come 'data SoN = S String | N number' e quindi scrivere un'istanza 'IsForeign' per tipo' SoN' usando l'operatore '' <|> per combinare le due alternative: 'lette f = S <$> readString f <|> N <$> readNumber f' –

1

Solo un po 'più di 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

È bene vedere la versione FFI pura per il confronto. – davidchambers

0

Come gb. ha scritto, questo è esattamente ciò per cui è stato creato il tipo di dati Foreign. Fuori della parte superiore della mia testa:

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 }