Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rs.wordpress.api.android

import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.junit.Assert
import org.junit.Test
import rs.wordpress.api.kotlin.WpApiClient
Expand All @@ -17,7 +18,7 @@ class UsersEndpointAndroidTest {
username = BuildConfig.TEST_ADMIN_USERNAME,
password = BuildConfig.TEST_ADMIN_PASSWORD
)
private val client = WpApiClient(siteUrl, authentication)
private val client = WpApiClient(siteUrl.toHttpUrlOrNull()!!, authentication)

@Test
fun testUserListRequest() = runTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rs.wordpress.api.kotlin

import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.junit.jupiter.api.Test
import uniffi.wp_api.SparseUserField
import uniffi.wp_api.UserListParams
Expand All @@ -17,7 +18,7 @@ class UsersEndpointTest {
private val authentication = wpAuthenticationFromUsernameAndPassword(
username = testCredentials.adminUsername, password = testCredentials.adminPassword
)
private val client = WpApiClient(siteUrl, authentication)
private val client = WpApiClient(siteUrl.toHttpUrlOrNull()!!, authentication)

@Test
fun testUserListRequest() = runTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ package rs.wordpress.api.kotlin
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import uniffi.wp_api.ApiBaseUrl
import uniffi.wp_api.RequestExecutor
import uniffi.wp_api.WpApiException
import uniffi.wp_api.WpAuthentication
import uniffi.wp_api.WpRequestBuilder
import uniffi.wp_api.WpRestErrorWrapper
import uniffi.wp_api.apiBaseUrlFromStr

class WpApiClient
@Throws(WpApiException::class)
constructor(
siteUrl: String,
siteUrl: HttpUrl,
authentication: WpAuthentication,
private val requestExecutor: RequestExecutor = WpRequestExecutor(),
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
// Don't expose `WpRequestBuilder` directly so we can control how it's used
private val requestBuilder by lazy {
WpRequestBuilder(siteUrl, authentication, requestExecutor)
WpRequestBuilder(siteUrl.toApiBaseUrl(), authentication, requestExecutor)
}

// Provides the _only_ way to execute requests using our Kotlin wrapper.
Expand All @@ -46,3 +48,10 @@ constructor(
}
}
}

fun HttpUrl.toApiBaseUrl(): ApiBaseUrl {
val absoluteUrl = this.toString()
// This call will never return a null value, because absoluteUrl is always
// a valid url string.
return apiBaseUrlFromStr(absoluteUrl)!!
}
2 changes: 1 addition & 1 deletion native/swift/Example/Example/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ struct LoginView: View {
return nil
}

