Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow modifying record labels #84

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

JonathanLorimer
Copy link

@JonathanLorimer JonathanLorimer commented Jul 10, 2023

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 JonathanLorimer marked this pull request as ready for review July 11, 2023 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant