purescript-bridge icon indicating copy to clipboard operation
purescript-bridge copied to clipboard

Allow modifying record labels

Open JonathanLorimer opened this issue 2 years ago • 0 comments

This PR allows end users to provide options to the way that type representations are constructed. This allows users to hook into the generic machinery and make the types match some alternate representation (say formatting based on aeson encoding rather than haskell representation).

Here is an example to Illustrate:

Here is my haskell type

type LoginRequestJsonOpts = '[OmitNothingFields, FieldLabelModifier '[StripPrefix "loginRequest", ToCamel]]

data LoginRequest = LoginRequest
  { loginRequestEmail :: Email
  , loginRequestPassword :: Text
  }
  deriving stock (Generic)
  deriving (FromJSON) via (CustomJSON LoginRequestJsonOpts LoginRequest)
  deriving (ToJSON) via (CustomJSON LoginRequestJsonOpts LoginRequest)

I can build my bridge like so:

withRecordModifier :: forall a. AesonOptions (a :: [Type]) => DataConstructorOpts
withRecordModifier = defaultDataConstructorOpts & recLabelModifier .~ fieldLabelModifier (aesonOptions @a)

apiTypeContract :: [SumType 'Haskell]
apiTypeContract =
    [ mkSumType $ Proxy @UserId
    , mkSumTypeWith (withRecordModifier @LoginRequestJsonOpts) (Proxy @LoginRequest)
    , mkSumTypeWith (withRecordModifier @LoginResponseJsonOpts) $ Proxy @LoginResponse
    ]

and this will output purescript like this:

module Generated.Api.Login where

import Data.Argonaut.Aeson.Decode.Generic (genericDecodeAeson)
import Data.Argonaut.Aeson.Encode.Generic (genericEncodeAeson)
import Data.Argonaut.Aeson.Options as Argonaut
import Data.Argonaut.Decode.Class (class DecodeJson, class DecodeJsonField, decodeJson)
import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson)
import Data.Email (Email)
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Generated.Tables.Users (UserId)
import Prim (String)

import Prelude

newtype LoginRequest =
    LoginRequest {
      email :: Email
    , password :: String
    }

instance encodeJsonLoginRequest :: EncodeJson LoginRequest where
  encodeJson = genericEncodeAeson Argonaut.defaultOptions
instance decodeJsonLoginRequest :: DecodeJson LoginRequest where
  decodeJson = genericDecodeAeson Argonaut.defaultOptions
derive instance genericLoginRequest :: Generic LoginRequest _
derive instance newtypeLoginRequest :: Newtype LoginRequest _

newtype LoginResponse =
    LoginResponse {
      email :: Email
    , userId :: UserId
    , preferredName :: String
    }

instance encodeJsonLoginResponse :: EncodeJson LoginResponse where
  encodeJson = genericEncodeAeson Argonaut.defaultOptions
instance decodeJsonLoginResponse :: DecodeJson LoginResponse where
  decodeJson = genericDecodeAeson Argonaut.defaultOptions
derive instance genericLoginResponse :: Generic LoginResponse _
derive instance newtypeLoginResponse :: Newtype LoginResponse _

This way my Haskell types can be the source of truth, but by modifying my aeson instances I can modify my purescript to be more idiomatic (no record field prefixes), and the JSON serialization / de-serialization stays in sync.

JonathanLorimer avatar Jul 10 '23 23:07 JonathanLorimer