2016-12-20 11 views
1

Как выразить следующую идею в Haskell? Хотя синтаксис полностью составлен, вот что я пытаюсь достичь:Параметрированные, но безопасные по типу ключи в обработке JSON

  • Моего приложения имеет высоко вложенные основные тип данных с каждым «уровнем», имеющими экземпляры FromJson/ToJson
  • JSON API подача питание пользовательский интерфейс имеет возможность манипулировать отдельными «уровнями гнездования», например. для редактирования адреса вам не нужно редактировать полный заказ.
  • Однако я хочу убедиться, что наряду с данными, которые были изменены пользовательским интерфейсом, полный заказ также отправляется обратно. Это гарантирует, что если редактирование привело к некоторому зависимому полю в другой части объекта, который будет изменен, он будет передан обратно в пользовательский интерфейс.

Редактировать: Основной вопрос заключается не в логике приложения, а в принципе. Основной вопрос заключается в том, как представлять ключи JSON безопасным типом, имея возможность параметризовать их. Простым решением является наличие другого конкретного типа для каждого возвращаемого типа API, например {orderItems :: [OrderItem], order :: Order} или {address :: Address, order :: Order} или {email :: Email, customer :: Customer}. Но они быстро повторятся. Я хочу иметь тип данных, который представляет собой идею JSON с парой первичного ключа и пары вторичных/поддерживающих ключей, где имена ключей могут быть легко изменены.

псевдо-код, указанный ниже, является обобщением этой идеи:

data IncomingJson rootname payload = (FromJson payload, ToString rootname) => IncomingJson 
    { 
    rootname :: payload 
    } 

data OutgoingJson rootname payload sidename sidepayload = (ToJson payload, ToString rootname, ToJson sidepayload, ToString sidename) => IncomingJson 
    { 
    rootname :: payload 
    , sidename :: sidepayload 
    } 

createOrder :: IncomingJson "order" NewOrder -> OutgoingJson "order" Order Nothing() 

editOrderItems :: IncomingJson "items" [OrderItem] -> OutgoingJson "items" [OrderItem] "order" Order 

editOrderAddress :: IncomingJson "address" Address -> OutgoingJson "address" Address "order" Order 
+0

