Params Specification

For an item to work with different values, the values must be passed in.

Item implementors define these as part of the Item trait:

trait Item {
    type Params: ..;
}

Use Cases

The following shows a number of use cases of these params:

  • State Apply: Param values must be known, and Peace should pass concrete values to the Item::{state_current, state_goal, apply} functions.

  • State Discovery (fallible):

    Param values may be known, if predecessors have previously executed.

    • try_state_current: StateDiscoverCmd::current

      e.g. Look up file contents on a remote host:

      match params_partial.dest_ip() {
          Some(dest_ip) => Some(file_hash(dest_ip, path)),
          None => None, // or `Some(FileState::None)`
      }
    • try_state_goal: StateDiscoverCmd::goal

      e.g. Look up source file contents:

      match params_partial.src_path() {
          Some(src_path) => file_hash(src_path),
          None => None, // or `Some(FileState::None)`
      }

By Item Function

  • try_state_current: Should work with field_partials.

  • try_state_goal: Should work with field_partials.

  • state_current: Needs real concrete param values.

  • state_goal: Needs real concrete param values.

  • state_diff: Doesn't need parameters or data; everything should be captured in States.

    But for presentation, it's useful to know what a file should be (current vs goal), or difference between params (multiple profile current vs current).

  • state_clean: Maybe always returns ItemState::None, and doesn't need parameters or data.

    However, presenting state_clean with e.g. a file path, would mean the None state contains the value, which means state_clean needs params.

    Arguably state_goal will show the path that would be created.

    StateDiff for cleaning should also show the deletion of the path.

  • apply_check: Doesn't need parameters or data.

  • apply_dry: Needs concrete param values, even if they are fake.

  • apply: Needs real concrete param values.

Encoding: Serialization / Deserialization

Because:

  • It is convenient to serialize Item::Params::Spec and store it, and deserialize it for use at a later time.
  • It is useful to support config-based parameter specification (no compiler needed).
  • It is not possible to serialize closures.

Then there must be a way to encode the same functionality that Item::Params::Spec::field_from_map provides, as something serializable.

Possibilities:

  • ToString and FromStr impls that represent the logic
  • Serialized form uses enum variants, and when deserializing, map that back to functions.
  • Custom language.

Code Implications

From the implementor's perspective, item trait needs to change to support the above use cases.

The following snippets are here to show the changes that include the above concepts. These are:

  • non-compilable.
  • just enough to show where types are changed.
  • show certain trait bounds (non-exhaustive).
  • do not include the encoding / decoding of field_from_map concept.

Framework

// Traits in Peace Framework
trait Item {
    type Params: Params;

    fn setup(&self, resources);
    fn try_state_current(fn_ctx, params_partial, data);
    fn try_state_goal(fn_ctx, params_partial, data);
    fn state_clean      (        params_partial, data);
    fn state_current    (fn_ctx, params,         data);
    fn state_goal    (fn_ctx, params,         data);
    fn apply_dry        (fn_ctx, params,         data, state_current, state_target, diff);
    fn apply            (fn_ctx, params,         data, state_current, state_target, diff);
    fn apply_check      (        params_partial, data, state_current, state_target, diff);
    fn state_diff       (        params_partial, data, state_a, state_b);

