diff --git a/Cargo.lock b/Cargo.lock index 981c9b96a..b0b9995c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,9 +150,9 @@ checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "basic-toml" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" dependencies = [ "serde", ] @@ -299,6 +299,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "dissimilar" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -813,18 +819,18 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -993,18 +999,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -1013,15 +1019,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1089,9 +1104,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.39" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -1132,6 +1147,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -1227,6 +1251,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1258,6 +1316,22 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa6f84ec205ebf87fb7a0abdbcd1467fa5af0e86878eb6d888b78ecbb10b6d5" +dependencies = [ + "dissimilar", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml 0.8.12", +] + [[package]] name = "unicase" version = "2.7.0" @@ -1334,7 +1408,7 @@ dependencies = [ "paste", "serde", "textwrap", - "toml", + "toml 0.5.11", "uniffi_meta", "uniffi_testing", "uniffi_udl", @@ -1391,7 +1465,7 @@ dependencies = [ "quote", "serde", "syn", - "toml", + "toml 0.5.11", "uniffi_meta", ] @@ -1578,6 +1652,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1716,6 +1799,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -1747,6 +1839,19 @@ dependencies = [ "wp_networking", ] +[[package]] +name = "wp_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "thiserror", + "trybuild", + "uniffi", +] + [[package]] name = "wp_networking" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ed0b883f6..18a27f5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,15 +2,15 @@ members = [ "wp_api", "wp_cli", + "wp_derive", "wp_networking", "wp_uniffi_bindgen", ] resolver = "2" [workspace.dependencies] -# External traits in UDL is only available since https://github.com/mozilla/uniffi-rs/pull/1867 -# Point to a specific commit until a new version is released uniffi = "0.27.0" +thiserror = "1.0" # We need to match the version used in `reqwest`: https://github.com/seanmonstar/reqwest/blob/master/Cargo.toml#L84 # So that we can implement our own traits and it'll work with both reqwest types as well as http types # diff --git a/wp_api/Cargo.toml b/wp_api/Cargo.toml index 1f0703a06..787e92fad 100644 --- a/wp_api/Cargo.toml +++ b/wp_api/Cargo.toml @@ -13,7 +13,7 @@ url = "2.5" parse_link_header = "0.3" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" -thiserror = "1.0" +thiserror = { workspace = true } uniffi = { workspace = true } [build-dependencies] diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 3b600b9e3..1d4829b9a 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -192,4 +192,4 @@ pub fn get_link_header(response: &WPNetworkResponse, name: &str) -> Option Self { + Self { + page: 1, + per_page: 10, + } + } +} + #[derive(uniffi::Record)] pub struct PostCreateParams { pub title: Option, diff --git a/wp_derive/Cargo.toml b/wp_derive/Cargo.toml new file mode 100644 index 000000000..73c5d36d6 --- /dev/null +++ b/wp_derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "wp_derive" +version = "0.1.0" +edition = "2021" +autotests = false + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/all_tests.rs" + +[dev-dependencies] +trybuild = { version = "1.0", features = ["diff"] } + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +serde = { version = "1.0", features = [ "derive" ] } +syn = { version = "2.0", features = ["extra-traits"] } +thiserror = { workspace = true } +uniffi = { workspace = true } diff --git a/wp_derive/src/lib.rs b/wp_derive/src/lib.rs new file mode 100644 index 000000000..66c9e85b3 --- /dev/null +++ b/wp_derive/src/lib.rs @@ -0,0 +1,327 @@ +//! [WordPress.org REST API](https://developer.wordpress.org/rest-api/reference/) endpoints accept +//! a `context` argument - "edit", "embed" or "view" (default) - which will dictate which fields will be +//! returned in the response. This is in addition to the `_filter` argument which lets developers +//! pick the returned fields one by one. +//! +//! This present a challenge in parsing the response because there are several schema variations +//! for each endpoint. One basic option to get around this would be to use a type where each field +//! is an [`Option`]. However, this would force developers to deal with nullability when they +//! shouldn't have to. The alternative would be to have a type for each known variation. The goal +//! of [`WPContextual`] derive macro is to simplify this process. +//! +//! --- +//! +//! Let's work through a simplified example to better understand why we need this macro and how to +//! work with it. +//! +//! The [`/posts` endpoint](https://developer.wordpress.org/rest-api/reference/posts/) +//! lists the following fields, among many others: +//! +//! | Field | Description | +//! | :---: | ----------- | +//! | id | JSON data type: `integer`
Context: `view`, `edit`, `embed` | +//! | content | JSON data type: `object`
Context: `view`, `edit` | +//! +//! Although it's not documented, here are some of the fields for `content`: +//! +//! | Field | Description | +//! | :---: | ----------- | +//! | raw | Context: `view`, `edit` | +//! | rendered | Context: `view` | +//! +//! One possible approach to parse these would be to use the following types: +//! +//! ``` +//! pub struct Post { +//! pub id: Option, +//! pub content: Option, +//! } +//! pub struct PostContent { +//! pub raw: Option, +//! pub rendered: Option +//! } +//! ``` +//! +//! We can use these types to parse the response for every context type. However, consider the case +//! where we make a request for `edit` context. Even though we know that, all these fields are +//! present in the response, we'll have to work with nullable values and unwrap them in the +//! consumer which is error prone. +//! +//! If we want to have non-optional context based types, it could look like this: +//! +//! ``` +//! pub struct PostWithEditContext { +//! pub id: u32, +//! pub content: PostContentWithEditContext, +//! } +//! pub struct PostWithEmbedContext { +//! pub id: u32, +//! } +//! pub struct PostWithViewContext { +//! pub id: u32, +//! pub content: PostContentWithViewContext, +//! } +//! pub struct PostContentWithEditContext { +//! pub raw: String, +//! pub rendered: String +//! } +//! pub struct PostContentWithViewContext { +//! pub rendered: String +//! } +//! ``` +//! +//! This is a much better solution, because we have strongly typed values. However, that's a lot +//! of code just for 2 types with only 2 fields. Furthermore, it's very difficult to tell which +//! fields are available for each `context` and has a higher maintainance cost. +//! +//! In order to address this, we have [`WPContextual`] derive macro. Let's start from simple, and +//! build up to the example: +//! +//! ``` +//! # use wp_derive::WPContextual; +//! #[derive(WPContextual)] +//! pub struct SparsePostContent { +//! #[WPContext(edit)] +//! pub raw: Option, +//! #[WPContext(edit, view)] +//! pub rendered: Option, +//! } +//! +//! # // We need these 2 lines for UniFFI +//! # uniffi::setup_scaffolding!(); +//! # fn main() {} +//! ``` +//! +//! If we look at only the type information, it is almost identical to our first approach: +//! +//! ``` +//! pub struct SparsePostContent { +//! pub raw: Option, +//! pub rendered: Option +//! } +//! ``` +//! +//! The only difference is that the new type starts with the `Sparse` prefix. This is a requirement +//! of [`WPContextual`]. `Sparse` is prefix commonly used to describe types that only have some of +//! the elements present. We have decided to add this requirement to convey this information, to +//! have consistency among our types and most importantly to avoid developers accidentally +//! defaulting to the sparse types instead of the strongly typed objects that are generated by the +//! derive macro. +//! +//! When we derive [`WPContextual`] for this `SparsePostContent` type we are telling the compiler +//! that we want to generate 3 new types from it: `PostContentWithEditContext`, +//! `PostContentWithEmbedContext`, `PostContentWithViewContext`. However, this is not enough, +//! because the compiler also needs to know which fields should be present for each field. This is +//! why we also need the `WPContext` attribute. Here is what the compiler will generate for this +//! type: +//! ``` +//! # use wp_derive::WPContextual; +//! #[derive(WPContextual)] +//! pub struct SparsePostContent { +//! #[WPContext(edit)] +//! pub raw: Option, +//! #[WPContext(edit, view)] +//! pub rendered: Option, +//! } +//! # // We need these 2 lines for UniFFI +//! # uniffi::setup_scaffolding!(); +//! # fn main() {} +//! ``` +//! +//! Given the above type, the compiler will generate these 2 types: +//! +//! ``` +//! pub struct PostContentWithEditContext { +//! pub raw: String, +//! pub rendered: String +//! } +//! pub struct PostContentWithViewContext { +//! pub rendered: String +//! } +//! +//! ``` +//! +//! You should note a few things: +//! * `PostContentWithEmbedContext` type is missing. That's because, in this example, there are +//! no fields in `SparsePostContent` that's available in `embed` context. +//! * The type of generated fields are `String` instead of `Option` which is exactly what +//! we did when we manually wrote these types. +//! * The information about which fields are available for each context are right there on top of +//! the fields, so the syntax acts as a documentation. +//! +//! Let's do the same for `Post` type as well: +//! +//! ``` +//! # use wp_derive::WPContextual; +//! #[derive(WPContextual)] +//! pub struct SparsePost { +//! #[WPContext(edit, embed, view)] +//! pub id: Option, +//! #[WPContext(edit)] +//! ##[WPContextualField] +//! pub content: Option, +//! } +//! # #[derive(WPContextual)] +//! # pub struct SparsePostContent { +//! # #[WPContext(edit)] +//! # pub raw: Option, +//! # #[WPContext(edit, view)] +//! # pub rendered: Option, +//! # } +//! # // We need these 2 lines for UniFFI +//! # uniffi::setup_scaffolding!(); +//! # fn main() {} +//! ``` +//! +//! Given the above type and the `SparsePostContent` which was omitted for brevity, the compiler +//! will generate the following types: +//! +//! ``` +//! pub struct PostWithEditContext { +//! pub id: u32, +//! pub content: SparsePostContent, +//! } +//! pub struct PostWithEmbedContext { +//! pub id: u32, +//! } +//! pub struct PostWithViewContext { +//! pub id: u32, +//! pub content: SparsePostContent, +//! } +//! # struct SparsePostContent {} +//! ``` +//! +//! This doesn't look correct. Notice the type of the `content` field is `SparsePostContent` in +//! each case. This is because the compiler doesn't know that `SparsePostContent` is also a +//! [`WPContextual`] type. We can help the compiler by adding the `WPContextualField` attribute: +//! +//! ``` +//! # use wp_derive::WPContextual; +//! #[derive(WPContextual)] +//! pub struct SparsePost { +//! #[WPContext(edit, embed, view)] +//! pub id: Option, +//! #[WPContext(edit)] +//! #[WPContextualField] +//! pub content: Option, +//! } +//! # #[derive(WPContextual)] +//! # pub struct SparsePostContent { +//! # #[WPContext(edit)] +//! # pub raw: Option, +//! # #[WPContext(edit, view)] +//! # pub rendered: Option, +//! # } +//! # // We need these 2 lines for UniFFI +//! # uniffi::setup_scaffolding!(); +//! # fn main() {} +//! ``` +//! +//! This will generate the following instead: +//! +//! ``` +//! pub struct PostWithEditContext { +//! pub id: u32, +//! pub content: PostContentWithEditContext, +//! } +//! pub struct PostWithEmbedContext { +//! pub id: u32, +//! } +//! pub struct PostWithViewContext { +//! pub id: u32, +//! pub content: PostContentWithViewContext, +//! } +//! # struct PostContentWithEditContext {} +//! # struct PostContentWithViewContext {} +//! ``` +//! +//! Notice that the types of the `content` fields are now `PostContentWithEditContext` & +//! `PostContentWithViewContext` which are the types that we used in our manually typed +//! example. +//! +//! Please see the documentation for [`WPContextual`] for technical details. +use proc_macro::TokenStream; +use syn::parse_macro_input; + +mod wp_contextual; + +/// Given `SparseFoo`, it will generate `FooWithEditContext`, `FooWithEmbedContext` & +/// `FooWithViewContext` types by turning `Option` fields into `T` +/// +/// * `[WPContextual]` types have to start `Sparse` prefix. This is a design decision we have made +/// to keep our type names descriptive and consistent. +/// * `[WPContext]` attribute is used to describe which `context`s the field belongs to. +/// * `[WPContextualField]` is used when a [`WPContextual`] type is a **field** of another +/// [`WPContextual`] type. This will tell the compiler to replace the given `Option` +/// type with the appropriate contextual type: `BazWithEditContext`, `BazWithEmbedContext` or +/// `BazWithViewContext`. +/// * 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 +/// [WordPress.org REST API](https://developer.wordpress.org/rest-api/reference/), so +/// [`serde::Serialize`] and [`serde::Deserialize`] are needed for parsing. We also would like +/// to use these types through FFI, so they need to derive [`uniffi::Record`]. +/// * If the generated type won't have any fields, that type will not be generated. +/// * If a **field** type is not `Option`, it'll not be altered. Only `Option` fields turn +/// into `T`. +/// +/// Here is a full example: +/// +/// ``` +/// use wp_derive::WPContextual; +/// +/// #[derive(WPContextual)] +/// pub struct SparseFoo { +/// #[WPContext(edit, embed, view)] +/// pub bar: Option, +/// #[WPContext(edit)] +/// #[WPContextualField] +/// pub baz: Option, +/// } +/// +/// #[derive(WPContextual)] +/// pub struct SparseBaz { +/// #[WPContext(edit)] +/// pub baz: Option, +/// #[WPContext(edit)] +/// pub qux: Vec, +/// } +/// # // We need these 2 lines for UniFFI +/// # uniffi::setup_scaffolding!(); +/// # fn main() {} +/// ``` +/// +/// Given above, compiler will generate the following types: +/// +/// ``` +/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] +/// pub struct FooWithEditContext { +/// pub bar: u32, +/// pub baz: BazWithEditContext, +/// } +/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] +/// pub struct FooWithEmbedContext { +/// pub bar: u32, +/// } +/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] +/// pub struct FooWithViewContext { +/// pub bar: u32, +/// } +/// #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] +/// pub struct BazWithEditContext { +/// pub baz: String, +/// pub qux: Vec, +/// } +/// # // We need these 2 lines for UniFFI +/// # uniffi::setup_scaffolding!(); +/// # fn main() {} +/// ``` +/// +/// * Notice that `BazWithEmbedContext` & `BazWithViewContext` types weren't generated since +/// they wouldn't have any fields. +/// * Notice the type for `qux: Vec` was preserved as this wasn't an `Option` type. +#[proc_macro_derive(WPContextual, attributes(WPContext, WPContextualField))] +pub fn derive(input: TokenStream) -> TokenStream { + wp_contextual::wp_contextual(parse_macro_input!(input)) + .unwrap_or_else(|err| err.into_compile_error().into()) +} diff --git a/wp_derive/src/main.rs b/wp_derive/src/main.rs new file mode 100644 index 000000000..eb9777ec7 --- /dev/null +++ b/wp_derive/src/main.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; +use wp_derive::WPContextual; + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostObject { + #[serde(rename(serialize = "ser_name"))] + #[WPContext(edit, view, embed)] + pub id: Option, + #[WPContext(edit)] + pub date: Option, + #[WPContext(embed)] + pub embed_date: Option, + #[WPContext(edit, view, embed)] + pub already_strongly_typed: u32, + #[WPContext(edit, view)] + #[WPContextualField] + pub guid: Option, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct PostGuid { + pub raw: Option, + pub rendered: Option, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostGuid { + #[WPContext(edit)] + pub raw: Option, + #[WPContext(edit, view)] + pub rendered: Option, +} + +uniffi::setup_scaffolding!(); + +fn main() {} diff --git a/wp_derive/src/wp_contextual.rs b/wp_derive/src/wp_contextual.rs new file mode 100644 index 000000000..af821c8c7 --- /dev/null +++ b/wp_derive/src/wp_contextual.rs @@ -0,0 +1,635 @@ +use std::{fmt::Display, slice::Iter, str::FromStr}; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, DeriveInput, Ident}; + +const IDENT_PREFIX: &str = "Sparse"; + +// TODO: Public documentation +pub fn wp_contextual(ast: DeriveInput) -> Result { + let original_ident = &ast.ident; + let original_ident_name = original_ident.to_string(); + + let ident_name_without_prefix = original_ident_name.strip_prefix(IDENT_PREFIX).ok_or( + WPContextualParseError::WPContextualMissingSparsePrefix + .into_syn_error(original_ident.span()), + )?; + let fields = + struct_fields(&ast.data).map_err(|err| err.into_syn_error(original_ident.span()))?; + let parsed_fields = parse_fields(fields)?; + + let contextual_token_streams = WPContextAttr::iter().map(|current_context| { + let cname = ident_name_for_context(ident_name_without_prefix, current_context); + let cident = Ident::new(&cname, original_ident.span()); + let cfields = generate_context_fields(&parsed_fields, current_context)?; + if !cfields.is_empty() { + Ok(quote! { + #[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)] + pub struct #cident { + #(#cfields,)* + } + } + .into()) + } else { + Ok(proc_macro::TokenStream::new()) + } + }); + contextual_token_streams + .collect::, syn::Error>>() + .map(TokenStream::from_iter) + .and_then(|t: TokenStream| { + if t.is_empty() { + Err(WPContextualParseError::EmptyResult.into_syn_error(original_ident.span())) + } else { + Ok(t) + } + }) +} + +// Validate that the given `data` is a `syn::Data::Struct` and extracts the fields from it +fn struct_fields( + data: &syn::Data, +) -> Result<&syn::punctuated::Punctuated, WPContextualParseError> { + if let syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }), + .. + }) = data + { + Ok(named) + } else { + Err(WPContextualParseError::WPContextualNotAStruct) + } +} + +// Turns a list of `syn::Field`s to a list of `WPParsedField`s by parsing its attributes. +// +// The following errors are directly handled by this function: +// * `WPContextualParseAttrError::UnexpectedAttrPathSegmentCount`: The attribute path has multiple +// segments, separated by `::`. This case doesn't seem to be a valid syntax regardless of how +// we handle it, but it's handled as an error just in case. +// * `WPContextualParseAttrError::MissingWPContextMeta`: #[WPContext] attribute doesn't have any +// contexts. +// * `WPContextualParseError::WPContextualFieldWithoutWPContext`: #[WPContextualField] is added to +// a field that doesn't have the #[WPContext] attribute. +// +// It'll also handle incorrectly formatted #[WPContext] attribute through +// `parse_contexts_from_tokens` helper. +fn parse_fields( + fields: &syn::punctuated::Punctuated, +) -> Result, syn::Error> { + let parsed_fields = fields + .iter() + .map(|f| { + let parsed_attrs = f + .attrs + .iter() + .map(|attr| { + if attr.path().segments.len() != 1 { + return Err(WPContextualParseAttrError::UnexpectedAttrPathSegmentCount + .into_syn_error(attr.path().span())); + } + let path_segment = attr + .path() + .segments + .first() + .expect("Already validated that there is only one segment"); + let segment_ident = &path_segment.ident; + if is_wp_contextual_field_ident(segment_ident) { + return Ok(WPParsedAttr::ParsedWPContextualField); + } + 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())?; + Ok(WPParsedAttr::ParsedWPContext { contexts }) + } else { + Err(WPContextualParseAttrError::MissingWPContextMeta + .into_syn_error(attr.meta.span())) + } + } else { + Ok(WPParsedAttr::ExternalAttr { attr: attr.clone() }) + } + }) + .collect::, syn::Error>>()?; + Ok(WPParsedField { + field: f.clone(), + parsed_attrs, + }) + }) + .collect::, syn::Error>>()?; + + // 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())); + }; + Ok(parsed_fields) +} + +// Generates fields for the given context. +// +// It'll filter out any fields that don't have the given context, handle any mappings due to +// #[WPContextualField] attribute and remove #[WPContext] and #[WPContextualField] attributes. +fn generate_context_fields( + parsed_fields_attrs: &[WPParsedField], + context: &WPContextAttr, +) -> Result, syn::Error> { + parsed_fields_attrs + .iter() + .filter(|pf| { + // Filter out any field that doesn't have this context + pf.parsed_attrs.iter().any(|parsed_attr| { + if let WPParsedAttr::ParsedWPContext { contexts } = parsed_attr { + contexts.iter().any(|c| c == context) + } else { + false + } + }) + }) + .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)?; + } + Ok::(syn::Field { + // Remove the WPContext & WPContextualField attributes from the generated field + attrs: pf + .parsed_attrs + .iter() + .filter_map(|parsed_attr| { + // The generated field should only contain external attributes + if let WPParsedAttr::ExternalAttr { attr } = parsed_attr { + Some(attr.to_owned()) + } else { + None + } + }) + .collect(), + vis: f.vis.clone(), + mutability: syn::FieldMutability::None, + ident: f.ident.clone(), + colon_token: f.colon_token, + ty: new_type, + }) + }) + .collect() +} + +// Returns a contextual type for the given type. +// +// ``` +// #[derive(WPContextual)] +// pub struct SparseFoo { +// #[WPContext(edit)] +// #[WPContextualField] +// pub bar: Option, +// } +// +// #[WPContextual] +// pub struct SparseBar { +// #[WPContext(edit)] +// pub baz: Option, +// } +// ``` +// +// Given the above, we'd like to generate: +// +// ``` +// pub struct FooWithEditContext { +// pub bar: BarWithEditContext, +// } +// +// pub struct BarWithEditContext { +// pub baz: u32, +// } +// ``` +// +// In this case, this function takes the `Option` type and `&WPContextAttr::Edit` +// and turns it into `BarWithEditContext` type. +fn contextual_field_type(ty: &syn::Type, context: &WPContextAttr) -> Result { + let mut ty = ty.clone(); + let inner_segment = find_contextual_field_inner_segment(&mut ty)?; + let ident_name_without_prefix = match inner_segment.ident.to_string().strip_prefix(IDENT_PREFIX) + { + Some(ident) => Ok(ident.to_string()), + None => Err(WPContextualParseError::WPContextualFieldMissingSparsePrefix + .into_syn_error(inner_segment.ident.span())), + }?; + inner_segment.ident = Ident::new( + &ident_name_for_context(&ident_name_without_prefix, context), + inner_segment.ident.span(), + ); + Ok(ty) +} + +// This is a recursive function that finds the inner path segment of a #[WPContextualField]. +// +// There are many cases that are not supported by #[WPContextualField] mainly because these cases +// are also not supported by UniFFI and/or serde. For example, we can't use tuples, references, +// functions etc as a #[WPContextualField], but we also wouldn't expect to. +// +// The main cases we need to handle are Option and Option> types where +// the API is either returning a single object or a list of objects. +// +// By making this a recursive function, we also handle cases such as Option>>, +// but it's very unlikely that this case will ever be used. +// We also support multiple segments, such as Option>. +// +// The error returned from this function is a generic one, pointing out that +// we don't support the given type. In this case, this is our best option because +// building an exhaustive list of errors is neither feasible nor useful. +fn find_contextual_field_inner_segment( + ty: &mut syn::Type, +) -> Result<&mut syn::PathSegment, syn::Error> { + let unsupported_err = + WPContextualParseError::WPContextualFieldTypeNotSupported.into_syn_error(ty.span()); + if let syn::Type::Path(ref mut p) = ty { + // A `syn::Type::Path` has to have at least one segment. + assert!(!p.path.segments.is_empty()); + + // If it has multiple segments, we are only interested in modifying the last one. + // + // Consider the following: + // ``` + // #[derive(WPContextual)] + // pub struct SparseFoo { + // #[WPContext(edit)] + // #[WPContextualField] + // pub bar: Option, + // #[WPContext(view)] + // #[WPContextualField] + // pub baz: Option, + // } + // ``` + // + // `SparseBar` only has one segment and `baz::SparseBaz` has two segments. In each case, + // we want to get the last segment, drop the `Sparse` prefix and attach the `With{}Context` + // postfix to it, depending on the context. In this case, the resulting generated code + // should look like the following: + // + // pub struct FooWithEditContext { + // pub bar: BarWithEditContext, + // } + // + // pub struct FooWithViewContext { + // pub baz: baz::BazWithEditContext, + // } + // ``` + let segment: &mut syn::PathSegment = p.path.segments.last_mut().unwrap(); + + match segment.arguments { + // No inner type + // + // ``` + // #[derive(WPContextual)] + // pub struct SparseFoo { + // #[WPContext(edit)] + // #[WPContextualField] + // pub bar: Option, + // } + // ``` + syn::PathArguments::None => Ok(segment), + // Type is surrounded with angled brackets + // + // ``` + // #[derive(WPContextual)] + // pub struct SparseFoo { + // #[WPContext(edit)] + // #[WPContextualField] + // pub bar: Option>, + // #[WPContext(view)] + // #[WPContextualField] + // pub baz: Option>>, + // } + // ``` + syn::PathArguments::AngleBracketed(ref mut path_args) => path_args + .args + .iter_mut() + .find_map(|generic_arg| { + if let syn::GenericArgument::Type(tty) = generic_arg { + Some(find_contextual_field_inner_segment(tty)) + } else { + None + } + }) + .ok_or(unsupported_err)?, + syn::PathArguments::Parenthesized(_) => Err(unsupported_err), + } + } else { + Err(unsupported_err) + } +} + +// Extracts `Foo` from `Option`. +// +// It currently doesn't support `std::option::Option` or `core::option::Option`. Although +// it'd be fairly straightforward to do so, it's also unnecessary as we want to encourage the +// usage of simple `Option` type for consistency. +fn extract_inner_type_of_option(ty: &syn::Type) -> Option { + if let syn::Type::Path(ref p) = ty { + let first_segment = &p.path.segments[0]; + + // `Option` type has only one segment with an ident `Option` + if p.path.segments.len() != 1 || first_segment.ident != "Option" { + return None; + } + + // PathArgument of an `Option` is always `AngleBracketed` + if let syn::PathArguments::AngleBracketed(ref angle_bracketed_type) = + first_segment.arguments + { + // `Option` has only one argument inside angle brackets + if angle_bracketed_type.args.len() != 1 { + return None; + } + + if let Some(syn::GenericArgument::Type(t)) = angle_bracketed_type.args.first() { + return Some(t.clone()); + } + } + } + None +} + +fn ident_name_for_context(ident_name_without_prefix: &str, context: &WPContextAttr) -> String { + format!("{}With{}Context", ident_name_without_prefix, context) +} + +fn is_wp_context_ident(ident: &Ident) -> bool { + ident.to_string().eq("WPContext") +} + +fn is_wp_contextual_field_ident(ident: &Ident) -> bool { + ident.to_string().eq("WPContextualField") +} + +// ``` +// #[WPContextual] +// pub struct SparseFoo { +// #[WPContext(edit, embed)] +// pub bar: Option, +// } +// ``` +// +// In this example, given the `TokenStream` for `edit, embed`, turns it into +// vec![WPContextAttr::Edit, WPContextAttr::Embed]. +fn parse_contexts_from_tokens( + tokens: proc_macro2::TokenStream, +) -> Result, syn::Error> { + tokens + .into_iter() + .filter_map(|t| match t { + proc_macro2::TokenTree::Ident(ident) => Some( + WPContextAttr::from_str(&ident.to_string()) + .map_err(|error_type| error_type.into_syn_error(ident.span())), + ), + proc_macro2::TokenTree::Punct(p) => { + if p.as_char() == ',' { + None + } else { + Some(Err( + WPContextualParseAttrError::UnexpectedPunct.into_syn_error(p.span()) + )) + } + } + proc_macro2::TokenTree::Group(g) => Some(Err( + WPContextualParseAttrError::UnexpectedToken.into_syn_error(g.span()), + )), + proc_macro2::TokenTree::Literal(l) => Some(Err( + WPContextualParseAttrError::UnexpectedLiteralToken.into_syn_error(l.span()), + )), + }) + .collect::, syn::Error>>() +} + +#[derive(Debug, PartialEq, Eq)] +struct WPParsedField { + field: syn::Field, + parsed_attrs: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +enum WPParsedAttr { + ParsedWPContextualField, + ParsedWPContext { contexts: Vec }, + ExternalAttr { attr: syn::Attribute }, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum WPContextAttr { + Edit, + Embed, + View, +} + +impl WPContextAttr { + pub fn iter() -> Iter<'static, WPContextAttr> { + [ + WPContextAttr::Edit, + WPContextAttr::Embed, + WPContextAttr::View, + ] + .iter() + } +} + +impl Display for WPContextAttr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Edit => "Edit", + Self::Embed => "Embed", + Self::View => "View", + } + ) + } +} + +impl FromStr for WPContextAttr { + type Err = WPContextualParseAttrError; + + fn from_str(input: &str) -> Result { + match input { + "edit" => Ok(Self::Edit), + "embed" => Ok(Self::Embed), + "view" => Ok(Self::View), + _ => Err(WPContextualParseAttrError::UnexpectedWPContextIdent { + input: input.to_string(), + }), + } + } +} + +#[derive(Debug, thiserror::Error)] +enum WPContextualParseError { + #[error( + "WPContextual didn't generate anything. Did you forget to add #[WPContext] attribute?" + )] + EmptyResult, + #[error( + "WPContextualField field types need to start with '{}' prefix", + IDENT_PREFIX + )] + WPContextualFieldMissingSparsePrefix, + #[error("#[WPContextualField] doesn't have any contexts. Did you forget to add #[WPContext] attribute?")] + WPContextualFieldWithoutWPContext, + #[error("Only Option & Option> types are supported by #[WPContextualField]")] + WPContextualFieldTypeNotSupported, + #[error("WPContextual types need to start with '{}' prefix. This prefix will be removed from the generated Structs, so it needs to be followed up with a proper Rust type name, starting with an uppercase letter.", IDENT_PREFIX)] + WPContextualMissingSparsePrefix, + #[error("#[WPContextual] is only implemented for Structs")] + WPContextualNotAStruct, +} + +impl WPContextualParseError { + fn into_syn_error(self, span: proc_macro2::Span) -> syn::Error { + syn::Error::new(span, self.to_string()) + } +} + +#[derive(Debug, thiserror::Error)] +enum WPContextualParseAttrError { + // It's possible to trigger this error by using something like `#[wp_derive::WPContext]`, + // however that's not a valid syntax. There is probably no valid syntax that uses `::` in the + // current setup, but in case we are missing anything, we should be able to improve the + // messaging by asking it to be reported. + #[error("Expected #[WPContext] or #[WPContextualField], found multi-segment path.\nPlease report this case to the `wp_derive` developers.")] + UnexpectedAttrPathSegmentCount, + #[error("Did you mean ','?")] + UnexpectedPunct, + #[error("Expected 'edit', 'embed' or 'view', found '{}'", input)] + UnexpectedWPContextIdent { input: String }, + // syn::Meta::Path or syn::Meta::NameValue + #[error("Expected #[WPContext(edit, embed, view)]. Did you forget to add context types?")] + MissingWPContextMeta, + #[error("Expected #[WPContext(edit, embed, view)], found unsupported tokens")] + UnexpectedToken, + #[error("Expected #[WPContext(edit, embed, view)]. Try removing the quotation?")] + UnexpectedLiteralToken, +} + +impl WPContextualParseAttrError { + fn into_syn_error(self, span: proc_macro2::Span) -> syn::Error { + syn::Error::new(span, self.to_string()) + } +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn find_contextual_field_inner_segment_simple() { + validate_find_contextual_field_inner_segment( + "Bar", + parse_quote! { + let foo: Option; + }, + ); + } + + #[test] + fn find_contextual_field_inner_segment_wrapped_in_vec() { + validate_find_contextual_field_inner_segment( + "Bar", + parse_quote! { + let foo: Option>; + }, + ); + } + + #[test] + fn find_contextual_field_inner_segment_wrapped_in_segmented_vec() { + validate_find_contextual_field_inner_segment( + "Bar", + parse_quote! { + let foo: Option>; + }, + ); + } + + #[test] + fn find_contextual_field_inner_segment_wrapped_in_multiple_vecs() { + validate_find_contextual_field_inner_segment( + "Bar", + parse_quote! { + let foo: Option>>; + }, + ); + } + + #[test] + fn find_contextual_field_inner_segment_error_tuple_not_supported() { + let mut input_type = type_from_simple_let_stmt(parse_quote! { + let foo: (u32, u32); + }); + assert_eq!( + find_contextual_field_inner_segment(&mut input_type) + .unwrap_err() + .to_string(), + WPContextualParseError::WPContextualFieldTypeNotSupported.to_string() + ); + } + + #[test] + fn extract_inner_type_of_option_simple() { + let input_type = type_from_simple_let_stmt(parse_quote! { + let foo: Option; + }); + let expected_type = type_from_simple_let_stmt(parse_quote! { + let foo: Foo; + }); + assert_eq!( + extract_inner_type_of_option(&input_type), + Some(expected_type) + ); + } + + fn validate_find_contextual_field_inner_segment(result: &str, stmt: syn::Stmt) { + let mut input_type = type_from_simple_let_stmt(stmt); + assert_eq!( + find_contextual_field_inner_segment(&mut input_type) + .unwrap() + .ident + .to_string(), + result + ); + } + + fn type_from_simple_let_stmt(stmt: syn::Stmt) -> syn::Type { + if let syn::Stmt::Local(syn::Local { + pat: syn::Pat::Type(syn::PatType { ty, .. }), + .. + }) = stmt + { + Some(*ty) + } else { + None + } + .unwrap() + } +} diff --git a/wp_derive/tests/all_tests.rs b/wp_derive/tests/all_tests.rs new file mode 100644 index 000000000..8633941b8 --- /dev/null +++ b/wp_derive/tests/all_tests.rs @@ -0,0 +1,19 @@ +#[test] +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/wp_contextual_field_with_multiple_segments.rs"); + t.pass("tests/wp_contextual_field_with_inner_type.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"); + t.compile_fail("tests/error_unexpected_wp_context_ident.rs"); + t.compile_fail("tests/error_unexpected_wp_context_meta_variant_path.rs"); + t.compile_fail("tests/error_unexpected_wp_context_meta_variant_name_value.rs"); + t.compile_fail("tests/error_unexpected_wp_context_punct.rs"); + 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_not_a_struct.rs"); +} diff --git a/wp_derive/tests/basic_wp_contextual.rs b/wp_derive/tests/basic_wp_contextual.rs new file mode 100644 index 000000000..9298ab8a0 --- /dev/null +++ b/wp_derive/tests/basic_wp_contextual.rs @@ -0,0 +1,15 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit, embed, view)] + pub bar: Option, +} + +fn main() { + let _ = FooWithEditContext { bar: 0 }; + let _ = FooWithEmbedContext { bar: 0 }; + let _ = FooWithViewContext { bar: 0 }; +} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/basic_wp_contextual_field.rs b/wp_derive/tests/basic_wp_contextual_field.rs new file mode 100644 index 000000000..6c77184bd --- /dev/null +++ b/wp_derive/tests/basic_wp_contextual_field.rs @@ -0,0 +1,22 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit)] + #[WPContextualField] + pub bar: Option, +} + +#[derive(WPContextual)] +pub struct SparseBar { + #[WPContext(edit)] + pub baz: u32, +} + +fn main() { + let _ = FooWithEditContext { + bar: BarWithEditContext { baz: 0 }, + }; +} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_empty_result.rs b/wp_derive/tests/error_empty_result.rs new file mode 100644 index 000000000..6d17d1377 --- /dev/null +++ b/wp_derive/tests/error_empty_result.rs @@ -0,0 +1,8 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo {} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_empty_result.stderr b/wp_derive/tests/error_empty_result.stderr new file mode 100644 index 000000000..3d2fd344e --- /dev/null +++ b/wp_derive/tests/error_empty_result.stderr @@ -0,0 +1,5 @@ +error: WPContextual didn't generate anything. Did you forget to add #[WPContext] attribute? + --> tests/error_empty_result.rs:4:12 + | +4 | pub struct SparseFoo {} + | ^^^^^^^^^ diff --git a/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.rs b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.rs new file mode 100644 index 000000000..c6ccb072e --- /dev/null +++ b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.rs @@ -0,0 +1,8 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct Foo {} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.stderr b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.stderr new file mode 100644 index 000000000..2b81654fa --- /dev/null +++ b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual.stderr @@ -0,0 +1,5 @@ +error: WPContextual types need to start with 'Sparse' prefix. This prefix will be removed from the generated Structs, so it needs to be followed up with a proper Rust type name, starting with an uppercase letter. + --> tests/error_missing_sparse_prefix_from_wp_contextual.rs:4:12 + | +4 | pub struct Foo {} + | ^^^ diff --git a/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.rs b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.rs new file mode 100644 index 000000000..98ddcaa64 --- /dev/null +++ b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.rs @@ -0,0 +1,16 @@ +// If a field is marked with `#[WPContextualField]` it needs to be a Sparse type + +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit)] + #[WPContextualField] + bar: Option, +} + +pub struct Bar {} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.stderr b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.stderr new file mode 100644 index 000000000..c564cbd50 --- /dev/null +++ b/wp_derive/tests/error_missing_sparse_prefix_from_wp_contextual_field.stderr @@ -0,0 +1,5 @@ +error: WPContextualField field types need to start with 'Sparse' prefix + --> tests/error_missing_sparse_prefix_from_wp_contextual_field.rs:9:17 + | +9 | bar: Option, + | ^^^ diff --git a/wp_derive/tests/error_unexpected_wp_context_ident.rs b/wp_derive/tests/error_unexpected_wp_context_ident.rs new file mode 100644 index 000000000..39cbd3001 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_ident.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(Edit)] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_ident.stderr b/wp_derive/tests/error_unexpected_wp_context_ident.stderr new file mode 100644 index 000000000..64e82a439 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_ident.stderr @@ -0,0 +1,5 @@ +error: Expected 'edit', 'embed' or 'view', found 'Edit' + --> tests/error_unexpected_wp_context_ident.rs:5:17 + | +5 | #[WPContext(Edit)] + | ^^^^ diff --git a/wp_derive/tests/error_unexpected_wp_context_literal.rs b/wp_derive/tests/error_unexpected_wp_context_literal.rs new file mode 100644 index 000000000..3231d91cb --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_literal.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext("edit")] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_literal.stderr b/wp_derive/tests/error_unexpected_wp_context_literal.stderr new file mode 100644 index 000000000..64c33ad1d --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_literal.stderr @@ -0,0 +1,5 @@ +error: Expected #[WPContext(edit, embed, view)]. Try removing the quotation? + --> tests/error_unexpected_wp_context_literal.rs:5:17 + | +5 | #[WPContext("edit")] + | ^^^^^^ diff --git a/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.rs b/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.rs new file mode 100644 index 000000000..0294ca038 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext = "baz"] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.stderr b/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.stderr new file mode 100644 index 000000000..42bd58ac4 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_meta_variant_name_value.stderr @@ -0,0 +1,5 @@ +error: Expected #[WPContext(edit, embed, view)]. Did you forget to add context types? + --> tests/error_unexpected_wp_context_meta_variant_name_value.rs:5:7 + | +5 | #[WPContext = "baz"] + | ^^^^^^^^^ diff --git a/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.rs b/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.rs new file mode 100644 index 000000000..ba3ba1430 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.stderr b/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.stderr new file mode 100644 index 000000000..df7ea2f95 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_meta_variant_path.stderr @@ -0,0 +1,5 @@ +error: Expected #[WPContext(edit, embed, view)]. Did you forget to add context types? + --> tests/error_unexpected_wp_context_meta_variant_path.rs:5:7 + | +5 | #[WPContext] + | ^^^^^^^^^ diff --git a/wp_derive/tests/error_unexpected_wp_context_punct.rs b/wp_derive/tests/error_unexpected_wp_context_punct.rs new file mode 100644 index 000000000..803f612ed --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_punct.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit. view)] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_punct.stderr b/wp_derive/tests/error_unexpected_wp_context_punct.stderr new file mode 100644 index 000000000..211b18fd8 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_punct.stderr @@ -0,0 +1,5 @@ +error: Did you mean ','? + --> tests/error_unexpected_wp_context_punct.rs:5:21 + | +5 | #[WPContext(edit. view)] + | ^ diff --git a/wp_derive/tests/error_unexpected_wp_context_token.rs b/wp_derive/tests/error_unexpected_wp_context_token.rs new file mode 100644 index 000000000..f7d56e03f --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_token.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext([edit])] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_unexpected_wp_context_token.stderr b/wp_derive/tests/error_unexpected_wp_context_token.stderr new file mode 100644 index 000000000..1303a6c85 --- /dev/null +++ b/wp_derive/tests/error_unexpected_wp_context_token.stderr @@ -0,0 +1,5 @@ +error: Expected #[WPContext(edit, embed, view)], found unsupported tokens + --> tests/error_unexpected_wp_context_token.rs:5:17 + | +5 | #[WPContext([edit])] + | ^^^^^^ diff --git a/wp_derive/tests/error_wp_contextual_field_without_wp_context.rs b/wp_derive/tests/error_wp_contextual_field_without_wp_context.rs new file mode 100644 index 000000000..cf0306d2d --- /dev/null +++ b/wp_derive/tests/error_wp_contextual_field_without_wp_context.rs @@ -0,0 +1,11 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContextualField] + pub bar: Option, +} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_wp_contextual_field_without_wp_context.stderr b/wp_derive/tests/error_wp_contextual_field_without_wp_context.stderr new file mode 100644 index 000000000..1e60102b3 --- /dev/null +++ b/wp_derive/tests/error_wp_contextual_field_without_wp_context.stderr @@ -0,0 +1,5 @@ +error: #[WPContextualField] doesn't have any contexts. Did you forget to add #[WPContext] attribute? + --> tests/error_wp_contextual_field_without_wp_context.rs:5:5 + | +5 | #[WPContextualField] + | ^ diff --git a/wp_derive/tests/error_wp_contextual_not_a_struct.rs b/wp_derive/tests/error_wp_contextual_not_a_struct.rs new file mode 100644 index 000000000..971b35238 --- /dev/null +++ b/wp_derive/tests/error_wp_contextual_not_a_struct.rs @@ -0,0 +1,8 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub enum SparseFoo {} + +fn main() {} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/error_wp_contextual_not_a_struct.stderr b/wp_derive/tests/error_wp_contextual_not_a_struct.stderr new file mode 100644 index 000000000..046bc77ed --- /dev/null +++ b/wp_derive/tests/error_wp_contextual_not_a_struct.stderr @@ -0,0 +1,5 @@ +error: #[WPContextual] is only implemented for Structs + --> tests/error_wp_contextual_not_a_struct.rs:4:10 + | +4 | pub enum SparseFoo {} + | ^^^^^^^^^ diff --git a/wp_derive/tests/wp_contextual_field_with_inner_type.rs b/wp_derive/tests/wp_contextual_field_with_inner_type.rs new file mode 100644 index 000000000..7ff8fde4e --- /dev/null +++ b/wp_derive/tests/wp_contextual_field_with_inner_type.rs @@ -0,0 +1,30 @@ +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit)] + #[WPContextualField] + pub bar: Option>, + #[WPContext(edit)] + #[WPContextualField] + pub bar_2: Option>, + #[WPContext(edit)] + #[WPContextualField] + pub bar_3: Option>>, +} + +#[derive(WPContextual)] +pub struct SparseBar { + #[WPContext(edit)] + pub baz: u32, +} + +fn main() { + let _ = FooWithEditContext { + bar: vec![BarWithEditContext { baz: 0 }], + bar_2: vec![BarWithEditContext { baz: 0 }], + bar_3: vec![vec![BarWithEditContext { baz: 0 }]], + }; +} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/wp_contextual_field_with_multiple_path_segments.rs b/wp_derive/tests/wp_contextual_field_with_multiple_path_segments.rs new file mode 100644 index 000000000..743083742 --- /dev/null +++ b/wp_derive/tests/wp_contextual_field_with_multiple_path_segments.rs @@ -0,0 +1,18 @@ +use wp_derive::WPContextual; + +mod wp_contextual_field_with_multiple_segments_helper; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit)] + #[WPContextualField] + pub bar: Option, +} + +fn main() { + let _ = FooWithEditContext { + bar: wp_contextual_field_with_multiple_segments_helper::BarWithEditContext { baz: 0 }, + }; +} + +uniffi::setup_scaffolding!(); diff --git a/wp_derive/tests/wp_contextual_field_with_multiple_path_segments_helper.rs b/wp_derive/tests/wp_contextual_field_with_multiple_path_segments_helper.rs new file mode 100644 index 000000000..b9bd0e019 --- /dev/null +++ b/wp_derive/tests/wp_contextual_field_with_multiple_path_segments_helper.rs @@ -0,0 +1,10 @@ +// Helper mod to be able to test multiple path segments in +// wp_contextual_field_with_multiple_path_segments + +use wp_derive::WPContextual; + +#[derive(WPContextual)] +pub struct SparseBar { + #[WPContext(edit)] + pub baz: u32, +} diff --git a/wp_derive/tests/wp_contextual_field_with_multiple_segments.rs b/wp_derive/tests/wp_contextual_field_with_multiple_segments.rs new file mode 100644 index 000000000..93d0e94f2 --- /dev/null +++ b/wp_derive/tests/wp_contextual_field_with_multiple_segments.rs @@ -0,0 +1,21 @@ +use wp_derive::WPContextual; + +// This test is validating that we are able to handle `#[WPContextualField]`s if its type +// has multiple path segments. That's why we use a helper mod and use fully qualified paths +// rather than the importing the mod. +mod wp_contextual_field_with_multiple_path_segments_helper; + +#[derive(WPContextual)] +pub struct SparseFoo { + #[WPContext(edit)] + #[WPContextualField] + pub bar: Option, +} + +fn main() { + let _ = FooWithEditContext { + bar: wp_contextual_field_with_multiple_path_segments_helper::BarWithEditContext { baz: 0 }, + }; +} + +uniffi::setup_scaffolding!(); diff --git a/wp_networking/src/lib.rs b/wp_networking/src/lib.rs index 628a59966..fc78b2d4a 100644 --- a/wp_networking/src/lib.rs +++ b/wp_networking/src/lib.rs @@ -23,7 +23,7 @@ impl WPNetworking { &self, params: Option, ) -> Result { - let wp_request = self.helper.post_list_request(PostListParams::default()); + let wp_request = self.helper.post_list_request(params.unwrap_or_default()); let request_headers: HeaderMap = (&wp_request.header_map.unwrap()).try_into().unwrap(); let response = self .client