diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt index 9a2c0cbc8..7407f1c1c 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/IntegrationTestHelpers.kt @@ -7,7 +7,9 @@ import uniffi.wp_api.WpApiClientDelegate import uniffi.wp_api.WpApiMiddlewarePipeline import uniffi.wp_api.WpAuthenticationProvider import uniffi.wp_api.WpErrorCode +import uniffi.wp_api.ParsedUrl import uniffi.wp_mobile.MockPostService +import uniffi.wp_mobile.SiteInfo import uniffi.wp_mobile.WpService import rs.wordpress.cache.kotlin.WordPressApiCache @@ -51,10 +53,15 @@ fun createTestServiceContext(): TestServiceContext { // Extract site URL by removing /wp-json suffix val siteUrl = apiRootUrl.removeSuffix("/wp-json") + val parsedSiteUrl = ParsedUrl.parse(siteUrl) + val parsedApiRoot = ParsedUrl.parse(apiRootUrl) + // Create self-hosted service - val service = WpService.selfHosted( - siteUrl = siteUrl, - apiRoot = apiRootUrl, + val service = WpService( + siteInfo = SiteInfo.SelfHosted( + siteUrl = parsedSiteUrl, + apiRoot = parsedApiRoot + ), delegate = WpApiClientDelegate( authProvider, requestExecutor = WpRequestExecutor(emptyList(), NetworkAvailabilityProvider { true }), @@ -64,11 +71,12 @@ fun createTestServiceContext(): TestServiceContext { cache = wordPressApiCache.cache ) - // Create mock post service with shared cache + // Create mock post service with shared cache. + // Use the parsed URL strings to ensure URL normalization matches the WpService's DbSite. val mockPostService = MockPostService( wordPressApiCache.cache, - siteUrl, - apiRootUrl + parsedSiteUrl.url(), + parsedApiRoot.url() ) return TestServiceContext(service, mockPostService) diff --git a/native/kotlin/example/composeApp/src/androidMain/kotlin/rs/wordpress/example/ui/welcome/WelcomeActivity.kt b/native/kotlin/example/composeApp/src/androidMain/kotlin/rs/wordpress/example/ui/welcome/WelcomeActivity.kt index d944807aa..7557eefe9 100644 --- a/native/kotlin/example/composeApp/src/androidMain/kotlin/rs/wordpress/example/ui/welcome/WelcomeActivity.kt +++ b/native/kotlin/example/composeApp/src/androidMain/kotlin/rs/wordpress/example/ui/welcome/WelcomeActivity.kt @@ -32,7 +32,6 @@ import uniffi.wp_api.parseAuthorizationUrl import uniffi.wp_api.wordpressComOauth2Configuration import uniffi.wp_mobile.Account import uniffi.wp_mobile.AccountRepository -import uniffi.wp_mobile.wordpressComSiteApiRoot class WelcomeActivity : ComponentActivity() { private val accountRepository: AccountRepository by inject() @@ -212,16 +211,14 @@ class WelcomeActivity : ComponentActivity() { val tokenResponse = tokenResult.response.data val blogId = tokenResponse.blogId ?: throw IllegalStateException("Expected blog_id in site-specific token response") - val siteUrl = discoveredSiteHost - ?: tokenResponse.blogUrl - ?: "WordPress.com" accountRepository.store( - Account.SelfHostedSite( + Account.WpCom( id = 0uL, - domain = siteUrl, - username = siteUrl, - password = tokenResponse.accessToken, - siteApiRoot = wordpressComSiteApiRoot(blogId) + username = discoveredSiteHost + ?: tokenResponse.blogUrl + ?: "WordPress.com", + token = tokenResponse.accessToken, + siteApiRoot = blogId.toString() ) ) siteSpecificOAuthState = null diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt index cb6da0a7d..47e1287bf 100644 --- a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt @@ -85,8 +85,17 @@ fun App(authenticationEnabled: Boolean, authenticateSite: (String, onSuccess: () navController.navigate("site") } is Account.WpCom -> { - currentWpComClient = createWpComApiClient(account) - navController.navigate("wpcom_site") + if (account.siteApiRoot.isNotEmpty()) { + // Site-specific WP.com account with a blog ID + currentWpService = createWpService(account, cache) + val apiClient = createWpApiClient(account) + currentApiClient = apiClient + currentSiteViewModel = SiteViewModel(apiClient) + navController.navigate("site") + } else { + currentWpComClient = createWpComApiClient(account) + navController.navigate("wpcom_site") + } } } } diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt index 9fd0ef44f..5555f74f6 100644 --- a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt @@ -16,6 +16,10 @@ import uniffi.wp_api.WpAuthenticationProvider import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword import uniffi.wp_mobile.Account import uniffi.wp_mobile.AccountRepository +import uniffi.wp_api.ParsedUrl +import uniffi.wp_api.WpComBaseUrl +import uniffi.wp_api.WpComDotOrgApiUrlResolver +import uniffi.wp_mobile.SiteInfo import uniffi.wp_mobile.WpService import java.io.File import java.net.URI @@ -47,9 +51,11 @@ fun createWpService( } else { wpAuthenticationFromUsernameAndPassword(account.username, account.password) } - return WpService.selfHosted( - siteUrl = account.domain, - apiRoot = account.siteApiRoot, + return WpService( + siteInfo = SiteInfo.SelfHosted( + siteUrl = ParsedUrl.parse(account.domain), + apiRoot = ParsedUrl.parse(account.siteApiRoot) + ), delegate = WpApiClientDelegate( WpAuthenticationProvider.staticWithAuth(auth), requestExecutor = WpRequestExecutor(emptyList(), networkAvailabilityProvider), @@ -60,6 +66,28 @@ fun createWpService( ) } +fun createWpService( + account: Account.WpCom, + cache: WordPressApiCache, + networkAvailabilityProvider: NetworkAvailabilityProvider = NetworkAvailabilityProvider { true } +): WpService { + val siteId = account.siteApiRoot.toULong() + return WpService( + siteInfo = SiteInfo.WordPressCom( + siteId = siteId + ), + delegate = WpApiClientDelegate( + WpAuthenticationProvider.staticWithAuth( + WpAuthentication.Bearer(token = account.token) + ), + requestExecutor = WpRequestExecutor(emptyList(), networkAvailabilityProvider), + middlewarePipeline = WpApiMiddlewarePipeline(listOf(DebugMiddleware())), + appNotifier = EmptyAppNotifier() + ), + cache = cache.cache + ) +} + fun createWpApiClient( account: Account.SelfHostedSite, networkAvailabilityProvider: NetworkAvailabilityProvider = NetworkAvailabilityProvider { true } @@ -77,6 +105,22 @@ fun createWpApiClient( ) } +fun createWpApiClient( + account: Account.WpCom, + networkAvailabilityProvider: NetworkAvailabilityProvider = NetworkAvailabilityProvider { true } +): WpApiClient { + return WpApiClient( + apiUrlResolver = WpComDotOrgApiUrlResolver( + siteId = account.siteApiRoot, + baseUrl = WpComBaseUrl.Production + ), + authProvider = WpAuthenticationProvider.staticWithAuth( + WpAuthentication.Bearer(token = account.token) + ), + requestExecutor = WpRequestExecutor(emptyList(), networkAvailabilityProvider) + ) +} + fun createWpComApiClient( account: Account.WpCom, networkAvailabilityProvider: NetworkAvailabilityProvider = NetworkAvailabilityProvider { true } diff --git a/native/swift/Example/Example/WordPressAPI+Extensions.swift b/native/swift/Example/Example/WordPressAPI+Extensions.swift index c393a79bd..079045ab6 100644 --- a/native/swift/Example/Example/WordPressAPI+Extensions.swift +++ b/native/swift/Example/Example/WordPressAPI+Extensions.swift @@ -15,9 +15,13 @@ extension WordPressAPI { throw CocoaError(.xpcConnectionInvalid) } + let siteUrl = try ParsedUrl.parse(input: apiRootUrl.asURL().deletingLastPathComponent().absoluteString) return WordPressAPI( urlSession: .shared, - apiRootUrl: apiRootUrl, + siteInfo: .selfHosted( + siteUrl: siteUrl, + apiRoot: apiRootUrl + ), authentication: loginCredentials, middlewarePipeline: MiddlewarePipeline(middlewares: [ DebugMiddleware() diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index edb4f5f23..9d5a67842 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -33,6 +33,7 @@ public typealias UserRole = WordPressAPIInternal.UserRole // MARK: - Service Layer public typealias WpService = WordPressAPIInternal.WpService +public typealias SiteInfo = WordPressAPIInternal.SiteInfo public typealias AnyPostFilter = WordPressAPIInternal.AnyPostFilter public typealias WpApiCache = WordPressAPIInternal.WpApiCache diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 5ab3e0aa4..320cd7745 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -16,7 +16,7 @@ public final class WordPressAPI: Sendable { case unableToParseResponse } - private let apiUrlResolver: ApiUrlResolver + private let siteInfo: SiteInfo private let urlSession: URLSession let requestExecutor: SafeRequestExecutor @@ -26,13 +26,13 @@ public final class WordPressAPI: Sendable { public convenience init( urlSession: URLSession, notifyingDelegate: URLSessionTaskDelegate? = nil, - apiRootUrl: ParsedUrl, + siteInfo: SiteInfo, authentication: WpAuthentication, middlewarePipeline: MiddlewarePipeline = .default, appNotifier: WpAppNotifier? = nil ) { self.init( - apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: apiRootUrl), + siteInfo: siteInfo, authenticationProvider: .staticWithAuth(auth: authentication), executor: WpRequestExecutor(urlSession: urlSession, notifyingDelegate: notifyingDelegate), middlewarePipeline: middlewarePipeline, @@ -42,13 +42,13 @@ public final class WordPressAPI: Sendable { public convenience init( urlSession: URLSession, - apiRootUrl: ParsedUrl, + siteInfo: SiteInfo, authenticationProvider: WpAuthenticationProvider, middlewarePipeline: MiddlewarePipeline = .default, appNotifier: WpAppNotifier? = nil ) { self.init( - apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: apiRootUrl), + siteInfo: siteInfo, authenticationProvider: authenticationProvider, executor: WpRequestExecutor(urlSession: urlSession), middlewarePipeline: middlewarePipeline, @@ -59,13 +59,13 @@ public final class WordPressAPI: Sendable { public convenience init( urlSession: URLSession, notifyingDelegate: URLSessionTaskDelegate? = nil, - apiUrlResolver: ApiUrlResolver, + siteInfo: SiteInfo, authenticationProvider: WpAuthenticationProvider, middlewarePipeline: MiddlewarePipeline = .default, appNotifier: WpAppNotifier? = nil ) { self.init( - apiUrlResolver: apiUrlResolver, + siteInfo: siteInfo, authenticationProvider: authenticationProvider, executor: WpRequestExecutor(urlSession: urlSession, notifyingDelegate: notifyingDelegate), middlewarePipeline: middlewarePipeline, @@ -76,22 +76,23 @@ public final class WordPressAPI: Sendable { public convenience init( urlSession: URLSession, notifyingDelegate: URLSessionTaskDelegate? = nil, - siteUrl: String, + siteUrl: ParsedUrl, apiRootUrl: ParsedUrl, username: String, password: String, middlewarePipeline: MiddlewarePipeline = .default, appNotifier: WpAppNotifier? = nil ) { + let siteInfo = SiteInfo.selfHosted(siteUrl: siteUrl, apiRoot: apiRootUrl) let executor = WpRequestExecutor(urlSession: urlSession, notifyingDelegate: notifyingDelegate) let provider = CookiesNonceAuthenticationProvider.withSiteUrl( - url: siteUrl, + url: siteUrl.url(), username: username, password: password, requestExecutor: executor ) self.init( - apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: apiRootUrl), + siteInfo: siteInfo, authenticationProvider: .dynamic(dynamicAuthenticationProvider: provider), executor: executor, middlewarePipeline: middlewarePipeline, @@ -108,6 +109,10 @@ public final class WordPressAPI: Sendable { middlewarePipeline: MiddlewarePipeline = .default, appNotifier: WpAppNotifier? = nil ) { + let siteInfo = SiteInfo.selfHosted( + siteUrl: details.parsedSiteUrl, + apiRoot: details.apiRootUrl + ) let executor = WpRequestExecutor(urlSession: urlSession, notifyingDelegate: notifyingDelegate) let provider = CookiesNonceAuthenticationProvider( username: username, @@ -116,7 +121,7 @@ public final class WordPressAPI: Sendable { requestExecutor: executor ) self.init( - apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: details.apiRootUrl), + siteInfo: siteInfo, authenticationProvider: .dynamic(dynamicAuthenticationProvider: provider), executor: executor, middlewarePipeline: middlewarePipeline, @@ -126,14 +131,14 @@ public final class WordPressAPI: Sendable { init( urlSession: URLSession = .shared, - apiUrlResolver: ApiUrlResolver, + siteInfo: SiteInfo, authenticationProvider: WpAuthenticationProvider, executor: SafeRequestExecutor, middlewarePipeline: MiddlewarePipeline, appNotifier: WpAppNotifier? ) { self.urlSession = urlSession - self.apiUrlResolver = apiUrlResolver + self.siteInfo = siteInfo self.apiClientDelegate = WpApiClientDelegate( authProvider: authenticationProvider, requestExecutor: executor, @@ -141,24 +146,14 @@ public final class WordPressAPI: Sendable { appNotifier: appNotifier ?? EmptyAppNotifier() ) self.requestBuilder = UniffiWpApiClient( - apiUrlResolver: self.apiUrlResolver, + apiUrlResolver: siteInfo.apiUrlResolver(), delegate: self.apiClientDelegate ) self.requestExecutor = executor } - public func createSelfHostedService(cache: WordPressApiCache) throws -> WpService { - let apiURL = apiUrlResolver.resolve(namespace: "", endpointSegments: []).asURL() - return try WpService.selfHosted( - siteUrl: apiURL.deletingLastPathComponent().absoluteString, - apiRoot: apiURL.absoluteString, - delegate: apiClientDelegate, - cache: cache.cache - ) - } - - public func createWordPressComService(siteId: WpComSiteId, cache: WordPressApiCache) throws -> WpService { - try WpService.wordpressCom(siteId: siteId, delegate: apiClientDelegate, cache: cache.cache) + public func createService(cache: WordPressApiCache) throws -> WpService { + try WpService(siteInfo: siteInfo, delegate: apiClientDelegate, cache: cache.cache) } public var users: UsersRequestExecutor { diff --git a/native/swift/Sources/wordpress-api/WordPressLoginClient.swift b/native/swift/Sources/wordpress-api/WordPressLoginClient.swift index 6bf2bcd2d..0ce309dc8 100644 --- a/native/swift/Sources/wordpress-api/WordPressLoginClient.swift +++ b/native/swift/Sources/wordpress-api/WordPressLoginClient.swift @@ -72,7 +72,10 @@ public final class WordPressLoginClient: @unchecked Sendable { let nonceRetrieval = WpRestNonceRetrieval(details: details, requestExecutor: requestExecutor) let nonce = try await nonceRetrieval.getNonce(username: username, password: password) return WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: details.apiRootUrl), + siteInfo: .selfHosted( + siteUrl: details.parsedSiteUrl, + apiRoot: details.apiRootUrl + ), authenticationProvider: .staticWithAuth(auth: .nonce(nonce: nonce)), executor: requestExecutor, middlewarePipeline: middleware, diff --git a/native/swift/Tests/integration-tests/Helpers.swift b/native/swift/Tests/integration-tests/Helpers.swift index 7dbdcb0ac..10209f8ca 100644 --- a/native/swift/Tests/integration-tests/Helpers.swift +++ b/native/swift/Tests/integration-tests/Helpers.swift @@ -11,6 +11,11 @@ func restoreTestServer() async throws { } extension TestCredentials { + var siteURL: ParsedUrl { + // swiftlint:disable:next force_try + try! ParsedUrl.parse(input: siteUrl) + } + var apiRootURL: ParsedUrl { // swiftlint:disable:next force_try try! ParsedUrl.parse(input: siteUrl + "/wp-json") @@ -27,7 +32,10 @@ extension WordPressAPI { return WordPressAPI( urlSession: .init(configuration: .ephemeral), notifyingDelegate: notifyingDelegate, - apiRootUrl: credentials.apiRootURL, + siteInfo: .selfHosted( + siteUrl: credentials.siteURL, + apiRoot: credentials.apiRootURL + ), authentication: credentials.adminAuthentication ) } diff --git a/native/swift/Tests/integration-tests/NonceAuthenticationTests.swift b/native/swift/Tests/integration-tests/NonceAuthenticationTests.swift index 0849a5aa8..53c172723 100644 --- a/native/swift/Tests/integration-tests/NonceAuthenticationTests.swift +++ b/native/swift/Tests/integration-tests/NonceAuthenticationTests.swift @@ -66,7 +66,7 @@ struct NonceAuthenticationTests { let credentials = TestCredentials.instance() let api = WordPressAPI( urlSession: .init(configuration: .ephemeral), - siteUrl: credentials.siteUrl, + siteUrl: credentials.siteURL, apiRootUrl: credentials.apiRootURL, username: credentials.adminUsername, password: credentials.adminAccountPassword diff --git a/native/swift/Tests/wordpress-api/Endpoints/Users.swift b/native/swift/Tests/wordpress-api/Endpoints/Users.swift index 58d257d70..72124e217 100644 --- a/native/swift/Tests/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Tests/wordpress-api/Endpoints/Users.swift @@ -40,8 +40,9 @@ struct UsersTests { ]) let api = try WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver( - apiRootUrl: ParsedUrl.parse(input: "https://wordpress.org/wp-json") + siteInfo: .selfHosted( + siteUrl: ParsedUrl.parse(input: "https://wordpress.org"), + apiRoot: ParsedUrl.parse(input: "https://wordpress.org/wp-json") ), authenticationProvider: .none(), executor: stubs, diff --git a/native/swift/Tests/wordpress-api/HttpTests.swift b/native/swift/Tests/wordpress-api/HttpTests.swift index d4cc6dd72..76984a053 100644 --- a/native/swift/Tests/wordpress-api/HttpTests.swift +++ b/native/swift/Tests/wordpress-api/HttpTests.swift @@ -10,8 +10,9 @@ class HTTPErrorTests { let stubs = HTTPStubs(stubs: [], missingStub: .failure(URLError(.timedOut))) let api = try WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver( - apiRootUrl: ParsedUrl.parse(input: "https://wordpress.org/wp-json") + siteInfo: .selfHosted( + siteUrl: ParsedUrl.parse(input: "https://wordpress.org"), + apiRoot: ParsedUrl.parse(input: "https://wordpress.org/wp-json") ), authenticationProvider: .none(), executor: stubs, diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index 120cb6ae2..6ade3ddb4 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -43,8 +43,9 @@ struct WordPressAPITests { func testExample() async throws { let stubs = try createStubs() let api = try WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver( - apiRootUrl: ParsedUrl.parse(input: "https://wordpress.org/wp-json") + siteInfo: .selfHosted( + siteUrl: ParsedUrl.parse(input: "https://wordpress.org"), + apiRoot: ParsedUrl.parse(input: "https://wordpress.org/wp-json") ), authenticationProvider: .none(), executor: stubs, @@ -60,8 +61,9 @@ struct WordPressAPITests { let stubs = try createStubs() let counter = CounterMiddleware() let api = try WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver( - apiRootUrl: ParsedUrl.parse(input: "https://wordpress.org/wp-json") + siteInfo: .selfHosted( + siteUrl: ParsedUrl.parse(input: "https://wordpress.org"), + apiRoot: ParsedUrl.parse(input: "https://wordpress.org/wp-json") ), authenticationProvider: .none(), executor: stubs, @@ -75,8 +77,9 @@ struct WordPressAPITests { @Test func testRoot() async throws { let api = try WordPressAPI( - apiUrlResolver: WpOrgSiteApiUrlResolver( - apiRootUrl: ParsedUrl.parse(input: "https://vanilla.wpmt.co/wp-json") + siteInfo: .selfHosted( + siteUrl: ParsedUrl.parse(input: "https://vanilla.wpmt.co"), + apiRoot: ParsedUrl.parse(input: "https://vanilla.wpmt.co/wp-json") ), authenticationProvider: .none(), executor: WpRequestExecutor(urlSession: .shared), diff --git a/wp_com_e2e/src/context.rs b/wp_com_e2e/src/context.rs index 18e95b024..6d1d64ea7 100644 --- a/wp_com_e2e/src/context.rs +++ b/wp_com_e2e/src/context.rs @@ -8,6 +8,7 @@ use wp_api::{ wp_com::{WpComSiteId, client::WpComApiClient}, }; use wp_mobile::service::WpService; +use wp_mobile::service::sites::SiteInfo; use wp_mobile_cache::WpApiCache; pub struct TestContext { @@ -52,8 +53,14 @@ impl TestContext { .perform_migrations() .expect("Migrations should succeed"); - let service = WpService::new_wordpress_com(WpComSiteId(site_id), delegate, cache.clone()) - .expect("Failed to create WpService"); + let service = WpService::new( + SiteInfo::WordPressCom { + site_id: WpComSiteId(site_id), + }, + delegate, + cache.clone(), + ) + .expect("Failed to create WpService"); Self { client, diff --git a/wp_mobile/src/service/mod.rs b/wp_mobile/src/service/mod.rs index 8564dd861..d4205d683 100644 --- a/wp_mobile/src/service/mod.rs +++ b/wp_mobile/src/service/mod.rs @@ -1,39 +1,9 @@ +use crate::service::sites::SiteInfo; use crate::service::{post_types::PostTypeService, posts::PostService, sites::SiteService}; use std::sync::Arc; -use url::Url; -use wp_api::prelude::{ - ApiUrlResolver, ParsedUrl, WpApiClient, WpApiClientDelegate, WpOrgSiteApiUrlResolver, -}; -use wp_api::wp_com::{WpComBaseUrl, WpComSiteId, endpoint::WpComDotOrgApiUrlResolver}; +use wp_api::prelude::{ApiUrlResolver, WpApiClient, WpApiClientDelegate}; use wp_mobile_cache::{WpApiCache, db_types::db_site::DbSite}; -const WPCOM_API_HOST: &str = "public-api.wordpress.com"; - -/// Returns the canonical API root URL for a WordPress.com site. -/// -/// This URL can be stored as the `api_root` for a self-hosted site account. -/// When passed to `WpService.selfHosted()`, it will automatically use -/// WordPress.com URL rewriting. -#[uniffi::export] -pub fn wordpress_com_site_api_root(site_id: u64) -> String { - format!("https://{WPCOM_API_HOST}/wp/v2/sites/{site_id}") -} - -/// Extracts the WordPress.com site ID from an API root URL, if it matches -/// the `https://public-api.wordpress.com/wp/v2/sites/{site_id}` pattern. -fn extract_wpcom_site_id(api_root: &str) -> Option { - let url = Url::parse(api_root).ok()?; - if url.host_str()? != WPCOM_API_HOST { - return None; - } - let segments: Vec<&str> = url.path_segments()?.collect(); - if segments.len() >= 4 && segments[0] == "wp" && segments[1] == "v2" && segments[2] == "sites" { - segments[3].parse::().ok().map(WpComSiteId) - } else { - None - } -} - pub mod entity_state_service; pub mod metadata; pub mod mock_post_service; @@ -101,66 +71,26 @@ impl WpService { #[uniffi::export] impl WpService { - /// Create a new service for a self-hosted WordPress site - /// - /// This will look up the site in the cache or create it if it doesn't exist. - /// - /// If the `api_root` is a WordPress.com API root URL (as produced by - /// `wordpress_com_site_api_root()`), this constructor will automatically - /// use WordPress.com URL rewriting and store the site as a WordPress.com site. - /// - /// # Arguments - /// * `site_url` - The base site URL (e.g., "https://example.com") - /// * `api_root` - The API root URL (e.g., "https://example.com/wp-json") - /// * `delegate` - API client delegate with auth provider, request executor, etc. - /// * `cache` - The cache instance for database operations - #[uniffi::constructor(name = "selfHosted")] - pub fn new_self_hosted( - site_url: String, - api_root: String, + /// Create a new service for a WordPress site. + #[uniffi::constructor] + pub fn new( + site_info: SiteInfo, delegate: WpApiClientDelegate, cache: Arc, ) -> Result { - if let Some(site_id) = extract_wpcom_site_id(&api_root) { - return Self::new_wordpress_com(site_id, delegate, cache); - } - - let api_root_url = ParsedUrl::parse(&api_root).map_err(|e| WpServiceError::InvalidUrl { - err_message: e.to_string(), - })?; - let api_url_resolver: Arc = - Arc::new(WpOrgSiteApiUrlResolver::new(Arc::new(api_root_url))); - let db_site = - SiteService::get_or_create_self_hosted_site(cache.clone(), site_url, api_root)?; - let sites = Arc::new(SiteService::new(cache.clone(), db_site)); - Ok(Self::build_services( - api_url_resolver, - delegate, - cache, - db_site, - sites, - )) - } - - /// Create a new service for a WordPress.com site - /// - /// This will look up the site in the cache or create it if it doesn't exist. - /// - /// # Arguments - /// * `site_id` - The WordPress.com site ID - /// * `delegate` - API client delegate with auth provider, request executor, etc. - /// * `cache` - The cache instance for database operations - #[uniffi::constructor(name = "wordpressCom")] - pub fn new_wordpress_com( - site_id: WpComSiteId, - delegate: WpApiClientDelegate, - cache: Arc, - ) -> Result { - let api_url_resolver: Arc = Arc::new(WpComDotOrgApiUrlResolver::new( - site_id.to_string(), - WpComBaseUrl::default(), - )); - let db_site = SiteService::get_or_create_wordpress_com_site(cache.clone(), site_id)?; + let api_url_resolver = site_info.api_url_resolver(); + let db_site = match &site_info { + SiteInfo::SelfHosted { site_url, api_root } => { + SiteService::get_or_create_self_hosted_site( + cache.clone(), + site_url.url(), + api_root.url(), + )? + } + SiteInfo::WordPressCom { site_id } => { + SiteService::get_or_create_wordpress_com_site(cache.clone(), *site_id)? + } + }; let sites = Arc::new(SiteService::new(cache.clone(), db_site)); Ok(Self::build_services( api_url_resolver, @@ -186,46 +116,3 @@ impl WpService { self.sites.clone() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_wordpress_com_site_api_root() { - assert_eq!( - wordpress_com_site_api_root(12345), - "https://public-api.wordpress.com/wp/v2/sites/12345" - ); - } - - #[test] - fn test_extract_wpcom_site_id_valid() { - let url = "https://public-api.wordpress.com/wp/v2/sites/12345"; - assert_eq!(extract_wpcom_site_id(url), Some(WpComSiteId(12345))); - } - - #[test] - fn test_extract_wpcom_site_id_roundtrip() { - let url = wordpress_com_site_api_root(67890); - assert_eq!(extract_wpcom_site_id(&url), Some(WpComSiteId(67890))); - } - - #[test] - fn test_extract_wpcom_site_id_self_hosted() { - assert_eq!(extract_wpcom_site_id("https://example.com/wp-json"), None); - } - - #[test] - fn test_extract_wpcom_site_id_wrong_path() { - assert_eq!( - extract_wpcom_site_id("https://public-api.wordpress.com/rest/v1.1/sites/12345"), - None - ); - } - - #[test] - fn test_extract_wpcom_site_id_invalid_url() { - assert_eq!(extract_wpcom_site_id("not a url"), None); - } -} diff --git a/wp_mobile/src/service/sites.rs b/wp_mobile/src/service/sites.rs index 79e354f2f..7d22d4740 100644 --- a/wp_mobile/src/service/sites.rs +++ b/wp_mobile/src/service/sites.rs @@ -1,6 +1,8 @@ use crate::service::WpServiceError; use std::sync::Arc; +use wp_api::prelude::{ApiUrlResolver, ParsedUrl, WpOrgSiteApiUrlResolver}; use wp_api::wp_com::WpComSiteId; +use wp_api::wp_com::{WpComBaseUrl, endpoint::WpComDotOrgApiUrlResolver}; use wp_mobile_cache::{ DbTable, SqliteDbError, WpApiCache, db_types::db_site::{DbSite, DbSiteType}, @@ -13,8 +15,28 @@ use wp_mobile_cache::{ /// Information about a site #[derive(Debug, Clone, uniffi::Enum)] pub enum SiteInfo { - SelfHosted { site_url: String, api_root: String }, - WordPressCom { site_id: WpComSiteId }, + SelfHosted { + site_url: Arc, + api_root: Arc, + }, + WordPressCom { + site_id: WpComSiteId, + }, +} + +#[uniffi::export] +impl SiteInfo { + pub fn api_url_resolver(&self) -> Arc { + match self { + Self::SelfHosted { api_root, .. } => { + Arc::new(WpOrgSiteApiUrlResolver::new(Arc::clone(api_root))) + } + Self::WordPressCom { site_id } => Arc::new(WpComDotOrgApiUrlResolver::new( + site_id.to_string(), + WpComBaseUrl::default(), + )), + } + } } /// Service for site-related operations @@ -32,7 +54,7 @@ impl SiteService { /// Get or create a DbSite for a self-hosted WordPress site /// /// Looks up an existing site by URL, or creates it if not found. - /// This is an internal helper called by WpService::new_self_hosted(). + /// This is an internal helper called by WpService::new(). /// /// # Arguments /// * `cache` - The cache instance to use for database operations @@ -70,7 +92,7 @@ impl SiteService { /// Get or create a DbSite for a WordPress.com site /// /// Looks up an existing site by site_id, or creates it if not found. - /// This is an internal helper called by WpService::new_wordpress_com(). + /// This is an internal helper called by WpService::new(). pub(crate) fn get_or_create_wordpress_com_site( cache: Arc, site_id: WpComSiteId, @@ -119,9 +141,21 @@ impl SiteService { full_entity .ok_or(WpServiceError::SiteNotFound) - .map(|entity| SiteInfo::SelfHosted { - site_url: entity.data.url, - api_root: entity.data.api_root, + .and_then(|entity| { + let site_url = ParsedUrl::parse(&entity.data.url).map_err(|e| { + WpServiceError::InvalidUrl { + err_message: e.to_string(), + } + })?; + let api_root = ParsedUrl::parse(&entity.data.api_root).map_err(|e| { + WpServiceError::InvalidUrl { + err_message: e.to_string(), + } + })?; + Ok(SiteInfo::SelfHosted { + site_url: Arc::new(site_url), + api_root: Arc::new(api_root), + }) }) } DbSiteType::WordPressCom => { diff --git a/wp_mobile_integration_tests/src/lib.rs b/wp_mobile_integration_tests/src/lib.rs index 4c0455daa..8e7c8b747 100644 --- a/wp_mobile_integration_tests/src/lib.rs +++ b/wp_mobile_integration_tests/src/lib.rs @@ -7,6 +7,7 @@ use url::Url; use wp_api::{EmptyAppNotifier, prelude::*, reqwest_request_executor::ReqwestRequestExecutor}; pub use wp_api_integration_tests::backend::{Backend, RestoreServer}; use wp_mobile::service::WpService; +use wp_mobile::service::sites::SiteInfo; use wp_mobile_cache::WpApiCache; pub fn test_site_url() -> ParsedUrl { @@ -46,9 +47,13 @@ pub fn create_test_context() -> TestContext { .perform_migrations() .expect("Migrations should succeed"); - let service = WpService::new_self_hosted( - TestCredentials::instance().site_url.to_string(), - test_site_url().to_string(), + let site_url = ParsedUrl::parse(TestCredentials::instance().site_url) + .expect("Site url is generated by our tooling"); + let service = WpService::new( + SiteInfo::SelfHosted { + site_url: Arc::new(site_url), + api_root: Arc::new(test_site_url()), + }, api_client_delegate(), cache.clone(), ) @@ -91,9 +96,13 @@ pub fn create_test_context_with_site( .perform_migrations() .expect("Migrations should succeed"); - let service = WpService::new_self_hosted( - site_url.to_string(), - api_root, + let parsed_site_url = ParsedUrl::parse(site_url).expect("Invalid site URL"); + let parsed_api_root = ParsedUrl::parse(&api_root).expect("Invalid API root URL"); + let service = WpService::new( + SiteInfo::SelfHosted { + site_url: Arc::new(parsed_site_url), + api_root: Arc::new(parsed_api_root), + }, delegate.clone(), cache.clone(), )