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
3 changes: 3 additions & 0 deletions wp_api/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ pub struct SparseUser {
#[WPContext(edit)]
pub extra_capabilities: Option<HashMap<String, bool>>,
#[WPContext(edit, embed, view)]
// According to our tests, `avatar_urls` is not available for all site types. It's marked with
// `#[WPContextualOption]` which will make it an `Option` in the generated contextual types.
#[WPContextualOption]
pub avatar_urls: Option<HashMap<String, String>>,
// meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/57
}
Expand Down
41 changes: 40 additions & 1 deletion wp_contextual/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,35 @@
//! `PostContentWithViewContext` which are the types that we used in our manually typed
//! example.
//!
//! ---
//!
//! In some instances, we might need a field to be included in contextual types, but still be an
//! `Option`. In these cases, `WPContextualOption` attribute can be used:
//!
//! ```
//! # use wp_contextual::WPContextual;
//! # use std::collections::HashMap;
//! #[derive(WPContextual)]
//! pub struct SparseUser {
//! #[WPContext(edit)]
//! #[WPContextualOption]
//! pub avatar_urls: Option<HashMap<String, String>>,
//! }
//! # // We need these 2 lines for UniFFI
//! # uniffi::setup_scaffolding!();
//! # fn main() {}
//! ```
//!
//! This will generate the following:
//!
//! ```
//! # use std::collections::HashMap;
//! pub struct UserWithEditContext {
//! pub avatar_urls: Option<HashMap<String, String>>,
//! }
//! ```
//! ---
//!
//! Please see the documentation for [`WPContextual`] for technical details.
use proc_macro::TokenStream;
use syn::parse_macro_input;
Expand All @@ -255,6 +284,7 @@ mod wp_contextual;
/// [`WPContextual`] type. This will tell the compiler to replace the given `Option<SparseBaz>`
/// type with the appropriate contextual type: `BazWithEditContext`, `BazWithEmbedContext` or
/// `BazWithViewContext`.
/// * `[WPContextualOption]` is used to tell the compiler to keep the field's `Option` type.
/// * Generated types will have the following derive macros:
/// `#[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)]`. These types are meant
/// to be used for the
Expand All @@ -277,6 +307,9 @@ mod wp_contextual;
/// #[WPContext(edit)]
/// #[WPContextualField]
/// pub baz: Option<SparseBaz>,
/// #[WPContext(view)]
/// #[WPContextualOption]
/// pub foo_bar: Option<String>,
/// }
///
/// #[derive(WPContextual)]
Expand Down Expand Up @@ -306,6 +339,7 @@ mod wp_contextual;
/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)]
/// pub struct FooWithViewContext {
/// pub bar: u32,
/// pub foo_bar: Option<String>
/// }
/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)]
/// pub struct BazWithEditContext {
Expand All @@ -320,7 +354,12 @@ mod wp_contextual;
/// * Notice that `BazWithEmbedContext` & `BazWithViewContext` types weren't generated since
/// they wouldn't have any fields.
/// * Notice the type for `qux: Vec<u32>` was preserved as this wasn't an `Option<T>` type.
#[proc_macro_derive(WPContextual, attributes(WPContext, WPContextualField))]
/// * Notice the type for `foo_bar: Option<String>` was preserved since it's marked with
/// `#[WPContextualOption]`.
#[proc_macro_derive(
WPContextual,
attributes(WPContext, WPContextualField, WPContextualOption)
)]
pub fn derive(input: TokenStream) -> TokenStream {
wp_contextual::wp_contextual(parse_macro_input!(input))
.unwrap_or_else(|err| err.into_compile_error().into())
Expand Down
108 changes: 81 additions & 27 deletions wp_contextual/src/wp_contextual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use syn::{spanned::Spanned, DeriveInput, Ident};

const IDENT_PREFIX: &str = "Sparse";

// TODO: Public documentation
pub fn wp_contextual(ast: DeriveInput) -> Result<TokenStream, syn::Error> {
let original_ident = &ast.ident;
let original_ident_name = original_ident.to_string();
Expand Down Expand Up @@ -72,6 +71,10 @@ fn struct_fields(
// contexts.
// * `WPContextualParseError::WPContextualFieldWithoutWPContext`: #[WPContextualField] is added to
// a field that doesn't have the #[WPContext] attribute.
// * `WPContextualParseError::WPContextualOptionWithoutWPContext`: #[WPContextualOption] is added to
// a field that doesn't have the #[WPContext] attribute.
// * `WPContextualParseError::WPContextualBothOptionAndField`: #[WPContextualField] and
// #[WPContextualOption] attributes were used together.
//
// It'll also handle incorrectly formatted #[WPContext] attribute through
// `parse_contexts_from_tokens` helper.
Expand All @@ -98,6 +101,9 @@ fn parse_fields(
if is_wp_contextual_field_ident(segment_ident) {
return Ok(WPParsedAttr::ParsedWPContextualField);
}
if is_wp_contextual_option_ident(segment_ident) {
return Ok(WPParsedAttr::ParsedWPContextualOption);
}
if is_wp_context_ident(segment_ident) {
if let syn::Meta::List(meta_list) = &attr.meta {
let contexts = parse_contexts_from_tokens(meta_list.tokens.clone())?;
Expand All @@ -118,24 +124,54 @@ fn parse_fields(
})
.collect::<Result<Vec<WPParsedField>, syn::Error>>()?;

let assert_has_wp_context_attribute_if_it_has_given_attribute =
|attribute_to_check: WPParsedAttr, error_type: WPContextualParseError| {
if let Some(pf) = parsed_fields
.iter()
.filter(|pf| pf.parsed_attrs.contains(&attribute_to_check))
.find(|pf| {
!pf.parsed_attrs.iter().any(|pf| match pf {
WPParsedAttr::ParsedWPContext { contexts } => !contexts.is_empty(),
_ => false,
})
})
{
Err(error_type.into_syn_error(pf.field.span()))
} else {
Ok(())
}
};

// Check if there are any fields that has #[WPContextualField] attribute,
// but not the #[WPContext] attribute
if let Some(pf) = parsed_fields
.iter()
.filter(|pf| {
pf.parsed_attrs
.contains(&WPParsedAttr::ParsedWPContextualField)
})
.find(|pf| {
!pf.parsed_attrs.iter().any(|pf| match pf {
WPParsedAttr::ParsedWPContext { contexts } => !contexts.is_empty(),
_ => false,
})
})
{
return Err(WPContextualParseError::WPContextualFieldWithoutWPContext
.into_syn_error(pf.field.span()));
};
assert_has_wp_context_attribute_if_it_has_given_attribute(
WPParsedAttr::ParsedWPContextualField,
WPContextualParseError::WPContextualFieldWithoutWPContext,
)?;

// Check if there are any fields that has #[WPContextualField] attribute,
// but not the #[WPContext] attribute
assert_has_wp_context_attribute_if_it_has_given_attribute(
WPParsedAttr::ParsedWPContextualOption,
WPContextualParseError::WPContextualOptionWithoutWPContext,
)?;

// Check if there are any fields that has both #[WPContextualField] & #[WPContextualOption]
// attributes and return an error. These attributes are incompatible with each other because
// #[WPContextualOption] will leave the type as is, whereas #[WPContextualField] will modify
// it.
if let Some(pf) = parsed_fields.iter().find(|pf| {
pf.parsed_attrs
.contains(&WPParsedAttr::ParsedWPContextualField)
&& pf
.parsed_attrs
.contains(&WPParsedAttr::ParsedWPContextualOption)
}) {
return Err(
WPContextualParseError::WPContextualBothOptionAndField.into_syn_error(pf.field.span())
);
}

Ok(parsed_fields)
}

Expand All @@ -161,16 +197,25 @@ fn generate_context_fields(
})
.map(|pf| {
let f = &pf.field;
let mut new_type = extract_inner_type_of_option(&f.ty).unwrap_or(f.ty.clone());
if f.attrs.iter().any(|attr| {
attr.path()
.segments
.iter()
.any(|s| is_wp_contextual_field_ident(&s.ident))
}) {
// If the field has #[WPContextualField] attr, map it to its contextual field type
new_type = contextual_field_type(&new_type, context)?;
}

let new_type = if pf
.parsed_attrs
.contains(&WPParsedAttr::ParsedWPContextualOption)
{
f.ty.clone()
} else {
let mut new_type = extract_inner_type_of_option(&f.ty).unwrap_or(f.ty.clone());
if f.attrs.iter().any(|attr| {
attr.path()
.segments
.iter()
.any(|s| is_wp_contextual_field_ident(&s.ident))
}) {
// If the field has #[WPContextualField] attr, map it to its contextual field type
new_type = contextual_field_type(&new_type, context)?;
}
new_type
};
Ok::<syn::Field, syn::Error>(syn::Field {
// Remove the WPContext & WPContextualField attributes from the generated field
attrs: pf
Expand Down Expand Up @@ -383,6 +428,10 @@ fn is_wp_contextual_field_ident(ident: &Ident) -> bool {
ident.to_string().eq("WPContextualField")
}

fn is_wp_contextual_option_ident(ident: &Ident) -> bool {
ident.to_string().eq("WPContextualOption")
}

// ```
// #[WPContextual]
// pub struct SparseFoo {
Expand Down Expand Up @@ -431,6 +480,7 @@ struct WPParsedField {
#[derive(Debug, PartialEq, Eq)]
enum WPParsedAttr {
ParsedWPContextualField,
ParsedWPContextualOption,
ParsedWPContext { contexts: Vec<WPContextAttr> },
ExternalAttr { attr: syn::Attribute },
}
Expand Down Expand Up @@ -488,6 +538,8 @@ enum WPContextualParseError {
"WPContextual didn't generate anything. Did you forget to add #[WPContext] attribute?"
)]
EmptyResult,
#[error("#[WPContextualField] & #[WPContextualOption] can't be used together")]
WPContextualBothOptionAndField,
#[error(
"WPContextualField field types need to start with '{}' prefix",
IDENT_PREFIX
Expand All @@ -501,6 +553,8 @@ enum WPContextualParseError {
WPContextualMissingSparsePrefix,
#[error("#[WPContextual] is only implemented for Structs")]
WPContextualNotAStruct,
#[error("#[WPContextualOption] doesn't have any contexts. Did you forget to add #[WPContext] attribute?")]
WPContextualOptionWithoutWPContext,
}

impl WPContextualParseError {
Expand Down
3 changes: 3 additions & 0 deletions wp_contextual/tests/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/basic_wp_contextual.rs");
t.pass("tests/basic_wp_contextual_field.rs");
t.pass("tests/basic_wp_contextual_option.rs");
t.pass("tests/wp_contextual_field_with_multiple_segments.rs");
t.pass("tests/wp_contextual_field_with_inner_type.rs");
t.compile_fail("tests/error_both_wp_contextual_field_and_wp_contextual_option.rs");
t.compile_fail("tests/error_missing_sparse_prefix_from_wp_contextual.rs");
t.compile_fail("tests/error_missing_sparse_prefix_from_wp_contextual_field.rs");
t.compile_fail("tests/error_empty_result.rs");
Expand All @@ -15,5 +17,6 @@ fn tests() {
t.compile_fail("tests/error_unexpected_wp_context_literal.rs");
t.compile_fail("tests/error_unexpected_wp_context_token.rs");
t.compile_fail("tests/error_wp_contextual_field_without_wp_context.rs");
t.compile_fail("tests/error_wp_contextual_option_without_wp_context.rs");
t.compile_fail("tests/error_wp_contextual_not_a_struct.rs");
}
14 changes: 14 additions & 0 deletions wp_contextual/tests/basic_wp_contextual_option.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use wp_contextual::WPContextual;

#[derive(WPContextual)]
pub struct SparseFoo {
#[WPContext(edit)]
#[WPContextualOption]
pub bar: Option<u32>,
}

fn main() {
let _ = FooWithEditContext { bar: None };
}

uniffi::setup_scaffolding!();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use wp_contextual::WPContextual;

#[derive(WPContextual)]
pub struct SparseFoo {
#[WPContext(edit)]
#[WPContextualField]
#[WPContextualOption]
pub bar: Option<u32>,
}

fn main() {}

uniffi::setup_scaffolding!();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: #[WPContextualField] & #[WPContextualOption] can't be used together
--> tests/error_both_wp_contextual_field_and_wp_contextual_option.rs:5:5
|
5 | #[WPContext(edit)]
| ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use wp_contextual::WPContextual;

#[derive(WPContextual)]
pub struct SparseFoo {
#[WPContextualOption]
pub bar: Option<u32>,
}

fn main() {}

uniffi::setup_scaffolding!();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: #[WPContextualOption] doesn't have any contexts. Did you forget to add #[WPContext] attribute?
--> tests/error_wp_contextual_option_without_wp_context.rs:5:5
|
5 | #[WPContextualOption]
| ^