diff --git a/gleam.toml b/gleam.toml index b8a307c..d162fc5 100644 --- a/gleam.toml +++ b/gleam.toml @@ -11,7 +11,7 @@ gleam_stdlib = ">= 0.60.0 and < 1.0.0" gleam_json = ">= 3.0.1 and < 4.0.0" gleam_http = ">= 4.0.0 and < 5.0.0" gleam_httpc = ">= 4.1.1 and < 5.0.0" -birl = ">= 1.8.0 and < 2.0.0" +gleam_time = ">= 1.2.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.5.1 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 3e53fa5..574fa83 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,26 +2,23 @@ # You typically do not need to edit this file packages = [ - { name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" }, { name = "dot_env", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "F2B4815F1B5AF8F20A6EADBB393E715C4C35203EBD5BE8200F766EA83A0B18DE" }, { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, { name = "gleam_erlang", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "7E6A5234F927C4B24F8054AB1E4572206C41F9E6D5C6C02273CB7531E7E5CED0" }, { name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" }, { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" }, { name = "gleam_json", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "5BA154440B22D9800955B1AB854282FA37B97F30F409D76B0824D0A60C934188" }, - { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, - { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "gleam_time", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "D71F1AFF7FEB534FF55E5DC58E534E9201BA75A444619788A2E4DEA4EBD87D16" }, { name = "gleeunit", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D33B7736CF0766ED3065F64A1EBB351E72B2E8DE39BAFC8ADA0E35E92A6A934F" }, - { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, ] [requirements] -birl = { version = ">= 1.8.0 and < 2.0.0" } dot_env = { version = ">= 1.2.0 and < 2.0.0" } gleam_http = { version = ">= 4.0.0 and < 5.0.0" } gleam_httpc = { version = ">= 4.1.1 and < 5.0.0" } gleam_json = { version = ">= 3.0.1 and < 4.0.0" } gleam_stdlib = { version = ">= 0.60.0 and < 1.0.0" } +gleam_time = { version = ">= 1.2.0 and < 2.0.0" } gleeunit = { version = ">= 1.5.1 and < 2.0.0" } diff --git a/src/spacetraders_api.gleam b/src/spacetraders_api.gleam index 24d17cf..3074859 100644 --- a/src/spacetraders_api.gleam +++ b/src/spacetraders_api.gleam @@ -1,9 +1,10 @@ -import birl.{type Time} import gleam/dict.{type Dict} import gleam/dynamic/decode.{type Decoder} import gleam/json import gleam/list import gleam/option.{type Option} +import gleam/time/calendar.{type Date} +import gleam/time/timestamp.{type Timestamp} import gleam/uri.{type Uri} import spacetraders_models/account.{type Account} import spacetraders_models/agent.{type Agent} @@ -1409,14 +1410,14 @@ fn stats_decoder() -> Decoder(Stats) { } pub type Health { - Health(last_market_update: Option(Time)) + Health(last_market_update: Option(Timestamp)) } fn health_decoder() -> Decoder(Health) { use last_market_update <- decode.optional_field( "lastMarketUpdate", option.None, - decode.optional(time.datetime_decoder()), + decode.optional(time.rfc3339_timestamp_decoder()), ) decode.success(Health(last_market_update:)) } @@ -1461,11 +1462,11 @@ fn leaderboards_decoder() -> Decoder(Leaderboards) { } pub type ServerResets { - ServerResets(next: Time, frequency: String) + ServerResets(next: Timestamp, frequency: String) } fn server_resets_decoder() -> Decoder(ServerResets) { - use next <- decode.field("next", time.datetime_decoder()) + use next <- decode.field("next", time.rfc3339_timestamp_decoder()) use frequency <- decode.field("frequency", decode.string) decode.success(ServerResets(next:, frequency:)) } @@ -1500,7 +1501,7 @@ pub type ServerStatus { ServerStatus( status: String, version: String, - reset_date: Time, + reset_date: Date, description: String, stats: Stats, health: Health, @@ -1514,7 +1515,7 @@ pub type ServerStatus { fn server_status_decoder() -> Decoder(ServerStatus) { use status <- decode.field("status", decode.string) use version <- decode.field("version", decode.string) - use reset_date <- decode.field("resetDate", time.date_decoder()) + use reset_date <- decode.field("resetDate", time.rfc3339_date_decoder()) use description <- decode.field("description", decode.string) use stats <- decode.field("stats", stats_decoder()) use health <- decode.field("health", health_decoder()) diff --git a/src/spacetraders_models/account.gleam b/src/spacetraders_models/account.gleam index 82db3f5..fd04d4b 100644 --- a/src/spacetraders_models/account.gleam +++ b/src/spacetraders_models/account.gleam @@ -1,6 +1,6 @@ -import birl.{type Time} 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_sdk/internal/time @@ -9,7 +9,7 @@ pub type Account { id: AccountId, email: Option(String), token: Option(String), - created_at: Time, + created_at: Timestamp, ) } @@ -25,6 +25,6 @@ pub fn decoder() -> Decoder(Account) { option.None, decode.optional(decode.string), ) - use created_at <- decode.field("createdAt", time.datetime_decoder()) + use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder()) decode.success(Account(id:, email:, token:, created_at:)) } diff --git a/src/spacetraders_models/agent_event.gleam b/src/spacetraders_models/agent_event.gleam index 5a0fae7..0b64606 100644 --- a/src/spacetraders_models/agent_event.gleam +++ b/src/spacetraders_models/agent_event.gleam @@ -1,7 +1,7 @@ -import birl.{type Time} 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_sdk/internal/time @@ -11,7 +11,7 @@ pub type AgentEvent { type_: String, message: String, data: Option(Dynamic), - created_at: Time, + created_at: Timestamp, ) } @@ -24,6 +24,6 @@ pub fn decoder() -> Decoder(AgentEvent) { option.None, decode.optional(decode.dynamic), ) - use created_at <- decode.field("createdAt", time.datetime_decoder()) + use created_at <- decode.field("createdAt", time.rfc3339_timestamp_decoder()) decode.success(AgentEvent(id:, type_:, message:, data:, created_at:)) } diff --git a/src/spacetraders_models/chart.gleam b/src/spacetraders_models/chart.gleam index a5326b2..9203203 100644 --- a/src/spacetraders_models/chart.gleam +++ b/src/spacetraders_models/chart.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/agent_symbol.{type AgentSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} import spacetraders_sdk/internal/time @@ -8,7 +8,7 @@ pub type Chart { Chart( waypoint_symbol: WaypointSymbol, submitted_by: AgentSymbol, - submitted_on: Time, + submitted_on: Timestamp, ) } @@ -18,6 +18,9 @@ pub fn decoder() -> Decoder(Chart) { waypoint_symbol.decoder(), ) use submitted_by <- decode.field("submittedBy", agent_symbol.decoder()) - use submitted_on <- decode.field("submittedOn", time.datetime_decoder()) + use submitted_on <- decode.field( + "submittedOn", + time.rfc3339_timestamp_decoder(), + ) decode.success(Chart(waypoint_symbol:, submitted_by:, submitted_on:)) } diff --git a/src/spacetraders_models/chart_transaction.gleam b/src/spacetraders_models/chart_transaction.gleam index 2851eb4..8b1fbd5 100644 --- a/src/spacetraders_models/chart_transaction.gleam +++ b/src/spacetraders_models/chart_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} import spacetraders_sdk/internal/time @@ -9,7 +9,7 @@ pub type ChartTransaction { waypoint_symbol: WaypointSymbol, ship_symbol: ShipSymbol, total_price: Int, - timestamp: Time, + timestamp: Timestamp, ) } @@ -20,7 +20,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(ChartTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/contract.gleam b/src/spacetraders_models/contract.gleam index 8a537e9..7f6d673 100644 --- a/src/spacetraders_models/contract.gleam +++ b/src/spacetraders_models/contract.gleam @@ -1,6 +1,6 @@ -import birl.{type Time} 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} @@ -15,7 +15,7 @@ pub type Contract { terms: ContractTerms, accepted: Bool, fulfilled: Bool, - deadline_to_accept: Option(Time), + deadline_to_accept: Option(Timestamp), ) } @@ -29,7 +29,7 @@ pub fn decoder() -> Decoder(Contract) { use deadline_to_accept <- decode.optional_field( "deadlineToAccept", option.None, - decode.optional(time.datetime_decoder()), + decode.optional(time.rfc3339_timestamp_decoder()), ) decode.success(Contract( id:, diff --git a/src/spacetraders_models/contract_terms.gleam b/src/spacetraders_models/contract_terms.gleam index 202ceb6..1c6a514 100644 --- a/src/spacetraders_models/contract_terms.gleam +++ b/src/spacetraders_models/contract_terms.gleam @@ -1,20 +1,20 @@ -import birl.{type Time} 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_sdk/internal/time pub type ContractTerms { ContractTerms( - deadline: Time, + deadline: Timestamp, payment: ContractPayment, deliver: Option(List(ContractDeliverGood)), ) } pub fn decoder() -> Decoder(ContractTerms) { - use deadline <- decode.field("deadline", time.datetime_decoder()) + use deadline <- decode.field("deadline", time.rfc3339_timestamp_decoder()) use payment <- decode.field("payment", contract_payment.decoder()) use deliver <- decode.optional_field( "deliver", diff --git a/src/spacetraders_models/cooldown.gleam b/src/spacetraders_models/cooldown.gleam index fb510f5..85a75bc 100644 --- a/src/spacetraders_models/cooldown.gleam +++ b/src/spacetraders_models/cooldown.gleam @@ -1,6 +1,6 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} import gleam/option.{type Option} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_sdk/internal/time @@ -9,7 +9,7 @@ pub type Cooldown { ship_symbol: ShipSymbol, total_seconds: Int, remaining_seconds: Int, - expiration: Option(Time), + expiration: Option(Timestamp), ) } @@ -20,7 +20,7 @@ pub fn decoder() -> Decoder(Cooldown) { use expiration <- decode.optional_field( "expiration", option.None, - decode.optional(time.datetime_decoder()), + decode.optional(time.rfc3339_timestamp_decoder()), ) decode.success(Cooldown( ship_symbol:, diff --git a/src/spacetraders_models/market_transaction.gleam b/src/spacetraders_models/market_transaction.gleam index bf2f735..fc9cd7e 100644 --- a/src/spacetraders_models/market_transaction.gleam +++ b/src/spacetraders_models/market_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_models/trade_symbol.{type TradeSymbol} import spacetraders_models/transaction_type.{type TransactionType} @@ -15,7 +15,7 @@ pub type MarketTransaction { units: Int, price_per_unit: Int, total_price: Int, - timestamp: Time, + timestamp: Timestamp, ) } @@ -30,7 +30,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(MarketTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/repair_transaction.gleam b/src/spacetraders_models/repair_transaction.gleam index dcc42e7..4dcb633 100644 --- a/src/spacetraders_models/repair_transaction.gleam +++ b/src/spacetraders_models/repair_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} import spacetraders_sdk/internal/time @@ -9,7 +9,7 @@ pub type RepairTransaction { waypoint_symbol: WaypointSymbol, ship_symbol: ShipSymbol, total_price: Int, - timestamp: Time, + timestamp: Timestamp, ) } @@ -20,7 +20,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(RepairTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/scrap_transaction.gleam b/src/spacetraders_models/scrap_transaction.gleam index 76dfb35..5d35ee1 100644 --- a/src/spacetraders_models/scrap_transaction.gleam +++ b/src/spacetraders_models/scrap_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} import spacetraders_sdk/internal/time @@ -9,7 +9,7 @@ pub type ScrapTransaction { waypoint_symbol: WaypointSymbol, ship_symbol: ShipSymbol, total_price: Int, - timestamp: Time, + timestamp: Timestamp, ) } @@ -20,7 +20,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(ScrapTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/ship_fuel_consumed.gleam b/src/spacetraders_models/ship_fuel_consumed.gleam index 7ca00be..21807d8 100644 --- a/src/spacetraders_models/ship_fuel_consumed.gleam +++ b/src/spacetraders_models/ship_fuel_consumed.gleam @@ -1,13 +1,13 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_sdk/internal/time pub type ShipFuelConsumed { - ShipFuelConsumed(amount: Int, timestamp: Time) + ShipFuelConsumed(amount: Int, timestamp: Timestamp) } pub fn decoder() -> Decoder(ShipFuelConsumed) { use amount <- decode.field("amount", decode.int) - use timestamp <- decode.field("timestamp", time.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(ShipFuelConsumed(amount:, timestamp:)) } diff --git a/src/spacetraders_models/ship_modification_transaction.gleam b/src/spacetraders_models/ship_modification_transaction.gleam index 0438405..2e4fc96 100644 --- a/src/spacetraders_models/ship_modification_transaction.gleam +++ b/src/spacetraders_models/ship_modification_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_symbol.{type ShipSymbol} import spacetraders_models/trade_symbol.{type TradeSymbol} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} @@ -11,7 +11,7 @@ pub type ShipModificationTransaction { ship_symbol: ShipSymbol, trade_symbol: TradeSymbol, total_price: Int, - timestamp: Time, + timestamp: Timestamp, ) } @@ -23,7 +23,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(ShipModificationTransaction( waypoint_symbol:, ship_symbol:, diff --git a/src/spacetraders_models/ship_nav_route.gleam b/src/spacetraders_models/ship_nav_route.gleam index 36571c4..7fb9b90 100644 --- a/src/spacetraders_models/ship_nav_route.gleam +++ b/src/spacetraders_models/ship_nav_route.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/ship_nav_route_waypoint.{type ShipNavRouteWaypoint} import spacetraders_sdk/internal/time @@ -7,8 +7,8 @@ pub type ShipNavRoute { ShipNavRoute( destination: ShipNavRouteWaypoint, origin: ShipNavRouteWaypoint, - departure_time: Time, - arrival: Time, + departure_time: Timestamp, + arrival: Timestamp, ) } @@ -18,7 +18,10 @@ 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.datetime_decoder()) - use arrival <- decode.field("arrival", time.datetime_decoder()) + use departure_time <- decode.field( + "departureTime", + time.rfc3339_timestamp_decoder(), + ) + use arrival <- decode.field("arrival", time.rfc3339_timestamp_decoder()) decode.success(ShipNavRoute(destination:, origin:, departure_time:, arrival:)) } diff --git a/src/spacetraders_models/shipyard_transaction.gleam b/src/spacetraders_models/shipyard_transaction.gleam index 0d82b7f..b34b708 100644 --- a/src/spacetraders_models/shipyard_transaction.gleam +++ b/src/spacetraders_models/shipyard_transaction.gleam @@ -1,5 +1,5 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/agent_symbol.{type AgentSymbol} import spacetraders_models/ship_type.{type ShipType} import spacetraders_models/waypoint_symbol.{type WaypointSymbol} @@ -11,7 +11,7 @@ pub type ShipyardTransaction { ship_type: ShipType, price: Int, agent_symbol: AgentSymbol, - timestamp: Time, + timestamp: Timestamp, ) } @@ -23,7 +23,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.datetime_decoder()) + use timestamp <- decode.field("timestamp", time.rfc3339_timestamp_decoder()) decode.success(ShipyardTransaction( waypoint_symbol:, ship_type:, diff --git a/src/spacetraders_models/survey.gleam b/src/spacetraders_models/survey.gleam index 64be72c..a80c0ad 100644 --- a/src/spacetraders_models/survey.gleam +++ b/src/spacetraders_models/survey.gleam @@ -1,6 +1,7 @@ -import birl.{type Time} import gleam/dynamic/decode.{type Decoder} import gleam/json.{type Json} +import gleam/time/calendar +import gleam/time/timestamp.{type Timestamp} import spacetraders_models/survey_deposit.{type SurveyDeposit} import spacetraders_models/survey_signature.{type SurveySignature} import spacetraders_models/survey_size.{type SurveySize} @@ -12,7 +13,7 @@ pub type Survey { signature: SurveySignature, symbol: WaypointSymbol, deposits: List(SurveyDeposit), - expiration: Time, + expiration: Timestamp, size: SurveySize, ) } @@ -24,7 +25,7 @@ pub fn decoder() -> Decoder(Survey) { "deposits", decode.list(survey_deposit.decoder()), ) - use expiration <- decode.field("expiration", time.datetime_decoder()) + use expiration <- decode.field("expiration", time.rfc3339_timestamp_decoder()) use size <- decode.field("size", survey_size.decoder()) decode.success(Survey(signature:, symbol:, deposits:, expiration:, size:)) } @@ -34,7 +35,10 @@ 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(birl.to_iso8601(survey.expiration))), + #( + "expiration", + json.string(timestamp.to_rfc3339(survey.expiration, calendar.utc_offset)), + ), #("size", survey_size.encode(survey.size)), ]) } diff --git a/src/spacetraders_sdk/internal/jwt.gleam b/src/spacetraders_sdk/internal/jwt.gleam index 94d36eb..2474619 100644 --- a/src/spacetraders_sdk/internal/jwt.gleam +++ b/src/spacetraders_sdk/internal/jwt.gleam @@ -1,10 +1,11 @@ -import birl.{type Time} import gleam/bit_array import gleam/dynamic/decode.{type Decoder} 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_sdk/internal/time pub type JwtDecodeError { @@ -78,8 +79,8 @@ pub type JwtPayload { JwtPayload( identifier: String, version: String, - reset_date: Option(Time), - issued_at: Time, + reset_date: Option(Date), + issued_at: Timestamp, subject: String, ) } @@ -90,10 +91,10 @@ fn jwt_payload_decoder() -> Decoder(JwtPayload) { use reset_date <- decode.optional_field( "reset_date", option.None, - decode.optional(time.date_decoder()), + decode.optional(time.rfc3339_date_decoder()), ) use issued_at_int <- decode.field("iat", decode.int) - let issued_at = birl.from_unix(issued_at_int) + let issued_at = timestamp.from_unix_seconds(issued_at_int) use subject <- decode.field("sub", decode.string) decode.success(JwtPayload( identifier:, diff --git a/src/spacetraders_sdk/internal/time.gleam b/src/spacetraders_sdk/internal/time.gleam index 43bf3ea..13ec656 100644 --- a/src/spacetraders_sdk/internal/time.gleam +++ b/src/spacetraders_sdk/internal/time.gleam @@ -1,18 +1,219 @@ -import birl.{type Time} +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 datetime_decoder() -> Decoder(Time) { +pub fn rfc3339_timestamp_decoder() -> Decoder(Timestamp) { use value <- decode.then(decode.string) - case birl.parse(value) { - Ok(time) -> decode.success(time) - Error(Nil) -> decode.failure(birl.now(), "DateTime") + case timestamp.parse_rfc3339(value) { + Ok(timestamp) -> decode.success(timestamp) + Error(Nil) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp") } } -pub fn date_decoder() -> Decoder(Time) { +pub fn parse_rfc3339_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 rfc3339_date_decoder() -> Decoder(Date) { use value <- decode.then(decode.string) - case birl.from_naive(value) { - Ok(time) -> decode.success(time) - Error(Nil) -> decode.failure(birl.now(), "Date") + case parse_rfc3339_date(value) { + Ok(date) -> decode.success(date) + Error(Nil) -> decode.failure(Date(1970, January, 1), "Date") + } +} + +pub fn parse_rfc3339_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 rfc3339_time_of_day_decoder() -> Decoder(TimeOfDay) { + use value <- decode.then(decode.string) + case parse_rfc3339_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 { + <> 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)) + <> 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: <>, + 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 { + <> + 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:, + ) + } + <> 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)) } }