Skip to content

List /posts/<parent>/revisions endpoint #745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 30, 2025
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
9 changes: 9 additions & 0 deletions scripts/setup-test-site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ create_test_credentials () {
local PASSWORD_PROTECTED_POST_ID
local PASSWORD_PROTECTED_COMMENT_ID
local PASSWORD_PROTECTED_COMMENT_AUTHOR
local REVISIONED_POST_ID
local FIRST_POST_DATE_GMT
local WORDPRESS_VERSION
local INTEGRATION_TEST_CUSTOM_TEMPLATE_ID
Expand Down Expand Up @@ -125,6 +126,13 @@ create_test_credentials () {
curl --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d '{"slug":"INTEGRATION_TEST_CUSTOM_TEMPLATE", "content": "Integration test custom template content"}' http://localhost/wp-json/wp/v2/templates
INTEGRATION_TEST_CUSTOM_TEMPLATE_ID="twentytwentyfour//integration_test_custom_template"

# Setup a post with post revisions for integration tests
REVISIONED_POST_ID="$(wp post create --post_type=post --post_title=Revisioned_POST_FOR_INTEGRATION_TESTS --porcelain)"
for i in {1..10};
do
curl --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d "{\"content\":\"content_revision_$i\"}" "http://localhost/wp-json/wp/v2/posts/$REVISIONED_POST_ID"
done

rm -rf /app/test_credentials.json
jo -p \
site_url="$SITE_URL" \
Expand All @@ -145,6 +153,7 @@ create_test_credentials () {
first_post_date_gmt="$FIRST_POST_DATE_GMT" \
wordpress_core_version="\"$WORDPRESS_VERSION\"" \
integration_test_custom_template_id="$INTEGRATION_TEST_CUSTOM_TEMPLATE_ID" \
revisioned_post_id="$REVISIONED_POST_ID" \
> /app/test_credentials.json
}
create_test_credentials
Expand Down
6 changes: 6 additions & 0 deletions wp_api/src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
comments_endpoint::{CommentsRequestBuilder, CommentsRequestExecutor},
media_endpoint::{MediaRequestBuilder, MediaRequestExecutor},
plugins_endpoint::{PluginsRequestBuilder, PluginsRequestExecutor},
post_revisions_endpoint::{PostRevisionsRequestBuilder, PostRevisionsRequestExecutor},
post_types_endpoint::{PostTypesRequestBuilder, PostTypesRequestExecutor},
posts_endpoint::{PostsRequestBuilder, PostsRequestExecutor},
search_endpoint::{SearchRequestBuilder, SearchRequestExecutor},
Expand Down Expand Up @@ -55,6 +56,7 @@ pub struct WpApiRequestBuilder {
comments: Arc<CommentsRequestBuilder>,
media: Arc<MediaRequestBuilder>,
plugins: Arc<PluginsRequestBuilder>,
post_revisions: Arc<PostRevisionsRequestBuilder>,
post_types: Arc<PostTypesRequestBuilder>,
posts: Arc<PostsRequestBuilder>,
search: Arc<SearchRequestBuilder>,
Expand All @@ -80,6 +82,7 @@ impl WpApiRequestBuilder {
comments,
media,
plugins,
post_revisions,
post_types,
posts,
search,
Expand Down Expand Up @@ -115,6 +118,7 @@ pub struct WpApiClient {
comments: Arc<CommentsRequestExecutor>,
media: Arc<MediaRequestExecutor>,
plugins: Arc<PluginsRequestExecutor>,
post_revisions: Arc<PostRevisionsRequestExecutor>,
post_types: Arc<PostTypesRequestExecutor>,
posts: Arc<PostsRequestExecutor>,
search: Arc<SearchRequestExecutor>,
Expand All @@ -137,6 +141,7 @@ impl WpApiClient {
comments,
media,
plugins,
post_revisions,
post_types,
posts,
search,
Expand Down Expand Up @@ -169,6 +174,7 @@ api_client_generate_endpoint_impl!(WpApi, categories);
api_client_generate_endpoint_impl!(WpApi, comments);
api_client_generate_endpoint_impl!(WpApi, media);
api_client_generate_endpoint_impl!(WpApi, plugins);
api_client_generate_endpoint_impl!(WpApi, post_revisions);
api_client_generate_endpoint_impl!(WpApi, post_types);
api_client_generate_endpoint_impl!(WpApi, posts);
api_client_generate_endpoint_impl!(WpApi, search);
Expand Down
4 changes: 4 additions & 0 deletions wp_api/src/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ pub enum WpErrorCode {
PostInvalidId,
#[serde(rename = "rest_post_invalid_page_number")]
PostInvalidPageNumber,
#[serde(rename = "rest_post_invalid_parent")]
PostInvalidParent,
#[serde(rename = "rest_revision_invalid_offset_number")]
RevisionInvalidOffsetNumber,
#[serde(rename = "rest_taxonomy_invalid")]
TaxonomyInvalid,
#[serde(rename = "rest_template_already_trashed")]
Expand Down
1 change: 1 addition & 0 deletions wp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod media;
pub mod middleware;
pub mod parsed_url;
pub mod plugins;
pub mod post_revisions;
pub mod post_types;
pub mod posts;
pub mod prelude;
Expand Down
243 changes: 243 additions & 0 deletions wp_api/src/post_revisions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use crate::{
UserId, WpApiParamOrder,
date::WpGmtDateTime,
impl_as_query_value_for_new_type, impl_as_query_value_from_to_string,
posts::PostId,
url_query::{
AppendUrlQueryPairs, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, UrlQueryPairsMap,
},
};
use serde::{Deserialize, Serialize};
use std::{num::ParseIntError, str::FromStr};
use strum_macros::IntoStaticStr;
use wp_contextual::WpContextual;

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

impl FromStr for PostRevisionId {
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Self)
}
}

impl std::fmt::Display for PostRevisionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
uniffi::Enum,
strum_macros::EnumString,
strum_macros::Display,
)]
#[strum(serialize_all = "snake_case")]
pub enum WpApiParamPostRevisionsOrderBy {
#[default]
Date,
Id,
Include,
IncludeSlugs,
Relevance,
Slug,
Title,
}

impl_as_query_value_from_to_string!(WpApiParamPostRevisionsOrderBy);

#[derive(Debug, Default, PartialEq, Eq, uniffi::Record)]
pub struct PostRevisionListParams {
/// Current page of the collection.
/// Default: `1`
#[uniffi(default = None)]
pub page: Option<u32>,
/// Maximum number of items to be returned in result set.
#[uniffi(default = None)]
pub per_page: Option<u32>,
/// Limit results to those matching a string.
#[uniffi(default = None)]
pub search: Option<String>,
/// Ensure result set excludes specific IDs.
#[uniffi(default = [])]
pub exclude: Vec<PostRevisionId>,
/// Limit result set to specific IDs.
#[uniffi(default = [])]
pub include: Vec<PostRevisionId>,
/// Offset the result set by a specific number of items.
#[uniffi(default = None)]
pub offset: Option<u32>,
/// Order sort attribute ascending or descending.
/// Default: desc
/// One of: asc, desc
#[uniffi(default = None)]
pub order: Option<WpApiParamOrder>,
/// Sort collection by object attribute.
/// Default: date
/// One of: date, id, include, relevance, slug, include_slugs, title
#[uniffi(default = None)]
pub orderby: Option<WpApiParamPostRevisionsOrderBy>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, IntoStaticStr)]
enum PostRevisionListParamsField {
#[strum(serialize = "page")]
Page,
#[strum(serialize = "per_page")]
PerPage,
#[strum(serialize = "search")]
Search,
#[strum(serialize = "exclude")]
Exclude,
#[strum(serialize = "include")]
Include,
#[strum(serialize = "offset")]
Offset,
#[strum(serialize = "order")]
Order,
#[strum(serialize = "orderby")]
Orderby,
}

impl AppendUrlQueryPairs for PostRevisionListParams {
fn append_query_pairs(&self, query_pairs_mut: &mut QueryPairs) {
query_pairs_mut
.append_option_query_value_pair(PostRevisionListParamsField::Page, self.page.as_ref())
.append_option_query_value_pair(
PostRevisionListParamsField::PerPage,
self.per_page.as_ref(),
)
.append_option_query_value_pair(
PostRevisionListParamsField::Search,
self.search.as_ref(),
)
.append_vec_query_value_pair(PostRevisionListParamsField::Exclude, &self.exclude)
.append_vec_query_value_pair(PostRevisionListParamsField::Include, &self.include)
.append_option_query_value_pair(
PostRevisionListParamsField::Offset,
self.offset.as_ref(),
)
.append_option_query_value_pair(PostRevisionListParamsField::Order, self.order.as_ref())
.append_option_query_value_pair(
PostRevisionListParamsField::Orderby,
self.orderby.as_ref(),
);
}
}

impl FromUrlQueryPairs for PostRevisionListParams {
fn from_url_query_pairs(query_pairs: UrlQueryPairsMap) -> Option<Self> {
Some(Self {
page: query_pairs.get(PostRevisionListParamsField::Page),
per_page: query_pairs.get(PostRevisionListParamsField::PerPage),
search: query_pairs.get(PostRevisionListParamsField::Search),
exclude: query_pairs.get_csv(PostRevisionListParamsField::Exclude),
include: query_pairs.get_csv(PostRevisionListParamsField::Include),
offset: query_pairs.get(PostRevisionListParamsField::Offset),
order: query_pairs.get(PostRevisionListParamsField::Order),
orderby: query_pairs.get(PostRevisionListParamsField::Orderby),
})
}

fn supports_pagination() -> bool {
true
}
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparsePostRevision {
#[WpContext(edit, embed, view)]
pub id: Option<PostRevisionId>,
#[WpContext(edit, embed, view)]
pub author: Option<UserId>,
#[WpContext(edit, embed, view)]
pub date: Option<String>,
#[WpContext(edit, view)]
pub date_gmt: Option<WpGmtDateTime>,
#[WpContext(edit, view)]
pub modified: Option<String>,
#[WpContext(edit, view)]
pub modified_gmt: Option<WpGmtDateTime>,
#[WpContext(edit, embed, view)]
pub parent: Option<PostId>,
#[WpContext(edit, embed, view)]
pub slug: Option<String>,
#[WpContext(edit, view)]
#[WpContextualField]
pub guid: Option<crate::posts::SparsePostGuid>,
#[WpContext(edit, embed, view)]
#[WpContextualField]
pub title: Option<crate::posts::SparsePostTitle>,
#[WpContext(edit, view)]
#[WpContextualField]
pub content: Option<crate::posts::SparsePostContent>,
#[WpContext(edit, embed, view)]
#[WpContextualField]
pub excerpt: Option<crate::posts::SparsePostExcerpt>,
#[WpContext(edit, view)]
pub meta: Option<crate::posts::PostMeta>,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{generate, unit_test_common::assert_expected_and_from_query_pairs};
use rstest::*;

#[rstest]
#[case(PostRevisionListParams::default(), "")]
#[case(generate!(PostRevisionListParams, (page, Some(2))), "page=2")]
#[case(generate!(PostRevisionListParams, (per_page, Some(2))), "per_page=2")]
#[case(generate!(PostRevisionListParams, (search, Some("foo".to_string()))), "search=foo")]
#[case(generate!(PostRevisionListParams, (exclude, vec![PostRevisionId(1), PostRevisionId(2)])), "exclude=1%2C2")]
#[case(generate!(PostRevisionListParams, (include, vec![PostRevisionId(1), PostRevisionId(2)])), "include=1%2C2")]
#[case(generate!(PostRevisionListParams, (offset, Some(2))), "offset=2")]
#[case(generate!(PostRevisionListParams, (order, Some(WpApiParamOrder::Asc))), "order=asc")]
#[case(generate!(PostRevisionListParams, (order, Some(WpApiParamOrder::Desc))), "order=desc")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Date))), "orderby=date")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Id))), "orderby=id")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Include))), "orderby=include")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::IncludeSlugs))), "orderby=include_slugs")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Relevance))), "orderby=relevance")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Slug))), "orderby=slug")]
#[case(generate!(PostRevisionListParams, (orderby, Some(WpApiParamPostRevisionsOrderBy::Title))), "orderby=title")]
#[case(
post_revision_list_params_with_all_fields(),
&expected_query_pairs_for_post_revision_list_params_with_all_fields()
)]
#[trace]
fn test_post_list_query_pairs(
#[case] params: PostRevisionListParams,
#[case] expected_query: &str,
) {
assert_expected_and_from_query_pairs(params, expected_query);
}

fn expected_query_pairs_for_post_revision_list_params_with_all_fields() -> String {
"page=2&per_page=2&search=foo&exclude=1%2C2&include=1%2C2&offset=2&order=asc&orderby=id"
.to_string()
}

fn post_revision_list_params_with_all_fields() -> PostRevisionListParams {
PostRevisionListParams {
page: Some(2),
per_page: Some(2),
search: Some("foo".to_string()),
exclude: vec![PostRevisionId(1), PostRevisionId(2)],
include: vec![PostRevisionId(1), PostRevisionId(2)],
offset: Some(2),
order: Some(WpApiParamOrder::Asc),
orderby: Some(WpApiParamPostRevisionsOrderBy::Id),
}
}
}
3 changes: 3 additions & 0 deletions wp_api/src/posts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,10 @@ pub struct SparsePostContent {
#[WpContext(edit, view)]
pub rendered: Option<String>,
#[WpContext(edit, view)]
#[WpContextualOption]
pub protected: Option<bool>,
#[WpContext(edit)]
#[WpContextualOption]
pub block_version: Option<u32>,
}

Expand All @@ -563,6 +565,7 @@ pub struct SparsePostExcerpt {
#[WpContext(edit, embed, view)]
pub rendered: Option<String>,
#[WpContext(edit, embed, view)]
#[WpContextualOption]
pub protected: Option<bool>,
}

Expand Down
1 change: 1 addition & 0 deletions wp_api/src/request/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod categories_endpoint;
pub mod comments_endpoint;
pub mod media_endpoint;
pub mod plugins_endpoint;
pub mod post_revisions_endpoint;
pub mod post_types_endpoint;
pub mod posts_endpoint;
pub mod search_endpoint;
Expand Down
Loading