diff --git a/build_tools/services.rb b/build_tools/services.rb index 5469693679c..3e32e6d8e9b 100644 --- a/build_tools/services.rb +++ b/build_tools/services.rb @@ -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" diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index 548ce898377..37de7896706 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -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) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb index e53a963969c..2775443adb0 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb index e48bfe47bdf..dd901250bc5 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb index 446e80e17c5..87c57b137a6 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb index b69ad71b090..f4dd8518df6 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb index 4d0b088b78a..5f2d68f472d 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb index 8c818e150be..49f1a8a3fe9 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb @@ -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 diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/util.rb b/gems/aws-sdk-core/lib/aws-sdk-core/util.rb index db749aabafb..ee0dea174b1 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/util.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/util.rb @@ -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) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb index 836f56f8327..0813a03a3fb 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb @@ -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 diff --git a/gems/aws-sdk-core/spec/aws/json/builder_spec.rb b/gems/aws-sdk-core/spec/aws/json/builder_spec.rb index b1c08fb5199..59a31f09185 100644 --- a/gems/aws-sdk-core/spec/aws/json/builder_spec.rb +++ b/gems/aws-sdk-core/spec/aws/json/builder_spec.rb @@ -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: { diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb index 5dc874f4b4e..2e1e0888f64 100644 --- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb +++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb @@ -72,7 +72,7 @@ def normalize(json) "AttributeType": "S" } ], - "CreationDateTime": #{now.to_i}, + "CreationDateTime": #{Aws::Util.serialize_epoch_seconds(now)}, "ItemCount": 0, "KeySchema": [ { @@ -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 diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb index 7320bd3b41e..b537f891867 100644 --- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb +++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb @@ -49,7 +49,7 @@ def normalize(xml) name user-id arn:aws:iam::123456789012:user/name - #{now.utc.iso8601} + #{Aws::Util.serialize_date_time(now)} false diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb index 28e4b30ebb2..90e2092804c 100644 --- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb +++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb @@ -56,7 +56,7 @@ def normalize(xml) aws-sdk - #{now.utc.iso8601} + #{Aws::Util.serialize_date_time(now)} diff --git a/gems/aws-sdk-core/spec/aws/util_spec.rb b/gems/aws-sdk-core/spec/aws/util_spec.rb index bd7a39bb021..0808253d389 100644 --- a/gems/aws-sdk-core/spec/aws/util_spec.rb +++ b/gems/aws-sdk-core/spec/aws/util_spec.rb @@ -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) } diff --git a/gems/aws-sdk-glacier/spec/client_spec.rb b/gems/aws-sdk-glacier/spec/client_spec.rb index 0373a1b24e9..8aa87bb2a81 100644 --- a/gems/aws-sdk-glacier/spec/client_spec.rb +++ b/gems/aws-sdk-glacier/spec/client_spec.rb @@ -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