agrafix / elm-bridge

Haskell: Derive Elm types from Haskell types
BSD 3-Clause "New" or "Revised" License
100 stars 27 forks source link

Good way to deal with Persistent's Entity? #30

Open gurgl opened 6 years ago

gurgl commented 6 years ago

Im adapting to Yesod Persistent. Thus my old Game type becomes "Entity Game" that is constructed as "Entity GameId Game" where type GameId = Key Game (or something like that).

ive managed to make stuff work by adding a wrapper that can be incorpoarated in elm-bridge, but it feels clumsy

data Ent a b = Ent { _key :: a, _val :: b } deriving (Show,Generic)

Incase you are familiar with Persistent, how would one incorporate encoding of (nested) entity types in elm-bridge.

trying to get :

deriveBoth defaultOptions { fieldLabelModifier = drop 1} ''PS.Entity

..to compile gives :

Can't derive this sum: ForallC [] [AppT (ConT Database.Persist.Class.PersistEntity.PersistEntity) (VarT record_7566047373982557936)] (RecC Database.Persist.Class.PersistEntity.Entity [(Database.Persist.Class.PersistEntity.entityKey,Bang NoSourceUnpackedness NoSourceStrictness,AppT (ConT Database.Persist.Class.PersistEntity.Key) (VarT record_7566047373982557936)),(Database.Persist.Class.PersistEntity.entityVal,Bang NoSourceUnpackedness NoSourceStrictness,VarT record_7566047373982557936)])

bartavelle commented 6 years ago

I use a newtype to share that kind of data, and helper functions to convert it from and to Entitys. The deriving error is probably a bug, but all in all you should not do it as it will result in orphan instances ...

bartavelle commented 6 years ago

(I will look at why deriving fails though)

gurgl commented 6 years ago

Thanks. If you have time i would be glad if you could elaborate on the newtype approach. Im a haskell noob so its not totally clear to me how that would look. It feels like I need to make my newtype an instance of IsElmDefinition as makeModuleWithVersion only accepts that? How I would define its required method

compileElmDef :: Proxy a -> ETypeDef

...is not totally clear to me.

bartavelle commented 6 years ago

Ok, this is not actually a newtype, more a simple data type. I do not have the code right here, and might be writing wrong stuff from memory, but it basically is something like:

data WithKey a
  = WithKey
  { _key :: Int64
  , _content :: a
  }

You then need to write:

fromEntity :: SomeSqlConstraintICan'tRemember a => Entity a -> WithKey a
toEntity :: SomeSqlConstraintICan'tRemember a => WithKey a -> Entity a

You can then derive the Elm definition from WithKey in a safe way, at the cost of converting your data. I used Int64 but it depends on your backend, as persistent has a function to extract a Key.

Is that clearer?

gurgl commented 6 years ago

Thanks. It could work for simple cases but for entites nested in other types it would require all types wrapping the wrapper to be changed too.

Persistent also leads to other problems. All (primary-) keys are (type family?) newtype instances Key Entity = ...

I guess it gets complicated. Been great using elm-bridge - it has performed great for concrete types. but i guess i need to resort to manual work here.

bartavelle commented 6 years ago

The fact that all keys are the same makes it easy to convert all at once with a mutation in elm-bridge. Same with the other kinds of newtypes that I use (for example for storing json encoded data in the database).

You could use the same process for nested entities, but I would really need a more precise example :)

saurabhnanda commented 6 years ago

@gurgl here's another solution:

update :: IO ()
update = writeFile "elm-ui/src/AutoGen.elm" ([qc|module AutoGen exposing (..)
import Json.Decode
import Json.Encode exposing (Value)
import Json.Helpers exposing (..)
import Dict
import Set

type Key record = Key Int

jsonDecKey : Json.Decode.Decoder (Key record)
jsonDecKey = Json.Decode.map Key <| Json.Decode.field "id" Json.Decode.int

jsonEncKey : Key record -> Value
jsonEncKey (Key k) = Json.Encode.int k

type alias Entity record =
    \{ entityKey : Key record
    , entityVal : record
    }

jsonDecEntity : Json.Decode.Decoder record -> Json.Decode.Decoder ( Entity record )
jsonDecEntity localDecoder_record =
   jsonDecKey >>= \pentityKey ->
   localDecoder_record >>= \pentityVal ->
   Json.Decode.succeed \{entityKey = pentityKey, entityVal = pentityVal}

jsonEncEntity : (record -> Value) -> Entity record -> Value
jsonEncEntity localEncoder_record val =
   let
       r = localEncoder_record val.entityVal
   in
       case Json.Decode.decodeValue (Json.Decode.keyValuePairs Json.Decode.value) r of
           Ok ok ->
               Json.Encode.object <| ("id", jsonEncKey val.entityKey) :: ok
           Err _ ->
               r

{autoGenCode}
|])
  where
    autoGenCode = makeModuleContent (myAlterations . defaultAlterations)
      [ DefineElm (Proxy :: Proxy User) -- NOTE: This is a Persistent record
      ]

Here's how one can use this in the Elm REPL:

> u = "{\"email\":\"saurabhnanda@gmail.com\",\"accessToken\":\"TODO\",\"createdAt\":\"2018-06-22T19:41:14.834185Z\",\"refreshToken\":\"TODO\",\"tokenExpiresAt\":\"2018-06-22T19:41:14.834185Z\",\"id\":1,\"updatedAt\":\"2018-06-22T19:41:14.834185Z\"}"
"{\"email\":\"saurabhnanda@gmail.com\",\"accessToken\":\"TODO\",\"createdAt\":\"2018-06-22T19:41:14.834185Z\",\"refreshToken\":\"TODO\",\"tokenExpiresAt\":\"2018-06-22T19:41:14.834185Z\",\"id\":1,\"updatedAt\":\"2018-06-22T19:41:14.834185Z\"}"
    : String
> import Json.Decode
> import AutoGen exposing (..)
> Json.Decode.decodeString (jsonDecEntity jsonDecUser) u
Ok { entityKey = Key 1, entityVal = { createdAt = DateTime { date = Date { year = 2018, month = 6, day = 22 }, offset = 70874834 }, updatedAt = DateTime { date = Date { year = 2018, month = 6, day = 22 }, offset = 70874834 }, email = Email "saurabhnanda@gmail.com", refreshToken = "TODO", accessToken = "TODO", tokenExpiresAt = DateTime { date = Date { year = 2018, month = 6, day = 22 }, offset = 70874834 } } }
    : Result.Result String (AutoGen.Entity AutoGen.User)
> 
itsfarseen commented 4 years ago

The fact that all keys are the same makes it easy to convert all at once with a mutation in elm-bridge. Same with the other kinds of newtypes that I use (for example for storing json encoded data in the database).

You could use the same process for nested entities, but I would really need a more precise example :)

Could you explain what you mean by mutation in elm-bridge?

bartavelle commented 4 years ago

A better term would be Alterations, as seen in Elm.Module. Look at the default alterations source to get some inspiration!