    // Once more, with types:
    fn setup(&self, &mut Resources<Empty>);
    fn try_state_current(FnCtx<'_>, Self::Params<'_>::Partial, Self::Data<'_>);
    fn try_state_goal(FnCtx<'_>, Self::Params<'_>::Partial, Self::Data<'_>);
    fn state_clean      (           Self::Params<'_>::Partial, Self::Data<'_>);
    fn state_current    (FnCtx<'_>, Self::Params<'_>         , Self::Data<'_>);
    fn state_goal    (FnCtx<'_>, Self::Params<'_>         , Self::Data<'_>);
    fn apply_dry        (FnCtx<'_>, Self::Params<'_>         , Self::Data<'_>, Self::State, Self::State, Self::StateDiff);
    fn apply            (FnCtx<'_>, Self::Params<'_>         , Self::Data<'_>, Self::State, Self::State, Self::StateDiff);
    fn apply_check      (           Self::Params<'_>::Partial, Self::Data<'_>, Self::State, Self::State, Self::StateDiff);
    fn state_diff       (           Self::Params<'_>::Partial, Self::Data<'_>, Self::State, Self::State);
}

/// For Peace to access <Item::Params as Params>::Spec
trait Params {
    type Spec: Serialize + Deserialize;
    type SpecBuilder: SpecBuilder<Output = Self::Spec>;
    type Partial: Serialize + Deserialize;
}

enum ValueSpec<T> {
    Value(T),
    From,
    FromMap(Box<dyn Fn(&Resources) -> Option<T>>),
}

Also need to provide a Params derive macro.

Design Note

The apply_check and state_diff functions usually are not expected to need params and data, but some items may use them, such as the ShCmdItem which accesses params to determine the script to run.

Regarding params will be Params or Params<'_>::Partial for state_diff, if we call state_diff for a file upload item, we must have both the current state and goal state. Then we need to ask, does having both states imply Params is fully resolvable?

If we call state_current for a file upload:

  • the destination server may not be there
  • so params may not have the IP
  • we may still return Some(State::Empty)
  • So params may still be partial, even if State is Some.

If we call state_goal for a file upload:

  • the source file may not be there
  • so params may not have the source content hash
  • we may still return Some(State::Empty)
  • So params may still be partial, even if State is Some.

Implementor

// Implementation
struct FileUploadItem;

impl Item for FileUploadItem {
    type Params = FileUploadParams;
}

#[derive(Clone, Debug, Params, Serialize, Deserialize)]
struct FileUploadParams {
    src: PathBuf,
    dest_ip: IpAddr,
    dest_path: PathBuf,
}

Auto generated by Params derive:

impl Params for FileUploadParams {
    type Spec = FileUploadParamsSpec;
    type SpecBuilder = FileUploadParamsSpecBuilder;
    type Partial = FileUploadParamsPartial;
}

// Serialize / Deserialize not needed.
struct FileUploadParamsPartial {
    src: Option<PathBuf>,
    dest_ip: Option<IpAddr>,
    dest_path: Option<PathBuf>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct FileUploadParamsSpec {
    src: ValueSpec<PathBuf>,
    dest_ip: ValueSpec<IpAddr>,
    dest_path: ValueSpec<PathBuf>,
}

#[derive(Clone, Debug)]
struct FileUploadParamsSpecBuilder {
    src: Option<ValueSpec<PathBuf>>,
    dest_ip: Option<ValueSpec<IpAddr>>,
    dest_path: Option<ValueSpec<PathBuf>>,
}

See:

Implementation

See the params_derive crate for code gen.

The following command helps to see what's been generated.

cargo expand --package peace_item_blank blank_params \
  | sd -f cm '^    #\[doc\(hidden\)\][\n](^[ ]{4}[a-z# ].+[\n])+^[ ]{4}\};\n' '' \
  | sd -f cm '^    #\[automatically_derived\][\n](^[ ]{4}[# a-z{].*[\n])+^[ ]{4}\{?\}\n' '' \
  | sd -f cm '^    #\[allow\(unused_qualifications\)\][\n](^[ ]{4}[# a-z{].*[\n])+^[ ]{4}\}\n' '' \
  | sd -f cm '^    #\[serde\(bound = ""\)\]' '    #[derive(Serialize, Deserialize)]\n    #[serde(bound = "")]' \
  | sd -f cm '^    extern crate test;[\n](^[ ]{4}.*[\n])+^\}' '}' \
  | sd -f cm '^(    pub struct [A-Za-z0-9_]+Partial)' '    #[derive(PartialEq, Eq)]\n$1' \
  | sd -f cm '^(    #\[derivative\()' '    #[derive(derivative::Derivative)]\n$1' \
  | xclip -se c