Возможно, вам просто нужно [объектив] (http://hackage.haskell.org/package/lens)? – freestyle

+0

@freestyle Как может линза помочь в этой ситуации? Извините, но я едва знаю, как использовать объектив, не говоря уже о том, чтобы сгибать его до таких сложных ситуаций. –

+0

Возможно, я вас неправильно понимаю. У вас есть типы данных, которые представляют некоторую информацию из вашего домена приложения, и приложение имеет состояние этого типа. Иногда часть этого состояния может быть изменена (например, по запросу из пользовательского интерфейса). И вы хотите иметь функции, которые манипулируют только частями этого состояния. Но после того, как они будут применены, вы хотите иметь обновленное состояние. Это правда? – freestyle

ответ

1

(Edit: попытка полного ответа на пересмотренном вопрос.)

В примере кода ниже, может быть близко к чему вы хотите. В этом примере определяются OutgoingJSON и IncomingJSON с пользовательскими ToJSON и FromJSON экземплярами соответственно. (Я также включил ToJSON для типа данных IncomingJSON, хотя я подозреваю, что он вам не нужен.) Он полагается на каждый тип данных, которому назначается ключ JSON через короткий экземпляр KeyedJSON. Можно использовать GHC.Generics или альтернативу автоматизации, но это кажется уродливым и нелогичным. (Вы не действительно хотите, чтобы ваши JSON ключей напрямую связан с именами типа данных Haskell, не так ли?)

Если вы загрузите это и смотреть на типы inExample1 и outExample1, они должны соответствовать тому, что вы ожидаете. inExample2 и inExample3 демонстрируют безопасный синтаксический разбор блока JSON - он преуспевает, если ключ для ожидаемого типа существует в блоке JSON и сбой, если он этого не делает. Наконец, outExample1AsJSON показывает, как пример OutgoingJSON будет сериализован с помощью необходимых первичных и вторичных ключей.

{-# LANGUAGE DeriveAnyClass #-} 
{-# LANGUAGE DeriveGeneriC#-} 
{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE OverloadedStrings #-} 
{-# LANGUAGE ScopedTypeVariables #-} 

module JsonExample where 

import GHC.Generics 
import Data.Aeson 
import Data.Text (Text) 
import Data.ByteString.Lazy (ByteString) 
import qualified Data.ByteString.Lazy.Char8 as C 

data Address = Address String deriving (Generic, ToJSON, FromJSON, Show) 
data OrderItem = OrderItem Int String deriving (Generic, ToJSON, FromJSON, Show) 
data Order = Order { address :: Address 
        , items :: [OrderItem] 
        } deriving (Generic, ToJSON, FromJSON, Show) 

class KeyedJSON a    where jsonKey :: a -> Text 
instance KeyedJSON Address  where jsonKey _ = "address" 
instance KeyedJSON [OrderItem] where jsonKey _ = "orderitems" 
instance KeyedJSON Order  where jsonKey _ = "order" 

-- 
-- OutgoingJSON 
-- 

data OutgoingJSON primary secondary 
    = OutgoingJSON primary secondary deriving (Show) 
instance (ToJSON primary, KeyedJSON primary, 
      ToJSON secondary, KeyedJSON secondary) => 
     ToJSON (OutgoingJSON primary secondary) where 
    toJSON (OutgoingJSON prim sec) = 
    object [ jsonKey prim .= toJSON prim 
      , jsonKey sec .= toJSON sec 
      ] 

-- 
-- IncomingJSON 
-- 

data IncomingJSON primary 
    = IncomingJSON primary deriving (Show) 
-- don't know if ToJSON instance is needed? 
instance (ToJSON primary, KeyedJSON primary) => ToJSON (IncomingJSON primary) where 
    toJSON (IncomingJSON prim) = 
    object [ jsonKey prim .= toJSON prim ] 
instance (FromJSON primary, KeyedJSON primary) => FromJSON (IncomingJSON primary) where 
    parseJSON (Object v) = do 
    let key = jsonKey (undefined :: primary) 
    IncomingJSON <$> (v .: key >>= parseJSON) 

-- Simple examples of typed `IncomingJSON` and `OutgoingJSON` values 

-- inExample1 :: IncomingJSON Address 
inExample1 = IncomingJSON 
       (Address "123 New Street") 

-- outExample1 :: OutgoingJSON Address Order 
outExample1 = OutgoingJSON 
       (Address "15 Old Street") 
       (Order (Address "15 Old Street") [OrderItem 1 "partridge", OrderItem 5 "golden rings"]) 

-- Reading a JSON address in a type-safe manner 
aJSONAddress :: ByteString 
aJSONAddress = C.pack "{\"address\":\"123 New Street\"}" 

-- This returns a `Just (IncomingJSON Address)` 
inExample2 :: Maybe (IncomingJSON Address) 
inExample2 = decode aJSONAddress 

-- This returns `Nothing` 
inExample3 :: Maybe (IncomingJSON Order) 
inExample3 = decode aJSONAddress 

-- This demonstrates the JSON serialization of outExample1 
outExample1AsJSON = encode outExample1 
+0

в вашем примере, к чему приведет сериал 'OutgoingJson'? '{outRootJson: что-то, вне JSON: something}' i.e фактические ключи в JSON не изменятся, правильно? Ключи всегда будут 'outRootJson' и' outsideJson', правильно? –

+0

Проверьте изменение на мой вопрос. Значит ли это, что яснее? –

+0

Итак, основная идея в этом новом решении - «KeyedJSON», который решает имя root для каждого типа. Экземпляр 'ToJSON'' OutgoingJSON primary secondary' использует 'KeyedJSON' для определения ролей для каждой полезной нагрузки. Гениальный, и кажется, что он может работать! –