Skip to content
Merged
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
2 changes: 2 additions & 0 deletions wp_api/src/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ pub enum WpErrorCode {
CannotViewPlugin,
#[serde(rename = "rest_cannot_view_plugins")]
CannotViewPlugins,
#[serde(rename = "empty_content")]
EmptyContent,
#[serde(rename = "rest_forbidden_context")]
ForbiddenContext,
#[serde(rename = "rest_forbidden_orderby")]
Expand Down
139 changes: 121 additions & 18 deletions wp_api/src/posts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,84 @@ pub struct PostDeleteResponse {
pub previous: PostWithEditContext,
}

#[derive(Debug, Default, Serialize, uniffi::Record)]
pub struct PostCreateParams {
// The date the post was published, in the site's timezone.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
// The date the post was published, as GMT.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub date_gmt: Option<String>,
// An alphanumeric identifier for the post unique to its type.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub slug: Option<String>,
// A named status for the post.
// One of: publish, future, draft, pending, private
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<PostStatus>,
// A password to protect access to the content and excerpt.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
// The title for the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
// The content for the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
// The ID for the author of the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<UserId>,
// The excerpt for the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>,
// The ID of the featured media for the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub featured_media: Option<MediaId>,
// Whether or not comments are open on the post.
// One of: open, closed
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub comment_status: Option<PostCommentStatus>,
// Whether or not the post can be pinged.
// One of: open, closed
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ping_status: Option<PostPingStatus>,
// The format for the post.
// One of: standard, aside, chat, gallery, link, image, quote, status, video, audio
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<PostFormat>,
// Meta fields.
pub meta: Option<String>,
// Whether or not the post should be treated as sticky.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub sticky: Option<bool>,
// The theme file to use to display the post.
#[uniffi(default = None)]
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
// The terms assigned to the post in the category taxonomy.
#[uniffi(default = [])]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<CategoryId>,
// The terms assigned to the post in the post_tag taxonomy.
#[uniffi(default = [])]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<TagId>,
}

impl_as_query_value_for_new_type!(PostId);
uniffi::custom_newtype!(PostId, i32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -221,6 +299,11 @@ uniffi::custom_newtype!(CategoryId, i32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CategoryId(pub i32);

impl_as_query_value_for_new_type!(MediaId);
uniffi::custom_newtype!(MediaId, i32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaId(pub i32);

impl std::fmt::Display for PostId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
Expand All @@ -236,7 +319,8 @@ pub struct SparsePost {
#[WpContext(edit, view)]
pub date_gmt: Option<String>,
#[WpContext(edit, view)]
pub guid: Option<PostGuid>,
#[WpContextualField]
pub guid: Option<SparsePostGuid>,
#[WpContext(edit, embed, view)]
pub link: Option<String>,
#[WpContext(edit, view)]
Expand All @@ -257,13 +341,16 @@ pub struct SparsePost {
#[WpContext(edit)]
pub generated_slug: Option<String>,
#[WpContext(edit, embed, view)]
pub title: Option<PostTitle>,
#[WpContextualField]
pub title: Option<SparsePostTitle>,
#[WpContext(edit, view)]
pub content: Option<PostContent>,
#[WpContextualField]
pub content: Option<SparsePostContent>,
#[WpContext(edit, embed, view)]
pub author: Option<UserId>,
#[WpContext(edit, embed, view)]
pub excerpt: Option<PostExcerpt>,
#[WpContextualField]
pub excerpt: Option<SparsePostExcerpt>,
#[WpContext(edit, embed, view)]
pub featured_media: Option<i64>,
#[WpContext(edit, view)]
Expand All @@ -284,26 +371,42 @@ pub struct SparsePost {
pub tags: Option<Vec<TagId>>,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
pub struct PostGuid {
pub rendered: String,
#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparsePostGuid {
#[WpContext(edit)]
pub raw: Option<String>,
#[WpContext(edit, view)]
pub rendered: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
pub struct PostTitle {
pub rendered: String,
#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparsePostTitle {
#[WpContext(edit)]
pub raw: Option<String>,
#[WpContext(edit, embed, view)]
pub rendered: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
pub struct PostContent {
pub rendered: String,
pub protected: bool,
#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparsePostContent {
#[WpContext(edit)]
pub raw: Option<String>,
#[WpContext(edit, view)]
pub rendered: Option<String>,
#[WpContext(edit, view)]
pub protected: Option<bool>,
#[WpContext(edit)]
pub block_version: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
pub struct PostExcerpt {
pub rendered: String,
pub protected: bool,
#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparsePostExcerpt {
#[WpContext(edit)]
pub raw: Option<String>,
#[WpContext(edit, embed, view)]
pub rendered: Option<String>,
#[WpContext(edit, embed, view)]
pub protected: Option<bool>,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
Expand Down
7 changes: 7 additions & 0 deletions wp_api/src/request/endpoint/posts_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ enum PostsRequest {
List,
#[contextual_get(url = "/posts/<post_id>", params = &crate::posts::PostRetrieveParams, output = crate::posts::SparsePost, filter_by = crate::posts::SparsePostField)]
Retrieve,
#[post(url = "/posts", params = &crate::posts::PostCreateParams, output = crate::posts::PostWithEditContext)]
Create,
#[delete(url = "/posts/<post_id>", output = crate::posts::PostDeleteResponse)]
Delete,
#[delete(url = "/posts/<post_id>", output = crate::posts::PostWithEditContext)]
Expand Down Expand Up @@ -51,6 +53,11 @@ mod tests {
use rstest::*;
use std::sync::Arc;

#[rstest]
fn create_post(endpoint: PostsRequestEndpoint) {
validate_wp_v2_endpoint(endpoint.create(), "/posts");
}

#[rstest]
fn delete_post(endpoint: PostsRequestEndpoint) {
validate_wp_v2_endpoint(endpoint.delete(&PostId(54)), "/posts/54?force=true");
Expand Down
8 changes: 7 additions & 1 deletion wp_api_integration_tests/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use serde::{de::DeserializeOwned, Serialize};
use wp_api::users::UserId;
use wp_api::{posts::PostId, users::UserId};
use wp_cli::{WpCliPost, WpCliSiteSettings, WpCliUser, WpCliUserMeta};

const BACKEND_ADDRESS: &str = "http://127.0.0.1:4000";
const BACKEND_PATH_RESTORE: &str = "/restore";
const BACKEND_PATH_SITE_SETTINGS: &str = "/wp-cli/site-settings";
const BACKEND_PATH_POST: &str = "/wp-cli/post";
const BACKEND_PATH_POSTS: &str = "/wp-cli/posts";
const BACKEND_PATH_USER: &str = "/wp-cli/user";
const BACKEND_PATH_USERS: &str = "/wp-cli/users";
Expand All @@ -21,6 +22,11 @@ impl Backend {
pub async fn site_settings() -> Result<WpCliSiteSettings, reqwest::Error> {
Self::get(BACKEND_PATH_SITE_SETTINGS).await
}
pub async fn post(post_id: &PostId) -> WpCliPost {
Self::get(format!("{}?post_id={}", BACKEND_PATH_POST, post_id))
.await
.expect("Failed to parse fetched post from wp_cli")
}
pub async fn posts(post_status: Option<&str>) -> Vec<WpCliPost> {
let url = if let Some(post_status) = post_status {
format!("{}?post_status={}", BACKEND_PATH_POSTS, post_status)
Expand Down
17 changes: 15 additions & 2 deletions wp_api_integration_tests/tests/test_posts_err.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
use serial_test::parallel;
use wp_api::{posts::PostRetrieveParams, WpErrorCode};
use wp_api::{
posts::{PostCreateParams, PostRetrieveParams},
WpErrorCode,
};
use wp_api_integration_tests::{api_client, AssertWpError, PASSWORD_PROTECTED_POST_ID};

#[tokio::test]
#[parallel]
async fn retrieve_password_protected_err_() {
async fn create_post_err() {
api_client()
.posts()
.create(&PostCreateParams::default())
.await
.assert_wp_error(WpErrorCode::EmptyContent)
}

#[tokio::test]
#[parallel]
async fn retrieve_password_protected_post_err_wrong_password() {
api_client()
.posts()
.retrieve_with_view_context(
Expand Down
84 changes: 83 additions & 1 deletion wp_api_integration_tests/tests/test_posts_mut.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,81 @@
use serial_test::serial;
use wp_api::posts::{PostCreateParams, PostWithEditContext};
use wp_api_integration_tests::{
api_client,
backend::{Backend, RestoreServer},
FIRST_POST_ID,
AssertResponse, FIRST_POST_ID,
};
use wp_cli::WpCliPost;

#[tokio::test]
#[serial]
async fn create_post_with_just_title() {
test_create_post(
&PostCreateParams {
title: Some("foo".to_string()),
..Default::default()
},
|created_post, post_from_wp_cli| {
assert_eq!(created_post.title.raw, "foo");
assert_eq!(post_from_wp_cli.title, "foo");
},
)
.await;
}

#[tokio::test]
#[serial]
async fn create_post_with_just_content() {
test_create_post(
&PostCreateParams {
content: Some("foo".to_string()),
..Default::default()
},
|created_post, post_from_wp_cli| {
assert_eq!(created_post.content.raw, "foo");
assert_eq!(post_from_wp_cli.content, "foo");
},
)
.await;
}

#[tokio::test]
#[serial]
async fn create_post_with_just_excerpt() {
test_create_post(
&PostCreateParams {
excerpt: Some("foo".to_string()),
..Default::default()
},
|created_post, post_from_wp_cli| {
assert_eq!(created_post.excerpt.raw, "foo");
assert_eq!(post_from_wp_cli.excerpt, "foo");
},
)
.await;
}

#[tokio::test]
#[serial]
async fn create_post_with_title_content_and_excerpt() {
test_create_post(
&PostCreateParams {
title: Some("foo".to_string()),
content: Some("bar".to_string()),
excerpt: Some("baz".to_string()),
..Default::default()
},
|created_post, post_from_wp_cli| {
assert_eq!(created_post.title.raw, "foo");
assert_eq!(post_from_wp_cli.title, "foo");
assert_eq!(created_post.content.raw, "bar");
assert_eq!(post_from_wp_cli.content, "bar");
assert_eq!(created_post.excerpt.raw, "baz");
assert_eq!(post_from_wp_cli.excerpt, "baz");
},
)
.await;
}

#[tokio::test]
#[serial]
Expand Down Expand Up @@ -45,3 +117,13 @@ async fn trash_post() {

RestoreServer::db().await;
}

async fn test_create_post<F>(params: &PostCreateParams, assert: F)
where
F: Fn(PostWithEditContext, WpCliPost),
{
let created_post = api_client().posts().create(params).await.assert_response();
let created_post_from_wp_cli = Backend::post(&created_post.id).await;
assert(created_post, created_post_from_wp_cli);
RestoreServer::db().await;
}
8 changes: 8 additions & 0 deletions wp_api_integration_tests_backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ fn wp_cli_site_settings() -> Result<Json<WpCliSiteSettings>, Error> {
.map_err(|e| Error::AsString(e.to_string()))
}

#[get("/post?<post_id>")]
fn wp_cli_post(post_id: i64) -> Result<Json<WpCliPost>, Error> {
WpCliPost::get(post_id)
.map(Json)
.map_err(|e| Error::AsString(e.to_string()))
}

#[get("/posts?<post_status>")]
fn wp_cli_posts(post_status: Option<String>) -> Result<Json<Vec<WpCliPost>>, Error> {
WpCliPost::list(Some(WpCliPostListArguments { post_status }))
Expand Down Expand Up @@ -72,6 +79,7 @@ fn rocket() -> _ {
rocket::build()
.mount("/", routes![restore_wp_server])
.mount("/wp-cli/", routes![wp_cli_site_settings])
.mount("/wp-cli/", routes![wp_cli_post])
.mount("/wp-cli/", routes![wp_cli_posts])
.mount("/wp-cli/", routes![wp_cli_user])
.mount("/wp-cli/", routes![wp_cli_users])
Expand Down
Loading