From 3d49236c138aa1e26f1f0edcf0f31117e09fcaa2 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Fri, 13 Feb 2026 15:02:55 +0000 Subject: [PATCH] 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::coffee::nearby_cafes and update all references. --- src/domain/coffee/mod.rs | 1 + src/domain/coffee/nearby_cafes.rs | 16 ++++++++++++++++ src/domain/mod.rs | 2 +- src/infrastructure/foursquare.rs | 20 +++++--------------- src/presentation/web/views/cafes.rs | 6 +++--- tests/server/nearby_api.rs | 8 ++++---- 6 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 src/domain/coffee/nearby_cafes.rs diff --git a/src/domain/coffee/mod.rs b/src/domain/coffee/mod.rs index 26836f4..4052df4 100644 --- a/src/domain/coffee/mod.rs +++ b/src/domain/coffee/mod.rs @@ -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; diff --git a/src/domain/coffee/nearby_cafes.rs b/src/domain/coffee/nearby_cafes.rs new file mode 100644 index 0000000..1491d58 --- /dev/null +++ b/src/domain/coffee/nearby_cafes.rs @@ -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, + pub distance_meters: u32, +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 3d7b29b..6e46ca5 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -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; diff --git a/src/infrastructure/foursquare.rs b/src/infrastructure/foursquare.rs index 67e2e2a..73c3043 100644 --- a/src/infrastructure/foursquare.rs +++ b/src/infrastructure/foursquare.rs @@ -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, - 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, AppError> { +) -> Result, 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 { +fn parse_cafe(place: FoursquarePlace, location: &SearchLocation) -> Option { if place.name.is_empty() { return None; } @@ -111,7 +101,7 @@ fn parse_cafe(place: FoursquarePlace, location: &SearchLocation) -> Option for NearbyCafeView { - fn from(cafe: NearbyCafe) -> Self { +impl From for NearbyCafeView { + fn from(cafe: NearbyCafeResult) -> Self { let distance = if cafe.distance_meters < 1000 { format!("{} m", cafe.distance_meters) } else { diff --git a/tests/server/nearby_api.rs b/tests/server/nearby_api.rs index b9f796a..ac7eb14 100644 --- a/tests/server/nearby_api.rs +++ b/tests/server/nearby_api.rs @@ -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 = response.json().await.expect("Failed to parse response"); + let cafes: Vec = 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 = response.json().await.expect("Failed to parse response"); + let cafes: Vec = 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 = response.json().await.expect("Failed to parse response"); + let cafes: Vec = 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");