Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build_tools/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class ServiceEnumerator
MANIFEST_PATH = File.expand_path('../../services.json', __FILE__)

# Minimum `aws-sdk-core` version for new gem builds
MINIMUM_CORE_VERSION = "3.255.0"
MINIMUM_CORE_VERSION = "3.256.0"

# Minimum `aws-sdk-core` version for new S3 gem builds
MINIMUM_CORE_VERSION_S3 = "3.255.0"
MINIMUM_CORE_VERSION_S3 = "3.256.0"

EVENTSTREAM_PLUGIN = "Aws::Plugins::EventStreamConfiguration"

Expand Down
2 changes: 2 additions & 0 deletions gems/aws-sdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Preserve millisecond precision when serializing `Time` values on the request path, instead of truncating to whole seconds.

3.255.0 (2026-09-09)
------------------

Expand Down
4 changes: 2 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ def encode(blob)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'iso8601' then value.utc.iso8601
when 'iso8601' then Util.serialize_date_time(value)
when 'rfc822' then value.utc.httpdate
else
# rest-json and jsonrpc default to unixTimestamp
value.to_i
Util.serialize_epoch_seconds(value)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ def blob(value)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# ec2 defaults to iso8601
value.utc.iso8601
Util.serialize_date_time(value)
end
end

Expand Down
4 changes: 2 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ def flat?(ref)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# query defaults to iso8601
value.utc.iso8601
Util.serialize_date_time(value)
end
end

Expand Down
4 changes: 2 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ def param_name(placeholder)

def timestamp(ref, value)
case ref['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# serializing as RFC 3399 date-time is the default
value.utc.iso8601
Util.serialize_date_time(value)
end
end

Expand Down
4 changes: 2 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def apply_header_value(headers, ref, value)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'iso8601' then value.utc.iso8601
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'iso8601' then Util.serialize_date_time(value)
else
# header default to rfc822
value.utc.httpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ def build_part(shape_ref, param_value, query_keys)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# querystring defaults to iso8601
value.utc.iso8601
Util.serialize_date_time(value)
end
end

Expand Down
24 changes: 24 additions & 0 deletions gems/aws-sdk-core/lib/aws-sdk-core/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ def deserialize_number(str)
end
end

# Serializes a {Time} to the Smithy `epoch-seconds` format: seconds
# since the Unix epoch with optional millisecond precision. Whole-second
# values are returned as an Integer so the wire format is unchanged for
# the common case; sub-millisecond precision is truncated to milliseconds.
# @see https://smithy.io/2.0/spec/protocol-traits.html#timestamp-formats
# @param [Time] value
# @return [Integer, Float]
def serialize_epoch_seconds(value)
return value.to_i if value.nsec.zero?

value.to_i + value.nsec / 1_000_000 / 1000.0
end

# Serializes a {Time} to the Smithy `date-time` format: an RFC 3339
# (ISO 8601) string with optional millisecond precision and no UTC
# offset. Fractional seconds are included only when present, and
# sub-millisecond precision is truncated to milliseconds.
# @see https://smithy.io/2.0/spec/protocol-traits.html#timestamp-formats
# @param [Time] value
# @return [String]
def serialize_date_time(value)
value.nsec.zero? ? value.utc.iso8601 : value.utc.iso8601(3)
end

# @param [String] value
# @return [Time]
def deserialize_time(value)
Expand Down
4 changes: 2 additions & 2 deletions gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ def blob(value)

def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
when 'unixTimestamp' then value.to_i
when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# xml defaults to iso8601
value.utc.iso8601
Util.serialize_date_time(value)
end
end

Expand Down
7 changes: 7 additions & 0 deletions gems/aws-sdk-core/spec/aws/json/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ def json(params)
expect(json(string: 'abc', integer: nil)).to eq('{"String":"abc"}')
end

it 'preserves millisecond precision on epoch timestamps' do
params = Structure.new(*rules.shape.member_names).new
params.timestamp = Time.at(123_456_789, 123_000)
# JRuby and MRI render the same float differently but both are valid JSON for the same value
expect(Aws::Json.load(json(params))['Timestamp']).to eq(123_456_789.123)
end

describe 'document types' do
it 'serializes BigDecimal values as JSON numbers' do
expect(json(document_type: {
Expand Down
6 changes: 3 additions & 3 deletions gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def normalize(json)
"AttributeType": "S"
}
],
"CreationDateTime": #{now.to_i},
"CreationDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
"ItemCount": 0,
"KeySchema": [
{
Expand All @@ -81,8 +81,8 @@ def normalize(json)
}
],
"ProvisionedThroughput": {
"LastIncreaseDateTime": #{now.to_i},
"LastDecreaseDateTime": #{now.to_i},
"LastIncreaseDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
"LastDecreaseDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 50,
"WriteCapacityUnits": 50
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def normalize(xml)
<UserName>name</UserName>
<UserId>user-id</UserId>
<Arn>arn:aws:iam::123456789012:user/name</Arn>
<CreateDate>#{now.utc.iso8601}</CreateDate>
<CreateDate>#{Aws::Util.serialize_date_time(now)}</CreateDate>
</member>
</Users>
<IsTruncated>false</IsTruncated>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def normalize(xml)
<Buckets>
<Bucket>
<Name>aws-sdk</Name>
<CreationDate>#{now.utc.iso8601}</CreationDate>
<CreationDate>#{Aws::Util.serialize_date_time(now)}</CreationDate>
</Bucket>
</Buckets>
<Owner>
Expand Down
34 changes: 34 additions & 0 deletions gems/aws-sdk-core/spec/aws/util_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@

module Aws
describe Util do
describe '.serialize_epoch_seconds' do
it 'returns an Integer for whole-second values' do
result = Util.serialize_epoch_seconds(Time.at(123_456_789))
expect(result).to eql(123_456_789)
end

it 'preserves millisecond precision for sub-second values' do
result = Util.serialize_epoch_seconds(Time.at(1_700_000_000, 123_000))
expect(result).to eq(1_700_000_000.123)
end

it 'truncates precision finer than milliseconds' do
result = Util.serialize_epoch_seconds(Time.at(1_700_000_000, 123_456))
expect(result).to eq(1_700_000_000.123)
end
end

describe '.serialize_date_time' do
it 'omits fractional seconds for whole-second values' do
result = Util.serialize_date_time(Time.at(1_700_000_000))
expect(result).to eq('2023-11-14T22:13:20Z')
end

it 'includes millisecond precision for sub-second values' do
result = Util.serialize_date_time(Time.at(1_700_000_000, 123_000))
expect(result).to eq('2023-11-14T22:13:20.123Z')
end

it 'truncates precision finer than milliseconds' do
result = Util.serialize_date_time(Time.at(1_700_000_000, 123_456))
expect(result).to eq('2023-11-14T22:13:20.123Z')
end
end

describe '.deserialize_time' do
let(:time) { Time.at(946_845_296.123) }

Expand Down
2 changes: 1 addition & 1 deletion gems/aws-sdk-glacier/spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module Glacier
body = Json.load(resp.context.http_request.body_contents)
time = body['InventoryRetrievalParameters']['StartDate']
expect(time).to be_a(String)
expect(time).to eq(now.utc.iso8601)
expect(time).to eq(Aws::Util.serialize_date_time(now))
end

it 'handles seeked io objects in :upload_archive' do
Expand Down
Loading