Skip to content

Commit

Permalink
fixup! feat(convert): add derive macros for To and FromBytes
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Feb 3, 2024
1 parent 45a24a6 commit fa43dc0
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 22 deletions.
3 changes: 3 additions & 0 deletions convert-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ proc-macro-crate = "3.1.0"
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = { version = "2.0.48", features = ["derive"] }

[dev-dependencies]
trybuild = "1.0.89"
37 changes: 20 additions & 17 deletions convert-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
use std::iter;

use manyhow::{ensure, manyhow, Result};
use manyhow::{ensure, error_message, manyhow, Result};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{format_ident, quote, ToTokens};
use syn::{parse_quote, Attribute, DeriveInput, Path};

/// Tries to resolve the path to `extism_convert` dynamically, falling back to feature flags when unsuccessful.
fn convert_path() -> Path {
match crate_name("extism") {
Ok(FoundCrate::Name(name)) => {
match (
crate_name("extism"),
crate_name("extism-convert"),
crate_name("extism-pdk"),
) {
(Ok(FoundCrate::Name(name)), ..) => {
let ident = format_ident!("{name}");
parse_quote!(::#ident::convert)
}
Ok(FoundCrate::Itself) => parse_quote!(crate),
Err(_) => match crate_name("extism-convert").or_else(|_| crate_name("extism_pdk")) {
Ok(FoundCrate::Name(name)) => {
let ident = format_ident!("{name}");
parse_quote!(::#ident)
}
Ok(FoundCrate::Itself) => parse_quote!(crate),
Err(_) => match () {
() if cfg!(feature = "extism-path") => parse_quote!(::extism::convert),
() if cfg!(feature = "extism-pdk-path") => parse_quote!(::extism_pdk),
_ => parse_quote!(::extism_convert),
},
},
(_, Ok(FoundCrate::Name(name)), ..) | (.., Ok(FoundCrate::Name(name))) => {
let ident = format_ident!("{name}");
parse_quote!(::#ident)
}
(Ok(FoundCrate::Itself), ..) => parse_quote!(::extism::convert),
(_, Ok(FoundCrate::Itself), ..) => parse_quote!(::extism_convert),
(.., Ok(FoundCrate::Itself)) => parse_quote!(::extism_pdk),
_ if cfg!(feature = "extism-path") => parse_quote!(::extism::convert),
_ if cfg!(feature = "extism-pdk-path") => parse_quote!(::extism_pdk),
_ => parse_quote!(::extism_convert),
}
}

Expand All @@ -36,7 +37,9 @@ fn extract_encoding(attrs: &[Attribute]) -> Result<Path> {
ensure!(!encodings.is_empty(), "encoding needs to be specified"; try = "`#[encoding(ToJson)]`");
ensure!(encodings.len() < 2, encodings[1], "only one encoding can be specified"; try = "remove `{}`", encodings[1].to_token_stream());

Ok(encodings[0].parse_args()?)
Ok(encodings[0].parse_args().map_err(
|e| error_message!(e.span(), "{e}"; note= "expects a path"; try = "`#[encoding(ToJson)]`"),
)?)
}

#[manyhow]
Expand Down
5 changes: 5 additions & 0 deletions convert-macros/tests/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}
23 changes: 23 additions & 0 deletions convert-macros/tests/ui/invalid-encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use extism_convert_macros::ToBytes;

#[derive(ToBytes)]
struct MissingEncoding;

#[derive(ToBytes)]
#[encoding]
struct EmptyAttr;

#[derive(ToBytes)]
#[encoding = "string"]
struct EqNoParen;

#[derive(ToBytes)]
#[encoding(something, else)]
struct NotAPath;

#[derive(ToBytes)]
#[encoding(Multiple)]
#[encoding(Encodings)]
struct MultipleEncodings;

fn main() {}
44 changes: 44 additions & 0 deletions convert-macros/tests/ui/invalid-encoding.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
error: encoding needs to be specified

= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:3:10
|
3 | #[derive(ToBytes)]
| ^^^^^^^
|
= note: this error originates in the derive macro `ToBytes` (in Nightly builds, run with -Z macro-backtrace for more info)

error: expected attribute arguments in parentheses: #[encoding(...)]

= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:7:3
|
7 | #[encoding]
| ^^^^^^^^

error: expected parentheses: #[encoding(...)]

= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:11:12
|
11 | #[encoding = "string"]
| ^

error: unexpected token

= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:15:21
|
15 | #[encoding(something, else)]
| ^

error: only one encoding can be specified

= try: remove `#[encoding(Encodings)]`
--> tests/ui/invalid-encoding.rs:20:1
|
20 | #[encoding(Encodings)]
| ^^^^^^^^^^^^^^^^^^^^^^
1 change: 1 addition & 0 deletions convert/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ serde_json = "1.0.105"
extism-convert-macros.workspace = true

[dev-dependencies]
# extism = { path = "../runtime" }
serde = { version = "1.0.186", features = ["derive"] }

[features]
Expand Down
8 changes: 5 additions & 3 deletions convert/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use base64::Engine;
/// For example, the following line creates a new JSON encoding using serde_json:
///
/// ```
/// extism_convert::encoding!(MyJson, serde_json::to_vec, serde_json::from_slice);
#[cfg_attr(feature = "extism-path", doc = "extism::convert::encoding!(MyJson, serde_json::to_vec, serde_json::from_slice);")]
#[cfg_attr(feature = "extism-pdk-path", doc = "extism_pdk::encoding!(MyJson, serde_json::to_vec, serde_json::from_slice);")]
#[cfg_attr(not(any(feature = "extism-path", feature = "extism-pdk-path")), doc = "extism_convert::encoding!(MyJson, serde_json::to_vec, serde_json::from_slice);")]
/// ```
///
/// This will create a struct `struct MyJson<T>(pub T)` and implement `ToBytes` using `serde_json::to_vec`
/// and `FromBytesOwned` using `serde_json::from_vec`
/// This will create a struct `struct MyJson<T>(pub T)` and implement [`ToBytes`] using [`serde_json::to_vec`]
/// and [`FromBytesOwned`] using [`serde_json::from_slice`]
#[macro_export]
macro_rules! encoding {
($pub:vis $name:ident, $to_vec:expr, $from_slice:expr) => {
Expand Down
78 changes: 76 additions & 2 deletions convert/src/from_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,87 @@ pub use extism_convert_macros::FromBytes;

/// `FromBytes` is used to define how a type should be decoded when working with
/// Extism memory. It is used for plugin output and host function input.
///
/// `FromBytes` can be derived by delegating encoding to generic type implementing
/// `FromBytes`, e.g., [`Json`], [`Msgpack`].
///
/// ```
#[cfg_attr(
feature = "extism-path",
doc = "use extism::convert::{Json, FromBytes};"
)]
#[cfg_attr(
feature = "extism-pdk-path",
doc = "use extism_pdk::{Json, FromBytes};"
)]
#[cfg_attr(
not(any(feature = "extism-path", feature = "extism-pdk-path")),
doc = "use extism_convert::{Json, FromBytes};"
)]
/// use serde::Deserialize;
///
/// #[derive(FromBytes, Deserialize, PartialEq, Debug)]
/// #[encoding(Json)]
/// struct Struct {
/// hello: String,
/// }
///
/// assert_eq!(Struct::from_bytes(br#"{"hello":"hi"}"#)?, Struct { hello: "hi".into() });
/// # Ok::<(), extism_convert::Error>(())
/// ```
///
/// Custom encodings can also be used, through new-types with a single generic
/// argument, i.e., `Type<T>(T)`, that implement `FromBytesOwned` for the struct.
///
/// ```
/// use std::str::{self, FromStr};
/// use std::convert::Infallible;
#[cfg_attr(
feature = "extism-path",
doc = "use extism::convert::{Error, FromBytes, FromBytesOwned};"
)]
#[cfg_attr(
feature = "extism-pdk-path",
doc = "use extism_pdk::{Error, FromBytes, FromBytesOwned};"
)]
#[cfg_attr(
not(any(feature = "extism-path", feature = "extism-pdk-path")),
doc = "use extism_convert::{Error, FromBytes, FromBytesOwned};"
)]
///
/// // Custom serialization using `FromStr`
/// struct StringEnc<T>(T);
/// impl<T: FromStr> FromBytesOwned for StringEnc<T> where Error: From<<T as FromStr>::Err> {
/// fn from_bytes_owned(data: &[u8]) -> Result<Self, Error> {
/// Ok(Self(str::from_utf8(data)?.parse()?))
/// }
/// }
///
/// #[derive(FromBytes, PartialEq, Debug)]
/// #[encoding(StringEnc)]
/// struct Struct {
/// hello: String,
/// }
///
/// impl FromStr for Struct {
/// type Err = Infallible;
/// fn from_str(s: &str) -> Result<Self, Infallible> {
/// Ok(Self { hello: s.to_owned() })
/// }
/// }
///
/// assert_eq!(Struct::from_bytes(b"hi")?, Struct { hello: "hi".into() });
/// # Ok::<(), extism_convert::Error>(())
/// ```
pub trait FromBytes<'a>: Sized {
/// Decode a value from a slice of bytes
fn from_bytes(data: &'a [u8]) -> Result<Self, Error>;
}

