-module(aws@internal@codec@json_timestamp). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/aws/internal/codec/json_timestamp.gleam"). -export([parse_iso8601/1, parse_http_date/1, format_iso8601/1, format_http_date/1, decoder/0, decoder_precise/0, timestamp_to_int/1, int_to_timestamp/1, format_iso8601_precise/1, format_http_date_precise/1, epoch_seconds_text/1, encode_epoch_seconds/1]). -export_type([timestamp/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " JSON-side timestamp decoder. AWS protocol families disagree on\n" " the wire shape of `@timestamp` fields:\n" "\n" " * awsJson1_0 / awsJson1_1 default: epoch-seconds number\n" " (Int OR Float — services like KitchenSinkOperation send doubles)\n" " * restJson1 / restXml default: ISO 8601 string\n" " * Any protocol with `@timestampFormat(\"http-date\")`: HTTP-date string\n" "\n" " We never know which the server will send for a given field —\n" " fractional-second tests in particular surface Floats where the\n" " schema declares an Int member. Returning `option.None` on decode\n" " failure would mask data; instead we accept all three forms and\n" " coerce to `Int` (epoch seconds).\n" ). -type timestamp() :: {timestamp, integer(), integer()}. -file("src/aws/internal/codec/json_timestamp.gleam", 28). ?DOC( " Parse an ISO 8601 timestamp string (\"2024-01-02T03:04:05Z\") into a\n" " `Timestamp` at second precision. `nanoseconds` is always 0 until\n" " the FFI gains fractional-second support. Used by the header\n" " extractor for members carrying `@timestampFormat(\"date-time\")`.\n" ). -spec parse_iso8601(binary()) -> {ok, timestamp()} | {error, nil}. parse_iso8601(S) -> case aws_ffi:parse_iso8601(S) of {ok, N} -> {ok, {timestamp, N, 0}}; {error, _} -> {error, nil} end. -file("src/aws/internal/codec/json_timestamp.gleam", 39). ?DOC( " Parse an HTTP-date timestamp string (\"Tue, 29 Apr 2014 18:30:38 GMT\")\n" " into a `Timestamp`. The default `@timestampFormat` for\n" " `@httpHeader` bindings per Smithy core — used by `Last-Modified`,\n" " `Expires`, `Date`, etc.\n" ). -spec parse_http_date(binary()) -> {ok, timestamp()} | {error, nil}. parse_http_date(S) -> case aws_ffi:parse_http_date(S) of {ok, N} -> {ok, {timestamp, N, 0}}; {error, _} -> {error, nil} end. -file("src/aws/internal/codec/json_timestamp.gleam", 48). ?DOC(" `2024-01-02T03:04:05Z`. Inverse of `parse_iso8601_ffi`.\n"). -spec format_iso8601(integer()) -> binary(). format_iso8601(Seconds) -> aws_ffi:format_iso8601(Seconds). -file("src/aws/internal/codec/json_timestamp.gleam", 53). ?DOC( " `Tue, 29 Apr 2014 18:30:38 GMT`. Used by\n" " `@timestampFormat(\"http-date\")` body fields and headers.\n" ). -spec format_http_date(integer()) -> binary(). format_http_date(Seconds) -> aws_ffi:format_http_date(Seconds). -file("src/aws/internal/codec/json_timestamp.gleam", 63). ?DOC( " Decode `Int | Float | String` into epoch seconds. Falls back to 0\n" " when none of the forms match, which matches `gleam/dynamic`'s\n" " default `decode.failure` payload style and lets the caller surface\n" " the decode failure via the standard `Decoder` machinery rather than\n" " crashing on bad data.\n" ). -spec decoder() -> gleam@dynamic@decode:decoder(integer()). decoder() -> gleam@dynamic@decode:one_of( {decoder, fun gleam@dynamic@decode:decode_int/1}, [gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_float/1}, fun(F) -> gleam@dynamic@decode:success(erlang:trunc(F)) end ), gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(S) -> case aws_ffi:parse_iso8601(S) of {ok, N} -> gleam@dynamic@decode:success(N); {error, _} -> case aws_ffi:parse_http_date(S) of {ok, N@1} -> gleam@dynamic@decode:success(N@1); {error, _} -> gleam@dynamic@decode:failure( 0, <<"timestamp: unrecognised wire form"/utf8>> ) end end end )] ). -file("src/aws/internal/codec/json_timestamp.gleam", 223). -spec float_floor(float()) -> integer(). float_floor(F) -> erlang:trunc(math:floor(F)). -file("src/aws/internal/codec/json_timestamp.gleam", 198). -spec float_to_timestamp(float()) -> timestamp(). float_to_timestamp(F) -> Seconds = float_floor(F), Fractional = F - erlang:float(Seconds), Nanos = erlang:trunc(Fractional * 1000000000.0), case Nanos < 0 of true -> {timestamp, Seconds, 0}; false -> case Nanos > 999999999 of true -> {timestamp, Seconds + 1, Nanos - 1000000000}; false -> {timestamp, Seconds, Nanos} end end. -file("src/aws/internal/codec/json_timestamp.gleam", 110). ?DOC( " Decode an AWS timestamp wire value into a `Timestamp` that\n" " preserves sub-second precision when present.\n" "\n" " * `Int` → `Timestamp(seconds: n, nanoseconds: 0)`\n" " * `Float` → fractional seconds extracted via floor + scaling\n" " to nanoseconds. Negative timestamps (pre-1970) handled by\n" " normalising the fractional remainder so `nanoseconds` is\n" " always in `[0, 999_999_999]`.\n" " * `String` → ISO 8601 / HTTP-date, parsed with second-level\n" " precision today (fractional ISO timestamps would need an\n" " FFI extension; tracked separately).\n" ). -spec decoder_precise() -> gleam@dynamic@decode:decoder(timestamp()). decoder_precise() -> gleam@dynamic@decode:one_of( gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_int/1}, fun(N) -> gleam@dynamic@decode:success({timestamp, N, 0}) end ), [gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_float/1}, fun(F) -> gleam@dynamic@decode:success(float_to_timestamp(F)) end ), gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(S) -> case aws_ffi:parse_iso8601(S) of {ok, N@1} -> gleam@dynamic@decode:success({timestamp, N@1, 0}); {error, _} -> case aws_ffi:parse_http_date(S) of {ok, N@2} -> gleam@dynamic@decode:success( {timestamp, N@2, 0} ); {error, _} -> gleam@dynamic@decode:failure( {timestamp, 0, 0}, <<"timestamp: unrecognised wire form"/utf8>> ) end end end )] ). -file("src/aws/internal/codec/json_timestamp.gleam", 138). ?DOC( " Convert `Timestamp` back to integer epoch seconds, dropping the\n" " nanosecond component. Symmetric with `int_to_timestamp` and\n" " useful when callers want to bridge to the existing `Int` API.\n" ). -spec timestamp_to_int(timestamp()) -> integer(). timestamp_to_int(T) -> erlang:element(2, T). -file("src/aws/internal/codec/json_timestamp.gleam", 144). ?DOC( " Promote an `Int` epoch seconds value to a `Timestamp` with\n" " zero nanoseconds. Symmetric with `timestamp_to_int`.\n" ). -spec int_to_timestamp(integer()) -> timestamp(). int_to_timestamp(Seconds) -> {timestamp, Seconds, 0}. -file("src/aws/internal/codec/json_timestamp.gleam", 154). ?DOC( " Format a `Timestamp` as ISO 8601 (`2024-01-02T03:04:05Z`).\n" " Wire-equivalent to `format_iso8601(t.seconds)` — sub-second\n" " precision is dropped because the underlying FFI doesn't\n" " emit fractional seconds yet. Promoted to a distinct entry\n" " point so the codegen can call this from `Timestamp`-typed\n" " code paths without a redundant `timestamp_to_int` step.\n" ). -spec format_iso8601_precise(timestamp()) -> binary(). format_iso8601_precise(T) -> aws_ffi:format_iso8601(erlang:element(2, T)). -file("src/aws/internal/codec/json_timestamp.gleam", 162). ?DOC( " Format a `Timestamp` as HTTP-date\n" " (`Tue, 29 Apr 2014 18:30:38 GMT`). Same nanosecond caveat as\n" " `format_iso8601_precise` — HTTP-date is whole-second precision\n" " by definition.\n" ). -spec format_http_date_precise(timestamp()) -> binary(). format_http_date_precise(T) -> aws_ffi:format_http_date(erlang:element(2, T)). -file("src/aws/internal/codec/json_timestamp.gleam", 171). ?DOC( " Render a `Timestamp` as a plain epoch-seconds integer string\n" " (`\"1700000000\"`). Used by URI / query / header / XML emitters\n" " when `@timestampFormat(\"epoch-seconds\")` is in force — the\n" " wire form is the integer-as-decimal-digits, no fractional\n" " component.\n" ). -spec epoch_seconds_text(timestamp()) -> binary(). epoch_seconds_text(T) -> erlang:integer_to_binary(erlang:element(2, T)). -file("src/aws/internal/codec/json_timestamp.gleam", 186). ?DOC( " Encode a `Timestamp` as a JSON epoch-seconds number. When\n" " `nanoseconds == 0` we emit a JSON Int (`1700000000`) so the\n" " wire bytes match the existing `json.int` path the codegen\n" " uses for the `Int` API — flipping a member to precise must\n" " not perturb the wire form for callers who never set\n" " nanoseconds. When `nanoseconds > 0` we emit a JSON Float\n" " (`1700000000.5`) so the fractional component reaches the\n" " server intact.\n" ). -spec encode_epoch_seconds(timestamp()) -> gleam@json:json(). encode_epoch_seconds(T) -> case erlang:element(3, T) of 0 -> gleam@json:int(erlang:element(2, T)); _ -> gleam@json:float( erlang:float(erlang:element(2, T)) + (erlang:float( erlang:element(3, T) ) / 1000000000.0) ) end.