diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index 36fb42e90..dc18e7e78 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -6,7 +6,7 @@ pub(crate) mod application_passwords_endpoint; pub(crate) mod plugins_endpoint; pub(crate) mod users_endpoint; -const WP_JSON_PATH_SEGMENTS: [&str; 3] = ["wp-json", "wp", "v2"]; +const WP_JSON_PATH_SEGMENTS: [&str; 1] = ["wp-json"]; uniffi::custom_newtype!(WpEndpointUrl, String); #[derive(Debug, Clone)] @@ -83,17 +83,6 @@ impl ApiBaseUrl { .expect("ApiBaseUrl is already parsed, so this can't result in an error") } - fn by_extending(&self, segments: I) -> Url - where - I: IntoIterator, - I::Item: AsRef, - { - self.url - .clone() - .extend(segments) - .expect("ApiBaseUrl is already parsed, so this can't result in an error") - } - pub fn by_extending_and_splitting_by_forward_slash(&self, segments: I) -> Url where I: IntoIterator, @@ -104,7 +93,10 @@ impl ApiBaseUrl { .extend(segments.into_iter().flat_map(|s| { s.as_ref() .split('/') - .map(str::to_string) + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y.to_string()), + }) .collect::>() })) .expect("ApiBaseUrl is already parsed, so this can't result in an error") @@ -197,9 +189,23 @@ mod tests { format!("{}/bar", expected_wp_json_url) ); assert_eq!( - api_base_url.by_extending(["bar", "baz"]).as_str(), + api_base_url + .by_extending_and_splitting_by_forward_slash(["bar", "baz"]) + .as_str(), format!("{}/bar/baz", expected_wp_json_url) ); + assert_eq!( + api_base_url + .by_extending_and_splitting_by_forward_slash(["bar", "baz/quox"]) + .as_str(), + format!("{}/bar/baz/quox", expected_wp_json_url) + ); + assert_eq!( + api_base_url + .by_extending_and_splitting_by_forward_slash(["/bar", "/baz/quox"]) + .as_str(), + format!("{}/bar/baz/quox", expected_wp_json_url) + ); } fn wp_json_endpoint(base_url: &str) -> String { @@ -215,10 +221,14 @@ mod tests { ApiBaseUrl::try_from("https://example.com").unwrap().into() } - pub fn validate_endpoint(endpoint_url: ApiEndpointUrl, path: &str) { + pub fn validate_wp_v2_endpoint(endpoint_url: ApiEndpointUrl, path: &str) { + validate_endpoint("/wp/v2", endpoint_url, path); + } + + fn validate_endpoint(namespace: &str, endpoint_url: ApiEndpointUrl, path: &str) { assert_eq!( endpoint_url.as_str(), - format!("{}{}", fixture_api_base_url().as_str(), path) + format!("{}{}{}", fixture_api_base_url().as_str(), namespace, path) ); } } diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index b3d459d44..ece12bd8a 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -9,6 +9,7 @@ use crate::application_passwords::{ use crate::users::UserId; #[derive(WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(SparseApplicationPasswordField)] enum ApplicationPasswordsRequest { #[post(url = "/users//application-passwords", params = &ApplicationPasswordCreateParams, output = ApplicationPasswordWithEditContext)] @@ -32,7 +33,7 @@ mod tests { use super::*; use crate::{ request::endpoint::{ - tests::{fixture_api_base_url, validate_endpoint}, + tests::{fixture_api_base_url, validate_wp_v2_endpoint}, ApiBaseUrl, }, WpContext, @@ -42,7 +43,7 @@ mod tests { #[rstest] fn create_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.create(&UserId(1)), "/users/1/application-passwords", ); @@ -50,7 +51,7 @@ mod tests { #[rstest] fn delete_single_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.delete( &UserId(2), &ApplicationPasswordUuid { @@ -63,7 +64,7 @@ mod tests { #[rstest] fn delete_all_application_passwords(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.delete_all(&UserId(1)), "/users/1/application-passwords", ); @@ -71,7 +72,7 @@ mod tests { #[rstest] fn list_application_passwords_with_edit_context(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_edit_context(&UserId(2)), "/users/2/application-passwords?context=edit", ); @@ -81,7 +82,7 @@ mod tests { fn list_application_passwords_with_embed_context( endpoint: ApplicationPasswordsRequestEndpoint, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_embed_context(&UserId(71)), "/users/71/application-passwords?context=embed", ); @@ -89,7 +90,7 @@ mod tests { #[rstest] fn list_application_passwords_with_view_context(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_view_context(&UserId(9999)), "/users/9999/application-passwords?context=view", ); @@ -104,7 +105,7 @@ mod tests { #[case] fields: &[SparseApplicationPasswordField], #[case] expected_path: &str, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_list(&UserId(2), context, fields), expected_path, ); @@ -114,7 +115,7 @@ mod tests { fn retrieve_current_application_passwords_with_edit_context( endpoint: ApplicationPasswordsRequestEndpoint, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.retrieve_current_with_edit_context(&UserId(2)), "/users/2/application-passwords/introspect?context=edit", ); @@ -129,7 +130,7 @@ mod tests { #[case] fields: &[SparseApplicationPasswordField], #[case] expected_path: &str, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_retrieve_current(&UserId(2), context, fields), expected_path, ); @@ -139,7 +140,7 @@ mod tests { fn retrieve_application_passwords_with_embed_context( endpoint: ApplicationPasswordsRequestEndpoint, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.retrieve_with_embed_context( &UserId(2), &ApplicationPasswordUuid { @@ -162,7 +163,7 @@ mod tests { let uuid = ApplicationPasswordUuid { uuid: "584a87d5-4f18-4c33-a315-4c05ed1fc485".to_string(), }; - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_retrieve(&UserId(2), &uuid, context, fields), expected_path, ); @@ -170,7 +171,7 @@ mod tests { #[rstest] fn update_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.update( &UserId(2), &ApplicationPasswordUuid { diff --git a/wp_api/src/request/endpoint/plugins_endpoint.rs b/wp_api/src/request/endpoint/plugins_endpoint.rs index a1f819823..459574b90 100644 --- a/wp_api/src/request/endpoint/plugins_endpoint.rs +++ b/wp_api/src/request/endpoint/plugins_endpoint.rs @@ -6,6 +6,7 @@ use crate::{ use wp_derive_request_builder::WpDerivedRequest; #[derive(WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(SparsePluginField)] enum PluginsRequest { #[post(url = "/plugins", params = &PluginCreateParams, output = PluginWithEditContext)] @@ -26,7 +27,7 @@ mod tests { use crate::{ generate, request::endpoint::{ - tests::{fixture_api_base_url, validate_endpoint}, + tests::{fixture_api_base_url, validate_wp_v2_endpoint}, ApiBaseUrl, }, PluginStatus, WpContext, @@ -36,7 +37,7 @@ mod tests { #[rstest] fn create_plugin(endpoint: PluginsRequestEndpoint) { - validate_endpoint(endpoint.create(), "/plugins"); + validate_wp_v2_endpoint(endpoint.create(), "/plugins"); } #[rstest] @@ -52,7 +53,7 @@ mod tests { #[case] plugin_slug: PluginSlug, #[case] expected_path: &str, ) { - validate_endpoint(endpoint.delete(&plugin_slug), expected_path); + validate_wp_v2_endpoint(endpoint.delete(&plugin_slug), expected_path); } #[rstest] @@ -65,7 +66,7 @@ mod tests { #[case] params: PluginListParams, #[case] expected_path: &str, ) { - validate_endpoint(endpoint.list_with_edit_context(¶ms), expected_path); + validate_wp_v2_endpoint(endpoint.list_with_edit_context(¶ms), expected_path); } #[rstest] @@ -76,7 +77,7 @@ mod tests { #[case] params: PluginListParams, #[case] expected_path: &str, ) { - validate_endpoint(endpoint.list_with_embed_context(¶ms), expected_path); + validate_wp_v2_endpoint(endpoint.list_with_embed_context(¶ms), expected_path); } #[rstest] @@ -87,7 +88,7 @@ mod tests { #[case] params: PluginListParams, #[case] expected_path: &str, ) { - validate_endpoint(endpoint.list_with_view_context(¶ms), expected_path); + validate_wp_v2_endpoint(endpoint.list_with_view_context(¶ms), expected_path); } #[rstest] @@ -123,7 +124,7 @@ mod tests { #[case] fields: &[SparsePluginField], #[case] expected_path: &str, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_list(context, ¶ms, fields), expected_path, ); @@ -148,7 +149,7 @@ mod tests { #[case] plugin_slug: PluginSlug, #[case] expected_path: &str, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.retrieve_with_view_context(&plugin_slug), expected_path, ); @@ -186,7 +187,7 @@ mod tests { #[case] fields: &[SparsePluginField], #[case] expected_path: &str, ) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_retrieve(&plugin_slug, context, fields), expected_path, ); @@ -205,7 +206,7 @@ mod tests { #[case] plugin_slug: PluginSlug, #[case] expected_path: &str, ) { - validate_endpoint(endpoint.update(&plugin_slug), expected_path); + validate_wp_v2_endpoint(endpoint.update(&plugin_slug), expected_path); } #[fixture] diff --git a/wp_api/src/request/endpoint/users_endpoint.rs b/wp_api/src/request/endpoint/users_endpoint.rs index 2819c4bec..5ea6ad736 100644 --- a/wp_api/src/request/endpoint/users_endpoint.rs +++ b/wp_api/src/request/endpoint/users_endpoint.rs @@ -6,6 +6,7 @@ use crate::{ use wp_derive_request_builder::WpDerivedRequest; #[derive(WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] @@ -31,7 +32,7 @@ mod tests { use super::*; use crate::{ request::endpoint::{ - tests::{fixture_api_base_url, validate_endpoint}, + tests::{fixture_api_base_url, validate_wp_v2_endpoint}, ApiBaseUrl, }, WpApiParamUsersHasPublishedPosts, WpContext, @@ -41,12 +42,12 @@ mod tests { #[rstest] fn create_user(endpoint: UsersRequestEndpoint) { - validate_endpoint(endpoint.create(), "/users"); + validate_wp_v2_endpoint(endpoint.create(), "/users"); } #[rstest] fn delete_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.delete( &UserId(54), &UserDeleteParams { @@ -59,7 +60,7 @@ mod tests { #[rstest] fn delete_current_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.delete_me(&UserDeleteParams { reassign: UserId(98), }), @@ -69,7 +70,7 @@ mod tests { #[rstest] fn list_users(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_edit_context(&UserListParams::default()), "/users?context=edit", ); @@ -77,7 +78,7 @@ mod tests { #[rstest] fn list_users_default_params_empty_fields(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_edit_context(&UserListParams::default()), "/users?context=edit", ); @@ -100,7 +101,7 @@ mod tests { who: None, has_published_posts: Some(WpApiParamUsersHasPublishedPosts::True), }; - validate_endpoint( + validate_wp_v2_endpoint( endpoint.list_with_edit_context(¶ms), "/users?context=edit&page=2&per_page=60&search=foo&slug=bar%2Cbaz&has_published_posts=true", ); @@ -108,7 +109,7 @@ mod tests { #[rstest] fn filter_list_users_default_params_empty_fields(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_list(WpContext::Edit, &UserListParams::default(), &[]), "/users?context=edit&_fields=", ); @@ -134,7 +135,7 @@ mod tests { "page".to_string(), ])), }; - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_list(WpContext::Edit, ¶ms, &[SparseUserField::Name, SparseUserField::Email]), "/users?context=edit&page=2&per_page=60&search=foo&slug=bar%2Cbaz&has_published_posts=post%2Cpage&_fields=name%2Cemail", ); @@ -142,7 +143,7 @@ mod tests { #[rstest] fn retrieve_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.retrieve_with_view_context(&UserId(98)), "/users/98?context=view", ); @@ -150,7 +151,7 @@ mod tests { #[rstest] fn filter_retrieve_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_retrieve( &UserId(98), WpContext::View, @@ -162,7 +163,7 @@ mod tests { #[rstest] fn retrieve_current_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.retrieve_me_with_embed_context(), "/users/me?context=embed", ); @@ -170,7 +171,7 @@ mod tests { #[rstest] fn filter_retrieve_current_user(endpoint: UsersRequestEndpoint) { - validate_endpoint( + validate_wp_v2_endpoint( endpoint.filter_retrieve_me( WpContext::Embed, &[SparseUserField::Roles, SparseUserField::Capabilities], @@ -181,12 +182,12 @@ mod tests { #[rstest] fn update_user(endpoint: UsersRequestEndpoint) { - validate_endpoint(endpoint.update(&UserId(98)), "/users/98"); + validate_wp_v2_endpoint(endpoint.update(&UserId(98)), "/users/98"); } #[rstest] fn update_current_user(endpoint: UsersRequestEndpoint) { - validate_endpoint(endpoint.update_me(), "/users/me"); + validate_wp_v2_endpoint(endpoint.update_me(), "/users/me"); } #[fixture] diff --git a/wp_derive_request_builder/src/generate.rs b/wp_derive_request_builder/src/generate.rs index 5988e17c1..64b0bdeee 100644 --- a/wp_derive_request_builder/src/generate.rs +++ b/wp_derive_request_builder/src/generate.rs @@ -9,8 +9,8 @@ use strum_macros::EnumIter; use syn::Ident; use crate::{ + outer_attr::{NamespaceAttr, SparseFieldAttr}, parse::{ParsedEnum, ParsedVariant, RequestType}, - sparse_field_attr::SparseFieldAttr, }; mod helpers_to_generate_tokens; @@ -165,7 +165,8 @@ fn generate_endpoint_type(config: &Config, parsed_enum: &ParsedEnum) -> TokenStr let url_parts = variant.attr.url_parts.as_slice(); let params_type = &variant.attr.params; let request_type = variant.attr.request_type; - let url_from_api_base_url = fn_body_get_url_from_api_base_url(url_parts); + let url_from_api_base_url = + fn_body_get_url_from_api_base_url(&config.namespace_attr, url_parts); let query_pairs = fn_body_query_pairs(params_type, request_type); ContextAndFilterHandler::from_request_type(request_type) @@ -268,6 +269,7 @@ impl Display for WpContext { pub struct Config { pub crate_ident: Ident, pub sparse_field_type: SparseFieldAttr, + pub namespace_attr: NamespaceAttr, pub generated_idents: ConfigGeneratedIdents, pub static_types: ConfigStaticTypes, } @@ -287,7 +289,8 @@ impl Config { Self { crate_ident, - sparse_field_type: parsed_enum.sparse_field_attr.clone(), + sparse_field_type: parsed_enum.outer_attr.sparse_field_attr.clone(), + namespace_attr: parsed_enum.outer_attr.namespace_attr.clone(), generated_idents, static_types, } diff --git a/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs b/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs index 585adef96..1deef35ce 100644 --- a/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs +++ b/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs @@ -5,8 +5,8 @@ use syn::Ident; use super::{ContextAndFilterHandler, PartOf, WpContext}; use crate::{ + outer_attr::{NamespaceAttr, SparseFieldAttr}, parse::{ParsedEnum, RequestType}, - sparse_field_attr::SparseFieldAttr, variant_attr::{ParamsType, UrlPart}, }; @@ -224,7 +224,10 @@ fn fn_arg_fields(context_and_filter_handler: ContextAndFilterHandler) -> TokenSt } } -pub fn fn_body_get_url_from_api_base_url(url_parts: &[UrlPart]) -> TokenStream { +pub fn fn_body_get_url_from_api_base_url( + namespace_attr: &NamespaceAttr, + url_parts: &[UrlPart], +) -> TokenStream { let url_parts = url_parts .iter() .map(|part| match part { @@ -235,8 +238,9 @@ pub fn fn_body_get_url_from_api_base_url(url_parts: &[UrlPart]) -> TokenStream { UrlPart::Static(static_part) => quote! { #static_part }, }) .collect::>(); + let namespace = namespace_attr.token.clone(); quote! { - let mut url = self.api_base_url.by_extending_and_splitting_by_forward_slash([ #(#url_parts,)* ]); + let mut url = self.api_base_url.by_extending_and_splitting_by_forward_slash([ #namespace, #(#url_parts,)* ]); } } @@ -378,9 +382,10 @@ pub fn fn_body_get_request_from_request_builder( #[cfg(test)] mod tests { #![allow(clippy::too_many_arguments)] - use crate::sparse_field_attr; + use std::str::FromStr; use super::*; + use proc_macro2::Literal; use quote::ToTokens; use rstest::rstest; use syn::parse_quote; @@ -910,34 +915,40 @@ mod tests { #[rstest] #[case( url_static_users(), - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"users\" ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , \"users\" ,]) ;" )] #[case( url_users_with_user_id(), - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"users\" , & user_id . to_string () ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , \"users\" , & user_id . to_string () ,]) ;" )] #[case( url_users_with_user_id(), - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"users\" , & user_id . to_string () ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , \"users\" , & user_id . to_string () ,]) ;" )] #[case( vec![UrlPart::Dynamic("user_id".to_string()), UrlPart::Dynamic("user_type".to_string())], - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([& user_id . to_string () , & user_type . to_string () ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , & user_id . to_string () , & user_type . to_string () ,]) ;" )] #[case( vec![UrlPart::Static("users".to_string()), UrlPart::Dynamic("user_id".to_string()), UrlPart::Dynamic("user_type".to_string()), ], - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"users\" , & user_id . to_string () , & user_type . to_string () ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , \"users\" , & user_id . to_string () , & user_type . to_string () ,]) ;" )] #[case( vec![UrlPart::Static("users".to_string()), UrlPart::Static("me".to_string()), UrlPart::Dynamic("user_id".to_string()), UrlPart::Dynamic("user_type".to_string()), ], - "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"users\" , \"me\" , & user_id . to_string () , & user_type . to_string () ,]) ;" + "let mut url = self . api_base_url . by_extending_and_splitting_by_forward_slash ([\"/wp/v2\" , \"users\" , \"me\" , & user_id . to_string () , & user_type . to_string () ,]) ;" )] fn test_fn_body_get_url_from_api_base_url( #[case] url_parts: Vec, #[case] expected_str: &str, ) { assert_eq!( - fn_body_get_url_from_api_base_url(&url_parts).to_string(), + fn_body_get_url_from_api_base_url( + &NamespaceAttr { + token: quote! { "/wp/v2" }.into_iter().next().unwrap(), + }, + &url_parts + ) + .to_string(), expected_str ); } diff --git a/wp_derive_request_builder/src/lib.rs b/wp_derive_request_builder/src/lib.rs index 728f1a103..d226c2264 100644 --- a/wp_derive_request_builder/src/lib.rs +++ b/wp_derive_request_builder/src/lib.rs @@ -3,13 +3,13 @@ use proc_macro::TokenStream; use syn::parse_macro_input; mod generate; +mod outer_attr; mod parse; -mod sparse_field_attr; mod variant_attr; #[proc_macro_derive( WpDerivedRequest, - attributes(SparseField, post, delete, contextual_get) + attributes(SparseField, Namespace, post, delete, contextual_get) )] pub fn derive(input: TokenStream) -> TokenStream { let parsed_enum = parse_macro_input!(input as parse::ParsedEnum); diff --git a/wp_derive_request_builder/src/outer_attr.rs b/wp_derive_request_builder/src/outer_attr.rs new file mode 100644 index 000000000..23a609582 --- /dev/null +++ b/wp_derive_request_builder/src/outer_attr.rs @@ -0,0 +1,103 @@ +use proc_macro2::{Literal, Span, TokenStream, TokenTree}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Attribute, Ident, Meta, MetaList, Result, +}; + +#[derive(Debug, Clone)] +pub struct NamespaceAttr { + pub token: TokenTree, +} + +impl NamespaceAttr { + fn new(tokens: TokenStream, attr_span: Span) -> Result { + let tokens_span = tokens.span(); + let mut iter = tokens.into_iter(); + if let Some(first) = iter.next() { + if iter.next().is_some() { + Err(OuterAttrParseError::NamespaceAttrHasMultipleTokens.into_syn_error(tokens_span)) + } else if let TokenTree::Literal(_) = first { + Ok(Self { token: first }) + } else { + Err(OuterAttrParseError::NamespaceAttrIsNotLiteral.into_syn_error(first.span())) + } + } else { + Err(OuterAttrParseError::NamespaceAttrHasNoTokens.into_syn_error(attr_span)) + } + } +} + +#[derive(Debug, Clone)] +pub struct SparseFieldAttr { + pub tokens: TokenStream, +} + +#[derive(Debug, Clone)] +pub struct OuterAttr { + pub namespace_attr: NamespaceAttr, + pub sparse_field_attr: SparseFieldAttr, +} + +impl Parse for OuterAttr { + fn parse(input: ParseStream) -> Result { + let attrs = Attribute::parse_outer(input)?; + + let (sparse_field, namespace) = attrs.into_iter().fold((None, None), |(acc), a| { + let attr_span = a.span(); + let Meta::List(meta_list) = a.meta else { + return acc; + }; + if meta_list.path.segments.len() == 1 { + let s = meta_list + .path + .segments + .first() + .expect("Already verified that there is only one segment"); + + match s.ident.to_string().as_str() { + "SparseField" => (Some((meta_list.tokens, attr_span)), acc.1), + "Namespace" => (acc.0, Some((meta_list.tokens, attr_span))), + // Unrecognized attributes may belong to another proc macro, so we need + // to ignore them and not return an error + _ => acc, + } + } else { + acc + } + }); + let sparse_field_attr = sparse_field + .map(|(tokens, _)| SparseFieldAttr { tokens }) + .ok_or(OuterAttrParseError::MissingSparseFieldAttr.into_syn_error(input.span()))?; + let namespace_attr = namespace + .ok_or(OuterAttrParseError::MissingNamespaceAttr.into_syn_error(input.span())) + .and_then(|(t, attr_span)| NamespaceAttr::new(t, attr_span))?; + + Ok(Self { + namespace_attr, + sparse_field_attr, + }) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum OuterAttrParseError { + #[error("Expecting #[Namespace(\"_path_\")] - Try wrapping the path in \"\"")] + NamespaceAttrIsNotLiteral, + #[error( + "Expecting #[Namespace(\"_path_\")] - Path should be a single literal separated by '/'" + )] + NamespaceAttrHasMultipleTokens, + #[error("Expecting #[Namespace(\"_path_\")] - Path is missing")] + NamespaceAttrHasNoTokens, + #[error("Missing #[Namespace(\"_path_\")] attribute")] + MissingNamespaceAttr, + #[error("Missing #[SparseField(_field_type_)] attribute")] + MissingSparseFieldAttr, +} + +impl OuterAttrParseError { + fn into_syn_error(self, span: proc_macro2::Span) -> syn::Error { + syn::Error::new(span, self.to_string()) + } +} diff --git a/wp_derive_request_builder/src/parse.rs b/wp_derive_request_builder/src/parse.rs index dbf4c03e7..399ce8c42 100644 --- a/wp_derive_request_builder/src/parse.rs +++ b/wp_derive_request_builder/src/parse.rs @@ -11,24 +11,27 @@ use syn::{ Ident, Token, }; -use crate::{sparse_field_attr::SparseFieldAttr, variant_attr::ParsedVariantAttribute}; +use crate::{ + outer_attr::{OuterAttr, SparseFieldAttr}, + variant_attr::ParsedVariantAttribute, +}; #[derive(Debug, Clone)] pub struct ParsedEnum { - pub sparse_field_attr: SparseFieldAttr, + pub outer_attr: OuterAttr, pub enum_ident: Ident, pub variants: Punctuated, } impl Parse for ParsedEnum { fn parse(input: ParseStream) -> syn::Result { - let sparse_field_attr = input.parse()?; + let outer_attr = input.parse()?; let _enum_token: Token![enum] = input.parse()?; let enum_ident: Ident = input.parse()?; let content: ParseBuffer; let brace_token = braced!(content in input); Ok(Self { - sparse_field_attr, + outer_attr, enum_ident, variants: content.parse_terminated(ParsedVariant::parse, Token![,])?, }) diff --git a/wp_derive_request_builder/src/sparse_field_attr.rs b/wp_derive_request_builder/src/sparse_field_attr.rs deleted file mode 100644 index f7247952e..000000000 --- a/wp_derive_request_builder/src/sparse_field_attr.rs +++ /dev/null @@ -1,54 +0,0 @@ -use proc_macro2::{TokenStream, TokenTree}; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned, - Attribute, Ident, Meta, MetaList, Result, -}; - -#[derive(Debug, Clone)] -pub struct SparseFieldAttr { - pub tokens: TokenStream, -} - -impl Parse for SparseFieldAttr { - fn parse(input: ParseStream) -> Result { - let attr = { - let attrs = Attribute::parse_outer(input)?; - if attrs.is_empty() { - return Err( - SparseFieldParseError::MissingSparseFieldAttr.into_syn_error(input.span()) - ); - } else if attrs.len() > 1 { - return Err( - SparseFieldParseError::MoreThanOneOuterAttr.into_syn_error(input.span()) - ); - } - attrs - .first() - .expect("Already verified that there is a single attr") - .to_owned() - }; - - if let Meta::List(MetaList { tokens, .. }) = attr.meta { - Ok(Self { tokens }) - } else { - Err(SparseFieldParseError::WrongFormat.into_syn_error(attr.span())) - } - } -} - -#[derive(Debug, thiserror::Error)] -enum SparseFieldParseError { - #[error("Missing #[SparseField(_field_type_)]")] - MissingSparseFieldAttr, - #[error("Only a single #[SparseField(_field_type_)] attribute is supported")] - MoreThanOneOuterAttr, - #[error("Expecting #[SparseField(_field_type_)]")] - WrongFormat, -} - -impl SparseFieldParseError { - fn into_syn_error(self, span: proc_macro2::Span) -> syn::Error { - syn::Error::new(span, self.to_string()) - } -} diff --git a/wp_derive_request_builder/tests/fail/missing_namespace_attr.rs b/wp_derive_request_builder/tests/fail/missing_namespace_attr.rs new file mode 100644 index 000000000..d3bfc1708 --- /dev/null +++ b/wp_derive_request_builder/tests/fail/missing_namespace_attr.rs @@ -0,0 +1,8 @@ +#[derive(wp_derive_request_builder::WpDerivedRequest, serde::Serialize)] +#[SparseField(crate::SparseUserField)] +enum UsersRequest { + #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] + List, +} + +fn main() {} diff --git a/wp_derive_request_builder/tests/fail/missing_namespace_attr.stderr b/wp_derive_request_builder/tests/fail/missing_namespace_attr.stderr new file mode 100644 index 000000000..22221cd69 --- /dev/null +++ b/wp_derive_request_builder/tests/fail/missing_namespace_attr.stderr @@ -0,0 +1,5 @@ +error: Missing #[Namespace("_path_")] attribute + --> tests/fail/missing_namespace_attr.rs:3:1 + | +3 | enum UsersRequest { + | ^^^^ diff --git a/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.rs b/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.rs index e8a0fe883..333393a76 100644 --- a/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.rs +++ b/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] enum UsersRequest { #[contextual_get(url = "/users", output = std::vec::Vec)] List, diff --git a/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.stderr b/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.stderr index 9fd025588..dbf5de561 100644 --- a/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.stderr +++ b/wp_derive_request_builder/tests/fail/missing_sparse_field_attr.stderr @@ -1,5 +1,5 @@ -error: Missing #[SparseField(_field_type_)] - --> tests/fail/missing_sparse_field_attr.rs:2:1 +error: Missing #[SparseField(_field_type_)] attribute + --> tests/fail/missing_sparse_field_attr.rs:3:1 | -2 | enum UsersRequest { +3 | enum UsersRequest { | ^^^^ diff --git a/wp_derive_request_builder/tests/fail/missing_url.rs b/wp_derive_request_builder/tests/fail/missing_url.rs index 063224066..c45579efa 100644 --- a/wp_derive_request_builder/tests/fail/missing_url.rs +++ b/wp_derive_request_builder/tests/fail/missing_url.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(params = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/fail/missing_url.stderr b/wp_derive_request_builder/tests/fail/missing_url.stderr index f6e0721d0..fdd40faae 100644 --- a/wp_derive_request_builder/tests/fail/missing_url.stderr +++ b/wp_derive_request_builder/tests/fail/missing_url.stderr @@ -1,5 +1,5 @@ error: Missing (url = "/foo") - --> tests/fail/missing_url.rs:4:7 + --> tests/fail/missing_url.rs:5:7 | -4 | #[contextual_get(params = &UserListParams, output = Vec)] +5 | #[contextual_get(params = &UserListParams, output = Vec)] | ^^^^^^^^^^^^^^ diff --git a/wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.stderr b/wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.stderr deleted file mode 100644 index 266e307a2..000000000 --- a/wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Only a single #[SparseField(_field_type_)] attribute is supported - --> tests/fail/multiple_sparse_field_attr.rs:4:1 - | -4 | enum UsersRequest { - | ^^^^ diff --git a/wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.rs b/wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.rs similarity index 84% rename from wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.rs rename to wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.rs index c8edc713b..cde3a2a22 100644 --- a/wp_derive_request_builder/tests/fail/multiple_sparse_field_attr.rs +++ b/wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.rs @@ -1,6 +1,6 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("wp", "v2")] #[SparseField(crate::SparseUserField)] -#[SparseField(crate::SparsePluginField)] enum UsersRequest { #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] List, diff --git a/wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.stderr b/wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.stderr new file mode 100644 index 000000000..e39c7d28b --- /dev/null +++ b/wp_derive_request_builder/tests/fail/namespace_attr_has_multiple_tokens.stderr @@ -0,0 +1,5 @@ +error: Expecting #[Namespace("_path_")] - Path should be a single literal separated by '/' + --> tests/fail/namespace_attr_has_multiple_tokens.rs:2:13 + | +2 | #[Namespace("wp", "v2")] + | ^^^^ diff --git a/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.rs b/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.rs new file mode 100644 index 000000000..06c629214 --- /dev/null +++ b/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.rs @@ -0,0 +1,9 @@ +#[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace()] +#[SparseField(crate::SparseUserField)] +enum UsersRequest { + #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] + List, +} + +fn main() {} diff --git a/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.stderr b/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.stderr new file mode 100644 index 000000000..336776e9a --- /dev/null +++ b/wp_derive_request_builder/tests/fail/namespace_attr_has_no_tokens.stderr @@ -0,0 +1,5 @@ +error: Expecting #[Namespace("_path_")] - Path is missing + --> tests/fail/namespace_attr_has_no_tokens.rs:2:1 + | +2 | #[Namespace()] + | ^ diff --git a/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.rs b/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.rs new file mode 100644 index 000000000..0fc48418e --- /dev/null +++ b/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.rs @@ -0,0 +1,9 @@ +#[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace(wp)] +#[SparseField(crate::SparseUserField)] +enum UsersRequest { + #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] + List, +} + +fn main() {} diff --git a/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.stderr b/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.stderr new file mode 100644 index 000000000..c9d80e31f --- /dev/null +++ b/wp_derive_request_builder/tests/fail/namespace_attr_is_not_literal.stderr @@ -0,0 +1,5 @@ +error: Expecting #[Namespace("_path_")] - Try wrapping the path in "" + --> tests/fail/namespace_attr_is_not_literal.rs:2:13 + | +2 | #[Namespace(wp)] + | ^^ diff --git a/wp_derive_request_builder/tests/fail/not_equals_sign.rs b/wp_derive_request_builder/tests/fail/not_equals_sign.rs index efb6883c8..ab8968b58 100644 --- a/wp_derive_request_builder/tests/fail/not_equals_sign.rs +++ b/wp_derive_request_builder/tests/fail/not_equals_sign.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url : "/users", params = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/fail/not_equals_sign.stderr b/wp_derive_request_builder/tests/fail/not_equals_sign.stderr index 7b2fdc2c7..5b4550161 100644 --- a/wp_derive_request_builder/tests/fail/not_equals_sign.stderr +++ b/wp_derive_request_builder/tests/fail/not_equals_sign.stderr @@ -1,5 +1,5 @@ error: Did you mean '='? - --> tests/fail/not_equals_sign.rs:4:26 + --> tests/fail/not_equals_sign.rs:5:26 | -4 | #[contextual_get(url : "/users", params = &UserListParams, output = Vec)] +5 | #[contextual_get(url : "/users", params = &UserListParams, output = Vec)] | ^ diff --git a/wp_derive_request_builder/tests/fail/output_not_ident.rs b/wp_derive_request_builder/tests/fail/output_not_ident.rs index 5eb0e83d3..f8e757cdb 100644 --- a/wp_derive_request_builder/tests/fail/output_not_ident.rs +++ b/wp_derive_request_builder/tests/fail/output_not_ident.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", params = &UserListParams, "output" = Vec)] diff --git a/wp_derive_request_builder/tests/fail/output_not_ident.stderr b/wp_derive_request_builder/tests/fail/output_not_ident.stderr index feaede8ae..27416b39d 100644 --- a/wp_derive_request_builder/tests/fail/output_not_ident.stderr +++ b/wp_derive_request_builder/tests/fail/output_not_ident.stderr @@ -1,5 +1,5 @@ error: Expecting key value pairs (url = "", params = FooParam, output = FooOutput) - --> tests/fail/output_not_ident.rs:4:64 + --> tests/fail/output_not_ident.rs:5:64 | -4 | #[contextual_get(url = "/users", params = &UserListParams, "output" = Vec)] +5 | #[contextual_get(url = "/users", params = &UserListParams, "output" = Vec)] | ^^^^^^^^ diff --git a/wp_derive_request_builder/tests/fail/params_not_ident.rs b/wp_derive_request_builder/tests/fail/params_not_ident.rs index d4ecb9ea5..45f0828b4 100644 --- a/wp_derive_request_builder/tests/fail/params_not_ident.rs +++ b/wp_derive_request_builder/tests/fail/params_not_ident.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", "params" = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/fail/params_not_ident.stderr b/wp_derive_request_builder/tests/fail/params_not_ident.stderr index 73badd232..a30b0f723 100644 --- a/wp_derive_request_builder/tests/fail/params_not_ident.stderr +++ b/wp_derive_request_builder/tests/fail/params_not_ident.stderr @@ -1,5 +1,5 @@ error: Expecting key value pairs (url = "", params = FooParam, output = FooOutput) - --> tests/fail/params_not_ident.rs:4:38 + --> tests/fail/params_not_ident.rs:5:38 | -4 | #[contextual_get(url = "/users", "params" = &UserListParams, output = Vec)] +5 | #[contextual_get(url = "/users", "params" = &UserListParams, output = Vec)] | ^^^^^^^^ diff --git a/wp_derive_request_builder/tests/fail/url_not_ident.rs b/wp_derive_request_builder/tests/fail/url_not_ident.rs index 66c2d0cb9..a49e11e58 100644 --- a/wp_derive_request_builder/tests/fail/url_not_ident.rs +++ b/wp_derive_request_builder/tests/fail/url_not_ident.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get("url" = "/users", params = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/fail/url_not_ident.stderr b/wp_derive_request_builder/tests/fail/url_not_ident.stderr index 87cd2b99a..b7ffd5694 100644 --- a/wp_derive_request_builder/tests/fail/url_not_ident.stderr +++ b/wp_derive_request_builder/tests/fail/url_not_ident.stderr @@ -1,5 +1,5 @@ error: Expecting key value pairs (url = "", params = FooParam, output = FooOutput) - --> tests/fail/url_not_ident.rs:4:22 + --> tests/fail/url_not_ident.rs:5:22 | -4 | #[contextual_get("url" = "/users", params = &UserListParams, output = Vec)] +5 | #[contextual_get("url" = "/users", params = &UserListParams, output = Vec)] | ^^^^^ diff --git a/wp_derive_request_builder/tests/fail/url_value_not_literal.rs b/wp_derive_request_builder/tests/fail/url_value_not_literal.rs index cb7826539..8c4ac23ea 100644 --- a/wp_derive_request_builder/tests/fail/url_value_not_literal.rs +++ b/wp_derive_request_builder/tests/fail/url_value_not_literal.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = users, params = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/fail/url_value_not_literal.stderr b/wp_derive_request_builder/tests/fail/url_value_not_literal.stderr index e8ba0b19e..3caced9b1 100644 --- a/wp_derive_request_builder/tests/fail/url_value_not_literal.stderr +++ b/wp_derive_request_builder/tests/fail/url_value_not_literal.stderr @@ -1,5 +1,5 @@ error: Url should be set as a String: (url = "/foo") - --> tests/fail/url_value_not_literal.rs:4:28 + --> tests/fail/url_value_not_literal.rs:5:28 | -4 | #[contextual_get(url = users, params = &UserListParams, output = Vec)] +5 | #[contextual_get(url = users, params = &UserListParams, output = Vec)] | ^^^^^ diff --git a/wp_derive_request_builder/tests/pass/basic_derived_request.rs b/wp_derive_request_builder/tests/pass/basic_derived_request.rs index b6ab241b1..3798a70c3 100644 --- a/wp_derive_request_builder/tests/pass/basic_derived_request.rs +++ b/wp_derive_request_builder/tests/pass/basic_derived_request.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/pass/external_outer_attr.rs b/wp_derive_request_builder/tests/pass/external_outer_attr.rs new file mode 100644 index 000000000..54e5839df --- /dev/null +++ b/wp_derive_request_builder/tests/pass/external_outer_attr.rs @@ -0,0 +1,10 @@ +#[derive(wp_derive_request_builder::WpDerivedRequest, serde::Serialize)] +#[Namespace("/wp/v2")] +#[SparseField(crate::SparseUserField)] +#[serde(deny_unknown_fields)] +enum UsersRequest { + #[contextual_get(url = "/users", params = &UserListParams, output = Vec)] + List, +} + +fn main() {} diff --git a/wp_derive_request_builder/tests/pass/multi_segment_output.rs b/wp_derive_request_builder/tests/pass/multi_segment_output.rs index e1bb054cc..31c5806fc 100644 --- a/wp_derive_request_builder/tests/pass/multi_segment_output.rs +++ b/wp_derive_request_builder/tests/pass/multi_segment_output.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", output = std::vec::Vec)] diff --git a/wp_derive_request_builder/tests/pass/multi_segment_params.rs b/wp_derive_request_builder/tests/pass/multi_segment_params.rs index ebe67f477..3f076587a 100644 --- a/wp_derive_request_builder/tests/pass/multi_segment_params.rs +++ b/wp_derive_request_builder/tests/pass/multi_segment_params.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", params = &crate::UserListParams, output = Vec)] diff --git a/wp_derive_request_builder/tests/pass/optional_params.rs b/wp_derive_request_builder/tests/pass/optional_params.rs index cfda1dca1..3331d0c2f 100644 --- a/wp_derive_request_builder/tests/pass/optional_params.rs +++ b/wp_derive_request_builder/tests/pass/optional_params.rs @@ -1,4 +1,5 @@ #[derive(wp_derive_request_builder::WpDerivedRequest)] +#[Namespace("/wp/v2")] #[SparseField(crate::SparseUserField)] enum UsersRequest { #[contextual_get(url = "/users", output = Vec)]