polyester/codecs/decode/
address_book.rs1use crate::codecs::decode::enums::enum_proto_name;
4use crate::codecs::scalars::{format_id, format_uint64_id};
5use crate::errors::{Error, Result};
6use crate::models::{
7 AddressBookEntriesList, AddressBookEntry, AddressBookTag, AddressBookViewInvalidation,
8 AddressBooksList,
9};
10use crate::proto::auth::v1::{
11 AddressBookEntry as ProtoAddressBookEntry, AddressBookTag as ProtoAddressBookTag,
12 AddressBookViewInvalidated, CopyAddressBookEntryResponse, CreateAddressBookEntryResponse,
13 CreateAddressBookTagResponse, ListAddressBookEntriesResponse, ListAddressBooksResponse,
14 UpdateAddressBookEntryResponse, UpdateAddressBookTagResponse,
15};
16
17pub fn list_books_from_proto(msg: &ListAddressBooksResponse) -> AddressBooksList {
18 AddressBooksList {
19 books: msg
20 .books
21 .iter()
22 .filter_map(|book| serde_json::to_value(book).ok())
23 .collect(),
24 }
25}
26
27fn entry_from_proto(msg: &ProtoAddressBookEntry) -> AddressBookEntry {
28 AddressBookEntry {
29 address_book_entry_id: format_uint64_id(msg.address_book_entry_id),
30 label: msg.label.clone(),
31 kind: enum_proto_name(&msg.kind),
32 revision: msg.revision,
33 }
34}
35
36pub fn list_entries_from_proto(msg: &ListAddressBookEntriesResponse) -> AddressBookEntriesList {
37 AddressBookEntriesList {
38 entries: msg.entries.iter().map(entry_from_proto).collect(),
39 next_page_token: msg.next_page_token.clone(),
40 }
41}
42
43pub fn entry_from_create_proto(msg: &CreateAddressBookEntryResponse) -> Result<AddressBookEntry> {
44 msg.entry
45 .as_option()
46 .map(entry_from_proto)
47 .ok_or_else(|| Error::transport("invalid CreateAddressBookEntry response: missing entry"))
48}
49
50pub fn entry_from_update_proto(msg: &UpdateAddressBookEntryResponse) -> Result<AddressBookEntry> {
51 msg.entry
52 .as_option()
53 .map(entry_from_proto)
54 .ok_or_else(|| Error::transport("invalid UpdateAddressBookEntry response: missing entry"))
55}
56
57pub fn entry_from_copy_proto(msg: &CopyAddressBookEntryResponse) -> Result<AddressBookEntry> {
58 msg.entry
59 .as_option()
60 .map(entry_from_proto)
61 .ok_or_else(|| Error::transport("invalid CopyAddressBookEntry response: missing entry"))
62}
63
64fn tag_from_proto(msg: &ProtoAddressBookTag) -> AddressBookTag {
65 AddressBookTag {
66 tag_id: format_uint64_id(msg.tag_id),
67 name: msg.name.clone(),
68 color: msg.color.clone(),
69 }
70}
71
72pub fn tag_from_create_proto(msg: &CreateAddressBookTagResponse) -> Result<AddressBookTag> {
73 msg.tag
74 .as_option()
75 .map(tag_from_proto)
76 .ok_or_else(|| Error::transport("invalid CreateAddressBookTag response: missing tag"))
77}
78
79pub fn tag_from_update_proto(msg: &UpdateAddressBookTagResponse) -> Result<AddressBookTag> {
80 msg.tag
81 .as_option()
82 .map(tag_from_proto)
83 .ok_or_else(|| Error::transport("invalid UpdateAddressBookTag response: missing tag"))
84}
85
86pub fn address_book_invalidation_from_proto(
87 msg: &AddressBookViewInvalidated,
88) -> AddressBookViewInvalidation {
89 let scope = msg
90 .scope
91 .as_option()
92 .filter(|s| s.root_account_id != 0)
93 .map(|s| format_id(s.root_account_id))
94 .unwrap_or_default();
95 let invalidated_at = msg
96 .invalidated_at
97 .as_option()
98 .map(|ts| format_rfc3339_nano(ts.seconds, ts.nanos))
99 .unwrap_or_default();
100 AddressBookViewInvalidation {
101 scope,
102 invalidated_at,
103 }
104}
105
106fn format_rfc3339_nano(seconds: i64, nanos: i32) -> String {
107 let secs = seconds.max(0) as u64;
108 let days = secs / 86_400;
109 let tod = secs % 86_400;
110 let hour = tod / 3600;
111 let min = (tod % 3600) / 60;
112 let sec = tod % 60;
113 let (y, m, d) = civil_from_days(days as i64);
114 let nanos = nanos.max(0) as u32;
115 if nanos == 0 {
116 format!("{y:04}-{m:02}-{d:02}T{hour:02}:{min:02}:{sec:02}Z")
117 } else {
118 format!("{y:04}-{m:02}-{d:02}T{hour:02}:{min:02}:{sec:02}.{nanos:09}Z")
119 }
120}
121
122fn civil_from_days(days: i64) -> (i64, u32, u32) {
123 let z = days + 719_468;
125 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
126 let doe = (z - era * 146_097) as u64;
127 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
128 let y = yoe as i64 + era * 400;
129 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
130 let mp = (5 * doy + 2) / 153;
131 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
132 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
133 let y = if m <= 2 { y + 1 } else { y };
134 (y, m, d)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::proto::auth::v1::{AccountScopeRef, AddressBook, AddressBookEntryKind};
141 use buffa_types::google::protobuf::Timestamp;
142
143 #[test]
144 fn list_books_serializes_rows() {
145 let msg = ListAddressBooksResponse {
146 books: vec![AddressBook {
147 label: "main".into(),
148 ..Default::default()
149 }],
150 ..Default::default()
151 };
152 let result = list_books_from_proto(&msg);
153 assert_eq!(result.books.len(), 1);
154 assert_eq!(result.books[0]["label"], "main");
155 }
156
157 #[test]
158 fn list_entries_maps_id_label_kind() {
159 let msg = ListAddressBookEntriesResponse {
160 entries: vec![ProtoAddressBookEntry {
161 address_book_entry_id: 7,
162 label: "vault".into(),
163 kind: AddressBookEntryKind::INTERNAL_ACCOUNT.into(),
164 revision: 5,
165 ..Default::default()
166 }],
167 next_page_token: "t".into(),
168 ..Default::default()
169 };
170 let result = list_entries_from_proto(&msg);
171 assert_eq!(result.entries.len(), 1);
172 assert_eq!(result.entries[0].address_book_entry_id, format_uint64_id(7));
173 assert_eq!(result.entries[0].label, "vault");
174 assert!(!result.entries[0].kind.is_empty());
175 assert_eq!(result.entries[0].revision, 5);
176 assert_eq!(result.next_page_token, "t");
177 }
178
179 #[test]
180 fn mutation_responses_reject_missing_required_entities() {
181 assert!(entry_from_create_proto(&CreateAddressBookEntryResponse::default()).is_err());
182 assert!(entry_from_update_proto(&UpdateAddressBookEntryResponse::default()).is_err());
183 assert!(entry_from_copy_proto(&CopyAddressBookEntryResponse::default()).is_err());
184 assert!(tag_from_create_proto(&CreateAddressBookTagResponse::default()).is_err());
185 assert!(tag_from_update_proto(&UpdateAddressBookTagResponse::default()).is_err());
186 }
187
188 #[test]
189 fn invalidation_formats_scope_and_time() {
190 let msg = AddressBookViewInvalidated {
191 scope: AccountScopeRef {
192 root_account_id: 42,
193 ..Default::default()
194 }
195 .into(),
196 invalidated_at: Timestamp {
197 seconds: 0,
198 nanos: 0,
199 ..Default::default()
200 }
201 .into(),
202 ..Default::default()
203 };
204 let out = address_book_invalidation_from_proto(&msg);
205 assert!(!out.scope.is_empty());
206 assert_eq!(out.invalidated_at, "1970-01-01T00:00:00Z");
207 }
208}