Skip to content

Commit 8f3df6b

Browse files
committed
[Vertex AI] Add Citation.publicationDate
1 parent ab94252 commit 8f3df6b

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

FirebaseVertexAI/Sources/GenerateContentResponse.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ public struct Citation: Sendable {
141141

142142
/// The license the cited source work is distributed under, if specified.
143143
public let license: String?
144+
145+
public let publicationDate: Date?
144146
}
145147

146148
/// A value enumerating possible reasons for a model to terminate a content generation request.
@@ -363,6 +365,7 @@ extension Citation: Decodable {
363365
case uri
364366
case title
365367
case license
368+
case publicationDate
366369
}
367370

368371
public init(from decoder: any Decoder) throws {
@@ -385,6 +388,18 @@ extension Citation: Decodable {
385388
} else {
386389
license = nil
387390
}
391+
if let publicationProtoDate = try container.decodeIfPresent(
392+
ProtoDate.self,
393+
forKey: .publicationDate
394+
) {
395+
if let publicationDate = publicationProtoDate.dateComponents.date {
396+
self.publicationDate = publicationDate
397+
} else {
398+
publicationDate = nil
399+
}
400+
} else {
401+
publicationDate = nil
402+
}
388403
}
389404
}
390405

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 whole or partial calendar date, such as a birthday.
18+
///
19+
/// The time of day and time zone are either specified elsewhere or are insignificant. The date is
20+
/// relative to the Gregorian Calendar. This can represent one of the following:
21+
/// - A full date, with non-zero year, month, and day values
22+
/// - A month and day value, with a zero year, such as an anniversary
23+
/// - A year on its own, with zero month and day values
24+
/// - A year and month value, with a zero day, such as a credit card expiration date
25+
///
26+
/// This represents a `google.type.Date`.
27+
struct ProtoDate {
28+
/// Year of the date.
29+
///
30+
/// Must be from 1 to 9999, or 0 to specify a date without a year.
31+
let year: Int?
32+
33+
/// Month of a year.
34+
///
35+
/// Must be from 1 to 12, or 0 to specify a year without a month and day.
36+
let month: Int?
37+
38+
/// Day of a month.
39+
///
40+
/// Must be from 1 to 31 and valid for the year and month, or 0 to specify a year by itself or a
41+
/// year and month where the day isn't significant.
42+
let day: Int?
43+
44+
var dateComponents: DateComponents {
45+
DateComponents(
46+
calendar: Calendar.current,
47+
timeZone: TimeZone.current,
48+
year: year,
49+
month: month,
50+
day: day
51+
)
52+
}
53+
54+
init(year: Int?, month: Int?, day: Int?) {
55+
self.year = year
56+
self.month = month
57+
self.day = day
58+
}
59+
}
60+
61+
// MARK: - Codable Conformance
62+
63+
extension ProtoDate: Decodable {
64+
enum CodingKeys: CodingKey {
65+
case year
66+
case month
67+
case day
68+
}
69+
70+
init(from decoder: any Decoder) throws {
71+
let container = try decoder.container(keyedBy: CodingKeys.self)
72+
if let year = try container.decodeIfPresent(Int.self, forKey: .year), year != 0 {
73+
guard year > 0 else {
74+
throw DecodingError.dataCorrupted(
75+
.init(codingPath: [CodingKeys.year], debugDescription: "Invalid year: \(year)")
76+
)
77+
}
78+
self.year = year
79+
} else {
80+
year = nil
81+
}
82+
83+
if let month = try container.decodeIfPresent(Int.self, forKey: .month), month != 0 {
84+
guard month > 0 else {
85+
throw DecodingError.dataCorrupted(
86+
.init(codingPath: [CodingKeys.month], debugDescription: "Invalid month: \(month)")
87+
)
88+
}
89+
self.month = month
90+
} else {
91+
month = nil
92+
}
93+
94+
if let day = try container.decodeIfPresent(Int.self, forKey: .day), day != 0 {
95+
guard day > 0 else {
96+
throw DecodingError.dataCorrupted(
97+
.init(codingPath: [CodingKeys.day], debugDescription: "Invalid day: \(day)")
98+
)
99+
}
100+
self.day = day
101+
} else {
102+
day = nil
103+
}
104+
105+
guard year != nil || month != nil || day != nil else {
106+
throw DecodingError.dataCorrupted(.init(
107+
codingPath: [CodingKeys.year, CodingKeys.month, CodingKeys.day],
108+
debugDescription: "Invalid date: missing year, month and day"
109+
))
110+
}
111+
}
112+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 XCTest
16+
17+
@testable import FirebaseVertexAI
18+
19+
final class ProtoDateTests: XCTestCase {
20+
let decoder = JSONDecoder()
21+
22+
// A full date, with non-zero year, month, and day values.
23+
func testProtoDate_fullDate_dateComponents() {
24+
let year = 2024
25+
let month = 12
26+
let day = 31
27+
let protoDate = ProtoDate(year: year, month: month, day: day)
28+
29+
let dateComponents = protoDate.dateComponents
30+
31+
XCTAssertTrue(dateComponents.isValidDate)
32+
XCTAssertEqual(dateComponents.year, year)
33+
XCTAssertEqual(dateComponents.month, month)
34+
XCTAssertEqual(dateComponents.day, day)
35+
}
36+
37+
// A month and day value, with a zero year, such as an anniversary.
38+
func testProtoDate_monthDay_dateComponents() {
39+
let month = 7
40+
let day = 1
41+
let protoDate = ProtoDate(year: nil, month: month, day: day)
42+
43+
let dateComponents = protoDate.dateComponents
44+
45+
XCTAssertTrue(dateComponents.isValidDate)
46+
XCTAssertNil(dateComponents.year)
47+
XCTAssertEqual(dateComponents.month, month)
48+
XCTAssertEqual(dateComponents.day, day)
49+
}
50+
51+
// A year on its own, with zero month and day values.
52+
func testProtoDate_yearOnly_dateComponents() {
53+
let year = 2024
54+
let protoDate = ProtoDate(year: year, month: nil, day: nil)
55+
56+
let dateComponents = protoDate.dateComponents
57+
58+
XCTAssertTrue(dateComponents.isValidDate)
59+
XCTAssertEqual(dateComponents.year, year)
60+
XCTAssertNil(dateComponents.month)
61+
XCTAssertNil(dateComponents.day)
62+
}
63+
64+
// A year and month value, with a zero day, such as a credit card expiration date
65+
func testProtoDate_yearMonth_dateComponents() {
66+
let year = 2024
67+
let month = 08
68+
let protoDate = ProtoDate(year: year, month: month, day: nil)
69+
70+
let dateComponents = protoDate.dateComponents
71+
72+
XCTAssertTrue(dateComponents.isValidDate)
73+
XCTAssertEqual(protoDate.year, year)
74+
XCTAssertEqual(protoDate.month, month)
75+
XCTAssertEqual(protoDate.day, nil)
76+
}
77+
78+
func testProtoDate_asDate() throws {
79+
let protoDate = ProtoDate(year: 2024, month: 12, day: 31)
80+
let dateFormatter = DateFormatter()
81+
dateFormatter.dateFormat = "yyyy-MM-dd"
82+
let expectedDate = try XCTUnwrap(dateFormatter.date(from: "2024-12-31"))
83+
84+
let date = try XCTUnwrap(protoDate.dateComponents.date)
85+
86+
XCTAssertEqual(date, expectedDate)
87+
}
88+
89+
func testDecodeProtoDate() throws {
90+
let json = """
91+
{
92+
"year" : 2024,
93+
"month" : 12,
94+
"day" : 31
95+
}
96+
"""
97+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
98+
99+
let protoDate = try decoder.decode(ProtoDate.self, from: jsonData)
100+
101+
XCTAssertEqual(protoDate.year, 2024)
102+
XCTAssertEqual(protoDate.month, 12)
103+
XCTAssertEqual(protoDate.day, 31)
104+
}
105+
106+
func testDecodeProtoDate_emptyDate_throws() throws {
107+
let json = "{}"
108+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
109+
110+
do {
111+
_ = try decoder.decode(ProtoDate.self, from: jsonData)
112+
} catch DecodingError.dataCorrupted {
113+
return
114+
}
115+
XCTFail("Expected a DecodingError.")
116+
}
117+
}

0 commit comments

Comments
 (0)