let client = try WordPressAPI(
let client = WordPressAPI(
urlSession: .shared,
baseUrl: apiRoot,
authenticationStategy: .none
Expand Down
4 changes: 2 additions & 2 deletions native/swift/Example/Example/UserListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension UserWithViewContext: Identifiable {}

// swiftlint:disable force_try
var api: WordPressAPI {
try! WordPressAPI(
WordPressAPI(
urlSession: .shared,
baseUrl: URL(string: loginManager.getDefaultSiteUrl()!)!,
authenticationStategy: try! loginManager.getLoginCredentials()!
Expand All @@ -34,7 +34,7 @@ extension UserWithViewContext: Identifiable {}

self.fetchUsersTask = Task { @MainActor in
do {
users = try await api.users.listWithViewContext(params: nil)
users = try await api.users.listWithViewContext(params: .init())
} catch let error {
shouldPresentAlert = true
self.error = MyError(underlyingError: error)
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Sources/wordpress-api/Login/API+Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import FoundationNetworking
public extension WordPressAPI {
static func findRestApiEndpointRoot(forSiteUrl url: URL, using session: URLSession) async throws -> URL? {
let request = WpNetworkRequest(method: .head, url: url, headerMap: [:])
let ephemeralClient = try WordPressAPI(urlSession: session, baseUrl: url, authenticationStategy: .none)
let ephemeralClient = WordPressAPI(urlSession: session, baseUrl: url, authenticationStategy: .none)
let response = try await ephemeralClient.perform(request: request)

return getLinkHeader(response: response, name: "https://api.w.org/")?.asUrl()
Expand Down
16 changes: 11 additions & 5 deletions native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public struct WordPressAPI {
private let urlSession: URLSession
package let requestBuilder: WpRequestBuilderProtocol

public init(urlSession: URLSession, baseUrl: URL, authenticationStategy: WpAuthentication) throws {
try self.init(
public init(urlSession: URLSession, baseUrl: URL, authenticationStategy: WpAuthentication) {
self.init(
urlSession: urlSession,
baseUrl: baseUrl,
authenticationStategy: authenticationStategy,
Expand All @@ -30,10 +30,10 @@ public struct WordPressAPI {
baseUrl: URL,
authenticationStategy: WpAuthentication,
executor: SafeRequestExecutor
) throws {
) {
self.urlSession = urlSession
self.requestBuilder = try WpRequestBuilder(
siteUrl: baseUrl.absoluteString,
self.requestBuilder = WpRequestBuilder(
siteUrl: baseUrl.asApiBaseUrl(),
authentication: authenticationStategy,
requestExecutor: executor
)
Expand Down Expand Up @@ -223,4 +223,10 @@ extension URL {
func asRestUrl() -> WpRestApiUrl {
WpRestApiUrl(stringValue: self.absoluteString)
}

func asApiBaseUrl() -> ApiBaseUrl {
// This call will never return a nil value, because `absoluteString` is
// always a valid url string.
apiBaseUrlFromStr(str: absoluteString)!
}
}
31 changes: 31 additions & 0 deletions native/swift/Tests/wordpress-api/URLTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import XCTest

@testable import WordPressAPI

#if canImport(WordPressAPIInternal)
@testable import WordPressAPIInternal
#endif

class URLTests: XCTestCase {

func testParseApiBaseUrl() throws {
let urls = try [
"http://example.com/path?query=value#fragment",
"http://example.com:8080/path",
"http://sub.sub2.example.com/path",
"http://192.168.1.1/path",
"http://example.com/a/very/long/path/that/goes/on/forever",
"http://example.com/path%20with%20spaces",
"http://example.com/~user!$&'()*+,;=:@/path",
"http://user:password@example.com/path",
"http://example.com",
"http://example.com./path"
]
.map { try XCTUnwrap(URL(string: $0)) }
for url in urls {
XCTAssertTrue(apiBaseUrlFromStr(str: url.absoluteString) != nil, "Invalid URL: \(url)")
}
}

}
4 changes: 2 additions & 2 deletions native/swift/Tests/wordpress-api/WordPressAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class WordPressAPITests: XCTestCase {
let stubs = HTTPStubs()
stubs.stub(path: "/wp-json/wp/v2/users/1", with: .json(response))

let api = try WordPressAPI(
let api = WordPressAPI(
urlSession: .shared,
baseUrl: URL(string: "https://wordpress.org")!,
authenticationStategy: .none,
Expand All @@ -56,7 +56,7 @@ final class WordPressAPITests: XCTestCase {
let stubs = HTTPStubs()
stubs.missingStub = .failure(URLError(.timedOut))

let api = try WordPressAPI(
let api = WordPressAPI(
urlSession: .shared,
baseUrl: URL(string: "https://wordpress.org")!,
authenticationStategy: .none,
Expand Down
14 changes: 5 additions & 9 deletions wp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,21 @@ pub struct WpRequestBuilder {
impl WpRequestBuilder {
#[uniffi::constructor]
pub fn new(
site_url: String,
site_url: ApiBaseUrl,
authentication: WpAuthentication,
request_executor: Arc<dyn RequestExecutor>,
) -> Result<Self, WpApiError> {
let api_base_url: Arc<ApiBaseUrl> = ApiBaseUrl::new(site_url.as_str())
.map_err(|err| WpApiError::SiteUrlParsingError {
reason: err.to_string(),
})?
.into();
) -> Self {
let api_base_url: Arc<ApiBaseUrl> = site_url.into();
let request_builder = Arc::new(request::RequestBuilder::new(
request_executor,
authentication.clone(),
));

Ok(Self {
Self {
users: UsersRequestBuilder::new(api_base_url.clone(), request_builder.clone()).into(),
plugins: PluginsRequestBuilder::new(api_base_url.clone(), request_builder.clone())
.into(),
})
}
}

pub fn users(&self) -> Arc<UsersRequestBuilder> {
Expand Down
54 changes: 42 additions & 12 deletions wp_api/src/request/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,48 @@ impl From<ApiEndpointUrl> for WpEndpointUrl {
}
}

#[derive(Debug, Clone)]
pub(crate) struct ApiBaseUrl {
url: Url,
#[derive(Debug, Clone, uniffi::Record)]
pub struct ApiBaseUrl {
standard_absolute_url: String,
}

impl ApiBaseUrl {
pub fn new(site_base_url: &str) -> Result<Self, url::ParseError> {
Url::parse(site_base_url).map(|parsed_url| {
impl TryFrom<&str> for ApiBaseUrl {
type Error = url::ParseError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
Url::parse(value).map(|parsed_url| {
let url = parsed_url
.extend(WP_JSON_PATH_SEGMENTS)
.expect("ApiBaseUrl is already parsed, so this can't result in an error");
Self { url }
Self {
standard_absolute_url: url.into(),
}
})
}
}

impl From<Url> for ApiBaseUrl {
fn from(url: Url) -> Self {
Self {
standard_absolute_url: url.into(),
}
}
}

impl ApiBaseUrl {
pub fn new(site_base_url: &str) -> Result<Self, url::ParseError> {
site_base_url.try_into()
}

fn url(&self) -> Url {
self.standard_absolute_url
.as_str()
.try_into()
.expect("standard_absolute_url was assigned from a Url instance, so this can't result in an error")
}

fn by_appending(&self, segment: &str) -> Url {
self.url
self.url()
.clone()
.append(segment)
.expect("ApiBaseUrl is already parsed, so this can't result in an error")
Expand All @@ -69,17 +94,22 @@ impl ApiBaseUrl {
I: IntoIterator,
I::Item: AsRef<str>,
{
self.url
self.url()
.clone()
.extend(segments)
.expect("ApiBaseUrl is already parsed, so this can't result in an error")
}

fn as_str(&self) -> &str {
self.url.as_str()
self.standard_absolute_url.as_str()
}
}

#[uniffi::export]
fn api_base_url_from_str(str: &str) -> Option<ApiBaseUrl> {
ApiBaseUrl::try_from(str).ok()
}

trait UrlExtension {
fn append(self, segment: &str) -> Result<Url, ()>;
fn extend<I>(self, segments: I) -> Result<Url, ()>
Expand Down Expand Up @@ -154,7 +184,7 @@ mod tests {
)]
test_base_url: &str,
) {
let api_base_url = ApiBaseUrl::new(test_base_url).unwrap();
let api_base_url: ApiBaseUrl = test_base_url.try_into().unwrap();
let expected_wp_json_url = wp_json_endpoint(test_base_url);
assert_eq!(expected_wp_json_url, api_base_url.as_str());
assert_eq!(
Expand All @@ -177,7 +207,7 @@ mod tests {

#[fixture]
pub fn fixture_api_base_url() -> Arc<ApiBaseUrl> {
ApiBaseUrl::new("https://example.com").unwrap().into()
ApiBaseUrl::try_from("https://example.com").unwrap().into()
}

pub fn validate_endpoint(endpoint_url: ApiEndpointUrl, path: &str) {
Expand Down
10 changes: 5 additions & 5 deletions wp_api/tests/integration_test_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use futures::Future;
use http::HeaderMap;
use std::{fs::read_to_string, process::Command, sync::Arc};
use wp_api::{
request::{RequestExecutor, RequestMethod, WpNetworkRequest, WpNetworkResponse},
request::{
endpoint::ApiBaseUrl, RequestExecutor, RequestMethod, WpNetworkRequest, WpNetworkResponse,
},
users::UserId,
RequestExecutionError, WpApiError, WpAuthentication, WpRequestBuilder, WpRestError,
WpRestErrorCode, WpRestErrorWrapper,
Expand All @@ -29,7 +31,6 @@ pub fn request_builder() -> WpRequestBuilder {
authentication,
Arc::new(AsyncWpNetworking::default()),
)
.expect("Site url is generated by our tooling")
}

pub fn request_builder_as_subscriber() -> WpRequestBuilder {
Expand All @@ -43,7 +44,6 @@ pub fn request_builder_as_subscriber() -> WpRequestBuilder {
authentication,
Arc::new(AsyncWpNetworking::default()),
)
.expect("Site url is generated by our tooling")
}

pub trait AssertWpError<T: std::fmt::Debug> {
Expand Down Expand Up @@ -93,7 +93,7 @@ impl<T: std::fmt::Debug> AssertWpError<T> for Result<T, WpApiError> {

#[derive(Debug)]
pub struct TestCredentials {
pub site_url: String,
pub site_url: ApiBaseUrl,
pub admin_username: String,
pub admin_password: String,
pub subscriber_username: String,
Expand All @@ -104,7 +104,7 @@ pub fn read_test_credentials_from_file() -> TestCredentials {
let file_contents = read_to_string("../test_credentials").unwrap();
let lines: Vec<&str> = file_contents.lines().collect();
TestCredentials {
site_url: lines[0].to_string(),
site_url: lines[0].try_into().unwrap(),
admin_username: lines[1].to_string(),
admin_password: lines[2].to_string(),
subscriber_username: lines[3].to_string(),
Expand Down
1 change: 0 additions & 1 deletion wp_api/tests/test_users_err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ async fn retrieve_user_err_unauthorized() {
WpAuthentication::None,
Arc::new(AsyncWpNetworking::default()),
)
.expect("Site url is generated by our tooling")
.users()
.retrieve_me_with_edit_context()
.await
Expand Down