1use redb::ReadableDatabase as _;
3use sl_types::map::{GridCoordinates, GridRectangle, RegionName, RegionNameError, USBNotecard};
4
5#[expect(
7 clippy::module_name_repetitions,
8 reason = "the type is going to be used outside the module"
9)]
10#[derive(Debug, thiserror::Error)]
11pub enum RegionNameToGridCoordinatesError {
12 #[error("HTTP error: {0}")]
14 Http(#[from] reqwest::Error),
15 #[error("failed to clone request for creation of cache policy")]
17 FailedToCloneRequest,
18 #[error("Unexpected prefix in response body: {0}")]
20 UnexpectedPrefix(String),
21 #[error("Unexpected suffix in response body: {0}")]
23 UnexpectedSuffix(String),
24 #[error("Unexpected infix in response body: {0}")]
26 UnexpectedInfix(String),
27 #[error("error parsing the X coordinate {0}: {1}")]
29 X(String, std::num::ParseIntError),
30 #[error("error parsing the Y coordinate {0}: {1}")]
32 Y(String, std::num::ParseIntError),
33}
34
35#[expect(
42 clippy::module_name_repetitions,
43 reason = "the function is going to be used outside the module"
44)]
45pub async fn region_name_to_grid_coordinates(
46 client: &reqwest::Client,
47 region_name: &RegionName,
48 cached_value_with_cache_policy: Option<(
49 Option<GridCoordinates>,
50 http_cache_semantics::CachePolicy,
51 )>,
52) -> Result<
53 (Option<GridCoordinates>, http_cache_semantics::CachePolicy),
54 RegionNameToGridCoordinatesError,
55> {
56 tracing::debug!(
57 "Looking up grid coordinates for region name {}",
58 region_name
59 );
60 let url = format!(
61 "https://cap.secondlife.com/cap/0/d661249b-2b5a-4436-966a-3d3b8d7a574f?var=coords&sim_name={}",
62 region_name.to_string().replace(' ', "%20")
63 );
64 let request = client.get(&url).build()?;
65 if let Some((cached_value, cache_policy)) = cached_value_with_cache_policy {
66 let now = std::time::SystemTime::now();
67 if let http_cache_semantics::BeforeRequest::Fresh(_) =
68 cache_policy.before_request(&request, now)
69 {
70 tracing::debug!("Using cached grid coordinates/absence");
71 return Ok((cached_value, cache_policy));
72 }
73 }
74 let response = client
75 .execute(
76 request
77 .try_clone()
78 .ok_or(RegionNameToGridCoordinatesError::FailedToCloneRequest)?,
79 )
80 .await?;
81 let cache_policy = http_cache_semantics::CachePolicy::new(&request, &response);
82 let response = response.text().await?;
83 if response == "var coords = {'error' : true };" {
84 tracing::debug!("Received negative response");
85 return Ok((None, cache_policy));
86 }
87 let Some(response) = response.strip_prefix("var coords = {'x' : ") else {
88 return Err(RegionNameToGridCoordinatesError::UnexpectedPrefix(
89 response.to_owned(),
90 ));
91 };
92 let Some(response) = response.strip_suffix(" };") else {
93 return Err(RegionNameToGridCoordinatesError::UnexpectedSuffix(
94 response.to_owned(),
95 ));
96 };
97 let parts = response.split(", 'y' : ").collect::<Vec<_>>();
98 let [x, y] = parts.as_slice() else {
99 return Err(RegionNameToGridCoordinatesError::UnexpectedInfix(
100 response.to_owned(),
101 ));
102 };
103 let x = x
104 .parse::<u32>()
105 .map_err(|err| RegionNameToGridCoordinatesError::X(x.to_string(), err))?;
106 let y = y
107 .parse::<u32>()
108 .map_err(|err| RegionNameToGridCoordinatesError::Y(y.to_string(), err))?;
109 let grid_coordinates = GridCoordinates::new(x, y);
110 tracing::debug!("Received response: {:?}", grid_coordinates);
111 Ok((Some(grid_coordinates), cache_policy))
112}
113
114#[derive(Debug, thiserror::Error)]
116pub enum GridCoordinatesToRegionNameError {
117 #[error("HTTP error: {0}")]
119 Http(#[from] reqwest::Error),
120 #[error("failed to clone request for creation of cache policy")]
122 FailedToCloneRequest,
123 #[error("Unexpected prefix in response body: {0}")]
125 UnexpectedPrefix(String),
126 #[error("Unexpected suffix in response body: {0}")]
128 UnexpectedSuffix(String),
129 #[error("error parsing the region name {0}: {1}")]
131 RegionName(String, RegionNameError),
132}
133
134pub async fn grid_coordinates_to_region_name(
141 client: &reqwest::Client,
142 grid_coordinates: &GridCoordinates,
143 cached_value_with_cache_policy: Option<(Option<RegionName>, http_cache_semantics::CachePolicy)>,
144) -> Result<(Option<RegionName>, http_cache_semantics::CachePolicy), GridCoordinatesToRegionNameError>
145{
146 tracing::debug!(
147 "Looking up region name for grid coordinates {:?}",
148 grid_coordinates
149 );
150 let url = format!(
151 "https://cap.secondlife.com/cap/0/b713fe80-283b-4585-af4d-a3b7d9a32492?var=region&grid_x={}&grid_y={}",
152 grid_coordinates.x(),
153 grid_coordinates.y()
154 );
155 let request = client.get(&url).build()?;
156 if let Some((cached_value, cache_policy)) = cached_value_with_cache_policy {
157 let now = std::time::SystemTime::now();
158 if let http_cache_semantics::BeforeRequest::Fresh(_) =
159 cache_policy.before_request(&request, now)
160 {
161 tracing::debug!("Returning cached region name/absence");
162 return Ok((cached_value, cache_policy));
163 }
164 }
165 let response = client
166 .execute(
167 request
168 .try_clone()
169 .ok_or(GridCoordinatesToRegionNameError::FailedToCloneRequest)?,
170 )
171 .await?;
172 let cache_policy = http_cache_semantics::CachePolicy::new(&request, &response);
173 let response = response.text().await?;
174 if response == "var region = {'error' : true };" {
175 tracing::debug!("Received negative response");
176 return Ok((None, cache_policy));
177 }
178 let Some(response) = response.strip_prefix("var region='") else {
179 return Err(GridCoordinatesToRegionNameError::UnexpectedPrefix(
180 response.to_string(),
181 ));
182 };
183 let Some(response) = response.strip_suffix("';") else {
184 return Err(GridCoordinatesToRegionNameError::UnexpectedSuffix(
185 response.to_string(),
186 ));
187 };
188 let region_name = RegionName::try_new(response)
189 .map_err(|err| GridCoordinatesToRegionNameError::RegionName(response.to_owned(), err))?;
190 tracing::debug!("Received region name: {region_name}");
191 Ok((Some(region_name), cache_policy))
192}
193
194#[expect(
197 clippy::module_name_repetitions,
198 reason = "the type is going to be used outside the module"
199)]
200#[derive(Debug)]
201pub struct RegionNameToGridCoordinatesCache {
202 client: reqwest::Client,
204 db: redb::Database,
206 grid_coordinate_cache:
208 lru::LruCache<RegionName, (Option<GridCoordinates>, http_cache_semantics::CachePolicy)>,
209 region_name_cache:
211 lru::LruCache<GridCoordinates, (Option<RegionName>, http_cache_semantics::CachePolicy)>,
212}
213
214#[derive(Debug, thiserror::Error)]
216pub enum CacheError {
217 #[error("error decoding the JSON serialized CachePolicy: {0}")]
219 CachePolicyJsonDecodeError(#[from] serde_json::Error),
220 #[error("redb database error: {0}")]
222 DatabaseError(#[from] redb::DatabaseError),
223 #[error("redb transaction error: {0}")]
225 TransactionError(#[from] redb::TransactionError),
226 #[error("redb table error: {0}")]
228 TableError(#[from] redb::TableError),
229 #[error("redb storage error: {0}")]
231 StorageError(#[from] redb::StorageError),
232 #[error("redb storage error: {0}")]
234 CommitError(#[from] redb::CommitError),
235 #[error("error looking up grid coordinates via HTTP: {0}")]
237 GridCoordinatesHttpError(#[from] RegionNameToGridCoordinatesError),
238 #[error("error looking up region name via HTTP: {0}")]
240 RegionNameHttpError(#[from] GridCoordinatesToRegionNameError),
241 #[error("error creating region name from cached string: {0}")]
243 RegionNameError(#[from] RegionNameError),
244 #[error("error handling system time for cache age calculations: {0}")]
246 SystemTimeError(#[from] std::time::SystemTimeError),
247}
248
249const GRID_COORDINATE_CACHE_TABLE: redb::TableDefinition<String, (u32, u32)> =
255 redb::TableDefinition::new("grid_coordinates_v2");
256
257const REGION_NAME_CACHE_TABLE: redb::TableDefinition<(u32, u32), String> =
259 redb::TableDefinition::new("region_name_v2");
260
261const GRID_COORDINATE_CACHE_POLICY_TABLE: redb::TableDefinition<String, String> =
264 redb::TableDefinition::new("grid_coordinate_cache_policy");
265
266const REGION_NAME_CACHE_POLICY_TABLE: redb::TableDefinition<(u32, u32), String> =
269 redb::TableDefinition::new("region_name_cache_policy_v2");
270
271const LEGACY_GRID_COORDINATE_CACHE_TABLE: redb::TableDefinition<String, (u16, u16)> =
276 redb::TableDefinition::new("grid_coordinates");
277
278const LEGACY_REGION_NAME_CACHE_TABLE: redb::TableDefinition<(u16, u16), String> =
281 redb::TableDefinition::new("region_name");
282
283const LEGACY_REGION_NAME_CACHE_POLICY_TABLE: redb::TableDefinition<(u16, u16), String> =
286 redb::TableDefinition::new("region_name_cache_policy");
287
288impl RegionNameToGridCoordinatesCache {
289 pub fn new(cache_directory: std::path::PathBuf) -> Result<Self, CacheError> {
295 let client = reqwest::Client::new();
296 let db = redb::Database::create(cache_directory.join("region_name.redb"))?;
297 {
301 let write_txn = db.begin_write()?;
302 write_txn.delete_table(LEGACY_GRID_COORDINATE_CACHE_TABLE)?;
303 write_txn.delete_table(LEGACY_REGION_NAME_CACHE_TABLE)?;
304 write_txn.delete_table(LEGACY_REGION_NAME_CACHE_POLICY_TABLE)?;
305 write_txn.commit()?;
306 }
307 let grid_coordinate_cache = lru::LruCache::unbounded();
308 let region_name_cache = lru::LruCache::unbounded();
309 Ok(Self {
310 client,
311 db,
312 grid_coordinate_cache,
313 region_name_cache,
314 })
315 }
316
317 pub async fn get_grid_coordinates(
323 &mut self,
324 region_name: &RegionName,
325 ) -> Result<Option<GridCoordinates>, CacheError> {
326 tracing::debug!("Retrieving grid coordinates for region {region_name:?}");
327 let cached_value_with_cache_policy = {
328 if let Some(memory_cached_value) = self.grid_coordinate_cache.get(region_name) {
329 Some(memory_cached_value.to_owned())
330 } else {
331 let read_txn = self.db.begin_read()?;
332 let cache_policy = {
333 if let Ok(table) = read_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE) {
334 if let Some(access_guard) =
335 table.get(region_name.to_owned().into_inner())?
336 {
337 let cache_policy: http_cache_semantics::CachePolicy =
338 serde_json::from_str(&access_guard.value())?;
339 Some(cache_policy)
340 } else {
341 None
342 }
343 } else {
344 None
345 }
346 };
347 if let Some(cache_policy) = cache_policy {
348 let cached_value = {
349 if let Ok(table) = read_txn.open_table(GRID_COORDINATE_CACHE_TABLE) {
350 if let Some(access_guard) =
351 table.get(region_name.to_owned().into_inner())?
352 {
353 let (x, y) = access_guard.value();
354 Some(GridCoordinates::new(x, y))
355 } else {
356 None
357 }
358 } else {
359 None
360 }
361 };
362 Some((cached_value, cache_policy))
363 } else {
364 None
365 }
366 }
367 };
368 match region_name_to_grid_coordinates(
369 &self.client,
370 region_name,
371 cached_value_with_cache_policy,
372 )
373 .await
374 {
375 Ok((Some(grid_coordinates), cache_policy)) => {
376 if cache_policy.is_storable() {
377 tracing::debug!("Storing grid coordinates in cache");
378 let write_txn = self.db.begin_write()?;
379 {
380 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
381 table.insert(
382 region_name.to_owned().into_inner(),
383 serde_json::to_string(&cache_policy)?,
384 )?;
385 }
386 {
387 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
388 table.insert(
389 region_name.to_owned().into_inner(),
390 (grid_coordinates.x(), grid_coordinates.y()),
391 )?;
392 }
393 write_txn.commit()?;
394 self.grid_coordinate_cache.put(
395 region_name.to_owned(),
396 (Some(grid_coordinates), cache_policy),
397 );
398 } else {
399 tracing::debug!("Grid coordinates are not storable");
400 let write_txn = self.db.begin_write()?;
401 {
402 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
403 table.remove(region_name.to_owned().into_inner())?;
404 }
405 {
406 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
407 table.remove(region_name.to_owned().into_inner())?;
408 }
409 write_txn.commit()?;
410 self.grid_coordinate_cache.pop(region_name);
411 }
412 tracing::debug!("Coordinates are {grid_coordinates:?}");
413 Ok(Some(grid_coordinates))
414 }
415 Ok((None, cache_policy)) => {
416 if cache_policy.is_storable() {
417 tracing::debug!("Storing negative response in cache");
418 let write_txn = self.db.begin_write()?;
419 {
420 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
421 table.insert(
422 region_name.to_owned().into_inner(),
423 serde_json::to_string(&cache_policy)?,
424 )?;
425 }
426 {
427 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
428 table.remove(region_name.to_owned().into_inner())?;
429 }
430 write_txn.commit()?;
431 self.grid_coordinate_cache
432 .put(region_name.to_owned(), (None, cache_policy));
433 } else {
434 tracing::debug!("Negative response is not storable");
435 let write_txn = self.db.begin_write()?;
436 {
437 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
438 table.remove(region_name.to_owned().into_inner())?;
439 }
440 {
441 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
442 table.remove(region_name.to_owned().into_inner())?;
443 }
444 write_txn.commit()?;
445 self.grid_coordinate_cache.pop(region_name);
446 }
447 tracing::debug!("No coordinates exist for that name");
448 Ok(None)
449 }
450 Err(err) => Err(CacheError::GridCoordinatesHttpError(err)),
451 }
452 }
453
454 pub async fn get_region_name(
460 &mut self,
461 grid_coordinates: &GridCoordinates,
462 ) -> Result<Option<RegionName>, CacheError> {
463 tracing::debug!("Retrieving region name for grid coordinates {grid_coordinates:?}");
464 let cached_value_with_cache_policy = {
465 if let Some(memory_cached_value) = self.region_name_cache.get(grid_coordinates) {
466 Some(memory_cached_value.to_owned())
467 } else {
468 let read_txn = self.db.begin_read()?;
469 let cache_policy = {
470 if let Ok(table) = read_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE) {
471 if let Some(access_guard) =
472 table.get((grid_coordinates.x(), grid_coordinates.y()))?
473 {
474 let cache_policy: http_cache_semantics::CachePolicy =
475 serde_json::from_str(&access_guard.value())?;
476 Some(cache_policy)
477 } else {
478 None
479 }
480 } else {
481 None
482 }
483 };
484 if let Some(cache_policy) = cache_policy {
485 let cached_value = {
486 if let Ok(table) = read_txn.open_table(REGION_NAME_CACHE_TABLE) {
487 if let Some(access_guard) =
488 table.get((grid_coordinates.x(), grid_coordinates.y()))?
489 {
490 let region_name = access_guard.value();
491 Some(RegionName::try_new(region_name)?)
492 } else {
493 None
494 }
495 } else {
496 None
497 }
498 };
499 Some((cached_value, cache_policy))
500 } else {
501 None
502 }
503 }
504 };
505 match grid_coordinates_to_region_name(
506 &self.client,
507 grid_coordinates,
508 cached_value_with_cache_policy,
509 )
510 .await
511 {
512 Ok((Some(region_name), cache_policy)) => {
513 if cache_policy.is_storable() {
514 tracing::debug!("Storing region name in cache");
515 let write_txn = self.db.begin_write()?;
516 {
517 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
518 table.insert(
519 (grid_coordinates.x(), grid_coordinates.y()),
520 serde_json::to_string(&cache_policy)?,
521 )?;
522 }
523 {
524 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
525 table.insert(
526 (grid_coordinates.x(), grid_coordinates.y()),
527 region_name.to_owned().into_inner(),
528 )?;
529 }
530 write_txn.commit()?;
531 self.region_name_cache.put(
532 grid_coordinates.to_owned(),
533 (Some(region_name.to_owned()), cache_policy),
534 );
535 } else {
536 tracing::warn!("Region name response is not storable");
537 let write_txn = self.db.begin_write()?;
538 {
539 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
540 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
541 }
542 {
543 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
544 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
545 }
546 write_txn.commit()?;
547 self.region_name_cache.pop(grid_coordinates);
548 }
549 tracing::debug!("Region name is {region_name:?}");
550 Ok(Some(region_name))
551 }
552 Ok((None, cache_policy)) => {
553 if cache_policy.is_storable() {
554 tracing::debug!("Storing negative response in cache");
555 let write_txn = self.db.begin_write()?;
556 {
557 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
558 table.insert(
559 (grid_coordinates.x(), grid_coordinates.y()),
560 serde_json::to_string(&cache_policy)?,
561 )?;
562 }
563 {
564 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
565 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
566 }
567 write_txn.commit()?;
568 self.region_name_cache
569 .put(grid_coordinates.to_owned(), (None, cache_policy));
570 } else {
571 tracing::debug!("Negative response is not storable");
572 let write_txn = self.db.begin_write()?;
573 {
574 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
575 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
576 }
577 {
578 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
579 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
580 }
581 write_txn.commit()?;
582 self.region_name_cache.pop(grid_coordinates);
583 }
584 tracing::debug!("No region name exists for those grid coordinates");
585 Ok(None)
586 }
587 Err(err) => Err(CacheError::RegionNameHttpError(err)),
588 }
589 }
590}
591
592#[derive(Debug, thiserror::Error)]
594pub enum USBNotecardToGridRectangleError {
595 #[error(
598 "There were no waypoints in the USB notecards which made determining a grid rectangle for it impossible"
599 )]
600 NoUSBNotecardWaypoints,
601 #[error("error converting region name to grid coordinates: {0}")]
603 CacheError(#[from] CacheError),
604 #[error("No grid coordinates were returned for one of the regions in the USB notecard: {0}")]
607 NoGridCoordinatesForRegion(RegionName),
608}
609
610pub async fn usb_notecard_to_grid_rectangle(
616 region_name_to_grid_coordinates_cache: &mut RegionNameToGridCoordinatesCache,
617 usb_notecard: &USBNotecard,
618) -> Result<GridRectangle, USBNotecardToGridRectangleError> {
619 let mut lower_left_x = None;
620 let mut lower_left_y = None;
621 let mut upper_right_x = None;
622 let mut upper_right_y = None;
623 for waypoint in usb_notecard.waypoints() {
624 let grid_coordinates = region_name_to_grid_coordinates_cache
625 .get_grid_coordinates(waypoint.location().region_name())
626 .await?;
627 if let Some(grid_coordinates) = grid_coordinates {
628 if let Some(llx) = lower_left_x {
629 lower_left_x = Some(std::cmp::min(llx, grid_coordinates.x()));
630 } else {
631 lower_left_x = Some(grid_coordinates.x());
632 }
633 if let Some(lly) = lower_left_y {
634 lower_left_y = Some(std::cmp::min(lly, grid_coordinates.y()));
635 } else {
636 lower_left_y = Some(grid_coordinates.y());
637 }
638 if let Some(urx) = upper_right_x {
639 upper_right_x = Some(std::cmp::max(urx, grid_coordinates.x()));
640 } else {
641 upper_right_x = Some(grid_coordinates.x());
642 }
643 if let Some(ury) = upper_right_y {
644 upper_right_y = Some(std::cmp::max(ury, grid_coordinates.y()));
645 } else {
646 upper_right_y = Some(grid_coordinates.y());
647 }
648 } else {
649 return Err(USBNotecardToGridRectangleError::NoGridCoordinatesForRegion(
650 waypoint.location().region_name().to_owned(),
651 ));
652 }
653 }
654 let Some(lower_left_x) = lower_left_x else {
655 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
656 };
657 let Some(lower_left_y) = lower_left_y else {
658 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
659 };
660 let Some(upper_right_x) = upper_right_x else {
661 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
662 };
663 let Some(upper_right_y) = upper_right_y else {
664 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
665 };
666 Ok(GridRectangle::new(
667 GridCoordinates::new(lower_left_x, lower_left_y),
668 GridCoordinates::new(upper_right_x, upper_right_y),
669 ))
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use pretty_assertions::assert_eq;
676
677 #[tokio::test]
678 async fn test_region_name_to_grid_coordinates() -> Result<(), Box<dyn std::error::Error>> {
679 let client = reqwest::Client::new();
680 assert_eq!(
681 region_name_to_grid_coordinates(&client, &RegionName::try_new("Thorkell")?, None)
682 .await?
683 .0,
684 Some(GridCoordinates::new(1136, 1075))
685 );
686 Ok(())
687 }
688
689 #[tokio::test]
690 async fn test_grid_coordinates_to_region_name() -> Result<(), Box<dyn std::error::Error>> {
691 let client = reqwest::Client::new();
692 assert_eq!(
693 grid_coordinates_to_region_name(&client, &GridCoordinates::new(1136, 1075), None)
694 .await?
695 .0,
696 Some(RegionName::try_new("Thorkell")?)
697 );
698 Ok(())
699 }
700
701 #[tokio::test]
702 async fn test_cache_region_name_to_grid_coordinates() -> Result<(), Box<dyn std::error::Error>>
703 {
704 let tempdir = tempfile::tempdir()?;
705 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
706 assert_eq!(
707 cache
708 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
709 .await?,
710 Some(GridCoordinates::new(1136, 1075))
711 );
712 Ok(())
713 }
714
715 #[tokio::test]
716 async fn test_cache_region_name_to_grid_coordinates_twice()
717 -> Result<(), Box<dyn std::error::Error>> {
718 let tempdir = tempfile::tempdir()?;
719 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
720 assert_eq!(
721 cache
722 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
723 .await?,
724 Some(GridCoordinates::new(1136, 1075))
725 );
726 assert_eq!(
727 cache
728 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
729 .await?,
730 Some(GridCoordinates::new(1136, 1075))
731 );
732 Ok(())
733 }
734
735 #[tokio::test]
736 async fn test_cache_region_name_to_grid_coordinates_negative_twice()
737 -> Result<(), Box<dyn std::error::Error>> {
738 let tempdir = tempfile::tempdir()?;
739 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
740 assert_eq!(
741 cache
742 .get_grid_coordinates(&RegionName::try_new("Thorkel")?)
743 .await?,
744 None,
745 );
746 assert_eq!(
747 cache
748 .get_grid_coordinates(&RegionName::try_new("Thorkel")?)
749 .await?,
750 None,
751 );
752 Ok(())
753 }
754
755 #[tokio::test]
756 async fn test_cache_grid_coordinates_to_region_name() -> Result<(), Box<dyn std::error::Error>>
757 {
758 let tempdir = tempfile::tempdir()?;
759 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
760 assert_eq!(
761 cache
762 .get_region_name(&GridCoordinates::new(1136, 1075))
763 .await?,
764 Some(RegionName::try_new("Thorkell")?)
765 );
766 Ok(())
767 }
768
769 #[tokio::test]
770 async fn test_cache_grid_coordinates_to_region_name_twice()
771 -> Result<(), Box<dyn std::error::Error>> {
772 let tempdir = tempfile::tempdir()?;
773 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
774 assert_eq!(
775 cache
776 .get_region_name(&GridCoordinates::new(1136, 1075))
777 .await?,
778 Some(RegionName::try_new("Thorkell")?)
779 );
780 assert_eq!(
781 cache
782 .get_region_name(&GridCoordinates::new(1136, 1075))
783 .await?,
784 Some(RegionName::try_new("Thorkell")?)
785 );
786 Ok(())
787 }
788
789 #[tokio::test]
790 async fn test_cache_grid_coordinates_to_region_name_negative_twice()
791 -> Result<(), Box<dyn std::error::Error>> {
792 let tempdir = tempfile::tempdir()?;
793 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
794 assert_eq!(
795 cache
796 .get_region_name(&GridCoordinates::new(11136, 1075))
797 .await?,
798 None,
799 );
800 assert_eq!(
801 cache
802 .get_region_name(&GridCoordinates::new(11136, 1075))
803 .await?,
804 None,
805 );
806 Ok(())
807 }
808}