Compare commits

...

3 commits
v1.0.0 ... main

Author SHA1 Message Date
4069651171 Add license
Some checks failed
test / test (push) Has been cancelled
2025-07-09 03:15:19 +10:00
02735d1744 Bump version to v1.1.0
Some checks are pending
test / test (push) Waiting to run
2025-07-09 01:52:58 +10:00
520b5d380c Move date and timestamp to separate model files 2025-07-09 01:52:46 +10:00
21 changed files with 199 additions and 283 deletions

9
LICENSE.md Normal file
View file

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2025 Lily Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,5 +1,5 @@
name = "spacetraders_models"
version = "1.0.0"
version = "1.1.0"
gleam = ">= 1.11.0"
description = "Gleam types for the spacetraders.io game API"
licences = ["MIT"]

View file

@ -1,8 +1,7 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/option.{type Option}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/account_id.{type AccountId}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
pub type Account {
Account(
@ -25,6 +24,6 @@ pub fn decoder() -> Decoder(Account) {
option.None,
decode.optional(decode.string),
)
use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder())
use created_at <- decode.field("createdAt", timestamp.decoder())
decode.success(Account(id:, email:, token:, created_at:))
}

View file

@ -1,9 +1,8 @@
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode.{type Decoder}
import gleam/option.{type Option}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/agent_event_id.{type AgentEventId}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
pub type AgentEvent {
AgentEvent(
@ -24,6 +23,6 @@ pub fn decoder() -> Decoder(AgentEvent) {
option.None,
decode.optional(decode.dynamic),
)
use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder())
use created_at <- decode.field("createdAt", timestamp.decoder())
decode.success(AgentEvent(id:, type_:, message:, data:, created_at:))
}

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/agent_symbol.{type AgentSymbol}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type Chart {
@ -18,9 +17,6 @@ pub fn decoder() -> Decoder(Chart) {
waypoint_symbol.decoder(),
)
use submitted_by <- decode.field("submittedBy", agent_symbol.decoder())
use submitted_on <- decode.field(
"submittedOn",
time.rfc3339_timestamp_decoder(),
)
use submitted_on <- decode.field("submittedOn", timestamp.decoder())
decode.success(Chart(waypoint_symbol:, submitted_by:, submitted_on:))
}

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type ChartTransaction {
@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(ChartTransaction) {
)
use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder())
use total_price <- decode.field("totalPrice", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(ChartTransaction(
waypoint_symbol:,
ship_symbol:,

View file

@ -1,11 +1,10 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/option.{type Option}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/contract_id.{type ContractId}
import spacetraders_models/contract_terms.{type ContractTerms}
import spacetraders_models/contract_type.{type ContractType}
import spacetraders_models/faction_symbol.{type FactionSymbol}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
pub type Contract {
Contract(
@ -29,7 +28,7 @@ pub fn decoder() -> Decoder(Contract) {
use deadline_to_accept <- decode.optional_field(
"deadlineToAccept",
option.None,
decode.optional(time.rfc3339_timestamp_decoder()),
decode.optional(timestamp.decoder()),
)
decode.success(Contract(
id:,

View file

@ -1,9 +1,8 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/option.{type Option}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/contract_deliver_good.{type ContractDeliverGood}
import spacetraders_models/contract_payment.{type ContractPayment}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
pub type ContractTerms {
ContractTerms(
@ -14,7 +13,7 @@ pub type ContractTerms {
}
pub fn decoder() -> Decoder(ContractTerms) {
use deadline <- decode.field("deadline", time.rfc3339_timestamp_decoder())
use deadline <- decode.field("deadline", timestamp.decoder())
use payment <- decode.field("payment", contract_payment.decoder())
use deliver <- decode.optional_field(
"deliver",

View file

@ -1,8 +1,7 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/option.{type Option}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
pub type Cooldown {
Cooldown(
@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(Cooldown) {
use expiration <- decode.optional_field(
"expiration",
option.None,
decode.optional(time.rfc3339_timestamp_decoder()),
decode.optional(timestamp.decoder()),
)
decode.success(Cooldown(
ship_symbol:,

View file

@ -0,0 +1,125 @@
import gleam/bit_array
import gleam/dynamic/decode.{type Decoder}
import gleam/int
import gleam/json.{type Json}
import gleam/pair
import gleam/result
import gleam/time/calendar.{
type Month, April, August, Date, December, February, January, July, June,
March, May, November, October, September,
}
pub type Date =
calendar.Date
pub fn parse(value: String) -> Result(Date, Nil) {
let bytes = bit_array.from_string(value)
use #(year, bytes) <- result.try(parse_year(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(month, bytes) <- result.try(parse_month_calendar(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(day, bytes) <- result.try(parse_day_calendar(from: bytes, year:, month:))
use Nil <- result.try(accept_empty(bytes))
Ok(Date(year:, month:, day:))
}
pub fn decoder() -> Decoder(Date) {
use value <- decode.then(decode.string)
case parse(value) {
Ok(date) -> decode.success(date)
Error(Nil) -> decode.failure(Date(1970, calendar.January, 1), "Date")
}
}
pub fn encode(date: Date) -> Json {
json.string(
int.to_string(date.year)
<> "-"
<> int.to_string(calendar.month_to_int(date.month))
<> "-"
<> int.to_string(date.day),
)
}
const byte_zero: Int = 0x30
const byte_nine: Int = 0x39
const byte_minus: Int = 0x2D
fn accept_byte(from bytes: BitArray, value value: Int) -> Result(BitArray, Nil) {
case bytes {
<<byte, remaining_bytes:bytes>> if byte == value -> Ok(remaining_bytes)
_ -> Error(Nil)
}
}
fn accept_empty(from bytes: BitArray) -> Result(Nil, Nil) {
case bytes {
<<>> -> Ok(Nil)
_ -> Error(Nil)
}
}
fn parse_digits(
from bytes: BitArray,
count count: Int,
) -> Result(#(Int, BitArray), Nil) {
do_parse_digits(from: bytes, count:, acc: 0, k: 0)
}
fn do_parse_digits(
from bytes: BitArray,
count count: Int,
acc acc: Int,
k k: Int,
) -> Result(#(Int, BitArray), Nil) {
case bytes {
_ if k >= count -> Ok(#(acc, bytes))
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine ->
do_parse_digits(
from: remaining_bytes,
count:,
acc: acc * 10 + { byte - 0x30 },
k: k + 1,
)
_ -> Error(Nil)
}
}
fn parse_year(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
parse_digits(from: bytes, count: 4)
}
// slightly modified version of parse_month that returns calendar.Month instead of Int
fn parse_month_calendar(from bytes: BitArray) -> Result(#(Month, BitArray), Nil) {
use #(month, bytes) <- result.try(parse_digits(from: bytes, count: 2))
calendar.month_from_int(month) |> result.map(pair.new(_, bytes))
}
// slightly modified version of parse_day that takes calendar.Month instead of Int
fn parse_day_calendar(
from bytes: BitArray,
year year: Int,
month month: Month,
) -> Result(#(Int, BitArray), Nil) {
use #(day, bytes) <- result.try(parse_digits(from: bytes, count: 2))
let max_day = case month {
January | March | May | July | August | October | December -> 31
April | June | September | November -> 30
February -> {
case is_leap_year(year) {
True -> 29
False -> 28
}
}
}
case 1 <= day && day <= max_day {
True -> Ok(#(day, bytes))
False -> Error(Nil)
}
}
fn is_leap_year(year: Int) -> Bool {
year % 4 == 0 && { year % 100 != 0 || year % 400 == 0 }
}

View file

@ -4,9 +4,8 @@ import gleam/json
import gleam/option.{type Option}
import gleam/result
import gleam/string
import gleam/time/calendar.{type Date}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/date.{type Date}
import spacetraders_models/timestamp.{type Timestamp}
pub type JwtDecodeError {
MissingHeader
@ -91,10 +90,10 @@ fn jwt_payload_decoder() -> Decoder(JwtPayload) {
use reset_date <- decode.optional_field(
"reset_date",
option.None,
decode.optional(time.iso8601_date_decoder()),
decode.optional(date.decoder()),
)
use issued_at_int <- decode.field("iat", decode.int)
let issued_at = timestamp.from_unix_seconds(issued_at_int)
use issued_at_unix <- decode.field("iat", decode.int)
let issued_at = timestamp.from_unix(issued_at_unix)
use subject <- decode.field("sub", decode.string)
decode.success(JwtPayload(
identifier:,

View file

@ -1,219 +0,0 @@
import gleam/bit_array
import gleam/dynamic/decode.{type Decoder}
import gleam/pair
import gleam/result
import gleam/time/calendar.{
type Date, type Month, type TimeOfDay, April, August, Date, December, February,
January, July, June, March, May, November, October, September, TimeOfDay,
}
import gleam/time/timestamp.{type Timestamp}
pub fn rfc3339_timestamp_decoder() -> Decoder(Timestamp) {
use value <- decode.then(decode.string)
case timestamp.parse_rfc3339(value) {
Ok(timestamp) -> decode.success(timestamp)
Error(Nil) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp")
}
}
pub fn parse_iso8601_date(input: String) -> Result(Date, Nil) {
let bytes = bit_array.from_string(input)
use #(year, bytes) <- result.try(parse_year(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(month, bytes) <- result.try(parse_month_calendar(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(day, bytes) <- result.try(parse_day_calendar(from: bytes, year:, month:))
use Nil <- result.try(accept_empty(bytes))
Ok(Date(year:, month:, day:))
}
pub fn iso8601_date_decoder() -> Decoder(Date) {
use value <- decode.then(decode.string)
case parse_iso8601_date(value) {
Ok(date) -> decode.success(date)
Error(Nil) -> decode.failure(Date(1970, January, 1), "Date")
}
}
pub fn parse_iso8601_time_of_day(input: String) -> Result(TimeOfDay, Nil) {
let bytes = bit_array.from_string(input)
use #(hours, bytes) <- result.try(parse_hours(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
use #(minutes, bytes) <- result.try(parse_minutes(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
use #(seconds, bytes) <- result.try(parse_seconds(from: bytes))
use #(nanoseconds, bytes) <- result.try(parse_second_fraction_as_nanoseconds(
from: bytes,
))
use Nil <- result.try(accept_empty(bytes))
Ok(TimeOfDay(hours:, minutes:, seconds:, nanoseconds:))
}
pub fn iso8601_time_of_day_decoder() -> Decoder(TimeOfDay) {
use value <- decode.then(decode.string)
case parse_iso8601_time_of_day(value) {
Ok(date) -> decode.success(date)
Error(Nil) -> decode.failure(TimeOfDay(0, 0, 0, 0), "TimeOfDay")
}
}
const byte_zero: Int = 0x30
const byte_nine: Int = 0x39
const byte_colon: Int = 0x3A
const byte_minus: Int = 0x2D
const nanoseconds_per_second: Int = 1_000_000_000
fn accept_byte(from bytes: BitArray, value value: Int) -> Result(BitArray, Nil) {
case bytes {
<<byte, remaining_bytes:bytes>> if byte == value -> Ok(remaining_bytes)
_ -> Error(Nil)
}
}
fn accept_empty(from bytes: BitArray) -> Result(Nil, Nil) {
case bytes {
<<>> -> Ok(Nil)
_ -> Error(Nil)
}
}
fn parse_digits(
from bytes: BitArray,
count count: Int,
) -> Result(#(Int, BitArray), Nil) {
do_parse_digits(from: bytes, count:, acc: 0, k: 0)
}
fn do_parse_digits(
from bytes: BitArray,
count count: Int,
acc acc: Int,
k k: Int,
) -> Result(#(Int, BitArray), Nil) {
case bytes {
_ if k >= count -> Ok(#(acc, bytes))
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine ->
do_parse_digits(
from: remaining_bytes,
count:,
acc: acc * 10 + { byte - 0x30 },
k: k + 1,
)
_ -> Error(Nil)
}
}
fn parse_year(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
parse_digits(from: bytes, count: 4)
}
// slightly modified version of parse_month that returns calendar.Month instead of Int
fn parse_month_calendar(from bytes: BitArray) -> Result(#(Month, BitArray), Nil) {
use #(month, bytes) <- result.try(parse_digits(from: bytes, count: 2))
calendar.month_from_int(month) |> result.map(pair.new(_, bytes))
}
// slightly modified version of parse_day that takes calendar.Month instead of Int
fn parse_day_calendar(
from bytes: BitArray,
year year: Int,
month month: Month,
) -> Result(#(Int, BitArray), Nil) {
use #(day, bytes) <- result.try(parse_digits(from: bytes, count: 2))
let max_day = case month {
January | March | May | July | August | October | December -> 31
April | June | September | November -> 30
February -> {
case is_leap_year(year) {
True -> 29
False -> 28
}
}
}
case 1 <= day && day <= max_day {
True -> Ok(#(day, bytes))
False -> Error(Nil)
}
}
fn is_leap_year(year: Int) -> Bool {
year % 4 == 0 && { year % 100 != 0 || year % 400 == 0 }
}
fn parse_hours(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(hours, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 0 <= hours && hours <= 23 {
True -> Ok(#(hours, bytes))
False -> Error(Nil)
}
}
fn parse_minutes(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(minutes, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 0 <= minutes && minutes <= 59 {
True -> Ok(#(minutes, bytes))
False -> Error(Nil)
}
}
fn parse_seconds(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(seconds, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 0 <= seconds && seconds <= 60 {
True -> Ok(#(seconds, bytes))
False -> Error(Nil)
}
}
fn parse_second_fraction_as_nanoseconds(from bytes: BitArray) {
case bytes {
<<".", byte, remaining_bytes:bytes>>
if byte_zero <= byte && byte <= byte_nine
-> {
do_parse_second_fraction_as_nanoseconds(
from: <<byte, remaining_bytes:bits>>,
acc: 0,
power: nanoseconds_per_second,
)
}
<<".", _:bytes>> -> Error(Nil)
_ -> Ok(#(0, bytes))
}
}
fn do_parse_second_fraction_as_nanoseconds(
from bytes: BitArray,
acc acc: Int,
power power: Int,
) -> Result(#(Int, BitArray), a) {
// Each digit place to the left in the fractional second is 10x fewer
// nanoseconds.
let power = power / 10
case bytes {
<<byte, remaining_bytes:bytes>>
if byte_zero <= byte && byte <= byte_nine && power < 1
-> {
// We already have the max precision for nanoseconds. Truncate any
// remaining digits.
do_parse_second_fraction_as_nanoseconds(
from: remaining_bytes,
acc:,
power:,
)
}
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine -> {
// We have not yet reached the precision limit. Parse the next digit.
let digit = byte - 0x30
do_parse_second_fraction_as_nanoseconds(
from: remaining_bytes,
acc: acc + digit * power,
power:,
)
}
_ -> Ok(#(acc, bytes))
}
}

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/trade_symbol.{type TradeSymbol}
import spacetraders_models/transaction_type.{type TransactionType}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
@ -30,7 +29,7 @@ pub fn decoder() -> Decoder(MarketTransaction) {
use units <- decode.field("units", decode.int)
use price_per_unit <- decode.field("pricePerUnit", decode.int)
use total_price <- decode.field("totalPrice", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(MarketTransaction(
waypoint_symbol:,
ship_symbol:,

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type RepairTransaction {
@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(RepairTransaction) {
)
use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder())
use total_price <- decode.field("totalPrice", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(RepairTransaction(
waypoint_symbol:,
ship_symbol:,

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type ScrapTransaction {
@ -20,7 +19,7 @@ pub fn decoder() -> Decoder(ScrapTransaction) {
)
use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder())
use total_price <- decode.field("totalPrice", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(ScrapTransaction(
waypoint_symbol:,
ship_symbol:,

View file

@ -1,6 +1,5 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/timestamp.{type Timestamp}
pub type ShipFuelConsumed {
ShipFuelConsumed(amount: Int, timestamp: Timestamp)
@ -8,6 +7,6 @@ pub type ShipFuelConsumed {
pub fn decoder() -> Decoder(ShipFuelConsumed) {
use amount <- decode.field("amount", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(ShipFuelConsumed(amount:, timestamp:))
}

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_symbol.{type ShipSymbol}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/trade_symbol.{type TradeSymbol}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
@ -23,7 +22,7 @@ pub fn decoder() -> Decoder(ShipModificationTransaction) {
use ship_symbol <- decode.field("shipSymbol", ship_symbol.decoder())
use trade_symbol <- decode.field("tradeSymbol", trade_symbol.decoder())
use total_price <- decode.field("totalPrice", decode.int)
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(ShipModificationTransaction(
waypoint_symbol:,
ship_symbol:,

View file

@ -1,7 +1,6 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/ship_nav_route_waypoint.{type ShipNavRouteWaypoint}
import spacetraders_models/timestamp.{type Timestamp}
pub type ShipNavRoute {
ShipNavRoute(
@ -18,10 +17,7 @@ pub fn decoder() -> Decoder(ShipNavRoute) {
ship_nav_route_waypoint.decoder(),
)
use origin <- decode.field("origin", ship_nav_route_waypoint.decoder())
use departure_time <- decode.field(
"departureTime",
time.rfc3339_timestamp_decoder(),
)
use arrival <- decode.field("arrival", time.rfc3339_timestamp_decoder())
use departure_time <- decode.field("departureTime", timestamp.decoder())
use arrival <- decode.field("arrival", timestamp.decoder())
decode.success(ShipNavRoute(destination:, origin:, departure_time:, arrival:))
}

View file

@ -1,8 +1,7 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/agent_symbol.{type AgentSymbol}
import spacetraders_models/internal/time
import spacetraders_models/ship_type.{type ShipType}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type ShipyardTransaction {
@ -23,7 +22,7 @@ pub fn decoder() -> Decoder(ShipyardTransaction) {
use ship_type <- decode.field("shipType", ship_type.decoder())
use price <- decode.field("price", decode.int)
use agent_symbol <- decode.field("agentSymbol", agent_symbol.decoder())
use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder())
use timestamp <- decode.field("timestamp", timestamp.decoder())
decode.success(ShipyardTransaction(
waypoint_symbol:,
ship_type:,

View file

@ -1,11 +1,9 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/json.{type Json}
import gleam/time/calendar
import gleam/time/timestamp.{type Timestamp}
import spacetraders_models/internal/time
import spacetraders_models/survey_deposit.{type SurveyDeposit}
import spacetraders_models/survey_signature.{type SurveySignature}
import spacetraders_models/survey_size.{type SurveySize}
import spacetraders_models/timestamp.{type Timestamp}
import spacetraders_models/waypoint_symbol.{type WaypointSymbol}
pub type Survey {
@ -25,7 +23,7 @@ pub fn decoder() -> Decoder(Survey) {
"deposits",
decode.list(survey_deposit.decoder()),
)
use expiration <- decode.field("expiration", time.rfc3339_timestamp_decoder())
use expiration <- decode.field("expiration", timestamp.decoder())
use size <- decode.field("size", survey_size.decoder())
decode.success(Survey(signature:, symbol:, deposits:, expiration:, size:))
}
@ -35,10 +33,7 @@ pub fn encode(survey: Survey) -> Json {
#("signature", survey_signature.encode(survey.signature)),
#("symbol", waypoint_symbol.encode(survey.symbol)),
#("deposits", json.array(survey.deposits, survey_deposit.encode)),
#(
"expiration",
json.string(timestamp.to_rfc3339(survey.expiration, calendar.utc_offset)),
),
#("expiration", timestamp.encode(survey.expiration)),
#("size", survey_size.encode(survey.size)),
])
}

View file

@ -0,0 +1,27 @@
import gleam/dynamic/decode.{type Decoder}
import gleam/json.{type Json}
import gleam/time/calendar
import gleam/time/timestamp
pub type Timestamp =
timestamp.Timestamp
pub fn parse(value: String) -> Result(Timestamp, Nil) {
timestamp.parse_rfc3339(value)
}
pub fn from_unix(seconds: Int) -> Timestamp {
timestamp.from_unix_seconds(seconds)
}
pub fn decoder() -> Decoder(Timestamp) {
use value <- decode.then(decode.string)
case parse(value) {
Ok(timestamp) -> decode.success(timestamp)
Error(Nil) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp")
}
}
pub fn encode(timestamp: Timestamp) -> Json {
json.string(timestamp.to_rfc3339(timestamp, calendar.utc_offset))
}