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::<u16>()
105 .map_err(|err| RegionNameToGridCoordinatesError::X(x.to_string(), err))?;
106 let y = y
107 .parse::<u16>()
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, (u16, u16)> =
251 redb::TableDefinition::new("grid_coordinates");
252
253const REGION_NAME_CACHE_TABLE: redb::TableDefinition<(u16, u16), String> =
255 redb::TableDefinition::new("region_name");
256
257const GRID_COORDINATE_CACHE_POLICY_TABLE: redb::TableDefinition<String, String> =
260 redb::TableDefinition::new("grid_coordinate_cache_policy");
261
262const REGION_NAME_CACHE_POLICY_TABLE: redb::TableDefinition<(u16, u16), String> =
265 redb::TableDefinition::new("region_name_cache_policy");
266
267impl RegionNameToGridCoordinatesCache {
268 pub fn new(cache_directory: std::path::PathBuf) -> Result<Self, CacheError> {
274 let client = reqwest::Client::new();
275 let db = redb::Database::create(cache_directory.join("region_name.redb"))?;
276 let grid_coordinate_cache = lru::LruCache::unbounded();
277 let region_name_cache = lru::LruCache::unbounded();
278 Ok(Self {
279 client,
280 db,
281 grid_coordinate_cache,
282 region_name_cache,
283 })
284 }
285
286 pub async fn get_grid_coordinates(
292 &mut self,
293 region_name: &RegionName,
294 ) -> Result<Option<GridCoordinates>, CacheError> {
295 tracing::debug!("Retrieving grid coordinates for region {region_name:?}");
296 let cached_value_with_cache_policy = {
297 if let Some(memory_cached_value) = self.grid_coordinate_cache.get(region_name) {
298 Some(memory_cached_value.to_owned())
299 } else {
300 let read_txn = self.db.begin_read()?;
301 let cache_policy = {
302 if let Ok(table) = read_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE) {
303 if let Some(access_guard) =
304 table.get(region_name.to_owned().into_inner())?
305 {
306 let cache_policy: http_cache_semantics::CachePolicy =
307 serde_json::from_str(&access_guard.value())?;
308 Some(cache_policy)
309 } else {
310 None
311 }
312 } else {
313 None
314 }
315 };
316 if let Some(cache_policy) = cache_policy {
317 let cached_value = {
318 if let Ok(table) = read_txn.open_table(GRID_COORDINATE_CACHE_TABLE) {
319 if let Some(access_guard) =
320 table.get(region_name.to_owned().into_inner())?
321 {
322 let (x, y) = access_guard.value();
323 Some(GridCoordinates::new(x, y))
324 } else {
325 None
326 }
327 } else {
328 None
329 }
330 };
331 Some((cached_value, cache_policy))
332 } else {
333 None
334 }
335 }
336 };
337 match region_name_to_grid_coordinates(
338 &self.client,
339 region_name,
340 cached_value_with_cache_policy,
341 )
342 .await
343 {
344 Ok((Some(grid_coordinates), cache_policy)) => {
345 if cache_policy.is_storable() {
346 tracing::debug!("Storing grid coordinates in cache");
347 let write_txn = self.db.begin_write()?;
348 {
349 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
350 table.insert(
351 region_name.to_owned().into_inner(),
352 serde_json::to_string(&cache_policy)?,
353 )?;
354 }
355 {
356 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
357 table.insert(
358 region_name.to_owned().into_inner(),
359 (grid_coordinates.x(), grid_coordinates.y()),
360 )?;
361 }
362 write_txn.commit()?;
363 self.grid_coordinate_cache.put(
364 region_name.to_owned(),
365 (Some(grid_coordinates), cache_policy),
366 );
367 } else {
368 tracing::debug!("Grid coordinates are not storable");
369 let write_txn = self.db.begin_write()?;
370 {
371 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
372 table.remove(region_name.to_owned().into_inner())?;
373 }
374 {
375 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
376 table.remove(region_name.to_owned().into_inner())?;
377 }
378 write_txn.commit()?;
379 self.grid_coordinate_cache.pop(region_name);
380 }
381 tracing::debug!("Coordinates are {grid_coordinates:?}");
382 Ok(Some(grid_coordinates))
383 }
384 Ok((None, cache_policy)) => {
385 if cache_policy.is_storable() {
386 tracing::debug!("Storing negative response in cache");
387 let write_txn = self.db.begin_write()?;
388 {
389 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
390 table.insert(
391 region_name.to_owned().into_inner(),
392 serde_json::to_string(&cache_policy)?,
393 )?;
394 }
395 {
396 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
397 table.remove(region_name.to_owned().into_inner())?;
398 }
399 write_txn.commit()?;
400 self.grid_coordinate_cache
401 .put(region_name.to_owned(), (None, cache_policy));
402 } else {
403 tracing::debug!("Negative response is not storable");
404 let write_txn = self.db.begin_write()?;
405 {
406 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_POLICY_TABLE)?;
407 table.remove(region_name.to_owned().into_inner())?;
408 }
409 {
410 let mut table = write_txn.open_table(GRID_COORDINATE_CACHE_TABLE)?;
411 table.remove(region_name.to_owned().into_inner())?;
412 }
413 write_txn.commit()?;
414 self.grid_coordinate_cache.pop(region_name);
415 }
416 tracing::debug!("No coordinates exist for that name");
417 Ok(None)
418 }
419 Err(err) => Err(CacheError::GridCoordinatesHttpError(err)),
420 }
421 }
422
423 pub async fn get_region_name(
429 &mut self,
430 grid_coordinates: &GridCoordinates,
431 ) -> Result<Option<RegionName>, CacheError> {
432 tracing::debug!("Retrieving region name for grid coordinates {grid_coordinates:?}");
433 let cached_value_with_cache_policy = {
434 if let Some(memory_cached_value) = self.region_name_cache.get(grid_coordinates) {
435 Some(memory_cached_value.to_owned())
436 } else {
437 let read_txn = self.db.begin_read()?;
438 let cache_policy = {
439 if let Ok(table) = read_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE) {
440 if let Some(access_guard) =
441 table.get((grid_coordinates.x(), grid_coordinates.y()))?
442 {
443 let cache_policy: http_cache_semantics::CachePolicy =
444 serde_json::from_str(&access_guard.value())?;
445 Some(cache_policy)
446 } else {
447 None
448 }
449 } else {
450 None
451 }
452 };
453 if let Some(cache_policy) = cache_policy {
454 let cached_value = {
455 if let Ok(table) = read_txn.open_table(REGION_NAME_CACHE_TABLE) {
456 if let Some(access_guard) =
457 table.get((grid_coordinates.x(), grid_coordinates.y()))?
458 {
459 let region_name = access_guard.value();
460 Some(RegionName::try_new(region_name)?)
461 } else {
462 None
463 }
464 } else {
465 None
466 }
467 };
468 Some((cached_value, cache_policy))
469 } else {
470 None
471 }
472 }
473 };
474 match grid_coordinates_to_region_name(
475 &self.client,
476 grid_coordinates,
477 cached_value_with_cache_policy,
478 )
479 .await
480 {
481 Ok((Some(region_name), cache_policy)) => {
482 if cache_policy.is_storable() {
483 tracing::debug!("Storing region name in cache");
484 let write_txn = self.db.begin_write()?;
485 {
486 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
487 table.insert(
488 (grid_coordinates.x(), grid_coordinates.y()),
489 serde_json::to_string(&cache_policy)?,
490 )?;
491 }
492 {
493 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
494 table.insert(
495 (grid_coordinates.x(), grid_coordinates.y()),
496 region_name.to_owned().into_inner(),
497 )?;
498 }
499 write_txn.commit()?;
500 self.region_name_cache.put(
501 grid_coordinates.to_owned(),
502 (Some(region_name.to_owned()), cache_policy),
503 );
504 } else {
505 tracing::warn!("Region name response is not storable");
506 let write_txn = self.db.begin_write()?;
507 {
508 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
509 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
510 }
511 {
512 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
513 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
514 }
515 write_txn.commit()?;
516 self.region_name_cache.pop(grid_coordinates);
517 }
518 tracing::debug!("Region name is {region_name:?}");
519 Ok(Some(region_name))
520 }
521 Ok((None, cache_policy)) => {
522 if cache_policy.is_storable() {
523 tracing::debug!("Storing negative response in cache");
524 let write_txn = self.db.begin_write()?;
525 {
526 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
527 table.insert(
528 (grid_coordinates.x(), grid_coordinates.y()),
529 serde_json::to_string(&cache_policy)?,
530 )?;
531 }
532 {
533 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
534 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
535 }
536 write_txn.commit()?;
537 self.region_name_cache
538 .put(grid_coordinates.to_owned(), (None, cache_policy));
539 } else {
540 tracing::debug!("Negative response is not storable");
541 let write_txn = self.db.begin_write()?;
542 {
543 let mut table = write_txn.open_table(REGION_NAME_CACHE_POLICY_TABLE)?;
544 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
545 }
546 {
547 let mut table = write_txn.open_table(REGION_NAME_CACHE_TABLE)?;
548 table.remove((grid_coordinates.x(), grid_coordinates.y()))?;
549 }
550 write_txn.commit()?;
551 self.region_name_cache.pop(grid_coordinates);
552 }
553 tracing::debug!("No region name exists for those grid coordinates");
554 Ok(None)
555 }
556 Err(err) => Err(CacheError::RegionNameHttpError(err)),
557 }
558 }
559}
560
561#[derive(Debug, thiserror::Error)]
563pub enum USBNotecardToGridRectangleError {
564 #[error(
567 "There were no waypoints in the USB notecards which made determining a grid rectangle for it impossible"
568 )]
569 NoUSBNotecardWaypoints,
570 #[error("error converting region name to grid coordinates: {0}")]
572 CacheError(#[from] CacheError),
573 #[error("No grid coordinates were returned for one of the regions in the USB notecard: {0}")]
576 NoGridCoordinatesForRegion(RegionName),
577}
578
579pub async fn usb_notecard_to_grid_rectangle(
585 region_name_to_grid_coordinates_cache: &mut RegionNameToGridCoordinatesCache,
586 usb_notecard: &USBNotecard,
587) -> Result<GridRectangle, USBNotecardToGridRectangleError> {
588 let mut lower_left_x = None;
589 let mut lower_left_y = None;
590 let mut upper_right_x = None;
591 let mut upper_right_y = None;
592 for waypoint in usb_notecard.waypoints() {
593 let grid_coordinates = region_name_to_grid_coordinates_cache
594 .get_grid_coordinates(waypoint.location().region_name())
595 .await?;
596 if let Some(grid_coordinates) = grid_coordinates {
597 if let Some(llx) = lower_left_x {
598 lower_left_x = Some(std::cmp::min(llx, grid_coordinates.x()));
599 } else {
600 lower_left_x = Some(grid_coordinates.x());
601 }
602 if let Some(lly) = lower_left_y {
603 lower_left_y = Some(std::cmp::min(lly, grid_coordinates.y()));
604 } else {
605 lower_left_y = Some(grid_coordinates.y());
606 }
607 if let Some(urx) = upper_right_x {
608 upper_right_x = Some(std::cmp::max(urx, grid_coordinates.x()));
609 } else {
610 upper_right_x = Some(grid_coordinates.x());
611 }
612 if let Some(ury) = upper_right_y {
613 upper_right_y = Some(std::cmp::max(ury, grid_coordinates.y()));
614 } else {
615 upper_right_y = Some(grid_coordinates.y());
616 }
617 } else {
618 return Err(USBNotecardToGridRectangleError::NoGridCoordinatesForRegion(
619 waypoint.location().region_name().to_owned(),
620 ));
621 }
622 }
623 let Some(lower_left_x) = lower_left_x else {
624 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
625 };
626 let Some(lower_left_y) = lower_left_y else {
627 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
628 };
629 let Some(upper_right_x) = upper_right_x else {
630 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
631 };
632 let Some(upper_right_y) = upper_right_y else {
633 return Err(USBNotecardToGridRectangleError::NoUSBNotecardWaypoints);
634 };
635 Ok(GridRectangle::new(
636 GridCoordinates::new(lower_left_x, lower_left_y),
637 GridCoordinates::new(upper_right_x, upper_right_y),
638 ))
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use pretty_assertions::assert_eq;
645
646 #[tokio::test]
647 async fn test_region_name_to_grid_coordinates() -> Result<(), Box<dyn std::error::Error>> {
648 let client = reqwest::Client::new();
649 assert_eq!(
650 region_name_to_grid_coordinates(&client, &RegionName::try_new("Thorkell")?, None)
651 .await?
652 .0,
653 Some(GridCoordinates::new(1136, 1075))
654 );
655 Ok(())
656 }
657
658 #[tokio::test]
659 async fn test_grid_coordinates_to_region_name() -> Result<(), Box<dyn std::error::Error>> {
660 let client = reqwest::Client::new();
661 assert_eq!(
662 grid_coordinates_to_region_name(&client, &GridCoordinates::new(1136, 1075), None)
663 .await?
664 .0,
665 Some(RegionName::try_new("Thorkell")?)
666 );
667 Ok(())
668 }
669
670 #[tokio::test]
671 async fn test_cache_region_name_to_grid_coordinates() -> Result<(), Box<dyn std::error::Error>>
672 {
673 let tempdir = tempfile::tempdir()?;
674 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
675 assert_eq!(
676 cache
677 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
678 .await?,
679 Some(GridCoordinates::new(1136, 1075))
680 );
681 Ok(())
682 }
683
684 #[tokio::test]
685 async fn test_cache_region_name_to_grid_coordinates_twice()
686 -> Result<(), Box<dyn std::error::Error>> {
687 let tempdir = tempfile::tempdir()?;
688 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
689 assert_eq!(
690 cache
691 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
692 .await?,
693 Some(GridCoordinates::new(1136, 1075))
694 );
695 assert_eq!(
696 cache
697 .get_grid_coordinates(&RegionName::try_new("Thorkell")?)
698 .await?,
699 Some(GridCoordinates::new(1136, 1075))
700 );
701 Ok(())
702 }
703
704 #[tokio::test]
705 async fn test_cache_region_name_to_grid_coordinates_negative_twice()
706 -> Result<(), Box<dyn std::error::Error>> {
707 let tempdir = tempfile::tempdir()?;
708 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
709 assert_eq!(
710 cache
711 .get_grid_coordinates(&RegionName::try_new("Thorkel")?)
712 .await?,
713 None,
714 );
715 assert_eq!(
716 cache
717 .get_grid_coordinates(&RegionName::try_new("Thorkel")?)
718 .await?,
719 None,
720 );
721 Ok(())
722 }
723
724 #[tokio::test]
725 async fn test_cache_grid_coordinates_to_region_name() -> Result<(), Box<dyn std::error::Error>>
726 {
727 let tempdir = tempfile::tempdir()?;
728 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
729 assert_eq!(
730 cache
731 .get_region_name(&GridCoordinates::new(1136, 1075))
732 .await?,
733 Some(RegionName::try_new("Thorkell")?)
734 );
735 Ok(())
736 }
737
738 #[tokio::test]
739 async fn test_cache_grid_coordinates_to_region_name_twice()
740 -> Result<(), Box<dyn std::error::Error>> {
741 let tempdir = tempfile::tempdir()?;
742 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
743 assert_eq!(
744 cache
745 .get_region_name(&GridCoordinates::new(1136, 1075))
746 .await?,
747 Some(RegionName::try_new("Thorkell")?)
748 );
749 assert_eq!(
750 cache
751 .get_region_name(&GridCoordinates::new(1136, 1075))
752 .await?,
753 Some(RegionName::try_new("Thorkell")?)
754 );
755 Ok(())
756 }
757
758 #[tokio::test]
759 async fn test_cache_grid_coordinates_to_region_name_negative_twice()
760 -> Result<(), Box<dyn std::error::Error>> {
761 let tempdir = tempfile::tempdir()?;
762 let mut cache = RegionNameToGridCoordinatesCache::new(tempdir.path().to_path_buf())?;
763 assert_eq!(
764 cache
765 .get_region_name(&GridCoordinates::new(11136, 1075))
766 .await?,
767 None,
768 );
769 assert_eq!(
770 cache
771 .get_region_name(&GridCoordinates::new(11136, 1075))
772 .await?,
773 None,
774 );
775 Ok(())
776 }
777}