refactor: move NearbyCafe to domain layer to fix dependency violation

The presentation layer was importing NearbyCafe directly from
infrastructure::foursquare, violating the dependency flow
(presentation -> application -> domain <- infrastructure). Introduce
NearbyCafeResult in domain::nearby_cafes and update all
references.
This commit is contained in:
Jon Seager 2026-02-13 15:02:55 +00:00
parent f7b210f234
commit 3d49236c13
No known key found for this signature in database
6 changed files with 30 additions and 23 deletions

View file

@ -3,6 +3,7 @@ pub mod brews;
pub mod cafes;
pub mod cups;
pub mod gear;
pub mod nearby_cafes;
pub mod roasters;
pub mod roasts;

View file

@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
/// A nearby cafe result from a location-based search.
///
/// This is a domain-level representation that decouples the presentation
/// layer from any specific third-party API (e.g. Foursquare).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NearbyCafeResult {
pub name: String,
pub latitude: f64,
pub longitude: f64,
pub city: String,
pub country: String,
pub website: Option<String>,
pub distance_meters: u32,
}

View file

@ -13,5 +13,5 @@ pub mod repositories;
// Re-exports for backward compatibility
pub use analytics::{ai_usage, country_stats, stats, timeline};
pub use auth::{passkey_credentials, registration_tokens, sessions, tokens, users};
pub use coffee::{bags, brews, cafes, cups, gear, roasters, roasts};
pub use coffee::{bags, brews, cafes, cups, gear, nearby_cafes, roasters, roasts};
pub use errors::RepositoryError;

View file

@ -1,9 +1,10 @@
use std::time::Duration;
use isocountry::CountryCode;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use crate::application::errors::AppError;
use crate::domain::nearby_cafes::NearbyCafeResult;
pub const FOURSQUARE_SEARCH_URL: &str = "https://places-api.foursquare.com/places/search";
const USER_AGENT: &str = "Brewlog/1.0";
@ -13,17 +14,6 @@ const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
const FIELDS: &str = "name,latitude,longitude,location,website,distance";
const API_VERSION: &str = "2025-06-17";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NearbyCafe {
pub name: String,
pub latitude: f64,
pub longitude: f64,
pub city: String,
pub country: String,
pub website: Option<String>,
pub distance_meters: u32,
}
/// Location mode for Foursquare search.
pub enum SearchLocation {
/// Search near GPS coordinates with a fixed radius.
@ -39,7 +29,7 @@ pub async fn search_nearby(
api_key: &str,
location: &SearchLocation,
query: &str,
) -> Result<Vec<NearbyCafe>, AppError> {
) -> Result<Vec<NearbyCafeResult>, AppError> {
let mut request = client
.get(base_url)
.header("User-Agent", USER_AGENT)
@ -90,7 +80,7 @@ pub async fn search_nearby(
Ok(cafes)
}
fn parse_cafe(place: FoursquarePlace, location: &SearchLocation) -> Option<NearbyCafe> {
fn parse_cafe(place: FoursquarePlace, location: &SearchLocation) -> Option<NearbyCafeResult> {
if place.name.is_empty() {
return None;
}
@ -111,7 +101,7 @@ fn parse_cafe(place: FoursquarePlace, location: &SearchLocation) -> Option<Nearb
let website = place.website.filter(|w| !w.trim().is_empty());
Some(NearbyCafe {
Some(NearbyCafeResult {
name: place.name,
latitude: lat,
longitude: lng,

View file

@ -1,6 +1,6 @@
use crate::domain::cafes::Cafe;
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
use crate::infrastructure::foursquare::NearbyCafe;
use crate::domain::nearby_cafes::NearbyCafeResult;
use super::{LegendEntry, build_map_data};
@ -144,8 +144,8 @@ pub struct NearbyCafeView {
pub location: String,
}
impl From<NearbyCafe> for NearbyCafeView {
fn from(cafe: NearbyCafe) -> Self {
impl From<NearbyCafeResult> for NearbyCafeView {
fn from(cafe: NearbyCafeResult) -> Self {
let distance = if cafe.distance_meters < 1000 {
format!("{} m", cafe.distance_meters)
} else {

View file

@ -1,4 +1,4 @@
use brewlog::infrastructure::foursquare::NearbyCafe;
use brewlog::domain::nearby_cafes::NearbyCafeResult;
use wiremock::matchers::{header, method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
@ -58,7 +58,7 @@ async fn nearby_search_returns_results() {
assert_eq!(response.status(), 200);
let cafes: Vec<NearbyCafe> = response.json().await.expect("Failed to parse response");
let cafes: Vec<NearbyCafeResult> = response.json().await.expect("Failed to parse response");
assert_eq!(cafes.len(), 2);
assert_eq!(cafes[0].name, "Prufrock Coffee");
@ -98,7 +98,7 @@ async fn nearby_search_returns_empty_for_no_matches() {
assert_eq!(response.status(), 200);
let cafes: Vec<NearbyCafe> = response.json().await.expect("Failed to parse response");
let cafes: Vec<NearbyCafeResult> = response.json().await.expect("Failed to parse response");
assert!(cafes.is_empty());
}
@ -199,7 +199,7 @@ async fn nearby_search_with_near_param() {
assert_eq!(response.status(), 200);
let cafes: Vec<NearbyCafe> = response.json().await.expect("Failed to parse response");
let cafes: Vec<NearbyCafeResult> = response.json().await.expect("Failed to parse response");
assert_eq!(cafes.len(), 2);
assert_eq!(cafes[0].name, "Prufrock Coffee");
assert_eq!(cafes[0].city, "London");