#Dealing with "extra fields" on existing records

1 messages · Page 1 of 1 (latest)

ember lava
#

I'm working on a discord API wrapper and having some grief with trying to define types for gateway event data. Some are very simple, but some like MESSAGE_CREATE are pretty painful in the way I've been doing them:

[<JsonConverter(typeof<MessageCreateReceiveEventExtraFieldsMentionConverter>)>]
type MessageCreateReceiveEventExtraFieldsMention = {
    User: User
    ExtraFields: MessageCreateReceiveEventExtraFieldsMentionExtraFields
}

and MessageCreateReceiveEventExtraFieldsMentionExtraFields = {
    [<JsonPropertyName "member">] Member: GuildMember option // TODO: Partial
}

and MessageCreateReceiveEventExtraFieldsMentionConverter () =
    inherit JsonConverter<MessageCreateReceiveEventExtraFieldsMention> ()

    override _.Read (reader, typeToConvert, options) =
        let success, document = JsonDocument.TryParseValue &reader
        if not success then raise (JsonException())

        let json = document.RootElement.GetRawText()

        {
            User = Json.deserializeF json;
            ExtraFields = Json.deserializeF json;
        }

    override _.Write (writer, value, options) =
        let user = Json.serializeF value.User
        let extraFields = Json.serializeF value.ExtraFields

        writer.WriteRawValue (Json.merge user extraFields)

// https://discord.com/developers/docs/events/gateway-events#message-create
[<JsonConverter(typeof<MessageCreateReceiveEventConverter>)>]
type MessageCreateReceiveEvent = {
    Message: Message
    ExtraFields: MessageCreateReceiveEventExtraFields
}

and MessageCreateReceiveEventExtraFields = {
    [<JsonPropertyName "guild_id">] GuildId: string option
    [<JsonPropertyName "member">] Member: GuildMember option // TODO: Partial
    [<JsonPropertyName "mentions">] Mentions: MessageCreateReceiveEventExtraFieldsMention list
}

and MessageCreateReceiveEventConverter () =
    inherit JsonConverter<MessageCreateReceiveEvent> ()

    override _.Read (reader, typeToConvert, options) =
        let success, document = JsonDocument.TryParseValue &reader
        if not success then raise (JsonException())

        let json = document.RootElement.GetRawText()

        {
            Message = Json.deserializeF json;
            ExtraFields = Json.deserializeF json;
        }

    override _.Write (writer, value, options) =
        let message = Json.serializeF value.Message
        let extraFields = Json.serializeF value.ExtraFields

        writer.WriteRawValue (Json.merge message extraFields)

Because you cant extend a record, I'm needing to make these ExtraFields types and then serialize the properties. This one is particularly bad because I have two layers of that needing to be done. In use, this also isn't pleasant as it ends up as data.ExtraFields.Mentions[0].ExtraFields.Member. On top of this, I need to create partial types, which are basically the same as the normal records but where everything are options.

Is there a cleaner way I could be doing this? This is with FSharp.SystemTextJson

south zenith
#

Not sure if this applies too much in what you're doing but here I go, I recently was writing a wrapper for the Threads API where the response fields are dynamic depending on what you sent in the parameters it feels kind of like the extra fields you're dealing with, as much as one wants to model that with records it can be quite a friction and everything could end up being optional

What I did was to instead defining a record, I simply defined DUs for the parameters (ProfileField params) and DUs for the responses (ProfileValue ), the F# code rather than returning a record, it returned a sequence of a ProfileValues which consumers can iterate to build the particular record they were looking for defined by the fields they requested + the responses containing those

I used Thoth.Json to decode the JSON objects from the API into the DU values I was looking for
You can see how it was defined in this file https://github.com/AngelMunoz/Threads.Lib/blob/main/Threads.Lib/Profiles.fs and how it was consumed in this file https://github.com/AngelMunoz/Threads.Lib/blob/main/Sample/Profile.fs#L33-L53

I'm not sure it fits your particular case but it could give you some ideas

ember lava
#

Looks interesting! I wouldn't want to do all the rewriting necessary to support this way everywhere as I've mostly implemented discord's http and websocket APIs, but I could see how that would help for this specific issue