Params Framework Support
For ergonomic usage and accurate data modelling, the following types need to exist for each item:
Params
ParamsPartial
ParamsSpec
ParamsSpecBuilder
Since Params
is defined by the item implementor, the other three structs can only be defined after it, and can be generated by a proc macro.
Questions:
-
How does Peace know what fields to fill in, and how?
The fields are known by the
ParamsSpec
impl, though it resides in the implementor's crate.Peace can reference the
ParamsSpec
through aParams::Spec
associated type. -
Does Peace need to know about the field, or can it be in the generated impl?
It needs to go, for each field on the
ParamsSpec
, fill in theParamsPartial
. This could be implemented byParamsSpec::partial_from(&Resources) -> ParamsPartial
:impl ParamsSpec { fn partial_from(resources: &Resources) -> ParamsPartial { // for each field (generated by proc macro) let value_0 = value_0_spec.try_value_from(resources); let value_1 = value_1_spec.try_value_from(resources); ParamsPartial { value_0, value_1 } } } enum ValueSpec<T> { Value(T), From, FromMap(Box<dyn Fn(&Resources) -> Option<T>>), } impl<T> ValueSpec<T> { fn try_value_from(&self, resources: &Resources) -> Option<T> { match self { Self::Value(t) => Some(t.clone()), Self::From => resources.try_borrow::<T>().cloned(), Self::FromMap(f) => f(resources), } } }
-
How does
ValueSpec::FromMap
get instantiated:Probably something like this:
impl ParamsSpecBuilder { // for each field fn field_from_map<F, U>(mut self, f: F) -> Self where F: &U -> Field { let from_map_boxed = Box::new(move |resources| { resources.try_borrow::<Field>().map(f) }); self.field_spec = ValueSpec::FromMap(from_map_boxed); self } }
To allow Peace to reference those types, and for Peace to expose that to users, we need the following traits:
dyn Params
: To know what type theParamsPartial
,ParamsSpec
, andParamsSpecBuilder
are.dyn ParamsSpec
: Peace needs the spec to generate aParamsPartial
fromResources
.
Implications On Serialization and Flow Params
It is inconvenient to have to register item params types as flow params, because the presence of the item could automatically tell Peace its params type.
However, users:
- Must provide the param values / spec for each item for the first execution of a flow.
- May have different preferences whether they have to provide the values / spec for subsequent executions, and whether it implicitly uses what was used before, or is explicit for each field.
What we need:
- Flow params are serialized separately. Flow params may be values that control how things are presented, but not affect the actual functional execution, so they may be log verbosity.
- Item params spec must be deserialized, and overwritten by provided values.
- Item params spec must be serialized before executing a command.
- Item params must be serialized as execution happens.
- Item params partials do not need to be serialized, only presented, as they can be re-calculated from state for each discover / read command execution.
Mapping Function Names
A common workflow is to initialize a project, and subsequently run commands against that project.
Because running commands requires instantiating a CmdCtx
, and because rust code cannot be serialized and deserialized (or, we'd have to ship a rust compiler), the above with_*_from_map
builder would have to be passed in to every CmdCtx*
instantiation, which is a poor development experience -- it would be a lot of duplication, and it would be duplication per mapped item parameter / field.
Instead, we add one layer of indirection, with a serializable name that serves as a function name / identifier:
let file_upload_params_spec = FileUploadParams::spec()
// ..
.with_dest_from_mapping_fn(AppMappingFns::AddressFromServer)
.build();
cmd_ctx_builder
.with_item_params(file_upload_params_spec)
.await?;
// Trait provided by Peace framework
pub trait MappingFns:
Clone + Copy + Debug + Hash + PartialEq + Eq + Serialize + DeserializeOwned + Send + Sync + 'static
{
/// Returns an iterator over all variants of these mapping functions.
fn iter() -> impl Iterator<Item = Self> + ExactSizeIterator;
/// Returns a string representation of the mapping function name.
///
/// # Implementors
///
/// The returned name is considered API, and should be stable.
fn name(self) -> MappingFnName;
/// Returns the mapping function corresponding to the given variant.
fn mapping_fn(self) -> Box<dyn MappingFn>;
}
// Usage
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMappingFns {
AddressFromServer,
}
impl MappingFns for AppMappingFns {
fn mapping_fn(self) -> Box<dyn MappingFn> {
match self {
Self::AppMappingFns => {
MappingFnImpl::from_func(|server| {
let ip = server.ip();
format!("user@${ip}:/path/to/dest")
})
}
}
}
}