Initial version of PartialDefault
This commit is contained in:
commit
61137b9c15
28
.github/workflows/ci.yml
vendored
Normal file
28
.github/workflows/ci.yml
vendored
Normal 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
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
30
Cargo.toml
Normal file
30
Cargo.toml
Normal 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
29
README.md
Normal 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
25
derive/Cargo.toml
Normal 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
17
derive/README.md
Normal 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
151
derive/src/body_impl.rs
Normal 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 = ¶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<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()
|
||||
})
|
||||
}
|
||||
}
|
||||
56
derive/src/default_attr.rs
Normal file
56
derive/src/default_attr.rs
Normal 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
69
derive/src/lib.rs
Normal 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
32
derive/src/util.rs
Normal 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
183
derive/tests/tests.rs
Normal 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
61
src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user