#Decode DateTime with `datetime.from_string`

1 messages · Page 1 of 1 (latest)

jade sedge
#

Hello everyone. I'm pretty new to Gleam. I want to decode a DateTime string into DateTime value but I can't because the result of datetime.from_string is a Result. I searched everywhere for a solution to no avail. Please help me find a solution. Here's the code:

import gleam/dynamic/decode
import gleam/json
import gleam/option.{type Option, None, Some}
import tempo.{type DateTime}
import tempo/datetime

pub type Link {
  Link(
    id: String,
    original_link: String,
    short_link: String,
    clicks: Int,
    user_id: Option(String),
    wait_time: Int,
    password: Option(String),
    created_at: DateTime,
    updated_at: DateTime,
    accessed_at: DateTime,
  )
}

pub fn json_to_link(json_string: String) -> Result(Link, json.DecodeError) {
  let link_decoder = {
    use id <- decode.field("id", decode.string)
    use original_link <- decode.field("original_link", decode.string)
    use short_link <- decode.field("short_link", decode.string)
    use clicks <- decode.field("clicks", decode.int)
    use password <- decode.optional_field("password", "", decode.string)
    use user_id <- decode.optional_field("user_id", "", decode.string)
    use wait_time <- decode.field("wait_time", decode.int)
    use created_at <- decode.field("created_at", decode.string)
    use updated_at <- decode.field("updated_at", decode.string)
    use accessed_at <- decode.field("accessed_at", decode.string)

    let password = case password {
      "" -> None
      _ -> Some(password)
    }

    let user_id = case user_id {
      "" -> None
      _ -> Some(user_id)
    }

    // I WANNA CONVERT created_at, updated_at, accessed_at from String to DateTime here

    decode.success(Link(
      id:,
      original_link:,
      short_link:,
      clicks:,
      password:,
      user_id:,
      wait_time:,
      created_at:,
      updated_at:,
      accessed_at:,
    ))
  }

  json.parse(from: json_string, using: link_decoder)
}

Thanks in advance.

feral spindle
#

You should be using gleam_time reather than gtempo for time ideally, they're the standard time types

jade sedge
#

I see

feral spindle
#

You can make timestamp_decoder() function which decodes a string with decode.then and then calls whatever parsing function you want (such as timestamp.parse_rfc3339) and then pattern matches on the result to call either decode.success or decode.failure

#

and then use that in your bigger decoder instead of decode.string

jade sedge
#

Thanks! I'll try that and let you know. 👍

civic stratus
#

it's also more conventional to expose a decoder function for your type and let the caller do the json.parse with that decoder, rather than doing the parsing inside your module

feral spindle
#

thing_decoder + thing_to_json

jade sedge
#

@feral spindle You meant something like this?

pub fn timestamp_decoder() -> decode.Decoder(Timestamp) {
  use timestamp_string <- decode.then(decode.string)
  case timestamp.parse_rfc3339(timestamp_string) {
    Ok(val) -> decode.success(val)
    Error(_) ->
      decode.failure(timestamp.system_time(), "Error decoding timestamp")
  }
}

pub fn link_decoder() -> decode.Decoder(Link) {
  use id <- decode.field("id", decode.string)
  use original_link <- decode.field("original_link", decode.string)
  use short_link <- decode.field("short_link", decode.string)
  use clicks <- decode.field("clicks", decode.int)
  use password <- decode.optional_field("password", "", decode.string)
  use user_id <- decode.optional_field("user_id", "", decode.string)
  use wait_time <- decode.field("wait_time", decode.int)
  use created_at <- decode.field("created_at", timestamp_decoder())
  use updated_at <- decode.field("updated_at", timestamp_decoder())
  use accessed_at <- decode.field("accessed_at", timestamp_decoder())

  let password = case password {
    "" -> None
    _ -> Some(password)
  }

  let user_id = case user_id {
    "" -> None
    _ -> Some(user_id)
  }

  decode.success(Link(
    id:,
    original_link:,
    short_link:,
    clicks:,
    password:,
    user_id:,
    wait_time:,
    created_at:,
    updated_at:,
    accessed_at:,
  ))
}

pub fn json_to_link(json_string: String) -> Result(Link, json.DecodeError) {
  let link_decoder = link_decoder()
  json.parse(from: json_string, using: link_decoder)
}
feral spindle
#

Error decoding timestamp" should be "Timestamp", it's not an error message, it's the name of the type that was expected

#

but yeah looks good!

jade sedge
#

Awesome! Thanks! 👍

civic stratus
#

also you probably want

use password <- decode.optional_field("password", None, decode.optional(decode.string))

instead of using empty string and then mapping that to an optional

jade sedge
#

Ohhh yea I was looking for a better solution. Perfect!

civic stratus
#

if it's just value optional and not also key optional then

use password <- decode.field("password", decode.optional(decode.string))

also works

jade sedge
#

I see. It's both optional, so I assume the first goes well

civic stratus
#

yeah use the first option if both key and value are optional

#

personally i'd also use timestamp.from_unix_seconds(0) for the failure value rather than the system time, but that doesnt matter all that much

jade sedge
civic stratus
#

yeah having a consistent output is nice

#

though I dont think the decode module actually ever returns that failure value to the user of a decoder lol

#

I think it's just used internally so the decoder can continue on errors and report multiple errors at once

jade sedge
# civic stratus I think it's just used internally so the decoder can continue on errors and repo...

Got it. There's another thing that bugs me for serialization. To hide password, I had no choice but to set up two different functions:


pub fn link_to_json_insecure(link: Link) -> String {
  json.object([
    #("id", json.string(link.id)),
    #("original_link", json.string(link.original_link)),
    #("short_link", json.string(link.short_link)),
    #("clicks", json.int(link.clicks)),
    #("user_id", json.nullable(link.user_id, of: json.string)),
    #("wait_time", json.int(link.wait_time)),
    #("password", json.nullable(link.password, of: json.string)),
    #(
      "created_at",
      link.created_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
    #(
      "updated_at",
      link.updated_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
    #(
      "accessed_at",
      link.accessed_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
  ])
  |> json.to_string
}

pub fn link_to_json(link: Link) -> String {
  json.object([
    #("id", json.string(link.id)),
    #("original_link", json.string(link.original_link)),
    #("short_link", json.string(link.short_link)),
    #("clicks", json.int(link.clicks)),
    #("user_id", json.nullable(link.user_id, of: json.string)),
    #("wait_time", json.int(link.wait_time)),
    #(
      "created_at",
      link.created_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
    #(
      "updated_at",
      link.updated_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
    #(
      "accessed_at",
      link.accessed_at
        |> timestamp.to_rfc3339(calendar.utc_offset)
        |> json.string,
    ),
  ])
  |> json.to_string
}

Do you seem to have a better solution for this? There are no optional default arguments for functions in gleam, so I cannot have a flag boolean for it.

civic stratus
#

What's the purpose of that json object? Like what is it being used for and do you really need one with and without the password?

jade sedge
#

Yes, accidentally exposing passwords to the APIs I don't want to have access to

#

A bit on the paranoid side

civic stratus
#

Is this object being used with an existing API though? Does that API expect that object with or without the password?

jade sedge
#

No I haven't designed the APIs yet

civic stratus
#

Ah ok

jade sedge
#

Maybe I shouldn't expose it at all and just do password operation internally

civic stratus
#

Maybe think about how it's going to be used first then and then decide which of those you want to keep

jade sedge
#

Yup, makes sense. Thanks!