Skip to content

Commit

Permalink
feat: add try_from attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
tenuous-guidance committed Oct 20, 2023
1 parent 622b67f commit d90d75e
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 67 deletions.
73 changes: 57 additions & 16 deletions confik-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ impl FromMeta for FieldFrom {
}
}

/// Handles `try_from` attributes for dealing with foreign types.
#[derive(Debug)]
struct FieldTryFrom {
ty: Type,
}

impl FromMeta for FieldTryFrom {
fn from_expr(ty: &Expr) -> darling::Result<Self> {
let Ok(ty) = parse2(ty.to_token_stream()) else {
return Err(syn::Error::new(
ty.span(),
format!("Unable to parse type from: {}", ty.to_token_stream()),
)
.into());
};

Ok(Self { ty })
}
}

/// Handles requesting to forward `serde` attributes.
#[derive(Debug)]
struct ForwardSerde {
Expand Down Expand Up @@ -115,7 +135,7 @@ struct VariantImplementer {

impl VariantImplementer {
/// Define the builder variant for a given target variant
fn define_builder(var_impl: &SpannedValue<Self>) -> TokenStream {
fn define_builder(var_impl: &SpannedValue<Self>) -> syn::Result<TokenStream> {
let Self {
ident,
fields,
Expand All @@ -126,17 +146,17 @@ impl VariantImplementer {
let field_vec = fields
.iter()
.map(FieldImplementer::define_builder)
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()?;
let fields = ast::Fields::new(fields.style, field_vec).into_token_stream();

let discriminant = discriminant
.as_ref()
.map(|disc| quote_spanned!(disc.span() => = discriminant));

quote_spanned! { var_impl.span() =>
Ok(quote_spanned! { var_impl.span() =>
#forward_serde
#ident #fields #discriminant
}
})
}

fn impl_merge(var_impl: &SpannedValue<Self>) -> TokenStream {
Expand Down Expand Up @@ -297,6 +317,10 @@ struct FieldImplementer {
/// Enables handling foreign types.
from: Option<FieldFrom>,

/// A type which implements `Configuration`, for which the field implements `TryFrom`.
/// Enables handling foreign types.
try_from: Option<FieldTryFrom>,

/// The field name, if a named field.
///
/// If not, then you will probably want to enumerate through the list of these and
Expand Down Expand Up @@ -351,13 +375,14 @@ impl FieldImplementer {
}

/// Define the builder field for a given target field.
fn define_builder(field_impl: &SpannedValue<Self>) -> TokenStream {
fn define_builder(field_impl: &SpannedValue<Self>) -> syn::Result<TokenStream> {
let Self {
ty,
ident,
secret,
forward_serde,
from,
try_from,
..
} = field_impl.as_ref();

Expand All @@ -367,7 +392,17 @@ impl FieldImplementer {

// Builder type based on original field type via [`confik::Configuration`]
// If `from` is set, then use that type instead.
let ty = from.as_ref().map_or(ty, |from| &from.ty);
let ty = match (from, try_from) {
(Some(from), Some(try_from)) => {
let msg = "Cannot support both `try_from` and `from` confik attributes";
let mut err = syn::Error::new(try_from.ty.span(), msg);
err.combine(syn::Error::new(from.ty.span(), msg));
return Err(err);
}
(Some(FieldFrom { ty }), None) | (None, Some(FieldTryFrom { ty })) => ty,
(None, None) => ty,
};

let ty = quote_spanned!(ty.span() => <#ty as ::confik::Configuration>::Builder);

// If secret then wrap in [`confik::SecretBuilder`]
Expand All @@ -377,11 +412,11 @@ impl FieldImplementer {
ty
};

quote_spanned! { ident.span() =>
Ok(quote_spanned! { ident.span() =>
#[serde(default)]
#forward_serde
#ident #ty
}
})
}

/// Define how to merge the given field in a struct impl.
Expand Down Expand Up @@ -477,6 +512,12 @@ impl FieldImplementer {
field_build = quote_spanned! {
field_build.span() => #field_build.into()
}
} else if field_impl.try_from.is_some() {
field_build = quote_spanned! {
field_build.span() => #field_build.try_into().map_err(|e|
::confik::FailedTryInto::new(e)
)?
}
}

match style {
Expand Down Expand Up @@ -618,7 +659,7 @@ impl RootImplementer {
}

/// Defines the builder for the target.
fn define_builder(&self) -> TokenStream {
fn define_builder(&self) -> syn::Result<TokenStream> {
let Self {
ident: target_name,
data,
Expand Down Expand Up @@ -648,7 +689,7 @@ impl RootImplementer {
let variants = variants
.iter()
.map(VariantImplementer::define_builder)
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()?;

quote_spanned! { target_name.span() =>
{
Expand All @@ -665,7 +706,7 @@ impl RootImplementer {
let field_vec = fields
.iter()
.map(FieldImplementer::define_builder)
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()?;
ast::Fields::new(fields.style, field_vec).into_token_stream()
}
};
Expand All @@ -684,14 +725,14 @@ impl RootImplementer {

let (_impl_generics, type_generics, where_clause) = generics.split_for_impl();

quote_spanned! { target_name.span() =>
Ok(quote_spanned! { target_name.span() =>
#[derive(::std::default::Default, ::confik::__exports::__serde::Deserialize, #additional_derives )]
#[serde(crate = "::confik::__exports::__serde")]
#forward_serde
#vis #enum_or_struct_token #builder_name #type_generics #where_clause
#bracketed_data
#terminator
}
})
}

/// Implement the `ConfigurationBuilder::merge` method for our builder.
Expand Down Expand Up @@ -757,7 +798,7 @@ impl RootImplementer {
.collect::<Vec<_>>();
quote! {
Ok(match self {
Self::ConfigBuilderUndefined => return Err(<::confik::MissingValue as ::std::default::Default>::default()),
Self::ConfigBuilderUndefined => return Err(::confik::Error::MissingValue(<::confik::MissingValue as ::std::default::Default>::default())),
#( #variants, )*
})
}
Expand All @@ -767,7 +808,7 @@ impl RootImplementer {
quote! {
// Allow useless conversions as the default handling may call `.into()` unnecessarily.
#[allow(clippy::useless_conversion)]
fn try_build(self) -> ::std::result::Result<Self::Target, ::confik::MissingValue> {
fn try_build(self) -> ::std::result::Result<Self::Target, ::confik::Error> {
#field_build
}
}
Expand Down Expand Up @@ -859,7 +900,7 @@ impl RootImplementer {
fn derive_macro_builder_inner(target_struct: DeriveInput) -> syn::Result<proc_macro::TokenStream> {
let implementer = RootImplementer::from_derive_input(&target_struct)?;
implementer.check_valid()?;
let builder_struct = implementer.define_builder();
let builder_struct = implementer.define_builder()?;
let builder_impl = implementer.impl_builder();
let target_impl = implementer.impl_target();

Expand Down
4 changes: 3 additions & 1 deletion confik-macros/tests/trybuild.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// only run on MSRV to avoid changes to compiler output causing CI failures
#[rustversion::stable(1.66)]
// #[rustversion::stable(1.75)]
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
Expand Down Expand Up @@ -27,6 +27,7 @@ fn compile_macros() {
t.pass("tests/trybuild/21-field-from.rs");
t.pass("tests/trybuild/22-dataless-types.rs");
t.pass("tests/trybuild/23-where-clause.rs");
t.pass("tests/trybuild/24-field-try_from.rs");

t.compile_fail("tests/trybuild/fail-default-parse.rs");
t.compile_fail("tests/trybuild/fail-default-invalid-expr.rs");
Expand All @@ -37,4 +38,5 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/fail-uncreatable-type.rs");
t.compile_fail("tests/trybuild/fail-not-a-type.rs");
t.compile_fail("tests/trybuild/fail-default-not-expression.rs");
t.compile_fail("tests/trybuild/fail-from-and-try_from.rs");
}
16 changes: 6 additions & 10 deletions confik-macros/tests/trybuild/fail-default-parse.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ error[E0277]: the trait bound `usize: From<i32>` is not satisfied
| ^----
| |
| the trait `From<i32>` is not implemented for `usize`
| this tail expression is of type `_`
| this tail expression is of type `i32`
|
= help: the following other types implement trait `From<T>`:
<f32 as From<i16>>
<f32 as From<i8>>
<f32 as From<u16>>
<f32 as From<u8>>
<f64 as From<f32>>
<f64 as From<i16>>
<f64 as From<i32>>
<f64 as From<i8>>
and $N others
<usize as From<bool>>
<usize as From<u8>>
<usize as From<u16>>
<usize as From<NonZeroUsize>>
<usize as From<std::ptr::Alignment>>
= note: required for `i32` to implement `Into<usize>`
20 changes: 5 additions & 15 deletions confik-macros/tests/trybuild/fail-field-from-unknown-type.stderr
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
error[E0412]: cannot find type `A` in this scope
--> tests/trybuild/fail-field-from-unknown-type.rs:6:21
|
5 | struct Config {
| - help: you might be missing a type parameter: `<A>`
6 | #[confik(from = A)]
| ^ not found in this scope
|
help: you might be missing a type parameter
|
5 | struct Config<A> {
| +++

error[E0412]: cannot find type `A` in this scope
--> tests/trybuild/fail-field-from-unknown-type.rs:6:21
|
6 | #[confik(from = A)]
| ^ not found in this scope

error[E0283]: type annotations needed
--> tests/trybuild/fail-field-from-unknown-type.rs:6:21
|
5 | struct Config {
| ------ in this derive macro expansion
6 | #[confik(from = A)]
| _____________________^
7 | | param: String,
| |_________^ cannot infer type
|
= note: cannot satisfy `_: Default`
= note: this error originates in the derive macro `::std::default::Default` (in Nightly builds, run with -Z macro-backtrace for more info)
3 changes: 3 additions & 0 deletions confik/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Add support for `try_from` attribute, following the rules of `from` but using `TryFrom`. Note: This required a change in the return type of `try_build` and a new `Error` variant.
- This will not break existing code unless it contains manual implementations of `Configuration`.

## 0.10.2

- Remove `Debug` implementation from configuration builders.
Expand Down
29 changes: 25 additions & 4 deletions confik/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
//! Although in theory [`UnexpectedSecret`] and [`MissingValue`] are also user facing, they are entirely
//! handled by the `derive` internals, so is counted as internal.

use std::error::Error as StdError;
use std::{borrow::Cow, error::Error as StdError};

use thiserror::Error;

use crate::{MissingValue, UnexpectedSecret};
use crate::{FailedTryInto, MissingValue, UnexpectedSecret};

/// Possible error values.
#[derive(Debug, Error)]
Expand All @@ -23,9 +23,30 @@ pub enum Error {
#[error("Source {1} returned an error")]
Source(#[source] Box<dyn StdError + Send + Sync>, String),

/// The value contained in the `path` was marked as a [`SecretBuilder`](crate::SecretBuilder) but
/// was parsed from a [`Source`](crate::Source) that was not marked as a secret
/// The value contained in the `path` was marked as a [`SecretBuilder`](crate::SecretBuilder)
/// but was parsed from a [`Source`](crate::Source) that was not marked as a secret
/// (see [`Source::allows_secrets`](crate::Source::allows_secrets)).
#[error("Found a secret in source {1} that does not permit secrets")]
UnexpectedSecret(#[source] UnexpectedSecret, String),

/// The value contained in the `path` was attempted to be converted and that conversion failed.
#[error(transparent)]
TryInto(#[from] FailedTryInto),
}

impl Error {
/// Used in chaining [`MissingValue`] errors during [`crate::Configuration::try_build`].
#[doc(hidden)]
pub fn prepend(self, path_segment: impl Into<Cow<'static, str>>) -> Self {
match self {
Self::MissingValue(err) => Self::MissingValue(err.prepend(path_segment)),
Self::TryInto(err) => Self::TryInto(err.prepend(path_segment)),
// This branch will probably never be hit but exists so that the function works the way
// a caller would expect if there is a use case for it in future.
Self::UnexpectedSecret(err, source) => {
Self::UnexpectedSecret(err.prepend(path_segment), source)
}
Self::Source(err, source) => Self::Source(err, source),
}
}
}
22 changes: 20 additions & 2 deletions confik/src/lib.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ This crate provides implementations of [`Configuration`] for a number of `std` t
- `secrecy`: v0.8
- Note: `#[config(secret)]` is not needed (although it is harmless) for `secrecy`'s types as they are always treated as secrets.

If there's another foreign type used in your config, then you will not be able to implement [`Configuration`] for it. Instead any type that implements [`Into`] can be used.
If there's another foreign type used in your config, then you will not be able to implement [`Configuration`] for it. Instead any type that implements [`Into`] or [`TryInto`] can be used.

```
struct ForeignType {
Expand All @@ -208,10 +208,28 @@ impl From<MyForeignTypeCopy> for ForeignType {
}
}
#[derive(confik::Configuration)]
struct MyForeignTypeIsize {
data: isize
}
impl TryFrom<MyForeignTypeIsize> for ForeignType {
type Error = <usize as TryFrom<isize>>::Error;
fn try_from(copy: MyForeignTypeIsize) -> Result<Self, Self::Error> {
Ok(Self {
data: copy.data.try_into()?,
})
}
}
#[derive(confik::Configuration)]
struct Config {
#[confik(from = MyForeignTypeCopy)]
foreign_data: ForeignType
foreign_data: ForeignType,
#[confik(try_from = MyForeignTypeIsize)]
foreign_data_isized: ForeignType,
}
```

Expand Down
Loading

0 comments on commit d90d75e

Please sign in to comment.