/// `FromBytesOwned` is similar to `FromBytes` but it doesn't borrow from the input slice.
/// `FromBytes` is automatically implemented for all types that implement `FromBytesOwned`
/// `FromBytesOwned` is similar to [`FromBytes`] but it doesn't borrow from the input slice.
/// [`FromBytes`] is automatically implemented for all types that implement `FromBytesOwned`.
///
/// `FromBytesOwned` can be derived through [`#[derive(FromBytes)]`](FromBytes).
pub trait FromBytesOwned: Sized {
/// Decode a value from a slice of bytes, the resulting value should not borrow the input
/// data.
Expand Down
3 changes: 3 additions & 0 deletions convert/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
//! similar to [axum extractors](https://docs.rs/axum/latest/axum/extract/index.html#intro) - they are
//! implemented as a tuple struct with a single field that is meant to be extracted using pattern matching.

// Makes proc-macros able to resolve `::extism_convert` correctly
extern crate self as extism_convert;

pub use anyhow::Error;

mod encoding;
Expand Down
74 changes: 74 additions & 0 deletions convert/src/to_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,68 @@ pub use extism_convert_macros::ToBytes;

/// `ToBytes` is used to define how a type should be encoded when working with
/// Extism memory. It is used for plugin input and host function output.
///
/// `ToBytes` can be derived by delegating encoding to generic type implementing
/// `ToBytes`, e.g., [`Json`], [`Msgpack`].
///
/// ```
#[cfg_attr(feature = "extism-path", doc = "use extism::convert::{Json, ToBytes};")]
#[cfg_attr(feature = "extism-pdk-path", doc = "use extism_pdk::{Json, ToBytes};")]
#[cfg_attr(
not(any(feature = "extism-path", feature = "extism-pdk-path")),
doc = "use extism_convert::{Json, ToBytes};"
)]
/// use serde::Serialize;
///
/// #[derive(ToBytes, Serialize)]
/// #[encoding(Json)]
/// struct Struct {
/// hello: String,
/// }
///
/// assert_eq!(Struct { hello: "hi".into() }.to_bytes()?, br#"{"hello":"hi"}"#);
/// # Ok::<(), extism_convert::Error>(())
/// ```
///
/// But custom types can also be used, as long as they are new-types with a single
/// generic argument, i.e., `Type<T>(T)`, that implement `ToBytes` for the struct.
///
/// ```
#[cfg_attr(
feature = "extism-path",
doc = "use extism::convert::{Error, ToBytes};"
)]
#[cfg_attr(feature = "extism-pdk-path", doc = "use extism_pdk::{Error, ToBytes};")]
#[cfg_attr(
not(any(feature = "extism-path", feature = "extism-pdk-path")),
doc = "use extism_convert::{Error, ToBytes};"
)]
///
/// // Custom serialization using `ToString`
/// struct StringEnc<T>(T);
/// impl<T: ToString> ToBytes<'_> for StringEnc<&T> {
/// type Bytes = String;
///
/// fn to_bytes(&self) -> Result<String, Error> {
/// Ok(self.0.to_string())
/// }
/// }
///
/// #[derive(ToBytes)]
/// #[encoding(StringEnc)]
/// struct Struct {
/// hello: String,
/// }
///
/// impl ToString for Struct {
/// fn to_string(&self) -> String {
/// self.hello.clone()
/// }
/// }
///
/// assert_eq!(Struct { hello: "hi".into() }.to_bytes()?, b"hi");
/// # Ok::<(), Error>(())
/// ```
pub trait ToBytes<'a> {
/// A configurable byte slice representation, allows any type that implements `AsRef<[u8]>`
type Bytes: AsRef<[u8]>;
Expand Down Expand Up @@ -113,3 +175,15 @@ impl<'a, T: ToBytes<'a>> ToBytes<'a> for Option<T> {
}
}
}

#[test]
fn test() {
use extism_convert::{Json, ToBytes};
use serde::Serialize;

#[derive(ToBytes, Serialize)]
#[encoding(Json)]
struct Struct {
hello: String,
}
}
12 changes: 12 additions & 0 deletions convert/tests/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

#[test]
fn test() {
use extism_convert::{Json, ToBytes};
use serde::Serialize;

#[derive(ToBytes, Serialize)]
#[encoding(Json)]
struct Struct {
hello: String,
}
}
4 changes: 4 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Makes proc-macros able to resolve `::extism` correctly
extern crate self as extism;

pub(crate) use extism_convert::*;
pub(crate) use std::collections::BTreeMap;
use std::str::FromStr;
pub(crate) use wasmtime::*;

#[doc(inline)]
pub use extism_convert as convert;

pub use anyhow::Error;
Expand Down

0 comments on commit fa43dc0

Please sign in to comment.