# optionable
A rust library to derive `optioned` structs/enums versions of existing types where all fields have been recursively
replaced with versions that support setting just a subset of the relevant fields (or none at all).
One motivation for this concept is the common problem when expressing patches that for a given rust struct `T`
a corresponding struct `T::Optioned` would be required where all fields are recursively optional to specify.
While trivial to write for plain structures this quickly becomes tedious for nested structs/enums.
#### Links
- [crates.io](https://crates.io/crates/optionable)
- [docs.rs documentation](https://docs.rs/optionable/)
## Kubernetes server-side-apply
Examples for the usage of this library for type-safe Kubernetes server-side-apply in Rust can be found [here](https://github.com/ngergs/optionable/tree/main/example/k8s).
The library allows to use server-side-apply with built-in Kubernetes types by providing optioned variants for all types
from [k8s-openapi](https://crates.io/crates/k8s-openapi). It also provides tooling to derive optioned variants for
`kube::CustomResource` implementations via an attribute macro.
For detailed documentation, see the documentation in [kube module](https://docs.rs/optionable/latest/optionable/kube/index.html) for the CRD use case and the [examples](https://github.com/ngergs/optionable/tree/main/example/k8s).
## Deriving optional structs/enums
The core utility of this library is to provide an `Optionable`-derive macro that derives such an optioned type
and implements the corresponding `Optionable`-trait (see below for details).
It supports nested structures, enums as well as various container types.
For detailed configuration options via helper attributes, see the [`Optionable`-derive macro docs](https://docs.rs/optionable/latest/optionable/derive.Optionable.html).
The general logic is the same as for other rust derives. If you want to use the derive `Optionable` for a struct/enum
every type used for a field needs to also have implemented the corresponding `Optionable` trait:
```rust
#[derive(Optionable)]
#[optionable(derive(Default, Serialize, Deserialize))]
struct Address {
street_name: String,
number: u8,
}
fn example() {
let _ = AddressOpt {
street_name: Some("a".to_owned()),
// fill the other fields with `None`
..Default::default()
};
}
```
The generated optioned type is (shown here with resolved associated types) as follows:
```rust
#[derive(Default, Serialize, Deserialize)]
struct AddressOpt {
street_name: Option<String>,
number: Option<u8>,
}
```
### Enum support
Deriving optioned versions also works with enums:
```rust
#[derive(Optionable)]
enum AddressEnum {
Plain(String),
AddressExplicit { street: String, number: u32 },
AddressNested(Address)
}
fn example() {
let _ = AddressEnumOpt::AddressExplicit {
street: Some("a".to_owned()),
number: None
};
}
```
## Core concept
The main `Optionable` trait is quite simple:
```rust
pub trait Optionable {
type Optioned;
}
```
It is a marker trait that allows to express for a given type `T` which type should be considered its `T::Optioned` type
such that `Option<T::Optioned>` would represent all variants of partial completeness.
For types without inner structure this means that the `Optioned` type will just resolve to the type itself, e.g.
```rust
impl Optionable for String {
type Optioned = String;
}
```
For many primitive types as well as common wrapper or collection types the `Optionable`-trait is already implemented.
### Conversion
Per default also conversion traits for struct/enums with sized fields will be derived.
The relevant traits are (shown here without comments and with some `where` clauses omitted):
```rust
pub trait OptionableConvert: Sized + Optionable {
fn into_optioned(self) -> Self::Optioned;
fn try_from_optioned(value: Self::Optioned) -> Result<Self, Error>;
fn merge(&mut self, other: Self::Optioned) -> Result<(), Error>;
}
// sealed, auto-implemented from `OptionableConvert` for every respective `T::Optioned`
pub trait OptionedConvert<T>
where
T: Optionable<Optioned=Self> + OptionableConvert,
{
fn from_optionable(value: T) -> Self;
fn try_into_optionable(self) -> Result<T, Error>;
}
```
## Crate features
- `derive`: Default-feature, re-exports the `Optionable` derive macro.
- `std`: Default-feature. Adds `Optionable`-implementations for many [std](https://doc.rust-lang.org/std/)-lib types.
- `alloc`: Adds `Optionable`-implementations for [alloc](https://doc.rust-lang.org/alloc/) types (only useful when not enabling the `std` feature).
- `chrono`: Derive `Optionable` for types from [chrono](https://docs.rs/chrono/latest/chrono/).
- `serde_json`: Derive `Optionable` for [serde_json](https://docs.rs/serde_json/latest/serde_json/)::Value.
- `k8s_openapi_v1_(30..=34)`: Adds `Optionable`-implementations for all [k8s-openapi](https://docs.rs/k8s-openapi/latest/k8s_openapi) types. Only on feature version, e.g. `k8s_openapi_v1_34` may be enabled at once.
- `k8s_openapi_convert`: Adds `OptionableConvert`-implementations for all optioned [k8s-openapi](https://docs.rs/k8s-openapi/latest/k8s_openapi) types specified by the `k8s_openapi_v1_(30..=34)` feature.
- `kube`: Adds a serialization helper used by `#[derive(Optionable]` if `#[derive(kube)]` is set to add `apiVersion` and `kind` from the `kube::Resource`-impl to the serialized output.
## Limitations
### External types
Due to the orphan rule the usage of the library becomes cumbersome if one has a use case which heavily relies on
crate-external types.
For well-established libraries adding corresponding `impl` to this crate (feature-gated) would be a worthwhile approach.
### Resolving associated types
Due to the use of associated types some IDE-hints do not fully resolve the associated types leaving you with
`<i32 as Optionable>::Optioned` instead of `i32`. Luckily, for checking type correctness and also for error messages
when using wrong types the associated types are resolved.
For the derived `Optioned`-structs/enums a related issue is that other derive macros for those derived types won't see the resolved
associated types. Therefore, corresponding type bounds have to be added (done by the `Optionable`-derive) to the `Optioned`-structs/enums:
```rust
#[derive(Optionable)]
#[optionable(derive(Serialize))]
struct DeriveExample<T> {
name: T,
}
// The generated code for the struct is shown below (simplified)
#[derive(Serialize)]
struct DeriveExampleOpt<T>
where
T: Optionable,
// extra `Serialize` bound on the struct level
<T as Optionable>::Optioned: Sized + Serialize,
{
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<<T as Optionable>::Optioned>
}
```
## Similar crates
One crate with similar scope is [optional_struct](https://crates.io/crates/optional_struct).
It focuses specifically on structs (not enums) and offers a more manual approach, especially in respect to nested
sub-struct,
providing many fine-grained configuration options.
Another crate is [struct-patch](https://crates.io/crates/struct-patch).
It focuses on patching structs (not enums), especially from serde inputs. Nesting is supported with manual helper
annotations.
## License
You can use this under the conditions of the [MIT license](LICENSE-MIT) or
the [Apache License, Version 2.0](LICENSE-APACHE) at your option.
### Contributing
Any contributor has to agree to have their contribution also dual-licensed under the MIT as well as Apache-2.0 license
as
specified above in the `License` subsection.