#struct-fields #proc-macro #introspection #named-fields #query-parameters #utility #field-name

extruct

Extruct is a proc-macro library that provides tools for listing named struct fields and implementing conversion from a larger struct containing fields with same names

4 releases

0.2.1 Oct 22, 2024
0.2.0 Aug 29, 2024
0.1.1 Aug 26, 2024
0.1.0 Aug 23, 2024

#171 in Procedural macros

34 downloads per month

MIT license

13KB

extruct

crates.io version crates.io License

Extruct

Extruct is a proc-macro library that provides tools for listing named struct fields and implementing conversion from a larger struct containing fields with same names.

Description

Extruct is primarily driven by the following use-case.
It is customary for REST API to return a whole bunch of properties of an object in a JSON response. Oftentimes, you as a user of this API are only interested in a few of those and don't really need others. To facilitate this, REST services typically support passing the list of properties the client is interested in through the fields query parameter. See examples of this pattern here and here.
Extruct offers two simple-to-use procedural macros that can be useful in implementing wrappers around REST API that can retrieve only specific properties from the backend.

The first macro is a derive macro Fields. It applies to structs with named fields and implements the Fields trait that returns a list of fields' names as string literals.

The second macro is an attribute macro extruct_from. It can be used when there exists a "superstruct" that contains all possible fields. In this case, when defining your own struct which contains only a subset of all those fields you can use extruct_from which will implement the std::convert::From trait converting an instance of the superstruct into an instance of your struct by converting all fields from the original struct into fields with the same name of the substruct.

Besides being handy for turning "full" objects into "partial", this ensures that all fields are named the same and their types are compatible. To prove this, extruct_from implements the ExtructedFrom trait which extends std::convert::From and verifies that the superstruct is not only convertible into substruct but also that all the respective fields have same names.

Examples

use extruct::ExtructedFrom;
use extruct::Fields;
use extruct::extruct_from;

#[derive(Fields)]
struct SuperStruct {
    one_field: String,
    another_field: u32,
    and_one_more: char,
}

#[derive(Fields)]
#[extruct_from(SuperStruct)]
struct SubStruct {
    and_one_more: String,
}

fn convert_preserving_names<T, S>(sup: S) -> T
where
    T: ExtructedFrom<S>,
{
    sup.into()
}

assert_eq!(SuperStruct::fields(), ["one_field", "another_field", "and_one_more"]);
assert_eq!(SubStruct::fields(), ["and_one_more"]);

let sup = SuperStruct {
    one_field: "str".to_owned(),
    another_field: 1135,
    and_one_more: 'x',
};

let sub: SubStruct = sup.into();
assert_eq!(sub.and_one_more, "x".to_owned());

Notes

The Fields derive macro can be applied to named structs and unit structs, but unnamed structs can only be annotated with #[derive(Fields)] if they are empty.

The extruct_from attribute macro can only be applied to non-generic named structs and can only reference a concrete (non-generic) struct as a superstruct. This limitation holds because the macro does not have visibility into the definition of the superstruct and therefore would not be able to specify trait bounds for generic fields when implementing the std::convert::From trait. If you want to use a specific instantiation of a generic struct as a superstruct, you can use a type alias like so:

// A generic struct cannot be referenced by the extruct_from() attribute macro...
struct GenericStruct<T, S> {
    one_field: T,
    another_field: u32,
    and_one_more: S,
}

// ... but a non-generic type alias can be referenced by the extruct_from()
// attribute macro just fine
type ConcreteStruct = GenericStruct<String, char>;

#[extruct_from(ConcreteStruct)]
struct SubStruct {
    and_one_more: String,
}

let sup = ConcreteStruct {
    one_field: "str".to_owned(),
    another_field: 1135,
    and_one_more: 'x',
};

let sub: SubStruct = sup.into();
assert_eq!(sub.and_one_more, "x".to_owned());

Dependencies

~215–650KB
~15K SLoC