Initial version of PartialDefault

This commit is contained in:
Jordan Rose 2023-10-25 12:07:36 -07:00
commit 61137b9c15
12 changed files with 683 additions and 0 deletions

28
.github/workflows/ci.yml vendored Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

30
Cargo.toml Normal file
View File

@ -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"]

29
README.md Normal file
View File

@ -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

25
derive/Cargo.toml Normal file
View File

@ -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"] }

17
derive/README.md Normal file
View File

@ -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

151
derive/src/body_impl.rs Normal file
View File

@ -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<TokenStream, Error> {
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<TokenStream, Error> {
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 = &param.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<TokenStream, Error> {
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::<Result<Vec<_>, Error>>()?;
quote! {
{
#( #field_assignments ),*
}
}
}
syn::Fields::Unnamed(ref fields) => {
let field_assignments = fields
.unnamed
.iter()
.map(field_default_expr)
.collect::<Result<Vec<TokenStream>, 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<TokenStream, Error> {
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()
})
}
}

View File

@ -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<Option<Self>, 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)
}
}
}

69
derive/src/lib.rs Normal file
View File

@ -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<T> {
/// Bar,
/// #[partial_default]
/// Baz {
/// #[partial_default(value = "12")]
/// a: i32,
/// b: i32,
/// #[partial_default(value = "Some(Default::default())")]
/// c: Option<i32>,
/// #[partial_default(value = "vec![1, 2, 3]")]
/// d: Vec<u32>,
/// #[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(),
}
}

32
derive/src/util.rs Normal file
View File

@ -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<T, F>(
iter: impl Iterator<Item = T>,
error_message_for_multiple_matches: &str,
pred: F,
) -> Result<Option<T>, Error>
where
T: Spanned,
F: Fn(&T) -> Result<bool, Error>,
{
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)
}

183
derive/tests/tests.rs Normal file
View File

@ -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<T>
where
T: Ord, // unrelated
{
#[partial_default(value = "Some(PartialDefault::partial_default())")]
x: Option<T>,
}
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<T> {
#[partial_default(value = r#"Some("0".parse().ok().unwrap())"#)]
x: Option<T>,
}
assert!(Foo::partial_default() == Foo { x: Some(0) });
}
#[test]
fn test_generics_type_parameters_no_bound() {
#[derive(PartialEq, PartialDefault)]
#[partial_default(bound = "")]
struct Foo<T> {
x: Option<T>,
}
assert!(Foo::partial_default() == Foo::<i32> { 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<u32>,
}
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()));
}

61
src/lib.rs Normal file
View File

@ -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<T: Default> PartialDefault for T {
fn partial_default() -> Self {
Self::default()
}
}