1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10pub const START_TARGET: &str = "/app/interaction-flow/start";
11pub const PROTOCOL_SCHEMA: &str = include_str!("../schema/interaction-flow-app.v1.schema.json");
12pub const RESOLVE_TARGET: &str = "/app/interaction-flow/resolve";
13pub const NEXT_TARGET: &str = "/app/interaction-flow/next";
14pub const BACK_TARGET: &str = "/app/interaction-flow/back";
15pub const COMPLETE_TARGET: &str = "/app/interaction-flow/complete";
16pub const CANCEL_TARGET: &str = "/app/interaction-flow/cancel";
17
18pub const REQUEST_TARGETS: &[&str] = &[
19 START_TARGET,
20 RESOLVE_TARGET,
21 NEXT_TARGET,
22 BACK_TARGET,
23 COMPLETE_TARGET,
24 CANCEL_TARGET,
25];
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(
29 tag = "kind",
30 rename_all = "camelCase",
31 rename_all_fields = "camelCase",
32 deny_unknown_fields
33)]
34pub enum InteractionFlowSource {
35 Plugin {
36 node_type: String,
37 node_id: String,
38 flow_id: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 subject_id: Option<String>,
41 },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46pub struct InteractionFlowStartRequest {
47 pub source: InteractionFlowSource,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct InteractionFlowResolveRequest {
53 pub session_id: String,
54 pub command_id: String,
55 pub expected_revision: u64,
56 #[serde(default)]
57 pub input: HashMap<String, Value>,
58 pub vars: Vec<String>,
59 #[serde(default)]
60 pub force: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct InteractionFlowTransitionRequest {
66 pub session_id: String,
67 pub command_id: String,
68 pub expected_revision: u64,
69 #[serde(default)]
70 pub input: HashMap<String, Value>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase", deny_unknown_fields)]
75pub struct InteractionFlowCompleteRequest {
76 pub session_id: String,
77 pub command_id: String,
78 pub expected_revision: u64,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase", deny_unknown_fields)]
83pub struct InteractionFlowCancelRequest {
84 pub session_id: String,
85 pub command_id: String,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub enum FlowRendererView {
91 TypedForm,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase", deny_unknown_fields)]
96pub struct FlowPresentationView {
97 pub renderer: FlowRendererView,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub title: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub description: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub content: Option<String>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub enum FlowFieldValueTypeView {
109 String,
110 Boolean,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub enum FlowFieldControlView {
116 Text,
117 Password,
118 LongText,
119 Select,
120 Boolean,
121}
122
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct FlowFormFieldView {
126 pub var: String,
127 pub value_type: FlowFieldValueTypeView,
128 pub control: FlowFieldControlView,
129 pub required: bool,
130 pub repeat: bool,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub label: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub options: Option<Vec<Value>>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub option_unavailable_text: Option<String>,
137 pub read_only: bool,
138 pub has_value: bool,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub min_rows: Option<u32>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct FlowResolutionView {
146 pub var: String,
147 pub available: bool,
148 #[serde(default)]
149 pub depends_on: Vec<String>,
150}
151
152#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase", deny_unknown_fields)]
154pub struct FlowFormView {
155 #[serde(default)]
156 pub fields: Vec<FlowFormFieldView>,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub enum ExternalAuthCompletionModeView {
162 Callback,
163 Poll,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167#[serde(
168 tag = "type",
169 rename_all = "camelCase",
170 rename_all_fields = "camelCase",
171 deny_unknown_fields
172)]
173pub enum FlowStepViewContent {
174 Form {
175 presentation: FlowPresentationView,
176 form: FlowFormView,
177 },
178 ExternalAuth {
179 presentation: FlowPresentationView,
180 authorization_url: String,
181 completion_mode: ExternalAuthCompletionModeView,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 poll_after_ms: Option<u64>,
184 },
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct InteractionFlowView {
190 pub session_id: String,
191 pub revision: u64,
192 pub step_id: String,
193 pub content: FlowStepViewContent,
194 #[serde(default)]
195 pub values: HashMap<String, Value>,
196 pub resolutions: Vec<FlowResolutionView>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub back_step_id: Option<String>,
199 pub ready_to_complete: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase", deny_unknown_fields)]
204pub struct InteractionFlowCompleteResponse {
205 pub completed: bool,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub result: Option<Value>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase", deny_unknown_fields)]
212pub struct InteractionFlowCancelResponse {
213 pub cancelled: bool,
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use serde_json::json;
220
221 #[test]
222 fn public_source_cannot_choose_a_handler_or_route() {
223 let request = serde_json::from_value::<InteractionFlowStartRequest>(json!({
224 "source": {
225 "kind": "plugin",
226 "nodeType": "camera",
227 "nodeId": "camera-1",
228 "flowId": "provider.connect",
229 "subjectId": "onvif",
230 "handler": "steal",
231 "target": "/camera/arbitrary"
232 }
233 }));
234 assert!(request.is_err());
235 }
236
237 #[test]
238 fn view_is_a_current_step_projection_without_definition_or_handlers() {
239 let view = InteractionFlowView {
240 session_id: "session-1".into(),
241 revision: 1,
242 step_id: "credentials".into(),
243 content: FlowStepViewContent::Form {
244 presentation: FlowPresentationView {
245 renderer: FlowRendererView::TypedForm,
246 title: Some("Connect".into()),
247 description: None,
248 content: None,
249 },
250 form: FlowFormView::default(),
251 },
252 values: HashMap::new(),
253 resolutions: Vec::new(),
254 back_step_id: None,
255 ready_to_complete: true,
256 };
257 let value = serde_json::to_value(view).unwrap();
258 assert!(value.get("steps").is_none());
259 assert!(value.get("resolvers").is_none());
260 assert!(value.to_string().find("handler").is_none());
261 }
262
263 #[test]
264 fn conformance_fixture_matches_the_published_app_schema() {
265 let schema: Value = serde_json::from_str(PROTOCOL_SCHEMA).unwrap();
266 let fixture: Value = serde_json::from_str(include_str!(
267 "../fixtures/interaction-flow.conformance.json"
268 ))
269 .unwrap();
270 let validator = jsonschema::validator_for(&schema).unwrap();
271 let errors = validator
272 .iter_errors(&fixture)
273 .map(|error| error.to_string())
274 .collect::<Vec<_>>();
275 assert!(errors.is_empty(), "fixture failed: {errors:?}");
276 }
277}