-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.