Skip to content

Commit 240b94e

Browse files
committed
Add protoduration support
1 parent 48ce789 commit 240b94e

File tree

4 files changed

+118
-2
lines changed

4 files changed

+118
-2
lines changed

FirebaseAI/Sources/AILog.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ enum AILog {
7272
case liveSessionFailedToSendClientMessage = 3021
7373
case liveSessionUnexpectedResponse = 3022
7474
case liveSessionGoingAwaySoon = 3023
75+
case decodedMissingProtoDurationSuffix = 3024
76+
case decodedInvalidProtoDurationString = 3025
77+
case decodedInvalidProtoDurationSeconds = 3026
78+
case decodedInvalidProtoDurationNanoseconds = 3027
7579

7680
// SDK State Errors
7781
case generateContentResponseNoCandidates = 4000

FirebaseAI/Sources/Types/Internal/Live/GoAway.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ struct GoAway: Decodable, Sendable {
2020
/// The remaining time before the connection will be terminated as ABORTED.
2121
/// The minimal time returned here is specified differently together with
2222
/// the rate limits for a given model.
23-
let timeLeft: TimeInterval?
23+
let timeLeft: ProtoDuration?
2424
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// Represents a signed, fixed-length span of time represented
18+
/// as a count of seconds and fractions of seconds at nanosecond
19+
/// resolution.
20+
///
21+
/// This represents a
22+
/// [`google.protobuf.duration`](https://protobuf.dev/reference/protobuf/google.protobuf/#duration).
23+
struct ProtoDuration {
24+
/// Signed seconds of the span of time.
25+
///
26+
/// Must be from -315,576,000,000 to +315,576,000,000 inclusive.
27+
///
28+
/// Note: these bounds are computed from:
29+
/// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
30+
let seconds: Int64
31+
32+
/// Signed fractions of a second at nanosecond resolution of the span of time.
33+
///
34+
/// Durations less than one second are represented with a 0
35+
/// `seconds` field and a positive or negative `nanos` field.
36+
///
37+
/// For durations of one second or more, a non-zero value for the `nanos` field must be
38+
/// of the same sign as the `seconds` field. Must be from -999,999,999
39+
/// to +999,999,999 inclusive.
40+
let nanos: Int32
41+
42+
/// Returns a `TimeInterval` representation of the `ProtoDuration`.
43+
var timeInterval: TimeInterval {
44+
return TimeInterval(seconds) + TimeInterval(nanos) / 1_000_000_000
45+
}
46+
}
47+
48+
// MARK: - Codable Conformance
49+
50+
extension ProtoDuration: Decodable {
51+
init(from decoder: any Decoder) throws {
52+
var text = try decoder.singleValueContainer().decode(String.self)
53+
if text.last != "s" {
54+
AILog.warning(
55+
code: .decodedMissingProtoDurationSuffix,
56+
"Missing 's' at end of proto duration: \(text)."
57+
)
58+
} else {
59+
text.removeLast()
60+
}
61+
62+
let seconds: String
63+
let nanoseconds: String
64+
65+
let maybeSplit = text.split(separator: ".")
66+
if maybeSplit.count > 2 {
67+
AILog.warning(
68+
code: .decodedInvalidProtoDurationString,
69+
"Too many decimal places in proto duration (expected only 1): \(maybeSplit)."
70+
)
71+
throw DecodingError.dataCorrupted(.init(
72+
codingPath: [],
73+
debugDescription: "Invalid proto duration string: \(text)"
74+
))
75+
}
76+
77+
if maybeSplit.count == 2 {
78+
seconds = String(maybeSplit[0])
79+
nanoseconds = String(maybeSplit[1])
80+
} else {
81+
seconds = text
82+
nanoseconds = "0"
83+
}
84+
85+
guard let secs = Int64(seconds) else {
86+
AILog.warning(
87+
code: .decodedInvalidProtoDurationSeconds,
88+
"Failed to parse the seconds to an Int64: \(seconds)."
89+
)
90+
91+
throw DecodingError.dataCorrupted(.init(
92+
codingPath: [],
93+
debugDescription: "Invalid proto duration seconds: \(text)"
94+
))
95+
}
96+
97+
guard let nanos = Int32(nanoseconds) else {
98+
AILog.warning(
99+
code: .decodedInvalidProtoDurationNanoseconds,
100+
"Failed to parse the nanoseconds to an Int32: \(nanoseconds)."
101+
)
102+
103+
throw DecodingError.dataCorrupted(.init(
104+
codingPath: [],
105+
debugDescription: "Invalid proto duration nanoseconds: \(text)"
106+
))
107+
}
108+
109+
self.seconds = secs
110+
self.nanos = nanos
111+
}
112+
}

FirebaseAI/Sources/Types/Public/Live/LiveServerGoAway.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public struct LiveServerGoAway: Sendable {
2323
/// The remaining time before the connection will be terminated as ABORTED.
2424
/// The minimal time returned here is specified differently together with
2525
/// the rate limits for a given model.
26-
public var timeLeft: TimeInterval? { goAway.timeLeft }
26+
public var timeLeft: TimeInterval? { goAway.timeLeft?.timeInterval }
2727

2828
init(_ goAway: GoAway) {
2929
self.goAway = goAway

0 commit comments

Comments
 (0)