Skip to content

Commit 62f4dec

Browse files
Kotlin/Swift local date (#1154)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-38604 ## πŸ“” Objective This is my first SDK PR and Claude helped me manage a lot of the heavy lifting, please let me know if I am properly honoring the best practices. This PR updates the date values in the SDK for some of the new cipher types to utilize the Rust `NaiveDate` and translate it to a Java `LocalDate`. ## 🚨 Breaking Changes This will create a breaking change for the Kotlin output of the SDK where previously certain date Strings will now be converted to the type-safe `LocalDate` class. This is required to enforce a consistent date format that can be parsed an utilized properly across the platforms. <!-- Does this PR introduce any breaking changes? If so, please describe the impact and migration path for clients. If you're unsure, the automated TypeScript compatibility check will run when you open/update this PR and provide feedback. For breaking changes: 1. Describe what changed in the client interface 2. Explain why the change was necessary 3. Provide migration steps for client developers 4. Link to any paired client PRs if needed Otherwise, you can remove this section. -->
1 parent 3831dfe commit 62f4dec

12 files changed

Lines changed: 220 additions & 79 deletions

File tree

β€Žcrates/bitwarden-core/src/uniffi_support.rsβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,21 @@ uniffi::custom_type!(Uuid, String, {
2121
lower: |obj| obj.to_string(),
2222
});
2323

24+
type NaiveDate = chrono::NaiveDate;
25+
uniffi::custom_type!(NaiveDate, String, {
26+
remote,
27+
try_lift: |val| convert_result(NaiveDate::from_str(&val)),
28+
lower: |obj| obj.to_string(),
29+
});
30+
2431
// Uniffi doesn't emit unused types, this is a dummy record to ensure that the custom type
2532
// converters are emitted
2633
#[allow(dead_code)]
2734
#[derive(uniffi::Record)]
2835
struct UniffiConverterDummyRecord {
2936
uuid: Uuid,
3037
date: DateTime,
38+
naive_date: NaiveDate,
3139
}
3240

3341
uniffi::custom_type!(SignedSecurityState, String, {

β€Žcrates/bitwarden-core/uniffi.tomlβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@ android = true
55
# Temporary workaround for https://github.com/mozilla/uniffi-rs/issues/2740
66
omit_checksums = true
77

8+
[bindings.kotlin.custom_types.NaiveDate]
9+
type_name = "LocalDate"
10+
imports = ["java.time.LocalDate"]
11+
into_custom = "LocalDate.parse({})"
12+
from_custom = "{}.toString()"
13+
814
[bindings.swift]
915
ffi_module_name = "BitwardenCoreFFI"
1016
module_name = "BitwardenCore"
1117
generate_immutable_records = true
1218
generate_codable_conformance = true
19+
20+
[bindings.swift.custom_types.NaiveDate]
21+
type_name = "Date"
22+
imports = ["Foundation", "BitwardenSdkSupport"]
23+
into_custom = "try NaiveDateFormatter.date(from: {})"
24+
from_custom = "NaiveDateFormatter.string(from: {})"

β€Žcrates/bitwarden-uniffi/src/uniffi_support.rsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ use uuid::Uuid;
55
type DateTime = chrono::DateTime<chrono::Utc>;
66
uniffi::use_remote_type!(bitwarden_core::DateTime);
77

8+
type NaiveDate = chrono::NaiveDate;
9+
uniffi::use_remote_type!(bitwarden_core::NaiveDate);
10+
811
uniffi::use_remote_type!(bitwarden_core::Uuid);
912

1013
uniffi::use_remote_type!(bitwarden_crypto::safe::PasswordProtectedKeyEnvelope);

β€Žcrates/bitwarden-uniffi/swift/Package.swiftβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ let package = Package(
2323
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2424
.target(
2525
name: "BitwardenSdk",
26-
dependencies: ["BitwardenFFI"],
26+
dependencies: ["BitwardenFFI", "BitwardenSdkSupport"],
2727
swiftSettings: [.unsafeFlags(["-suppress-warnings"])]),
28+
.target(name: "BitwardenSdkSupport"),
2829
.testTarget(
2930
name: "BitwardenSdkTests",
3031
dependencies: ["BitwardenSdk"]),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Hand-written support code for the generated bindings in `BitwardenSdk`. Referenced from the
2+
// `NaiveDate` custom type conversion configured in `bitwarden-core/uniffi.toml`.
3+
4+
import Foundation
5+
6+
public struct InvalidNaiveDateError: Error {}
7+
8+
public enum NaiveDateFormatter {
9+
private static let formatter: DateFormatter = {
10+
let formatter = DateFormatter()
11+
formatter.locale = Locale(identifier: "en_US_POSIX")
12+
formatter.timeZone = TimeZone(identifier: "UTC")
13+
formatter.dateFormat = "yyyy-MM-dd"
14+
formatter.isLenient = false
15+
return formatter
16+
}()
17+
18+
public static func date(from string: String) throws -> Date {
19+
guard let date = formatter.date(from: string) else {
20+
throw InvalidNaiveDateError()
21+
}
22+
return date
23+
}
24+
25+
public static func string(from date: Date) -> String {
26+
formatter.string(from: date)
27+
}
28+
}

β€Žcrates/bitwarden-vault/src/cipher/blob/conversions/drivers_license.rsβ€Ž

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
use super::{DriversLicenseDataV1, DriversLicenseView};
22

3-
impl_bidirectional_from!(
4-
DriversLicenseView,
5-
DriversLicenseDataV1,
6-
[
7-
first_name,
8-
middle_name,
9-
last_name,
10-
date_of_birth,
11-
license_number,
12-
issuing_country,
13-
issuing_state,
14-
issue_date,
15-
expiration_date,
16-
issuing_authority,
17-
license_class,
18-
]
19-
);
3+
impl From<&DriversLicenseView> for DriversLicenseDataV1 {
4+
fn from(src: &DriversLicenseView) -> Self {
5+
Self {
6+
first_name: src.first_name.clone(),
7+
middle_name: src.middle_name.clone(),
8+
last_name: src.last_name.clone(),
9+
date_of_birth: src.date_of_birth.map(|d| d.to_string()),
10+
license_number: src.license_number.clone(),
11+
issuing_country: src.issuing_country.clone(),
12+
issuing_state: src.issuing_state.clone(),
13+
issue_date: src.issue_date.map(|d| d.to_string()),
14+
expiration_date: src.expiration_date.map(|d| d.to_string()),
15+
issuing_authority: src.issuing_authority.clone(),
16+
license_class: src.license_class.clone(),
17+
}
18+
}
19+
}
20+
21+
impl From<&DriversLicenseDataV1> for DriversLicenseView {
22+
fn from(src: &DriversLicenseDataV1) -> Self {
23+
Self {
24+
first_name: src.first_name.clone(),
25+
middle_name: src.middle_name.clone(),
26+
last_name: src.last_name.clone(),
27+
date_of_birth: src.date_of_birth.as_deref().and_then(|s| s.parse().ok()),
28+
license_number: src.license_number.clone(),
29+
issuing_country: src.issuing_country.clone(),
30+
issuing_state: src.issuing_state.clone(),
31+
issue_date: src.issue_date.as_deref().and_then(|s| s.parse().ok()),
32+
expiration_date: src.expiration_date.as_deref().and_then(|s| s.parse().ok()),
33+
issuing_authority: src.issuing_authority.clone(),
34+
license_class: src.license_class.clone(),
35+
}
36+
}
37+
}
2038

2139
#[cfg(test)]
2240
mod tests {
@@ -36,12 +54,12 @@ mod tests {
3654
first_name: Some("John".to_string()),
3755
middle_name: Some("Michael".to_string()),
3856
last_name: Some("Doe".to_string()),
39-
date_of_birth: Some("1985-06-15".to_string()),
57+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1985, 6, 15),
4058
license_number: Some("DL-987654".to_string()),
4159
issuing_country: Some("US".to_string()),
4260
issuing_state: Some("NY".to_string()),
43-
issue_date: Some("2020-01-01".to_string()),
44-
expiration_date: Some("2028-01-01".to_string()),
61+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
62+
expiration_date: chrono::NaiveDate::from_ymd_opt(2028, 1, 1),
4563
issuing_authority: Some("NY DMV".to_string()),
4664
license_class: Some("D".to_string()),
4765
}),

β€Žcrates/bitwarden-vault/src/cipher/blob/conversions/passport.rsβ€Ž

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
use super::{PassportDataV1, PassportView};
22

3-
impl_bidirectional_from!(
4-
PassportView,
5-
PassportDataV1,
6-
[
7-
surname,
8-
given_name,
9-
date_of_birth,
10-
sex,
11-
birth_place,
12-
nationality,
13-
issuing_country,
14-
passport_number,
15-
passport_type,
16-
national_identification_number,
17-
issuing_authority,
18-
issue_date,
19-
expiration_date,
20-
]
21-
);
3+
impl From<&PassportView> for PassportDataV1 {
4+
fn from(src: &PassportView) -> Self {
5+
Self {
6+
surname: src.surname.clone(),
7+
given_name: src.given_name.clone(),
8+
date_of_birth: src.date_of_birth.map(|d| d.to_string()),
9+
sex: src.sex.clone(),
10+
birth_place: src.birth_place.clone(),
11+
nationality: src.nationality.clone(),
12+
issuing_country: src.issuing_country.clone(),
13+
passport_number: src.passport_number.clone(),
14+
passport_type: src.passport_type.clone(),
15+
national_identification_number: src.national_identification_number.clone(),
16+
issuing_authority: src.issuing_authority.clone(),
17+
issue_date: src.issue_date.map(|d| d.to_string()),
18+
expiration_date: src.expiration_date.map(|d| d.to_string()),
19+
}
20+
}
21+
}
22+
23+
impl From<&PassportDataV1> for PassportView {
24+
fn from(src: &PassportDataV1) -> Self {
25+
Self {
26+
surname: src.surname.clone(),
27+
given_name: src.given_name.clone(),
28+
date_of_birth: src.date_of_birth.as_deref().and_then(|s| s.parse().ok()),
29+
sex: src.sex.clone(),
30+
birth_place: src.birth_place.clone(),
31+
nationality: src.nationality.clone(),
32+
issuing_country: src.issuing_country.clone(),
33+
passport_number: src.passport_number.clone(),
34+
passport_type: src.passport_type.clone(),
35+
national_identification_number: src.national_identification_number.clone(),
36+
issuing_authority: src.issuing_authority.clone(),
37+
issue_date: src.issue_date.as_deref().and_then(|s| s.parse().ok()),
38+
expiration_date: src.expiration_date.as_deref().and_then(|s| s.parse().ok()),
39+
}
40+
}
41+
}
2242

2343
#[cfg(test)]
2444
mod tests {
@@ -37,7 +57,7 @@ mod tests {
3757
passport: Some(PassportView {
3858
surname: Some("Doe".to_string()),
3959
given_name: Some("Jane".to_string()),
40-
date_of_birth: Some("1990-01-01".to_string()),
60+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4161
sex: Some("F".to_string()),
4262
birth_place: Some("New York".to_string()),
4363
nationality: Some("American".to_string()),
@@ -46,8 +66,8 @@ mod tests {
4666
passport_type: Some("P".to_string()),
4767
national_identification_number: Some("123-45-6789".to_string()),
4868
issuing_authority: Some("US State Department".to_string()),
49-
issue_date: Some("2020-01-01".to_string()),
50-
expiration_date: Some("2030-01-01".to_string()),
69+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
70+
expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
5171
}),
5272
..create_shell_cipher_view(CipherType::Passport)
5373
};

β€Žcrates/bitwarden-vault/src/cipher/cipher.rsβ€Ž

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3529,7 +3529,7 @@ mod tests {
35293529
let passport = passport::PassportView {
35303530
given_name: Some("Jane".to_string()),
35313531
surname: Some("Doe".to_string()),
3532-
date_of_birth: Some("1990-01-01".to_string()),
3532+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
35333533
sex: Some("F".to_string()),
35343534
birth_place: Some("New York".to_string()),
35353535
nationality: Some("American".to_string()),
@@ -3538,8 +3538,8 @@ mod tests {
35383538
passport_type: Some("P".to_string()),
35393539
national_identification_number: Some("123-45-6789".to_string()),
35403540
issuing_authority: Some("US State Department".to_string()),
3541-
issue_date: Some("2020-01-01".to_string()),
3542-
expiration_date: Some("2030-01-01".to_string()),
3541+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
3542+
expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
35433543
};
35443544

35453545
let cipher_view = CipherView {
@@ -3567,12 +3567,12 @@ mod tests {
35673567
first_name: Some("John".to_string()),
35683568
middle_name: Some("Michael".to_string()),
35693569
last_name: Some("Doe".to_string()),
3570-
date_of_birth: Some("1985-06-15".to_string()),
3570+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1985, 6, 15),
35713571
license_number: Some("DL-987654".to_string()),
35723572
issuing_country: Some("US".to_string()),
35733573
issuing_state: Some("NY".to_string()),
3574-
issue_date: Some("2020-01-01".to_string()),
3575-
expiration_date: Some("2028-01-01".to_string()),
3574+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
3575+
expiration_date: chrono::NaiveDate::from_ymd_opt(2028, 1, 1),
35763576
issuing_authority: Some("NY DMV".to_string()),
35773577
license_class: Some("D".to_string()),
35783578
};
@@ -4005,12 +4005,12 @@ mod tests {
40054005
first_name: Some("Jane".to_string()),
40064006
middle_name: Some("Q".to_string()),
40074007
last_name: Some("Doe".to_string()),
4008-
date_of_birth: Some("1990-01-01".to_string()),
4008+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
40094009
license_number: Some("D1234567".to_string()),
40104010
issuing_country: Some("US".to_string()),
40114011
issuing_state: Some("CA".to_string()),
4012-
issue_date: Some("2020-01-01".to_string()),
4013-
expiration_date: Some("2030-01-01".to_string()),
4012+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4013+
expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
40144014
issuing_authority: Some("DMV".to_string()),
40154015
license_class: Some("C".to_string()),
40164016
});
@@ -4022,7 +4022,7 @@ mod tests {
40224022
v.passport = Some(PassportView {
40234023
surname: Some("Doe".to_string()),
40244024
given_name: Some("Jane".to_string()),
4025-
date_of_birth: Some("1990-01-01".to_string()),
4025+
date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
40264026
sex: Some("F".to_string()),
40274027
birth_place: Some("Anytown".to_string()),
40284028
nationality: Some("US".to_string()),
@@ -4031,8 +4031,8 @@ mod tests {
40314031
passport_type: Some("P".to_string()),
40324032
national_identification_number: Some("000-00-0000".to_string()),
40334033
issuing_authority: Some("State Dept".to_string()),
4034-
issue_date: Some("2020-01-01".to_string()),
4035-
expiration_date: Some("2030-01-01".to_string()),
4034+
issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4035+
expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
40364036
});
40374037
}),
40384038
),

0 commit comments

Comments
Β (0)