commit 61137b9c1521c148574f01d17aa26dea12cce2e5 Author: Jordan Rose Date: Wed Oct 25 12:07:36 2023 -0700 Initial version of PartialDefault diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff93234 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI +on: + pull_request: + push: + branches: [main] + +jobs: + test: + name: Tests + strategy: + fail-fast: false + matrix: + rust: [1.69.0, nightly] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: rustup toolchain default ${{ matrix.rust }} --profile minimal + - run: cargo test --workspace --all-features + + clippy: + name: Lints + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # Default to latest stable as installed on the runners. + # This does mean there may be updates that break this job at some point. + - run: cargo fmt --all -- --check + - run: cargo clippy --workspace --all-targets --all-features -- -D warning diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5c18426 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +# +# Copyright 2023 Signal Messenger, LLC. +# SPDX-License-Identifier: AGPL-3.0-only +# + +[workspace.package] +version = "0.1.0" +repository = "https://github.com/signalapp/partial-default" + +[package] +name = "partial-default" +edition = "2021" +version.workspace = true +repository.workspace = true + +description = "Provides PartialDefault, a trait similar to Default but with fewer guarantees" +license = "AGPL-3.0-only" +keywords = ["default", "trait", "empty", "uninitialized"] +categories = ["rust-patterns", "no-std::no-alloc"] + +[dependencies] +partial-default-derive = { path = "derive", version = "=0.1.0", optional = true } + +[features] +derive = ["dep:partial-default-derive"] + +[package.metadata.docs.rs] +all-features = true +# Provide those nice cfg callouts in the generate docs. +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..929e4ae --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +[`PartialDefault`] is a trait for giving a type a *non*-useful default value. + +The standard [`Default`][] trait documents its purpose as providing a "useful" default value. However, some types (such as a Credential) don't have meaningful defaults, and yet there are still uses for a known-initialized value: + +- serde's hidden [`Deserializer::deserialize_in_place`][deserialize_in_place], which is generally more efficient than the usual `deserialize` +- subtle's [`ConditionallySelectable::conditional_assign`][conditional_assign], for repeated assignments with at least one success +- APIs that must produce results even when signalling an error out of band (like JNI functions) + +`PartialDefault` satisfies this niche. A type that implements `PartialDefault` can provide a value that is safe to drop or assign over, but promises nothing else about that value. It provides a derive macro (opt-in, with the `derive` feature) and is `no_std` compatible. + +[`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html +[deserialize_in_place]: https://docs.rs/serde/1.0.189/src/serde/de/mod.rs.html#546-568 +[conditional_assign]: https://docs.rs/subtle/2.5.0/subtle/trait.ConditionallySelectable.html + +# License and Contributions + +`PartialDefault` was made to support [libsignal][], but is available for general use under the **[AGPLv3][]**. Still, this is meant to be a low-maintenance crate; do not expect active support or progress on feature requests. + +Signal does accept external contributions to this project; however, signing a [CLA (Contributor License Agreement)][cla] is required for all contributions. + +Copyright 2023 Signal Messenger, LLC. + +The `partial-default-derive` crate contains code adapted from the [`rust-smart-default`][] crate, Copyright (c) 2017 Idan Arye, under the [MIT license][]. + +[libsignal]: https://github.com/signalapp/libsignal +[AGPLv3]: https://www.gnu.org/licenses/agpl-3.0.html +[cla]: https://signal.org/cla/ +[`rust-smart-default`]: https://github.com/idanarye/rust-smart-default +[MIT license]: https://github.com/idanarye/rust-smart-default/blob/084c5cd5ddc3ddb98cc005b48141ec34607ecf7a/LICENSE diff --git a/derive/Cargo.toml b/derive/Cargo.toml new file mode 100644 index 0000000..00174a8 --- /dev/null +++ b/derive/Cargo.toml @@ -0,0 +1,25 @@ +# +# Original Copyright 2017 Idan Arye +# Modifications Copyright 2023 Signal Messenger, LLC. +# SPDX-License-Identifier: AGPL-3.0-only +# + +[package] +name = "partial-default-derive" +edition = "2021" +version.workspace = true +repository.workspace = true + +description = "Derive-macro support for PartialDefault" +license = "AGPL-3.0-only" + +[lib] +proc-macro = true + +[dependencies] +syn = "2" +quote = "1" +proc-macro2 = "1.0.69" + +[dev-dependencies] +partial-default = { path = "..", features = ["derive"] } diff --git a/derive/README.md b/derive/README.md new file mode 100644 index 0000000..9bf188a --- /dev/null +++ b/derive/README.md @@ -0,0 +1,17 @@ +This crate provides a derive-macro for the `partial-default` crate. See there for more details. + +# License and Contributions + +`PartialDefault` was made to support [libsignal][], but is available for general use under the **[AGPLv3][]**. Still, this is meant to be a low-maintenance crate; do not expect active support or progress on feature requests. + +Signal does accept external contributions to this project; however, signing a [CLA (Contributor License Agreement)][cla] is required for all contributions. + +Copyright 2023 Signal Messenger, LLC. + +The `partial-default-derive` crate contains code adapted from the [`rust-smart-default`][] crate, Copyright (c) 2017 Idan Arye, under the [MIT license][]. + +[libsignal]: https://github.com/signalapp/libsignal +[AGPLv3]: https://www.gnu.org/licenses/agpl-3.0.html +[cla]: https://signal.org/cla/ +[`rust-smart-default`]: https://github.com/idanarye/rust-smart-default +[MIT license]: https://github.com/idanarye/rust-smart-default/blob/084c5cd5ddc3ddb98cc005b48141ec34607ecf7a/LICENSE diff --git a/derive/src/body_impl.rs b/derive/src/body_impl.rs new file mode 100644 index 0000000..5121c6e --- /dev/null +++ b/derive/src/body_impl.rs @@ -0,0 +1,151 @@ +// +// Original Copyright 2017 Idan Arye +// Modifications Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use proc_macro2::TokenStream; + +use quote::quote; +use syn::parse::Error; +use syn::spanned::Spanned; +use syn::DeriveInput; + +use crate::default_attr::DefaultAttr; +use crate::util::find_only; + +pub fn impl_my_derive(input: &DeriveInput) -> Result { + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let where_clause_generics = where_clause.map(|clause| { + // Convert predicates individually to guarantee a trailing comma. + let predicates = clause.predicates.iter(); + quote! { #(#predicates,)* } + }); + let additional_generics = additional_generics_tt(input)?; + + let default_expr = match input.data { + syn::Data::Struct(ref body) => { + let body_assignment = default_body_tt(&body.fields)?; + quote! { + #name #body_assignment + } + } + syn::Data::Enum(ref body) => { + let default_variant = find_only( + body.variants.iter(), + "Only one variant can be marked #[partial_default]", + |variant| { + if let Some(meta) = DefaultAttr::find_in_attributes(&variant.attrs)? { + if matches!(meta, DefaultAttr::Empty) { + Ok(true) + } else { + Err(Error::new_spanned( + &variant.ident, + "Attribute #[partial_default] on variants should have no value", + )) + } + } else { + Ok(false) + } + }, + )? + .ok_or_else(|| Error::new(input.span(), "No default variant"))?; + let default_variant_name = &default_variant.ident; + let body_assignment = default_body_tt(&default_variant.fields)?; + quote! { + #name :: #default_variant_name #body_assignment + } + } + syn::Data::Union(_) => { + panic!() + } + }; + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::partial_default::PartialDefault for #name #ty_generics where #where_clause_generics #additional_generics { + fn partial_default() -> Self { + #default_expr + } + } + }) +} + +fn additional_generics_tt(item: &syn::DeriveInput) -> Result { + if let Some(default_attr) = DefaultAttr::find_in_attributes(&item.attrs)? { + if let DefaultAttr::Bound(bound) = default_attr { + bound.parse() + } else { + Err(Error::new( + item.ident.span(), + r#"Expected #[partial_default(bound = "...")"#, + )) + } + } else { + let bounds = item.generics.type_params().map(|param| { + let ident = ¶m.ident; + quote! { #ident: ::partial_default::PartialDefault } + }); + Ok(quote! { + #(#bounds),* + }) + } +} + +/// Return a token-tree for the default "body" - the part after the name that contains the values. +/// +/// That is, the `{ ... }` part for structs, the `(...)` part for tuples, and nothing for units. +fn default_body_tt(body: &syn::Fields) -> Result { + Ok(match body { + syn::Fields::Named(ref fields) => { + let field_assignments = fields + .named + .iter() + .map(|field| { + let field_name = field.ident.as_ref(); + let default_value = field_default_expr(field)?; + Ok(quote! { #field_name : #default_value }) + }) + .collect::, Error>>()?; + quote! { + { + #( #field_assignments ),* + } + } + } + syn::Fields::Unnamed(ref fields) => { + let field_assignments = fields + .unnamed + .iter() + .map(field_default_expr) + .collect::, Error>>()?; + quote! { + ( + #( #field_assignments ),* + ) + } + } + &syn::Fields::Unit => quote! {}, + }) +} + +/// Return a default expression for a field based on it's `#[default = "..."]` attribute. +/// +/// Errors if there is more than one, of if there is a `#[default]` attribute without value. +fn field_default_expr(field: &syn::Field) -> Result { + if let Some(default_attr) = DefaultAttr::find_in_attributes(&field.attrs)? { + if let DefaultAttr::Value(field_value) = default_attr { + field_value.parse() + } else { + Err(Error::new( + field.span(), + r#"Expected #[partial_default(value = "...")"#, + )) + } + } else { + Ok(quote! { + ::partial_default::PartialDefault::partial_default() + }) + } +} diff --git a/derive/src/default_attr.rs b/derive/src/default_attr.rs new file mode 100644 index 0000000..2bf9fab --- /dev/null +++ b/derive/src/default_attr.rs @@ -0,0 +1,56 @@ +// +// Original Copyright 2017 Idan Arye +// Modifications Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use syn::parse::Error; + +use crate::util::find_only; + +pub enum DefaultAttr { + Empty, + Bound(syn::LitStr), + Value(syn::LitStr), +} + +impl DefaultAttr { + pub fn find_in_attributes(attrs: &[syn::Attribute]) -> Result, Error> { + if let Some(default_attr) = find_only( + attrs.iter(), + "cannot have multiple #[partial_default] attributes on the same item", + |attr| Ok(attr.path().is_ident("partial_default")), + )? { + match &default_attr.meta { + syn::Meta::Path(_) => Ok(Some(Self::Empty)), + syn::Meta::List(meta) => { + let mut result = None; + meta.parse_nested_meta(|nested| { + if result.is_some() { + return Err(nested.error("invalid syntax for partial_default")); + } + + if nested.path.is_ident("bound") { + result = Some(Self::Bound(nested.value()?.parse()?)); + return Ok(()); + } + + if nested.path.is_ident("value") { + result = Some(Self::Value(nested.value()?.parse()?)); + return Ok(()); + } + + Err(nested.error("invalid syntax for partial_default")) + })?; + Ok(result) + } + syn::Meta::NameValue(_) => Err(Error::new_spanned( + &default_attr.meta, + "invalid syntax for partial_default", + )), + } + } else { + Ok(None) + } + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs new file mode 100644 index 0000000..7362371 --- /dev/null +++ b/derive/src/lib.rs @@ -0,0 +1,69 @@ +// +// Original Copyright 2017 Idan Arye +// Modifications Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +#![doc = include_str!("../README.md")] + +use syn::{parse_macro_input, DeriveInput}; + +mod body_impl; +mod default_attr; +mod util; + +/// Derive the `PartialDefault` trait. +/// +/// The value used for a field can be overridden using the `#[partial_default(value = +/// "alternative()")]` syntax, where `alternative()` is a Rust expression that evaluates to the +/// correct type. +/// +/// By default, the derived implementation will add a `T: PartialDefault` trait for every generic +/// parameter, like the built-in `derive(Default)`. You can override this by adding +/// `#[partial_default(bound = "T: MyTrait")]` to the type, which replaces any inferred bounds. Use +/// an empty string to impose no restrictions at all. +/// +/// # Examples +/// +/// ``` +/// use partial_default::PartialDefault; +/// +/// # fn main() { +/// #[derive(PartialDefault)] +/// #[partial_default(bound = "")] +/// # #[derive(PartialEq)] +/// # #[allow(dead_code)] +/// enum Foo { +/// Bar, +/// #[partial_default] +/// Baz { +/// #[partial_default(value = "12")] +/// a: i32, +/// b: i32, +/// #[partial_default(value = "Some(Default::default())")] +/// c: Option, +/// #[partial_default(value = "vec![1, 2, 3]")] +/// d: Vec, +/// #[partial_default(value = r#""four".to_owned()"#)] +/// e: String, +/// }, +/// Qux(T), +/// } +/// +/// assert!(Foo::<&u8>::partial_default() == Foo::<&u8>::Baz { +/// a: 12, +/// b: 0, +/// c: Some(0), +/// d: vec![1, 2, 3], +/// e: "four".to_owned(), +/// }); +/// # } +/// ``` +#[proc_macro_derive(PartialDefault, attributes(partial_default))] +pub fn derive_partial_default(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match body_impl::impl_my_derive(&input) { + Ok(output) => output.into(), + Err(error) => error.to_compile_error().into(), + } +} diff --git a/derive/src/util.rs b/derive/src/util.rs new file mode 100644 index 0000000..689beba --- /dev/null +++ b/derive/src/util.rs @@ -0,0 +1,32 @@ +// +// Original Copyright 2017 Idan Arye +// Modifications Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use syn::parse::Error; +use syn::spanned::Spanned; + +/// Return the value that fulfills the predicate if there is one in the iterator. +/// +/// Produces an error if there is more than one matching value. +pub fn find_only( + iter: impl Iterator, + error_message_for_multiple_matches: &str, + pred: F, +) -> Result, Error> +where + T: Spanned, + F: Fn(&T) -> Result, +{ + let mut result = None; + for item in iter { + if pred(&item)? { + if result.is_some() { + return Err(Error::new(item.span(), error_message_for_multiple_matches)); + } + result = Some(item); + } + } + Ok(result) +} diff --git a/derive/tests/tests.rs b/derive/tests/tests.rs new file mode 100644 index 0000000..958b24b --- /dev/null +++ b/derive/tests/tests.rs @@ -0,0 +1,183 @@ +// +// Original Copyright 2017 Idan Arye +// Modifications Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use partial_default::PartialDefault; + +#[test] +fn test_unit() { + #[derive(PartialEq, PartialDefault)] + struct Foo; + + assert!(Foo::partial_default() == Foo); +} + +#[test] +fn test_tuple() { + #[derive(PartialEq, PartialDefault)] + struct Foo( + #[partial_default(value = "10")] i32, + #[partial_default(value = "20")] i32, + // No default + i32, + ); + + assert!(Foo::partial_default() == Foo(10, 20, 0)); +} + +#[test] +fn test_struct() { + #[derive(PartialEq, PartialDefault)] + struct Foo { + #[partial_default(value = "10")] + x: i32, + #[partial_default(value = "20")] + y: i32, + // No default + z: i32, + } + + assert!(Foo::partial_default() == Foo { x: 10, y: 20, z: 0 }); +} + +#[test] +fn test_enum_of_units() { + #[derive(PartialEq, PartialDefault)] + pub enum Foo { + #[allow(dead_code)] + Bar, + #[partial_default] + Baz, + #[allow(dead_code)] + Qux, + } + + assert!(Foo::partial_default() == Foo::Baz); +} + +#[test] +fn test_enum_of_tuples() { + #[derive(PartialEq, PartialDefault)] + pub enum Foo { + #[allow(dead_code)] + Bar(i32), + #[partial_default] + Baz(#[partial_default(value = "10")] i32, i32), + #[allow(dead_code)] + Qux(i32), + } + + assert!(Foo::partial_default() == Foo::Baz(10, 0)); +} + +#[test] +fn test_enum_of_structs() { + #[derive(PartialEq, PartialDefault)] + pub enum Foo { + #[allow(dead_code)] + Bar { x: i32 }, + #[partial_default] + Baz { + #[partial_default(value = "10")] + y: i32, + z: i32, + }, + #[allow(dead_code)] + Qux { w: i32 }, + } + + assert!(Foo::partial_default() == Foo::Baz { y: 10, z: 0 }); +} + +#[test] +fn test_enum_mixed() { + #[derive(PartialEq, PartialDefault)] + enum Foo { + #[allow(dead_code)] + Bar, + #[partial_default] + Baz(#[partial_default(value = "10")] i32), + #[allow(dead_code)] + Qux { w: i32 }, + } + + assert!(Foo::partial_default() == Foo::Baz(10)); +} + +#[test] +fn test_generics_type_parameters() { + #[derive(PartialEq, PartialDefault)] + struct Foo + where + T: Ord, // unrelated + { + #[partial_default(value = "Some(PartialDefault::partial_default())")] + x: Option, + } + + assert!(Foo::partial_default() == Foo { x: Some(0) }); +} + +#[test] +fn test_generics_type_parameters_custom_bound() { + #[derive(PartialEq, PartialDefault)] + #[partial_default(bound = "T: std::str::FromStr")] + struct Foo { + #[partial_default(value = r#"Some("0".parse().ok().unwrap())"#)] + x: Option, + } + + assert!(Foo::partial_default() == Foo { x: Some(0) }); +} + +#[test] +fn test_generics_type_parameters_no_bound() { + #[derive(PartialEq, PartialDefault)] + #[partial_default(bound = "")] + struct Foo { + x: Option, + } + + assert!(Foo::partial_default() == Foo:: { x: None }); +} + +#[test] +fn test_generics_lifetime_parameters() { + // NOTE: A default value makes no sense with lifetime parameters, since ::partial_default() receives no + // paramters and therefore can receive no lifetimes. But it does make sense if you make a variant + // without ref fields the default. + + #[derive(PartialEq, PartialDefault)] + enum Foo<'a> { + #[partial_default] + Bar(i32), + #[allow(dead_code)] + Baz(&'a str), + } + + assert!(Foo::partial_default() == Foo::Bar(0)); +} + +#[test] +fn test_value_expression_with_macro() { + #[derive(PartialEq, PartialDefault)] + struct Foo { + #[partial_default(value = "vec![1, 2, 3]")] + v: Vec, + } + + assert!(Foo::partial_default().v == [1, 2, 3]); +} + +#[test] +fn test_string_conversion() { + #[derive(PartialEq, PartialDefault)] + struct Foo( + #[partial_default(value = r#""one""#)] &'static str, + #[partial_default(value = r#""two".to_owned()"#)] String, + ); + + assert!(Foo::partial_default() == Foo("one", "two".to_owned())); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..815aec5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,61 @@ +// +// Copyright 2023 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +#![doc = include_str!("../README.md")] +#![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +#[cfg(feature = "derive")] +pub use partial_default_derive::PartialDefault; + +/// A trait for giving a type a *non*-useful default value. +/// +/// The standard [`Default`] trait documents its purpose as providing a "useful" default value. +/// However, some types (such as a Credential) don't have meaningful defaults, and yet there are +/// still uses for a known-initialized value: +/// +/// - serde's hidden [`Deserializer::deserialize_in_place`][deserialize_in_place], which is +/// generally more efficient than the usual `deserialize` +/// - subtle's [`ConditionallySelectable::conditional_assign`][conditional_assign], for repeated +/// assignments with at least one success +/// - APIs that must produce results even when signalling an error out of band (like JNI functions) +/// +/// `PartialDefault` satisfies this niche. A type that implements `PartialDefault` can provide a +/// value that is safe to drop or assign over, but promises nothing else about that value. Using it +/// in any other way may panic or produce unexpected results, though it should not be possible to +/// violate memory safety. That is, [`partial_default`][Self::partial_default] should always be a +/// "safe" function in the Rust sense. +/// +/// The name "PartialDefault" is by analogy to [`PartialEq`]/[`Eq`] and [`PartialOrd`]/[`Ord`] in +/// the standard library: just as `PartialEq` provides weaker guarantees than `Eq` and `PartialOrd` +/// provides weaker guarantees than `Ord`, `PartialDefault` provides weaker guarantees than +/// `Default`. And just as every `Eq`-implementing type provides `PartialEq`, every +/// `Default`-implementing type provides `PartialDefault`. +/// +/// # Derivable +/// +/// Like [`Default`], `PartialDefault` supports `#[derive]` if all fields implement +/// `PartialDefault`. The value used for a field can be overridden using the +/// `#[partial_default(value = "alternative()")]` syntax, where `alternative()` is a Rust expression +/// that evaluates to the correct type. +/// +/// By default, all generic parameters must implement `PartialDefault` to support deriving +/// `PartialDefault`. You can override this by adding `#[partial_default(bound = "T: MyTrait")]` to +/// the type, which replaces any bounds inferred by `PartialDefault`. Use an empty string to impose +/// no restrictions at all. +/// +/// [deserialize_in_place]: https://docs.rs/serde/1.0.189/src/serde/de/mod.rs.html#546-568 +/// [conditional_assign]: https://docs.rs/subtle/2.5.0/subtle/trait.ConditionallySelectable.html +pub trait PartialDefault: Sized { + /// Returns a value that can be safely dropped or assigned over. + fn partial_default() -> Self; +} + +/// If a type does implement `Default`, its `PartialDefault` implementation will match. +impl PartialDefault for T { + fn partial_default() -> Self { + Self::default() + } +}