1use std::collections::HashMap;
14
15use anyhow::{Context, Result, bail};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19 container::{Bytes, Container, ContainerWriter},
20 pack::RecordId,
21 records::ContextRef,
22 util::codec::{read_u32_le, read_u64_le},
23};
24
25pub const SECTION_TUPLES: &str = "context/tuples";
26pub const CONTEXT_VERSION: u32 = 1;
27pub const CONTEXT_FLAG_AMBIGUOUS_ADMIN: u16 = 1;
28
29const TUPLE_BYTES: usize = 24;
30const MISSING_ID: u32 = u32::MAX;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct AdminContextTuple {
34 pub country_record_id: Option<RecordId>,
35 pub region_record_id: Option<RecordId>,
36 pub district_record_id: Option<RecordId>,
37 pub locality_record_id: Option<RecordId>,
38 pub neighbourhood_record_id: Option<RecordId>,
39 pub place_record_id: Option<RecordId>,
40}
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct RecordContext {
45 pub admin_context: AdminContextTuple,
46 pub flags: u16,
47}
48
49impl AdminContextTuple {
50 pub fn is_empty(self) -> bool {
51 self.ids().iter().all(Option::is_none)
52 }
53
54 pub fn parent_record_ids(self) -> impl Iterator<Item = RecordId> {
57 let [country, region, district, locality, neighbourhood, place] = self.ids();
58 [place, neighbourhood, locality, district, region, country]
59 .into_iter()
60 .flatten()
61 }
62
63 fn ids(self) -> [Option<RecordId>; 6] {
64 [
65 self.country_record_id,
66 self.region_record_id,
67 self.district_record_id,
68 self.locality_record_id,
69 self.neighbourhood_record_id,
70 self.place_record_id,
71 ]
72 }
73
74 fn from_ids(ids: [Option<RecordId>; 6]) -> Self {
75 let [
76 country_record_id,
77 region_record_id,
78 district_record_id,
79 locality_record_id,
80 neighbourhood_record_id,
81 place_record_id,
82 ] = ids;
83 Self {
84 country_record_id,
85 region_record_id,
86 district_record_id,
87 locality_record_id,
88 neighbourhood_record_id,
89 place_record_id,
90 }
91 }
92}
93
94#[derive(Debug, Default)]
95pub struct ContextTupleWriter {
96 tuples: Vec<AdminContextTuple>,
97 ids: HashMap<AdminContextTuple, u32>,
98}
99
100impl ContextTupleWriter {
101 pub fn intern(&mut self, context: RecordContext) -> Result<Option<ContextRef>> {
103 if context.admin_context.is_empty() {
104 return Ok(None);
105 }
106 let tuple_id = match self.ids.get(&context.admin_context) {
107 Some(id) => *id,
108 None => {
109 for id in context.admin_context.ids().into_iter().flatten() {
110 if id >= u64::from(MISSING_ID) {
111 bail!("context record id {id} does not fit the context table");
112 }
113 }
114 let id = u32::try_from(self.tuples.len()).context("context table is full")?;
115 self.tuples.push(context.admin_context);
116 self.ids.insert(context.admin_context, id);
117 id
118 }
119 };
120 Ok(Some(ContextRef {
121 tuple_id,
122 ambiguous: context.flags & CONTEXT_FLAG_AMBIGUOUS_ADMIN != 0,
123 }))
124 }
125
126 pub fn tuple_count(&self) -> u64 {
127 self.tuples.len() as u64
128 }
129
130 pub fn finish(self, pack: &mut ContainerWriter) -> Result<()> {
131 let mut bytes = Vec::with_capacity(8 + self.tuples.len() * TUPLE_BYTES);
132 bytes.extend_from_slice(&(self.tuples.len() as u64).to_le_bytes());
133 for tuple in &self.tuples {
134 for id in tuple.ids() {
135 let id = id.map_or(MISSING_ID, |id| id as u32);
136 bytes.extend_from_slice(&id.to_le_bytes());
137 }
138 }
139 pack.add(SECTION_TUPLES, CONTEXT_VERSION, &bytes)
140 }
141}
142
143#[derive(Clone)]
144pub struct ContextReader {
145 tuples: Bytes,
146 count: u64,
147}
148
149impl ContextReader {
150 pub fn open(container: &Container) -> Result<Self> {
151 let tuples = container.section(SECTION_TUPLES, CONTEXT_VERSION)?;
152 let count = read_u64_le(&tuples, 0).context("context table is truncated")?;
153 if tuples.len() as u64 != 8 + count * TUPLE_BYTES as u64 {
154 bail!("context table does not match its tuple count");
155 }
156 Ok(Self { tuples, count })
157 }
158
159 pub fn resolve(&self, reference: ContextRef) -> Result<RecordContext> {
160 let id = u64::from(reference.tuple_id);
161 if id >= self.count {
162 bail!("context tuple {id} is out of range");
163 }
164 let offset = 8 + id as usize * TUPLE_BYTES;
165 let ids = std::array::from_fn(|index| {
166 let value = read_u32_le(&self.tuples, offset + index * 4).expect("validated table");
167 (value != MISSING_ID).then_some(RecordId::from(value))
168 });
169 Ok(RecordContext {
170 admin_context: AdminContextTuple::from_ids(ids),
171 flags: if reference.ambiguous {
172 CONTEXT_FLAG_AMBIGUOUS_ADMIN
173 } else {
174 0
175 },
176 })
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn interns_tuples_and_resolves_references() {
186 let path =
187 std::env::temp_dir().join(format!("open-geocode-context-{}", uuid::Uuid::new_v4()));
188 let tuple = AdminContextTuple {
189 country_record_id: Some(1),
190 region_record_id: Some(2),
191 locality_record_id: Some(3),
192 ..AdminContextTuple::default()
193 };
194 let mut writer = ContextTupleWriter::default();
195 let first = writer
196 .intern(RecordContext {
197 admin_context: tuple,
198 flags: 0,
199 })
200 .expect("intern")
201 .expect("reference");
202 let second = writer
203 .intern(RecordContext {
204 admin_context: tuple,
205 flags: CONTEXT_FLAG_AMBIGUOUS_ADMIN,
206 })
207 .expect("intern")
208 .expect("reference");
209 assert_eq!(first.tuple_id, second.tuple_id);
210 assert!(second.ambiguous);
211 assert_eq!(
212 writer.intern(RecordContext::default()).expect("empty"),
213 None
214 );
215 assert_eq!(writer.tuple_count(), 1);
216
217 let mut pack = ContainerWriter::create(&path).expect("pack");
218 writer.finish(&mut pack).expect("finish");
219 pack.finish().expect("pack finish");
220 let reader = ContextReader::open(&Container::open(&path).expect("open")).expect("reader");
221 let resolved = reader.resolve(second).expect("resolve");
222 assert_eq!(resolved.admin_context, tuple);
223 assert_eq!(resolved.flags, CONTEXT_FLAG_AMBIGUOUS_ADMIN);
224 assert_eq!(
225 resolved
226 .admin_context
227 .parent_record_ids()
228 .collect::<Vec<_>>(),
229 vec![3, 2, 1]
230 );
231 }
232}