swarm-wire, swarm-ui: the flow schema lists the values a field takes, and the editor picks them instead of asking for typed names
The schema's event fields gain values, a values_from marker (agents, repos) and a closed flag; a node field gains the marker and the flag. kind is now listed as issue and PR, what the router sends. The flow editor shows a dropdown where the list is complete, suggestions where it is only examples, and a plain box where there is none.
This commit is contained in:
parent
3d142feb6a
commit
b6da2bc9d0
8 changed files with 503 additions and 26 deletions
|
|
@ -358,6 +358,48 @@ pub struct Described {
|
|||
pub description: String,
|
||||
}
|
||||
|
||||
/// `values_from` marker: the names of the agents, as `GET /api/agents` lists
|
||||
/// them.
|
||||
pub const VALUES_FROM_AGENTS: &str = "agents";
|
||||
|
||||
/// `values_from` marker: the repositories, as `GET /api/repos` lists them.
|
||||
pub const VALUES_FROM_REPOS: &str = "repos";
|
||||
|
||||
/// The `kind` field's values: what the router puts there, which is not the
|
||||
/// words of the field's description.
|
||||
const KINDS: &[(&str, &str)] = &[("issue", "An issue"), ("PR", "A pull request")];
|
||||
|
||||
/// Some of what the forge sends as `action`. Others exist, so it is a list of
|
||||
/// suggestions only.
|
||||
const ACTIONS: &[(&str, &str)] = &[
|
||||
("opened", "Created"),
|
||||
("closed", "Closed, or a pull request merged"),
|
||||
("reopened", "Opened again after being closed"),
|
||||
("edited", "Title or text changed"),
|
||||
("assigned", "An assignee added"),
|
||||
("unassigned", "An assignee removed"),
|
||||
("synchronized", "New commits pushed to a pull request"),
|
||||
];
|
||||
|
||||
/// One event field a condition or recipient can name, and what it can hold.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
pub struct EventField {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// Values the field is known to hold; empty when none are listed.
|
||||
pub values: Vec<Described>,
|
||||
/// Where an editor asks for the values only known live: one of
|
||||
/// [`VALUES_FROM_AGENTS`], [`VALUES_FROM_REPOS`]. Absent when there is
|
||||
/// none.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values_from: Option<String>,
|
||||
/// Whether `values` or the source is every value the field can hold,
|
||||
/// which an editor can show as a dropdown. When false they are
|
||||
/// suggestions and the editor still takes any other text.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// One field of a node kind.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
|
|
@ -369,6 +411,13 @@ pub struct KindField {
|
|||
/// The accepted values when the field takes one of a fixed few; empty
|
||||
/// otherwise.
|
||||
pub values: Vec<Described>,
|
||||
/// Where an editor asks for the values only known live, as
|
||||
/// [`EventField::values_from`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values_from: Option<String>,
|
||||
/// Whether the values are every one the field accepts, as
|
||||
/// [`EventField::closed`].
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// One kind of node and the fields it takes besides `id` and `kind`.
|
||||
|
|
@ -395,7 +444,7 @@ pub struct Limits {
|
|||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
pub struct Schema {
|
||||
pub source_events: Vec<Described>,
|
||||
pub fields: Vec<Described>,
|
||||
pub fields: Vec<EventField>,
|
||||
pub node_kinds: Vec<KindSchema>,
|
||||
pub limits: Limits,
|
||||
}
|
||||
|
|
@ -415,17 +464,56 @@ fn kind_field(name: &str, description: &str, required: bool) -> KindField {
|
|||
description: description.to_owned(),
|
||||
required,
|
||||
values: Vec::new(),
|
||||
values_from: None,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`FIELDS`] with what each is known to hold.
|
||||
fn event_fields() -> Vec<EventField> {
|
||||
FIELDS
|
||||
.iter()
|
||||
.map(|(name, description)| {
|
||||
let (values, values_from, closed) = match *name {
|
||||
"event" => (described(SOURCE_EVENTS), None, true),
|
||||
"kind" => (described(KINDS), None, true),
|
||||
"action" => (described(ACTIONS), None, false),
|
||||
"repo" => (Vec::new(), Some(VALUES_FROM_REPOS), false),
|
||||
"assignees" | "assignees_added" | "assignees_removed" | "todo_holders" => {
|
||||
(Vec::new(), Some(VALUES_FROM_AGENTS), false)
|
||||
}
|
||||
_ => (Vec::new(), None, false),
|
||||
};
|
||||
EventField {
|
||||
name: (*name).to_owned(),
|
||||
description: (*description).to_owned(),
|
||||
values,
|
||||
values_from: values_from.map(str::to_owned),
|
||||
closed,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The schema of this build's flows.
|
||||
#[must_use]
|
||||
pub fn schema() -> Schema {
|
||||
let mut op = kind_field("op", "What the todo does to the recipient's list", true);
|
||||
op.values = described(OPS);
|
||||
op.closed = true;
|
||||
let mut to = kind_field(
|
||||
"to",
|
||||
"The recipients: {field: <event field>} or {agent: <agent name>}",
|
||||
true,
|
||||
);
|
||||
to.values_from = Some(VALUES_FROM_AGENTS.to_owned());
|
||||
to.closed = true;
|
||||
let mut reviewer = kind_field("reviewer", "The forge login to request a review from", true);
|
||||
// A login need not belong to an agent: the classifier's does not.
|
||||
reviewer.values_from = Some(VALUES_FROM_AGENTS.to_owned());
|
||||
Schema {
|
||||
source_events: described(SOURCE_EVENTS),
|
||||
fields: described(FIELDS),
|
||||
fields: event_fields(),
|
||||
node_kinds: vec![
|
||||
KindSchema {
|
||||
kind: "source".to_owned(),
|
||||
|
|
@ -449,11 +537,7 @@ pub fn schema() -> Schema {
|
|||
kind: "todo".to_owned(),
|
||||
description: "A leaf: emits one todo op per recipient".to_owned(),
|
||||
fields: vec![
|
||||
kind_field(
|
||||
"to",
|
||||
"The recipients: {field: <event field>} or {agent: <agent name>}",
|
||||
true,
|
||||
),
|
||||
to,
|
||||
op,
|
||||
kind_field("key", "The todo key, with {field} placeholders", true),
|
||||
kind_field("prefix", "For a clear: key is a prefix", false),
|
||||
|
|
@ -467,11 +551,7 @@ pub fn schema() -> Schema {
|
|||
KindSchema {
|
||||
kind: "request_reviewer".to_owned(),
|
||||
description: "A leaf: asks the forge for a review of the pull request the event concerns, unless the reviewer's review of its head already stands; does nothing for an issue".to_owned(),
|
||||
fields: vec![kind_field(
|
||||
"reviewer",
|
||||
"The forge login to request a review from",
|
||||
true,
|
||||
)],
|
||||
fields: vec![reviewer],
|
||||
},
|
||||
],
|
||||
limits: Limits {
|
||||
|
|
@ -906,6 +986,48 @@ mod tests {
|
|||
assert_eq!(listed.len(), 2);
|
||||
assert!(op_names("nope").is_none(), "control");
|
||||
|
||||
// What an editor offers for a field is what the router puts there.
|
||||
let field = |name: &str| s.fields.iter().find(|f| f.name == name).expect(name);
|
||||
let names =
|
||||
|f: &EventField| -> Vec<String> { f.values.iter().map(|v| v.name.clone()).collect() };
|
||||
assert_eq!(names(field("kind")), ["issue", "PR"]);
|
||||
assert!(field("kind").closed);
|
||||
let events: Vec<String> = s.source_events.iter().map(|e| e.name.clone()).collect();
|
||||
assert_eq!(names(field("event")), events);
|
||||
assert!(field("event").closed);
|
||||
assert!(!field("action").closed, "the forge may send more actions");
|
||||
assert!(names(field("action")).contains(&"closed".to_owned()));
|
||||
assert_eq!(
|
||||
field("repo").values_from.as_deref(),
|
||||
Some(VALUES_FROM_REPOS)
|
||||
);
|
||||
assert_eq!(
|
||||
field("assignees").values_from.as_deref(),
|
||||
Some(VALUES_FROM_AGENTS)
|
||||
);
|
||||
assert!(field("title").values.is_empty() && field("title").values_from.is_none());
|
||||
// A marker is one an editor knows to resolve.
|
||||
for f in &s.fields {
|
||||
if let Some(from) = &f.values_from {
|
||||
assert!([VALUES_FROM_AGENTS, VALUES_FROM_REPOS].contains(&from.as_str()));
|
||||
}
|
||||
}
|
||||
let kind_field_of = |kind: &str, name: &str| {
|
||||
s.node_kinds
|
||||
.iter()
|
||||
.find(|k| k.kind == kind)
|
||||
.and_then(|k| k.fields.iter().find(|f| f.name == name))
|
||||
.expect(name)
|
||||
};
|
||||
assert!(kind_field_of("todo", "op").closed);
|
||||
assert_eq!(
|
||||
kind_field_of("todo", "to").values_from.as_deref(),
|
||||
Some(VALUES_FROM_AGENTS)
|
||||
);
|
||||
let reviewer = kind_field_of("request_reviewer", "reviewer");
|
||||
assert_eq!(reviewer.values_from.as_deref(), Some(VALUES_FROM_AGENTS));
|
||||
assert!(!reviewer.closed, "any forge login is allowed");
|
||||
|
||||
assert_eq!(
|
||||
(s.limits.max_nodes, s.limits.max_depth, s.limits.max_flows),
|
||||
(MAX_NODES, MAX_DEPTH, MAX_FLOWS)
|
||||
|
|
|
|||
Loading…
Reference in a new issue