1use crate::llm::actions::{
15 client_trait::{Client, ClientActionResult},
16 protocol_trait::Protocol,
17 ActionDefinition, Parameter, ParameterDefinition,
18};
19use crate::protocol::EventType;
20use crate::server::stomp::frame::StompFrame;
21use crate::state::app_state::AppState;
22use anyhow::{Context, Result};
23use serde_json::json;
24use std::sync::LazyLock;
25
26pub const STOMP_ACCEPT_VERSION: &str = "1.2";
34
35pub const STOMP_HEARTBEAT: &str = "0,0";
43
44pub const DISCONNECT_RECEIPT_ID: &str = "netget-stomp-disconnect";
53
54pub static STOMP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
59 EventType::new(
60 "stomp_connected",
61 "The STOMP session is open: the broker answered CONNECT with a valid CONNECTED frame",
62 json!({
63 "type": "send_stomp_subscribe",
64 "destination": "/queue/test",
65 "id": "sub-0",
66 "ack_mode": "auto"
67 }),
68 )
69 .with_parameters(vec![
70 Parameter {
71 name: "version".to_string(),
72 type_hint: "string".to_string(),
73 description: "Protocol version the broker agreed to (always 1.2 here)".to_string(),
74 required: true,
75 },
76 Parameter {
77 name: "session".to_string(),
78 type_hint: "string".to_string(),
79 description: "Broker-assigned session identifier, empty if the broker sent none"
80 .to_string(),
81 required: false,
82 },
83 Parameter {
84 name: "server".to_string(),
85 type_hint: "string".to_string(),
86 description: "Broker software identification, empty if the broker sent none"
87 .to_string(),
88 required: false,
89 },
90 ])
91 .with_actions(vec![
92 send_stomp_subscribe_action(),
93 send_stomp_send_action(),
94 disconnect_action(),
95 wait_for_more_action(),
96 ])
97});
98
99pub static STOMP_CLIENT_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
101 EventType::new(
102 "stomp_message_received",
103 "A MESSAGE frame was delivered on one of this client's subscriptions",
104 json!({
105 "type": "send_stomp_send",
106 "destination": "/queue/replies",
107 "body": "got it",
108 "encoding": "utf8"
109 }),
110 )
111 .with_parameters(vec![
112 Parameter {
113 name: "destination".to_string(),
114 type_hint: "string".to_string(),
115 description: "Destination the message was published to".to_string(),
116 required: true,
117 },
118 Parameter {
119 name: "message_id".to_string(),
120 type_hint: "string".to_string(),
121 description: "Broker-assigned message identifier".to_string(),
122 required: true,
123 },
124 Parameter {
125 name: "subscription".to_string(),
126 type_hint: "string".to_string(),
127 description: "Id of the subscription this delivery belongs to - the same id that \
128 was passed to send_stomp_subscribe"
129 .to_string(),
130 required: true,
131 },
132 Parameter {
133 name: "headers".to_string(),
134 type_hint: "object".to_string(),
135 description: "Every other header on the frame, as a flat string map. When the \
136 subscription uses ack_mode \"client\" or \"client-individual\" the \
137 broker puts an \"ack\" header here, and its value is what \
138 send_stomp_ack / send_stomp_nack take as their 'id'."
139 .to_string(),
140 required: false,
141 },
142 Parameter {
143 name: "body".to_string(),
144 type_hint: "string".to_string(),
145 description: "The message body, encoded as body_encoding says".to_string(),
146 required: false,
147 },
148 Parameter {
149 name: "body_encoding".to_string(),
150 type_hint: "string".to_string(),
151 description: "\"utf8\" when the body was printable text, \"hex\" when it was \
152 binary. Pass both fields straight into send_stomp_send to republish \
153 the exact bytes."
154 .to_string(),
155 required: true,
156 },
157 ])
158 .with_actions(vec![
159 send_stomp_send_action(),
160 send_stomp_ack_action(),
161 send_stomp_nack_action(),
162 send_stomp_subscribe_action(),
163 send_stomp_unsubscribe_action(),
164 wait_for_more_action(),
165 disconnect_action(),
166 ])
167});
168
169pub static STOMP_CLIENT_RECEIPT_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
174 EventType::new(
175 "stomp_receipt_received",
176 "The broker acknowledged a frame that carried a receipt header",
177 json!({"type": "wait_for_more"}),
178 )
179 .with_parameters(vec![Parameter {
180 name: "receipt_id".to_string(),
181 type_hint: "string".to_string(),
182 description: "Value of the receipt-id header, matching the receipt asked for".to_string(),
183 required: true,
184 }])
185 .with_actions(vec![
186 wait_for_more_action(),
187 send_stomp_send_action(),
188 disconnect_action(),
189 ])
190});
191
192pub static STOMP_CLIENT_ERROR_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
195 EventType::new(
196 "stomp_error_received",
197 "The broker sent an ERROR frame; per the STOMP specification it closes the connection \
198 immediately afterwards, so this session is over",
199 json!({"type": "wait_for_more"}),
200 )
201 .with_parameters(vec![
202 Parameter {
203 name: "message".to_string(),
204 type_hint: "string".to_string(),
205 description: "The ERROR frame's 'message' header - the broker's short reason"
206 .to_string(),
207 required: false,
208 },
209 Parameter {
210 name: "body".to_string(),
211 type_hint: "string".to_string(),
212 description: "The ERROR frame's body, encoded as body_encoding says".to_string(),
213 required: false,
214 },
215 Parameter {
216 name: "body_encoding".to_string(),
217 type_hint: "string".to_string(),
218 description: "\"utf8\" when the body was printable text, \"hex\" when it was binary"
219 .to_string(),
220 required: true,
221 },
222 ])
223 .with_actions(vec![wait_for_more_action()])
227});
228
229pub struct StompClientProtocol;
231
232impl StompClientProtocol {
233 pub fn new() -> Self {
234 Self
235 }
236}
237
238impl Default for StompClientProtocol {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244impl Protocol for StompClientProtocol {
245 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
253 vec![
254 send_stomp_subscribe_action(),
255 send_stomp_send_action(),
256 send_stomp_unsubscribe_action(),
257 send_stomp_ack_action(),
258 send_stomp_nack_action(),
259 wait_for_more_action(),
260 disconnect_action(),
261 ]
262 }
263
264 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
265 Vec::new()
266 }
267
268 fn protocol_name(&self) -> &'static str {
269 "STOMP"
270 }
271
272 fn get_event_types(&self) -> Vec<EventType> {
273 vec![
274 STOMP_CLIENT_CONNECTED_EVENT.clone(),
275 STOMP_CLIENT_MESSAGE_RECEIVED_EVENT.clone(),
276 STOMP_CLIENT_RECEIPT_RECEIVED_EVENT.clone(),
277 STOMP_CLIENT_ERROR_RECEIVED_EVENT.clone(),
278 ]
279 }
280
281 fn stack_name(&self) -> &'static str {
282 "ETH>IP>TCP>STOMP"
283 }
284
285 fn keywords(&self) -> Vec<&'static str> {
286 vec![
287 "stomp",
288 "stomp client",
289 "message broker client",
290 "subscribe to a queue",
291 ]
292 }
293
294 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
295 vec![
296 ParameterDefinition {
297 name: "host".to_string(),
298 type_hint: "string".to_string(),
299 description: "Virtual host for the CONNECT frame's 'host' header. Defaults to \
300 the host part of remote_addr, which is what the specification \
301 says a client should send when it has no better answer."
302 .to_string(),
303 required: false,
304 example: json!("/"),
305 default: None,
306 },
307 ParameterDefinition {
308 name: "login".to_string(),
309 type_hint: "string".to_string(),
310 description: "Value for the CONNECT frame's 'login' header. Omitted entirely \
311 when not supplied."
312 .to_string(),
313 required: false,
314 example: json!("guest"),
315 default: None,
316 },
317 ParameterDefinition {
318 name: "passcode".to_string(),
319 type_hint: "string".to_string(),
320 description: "Value for the CONNECT frame's 'passcode' header. Sent in \
321 cleartext - this client has no TLS."
322 .to_string(),
323 required: false,
324 example: json!("guest"),
325 default: None,
326 },
327 ParameterDefinition {
328 name: "use_stomp_command".to_string(),
329 type_hint: "boolean".to_string(),
330 description: "Send the 1.2-preferred 'STOMP' command instead of 'CONNECT'. \
331 Identical otherwise; some brokers log the two differently and \
332 1.0-only brokers reject STOMP, which is the point of the choice."
333 .to_string(),
334 required: false,
335 example: json!(false),
336 default: None,
337 },
338 ParameterDefinition {
339 name: "handshake_timeout_secs".to_string(),
340 type_hint: "number".to_string(),
341 description: "How long to wait for the broker's CONNECTED before giving up \
342 (default 20). A broker that never answers must not leave the \
343 client wedged in Connecting."
344 .to_string(),
345 required: false,
346 example: json!(20),
347 default: None,
348 },
349 ]
350 }
351
352 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
353 use crate::protocol::metadata::{
354 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
355 };
356
357 ProtocolMetadataV2::builder()
358 .state(DevelopmentState::Experimental)
359 .privilege_requirement(PrivilegeRequirement::None)
360 .implementation(
361 "Tokio TCP driving the STOMP 1.2 codec in src/server/stomp/frame.rs, reused \
362 rather than duplicated. The handshake, the content-length/NUL body rule, \
363 header escaping and the DISCONNECT receipt are deterministic Rust; the model \
364 chooses destinations, bodies and acknowledgements.",
365 )
366 .llm_control(
367 "SUBSCRIBE / SEND / UNSUBSCRIBE / ACK / NACK / DISCONNECT, decided per event. \
368 The model never sees or writes frame bytes.",
369 )
370 .e2e_testing(
371 "tests/client/stomp/e2e_test.rs drives this client against NetGet's own STOMP \
372 server over a real TCP socket, plus a hand-written broker that answers the \
373 handshake wrongly. Not #[ignore]d and it cannot skip - there is no external \
374 binary to detect as missing.",
375 )
376 .notes(
377 "EXPERIMENTAL, and precisely so: the only peer this client has ever been run \
378 against is NetGet's own STOMP server. That is same-project evidence - it \
379 shows the two halves of this repository agree with each other, not that \
380 either matches RFC-equivalent broker behaviour, and a shared misreading of \
381 the specification would be invisible to it. The async-stomp crate cannot \
382 close the gap because it is itself a client. Beta needs one exchange against \
383 a real broker (ActiveMQ, or RabbitMQ with the STOMP plugin); \
384 src/client/stomp/CLAUDE.md spells out the exact test. NOT IMPLEMENTED, and \
385 none of it is hidden: heart-beating (negotiated 0,0, which is a legal \
386 configuration rather than a silent omission), transactions \
387 (BEGIN/COMMIT/ABORT), STOMP 1.0/1.1 (a CONNECTED naming any other version is \
388 refused rather than guessed at) and TLS, so login/passcode travel in \
389 cleartext.",
390 )
391 .build()
392 }
393
394 fn description(&self) -> &'static str {
395 "STOMP 1.2 client for subscribing to and publishing on a message broker"
396 }
397
398 fn example_prompt(&self) -> &'static str {
399 "Connect to the STOMP broker at 127.0.0.1:61613, subscribe to /queue/test, and reply \
400 on /queue/replies to whatever arrives"
401 }
402
403 fn group_name(&self) -> &'static str {
404 "Application"
405 }
406
407 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
408 use crate::llm::actions::StartupExamples;
409
410 StartupExamples::new(
411 json!({
413 "type": "open_client",
414 "remote_addr": "127.0.0.1:61613",
415 "base_stack": "stomp",
416 "instruction": "Subscribe to /queue/test and reply on /queue/replies to every \
417 message that arrives"
418 }),
419 json!({
421 "type": "open_client",
422 "remote_addr": "127.0.0.1:61613",
423 "base_stack": "stomp",
424 "event_handlers": [{
425 "event_pattern": "stomp_message_received",
426 "handler": {
427 "type": "script",
428 "language": "python",
429 "code": "respond([{'type': 'send_stomp_send', 'destination': '/queue/replies', 'body': event['body'], 'encoding': event['body_encoding']}])"
430 }
431 }]
432 }),
433 json!({
435 "type": "open_client",
436 "remote_addr": "127.0.0.1:61613",
437 "base_stack": "stomp",
438 "event_handlers": [
439 {
440 "event_pattern": "stomp_connected",
441 "handler": {
442 "type": "static",
443 "actions": [{
444 "type": "send_stomp_subscribe",
445 "destination": "/queue/test",
446 "id": "sub-0",
447 "ack_mode": "auto"
448 }]
449 }
450 },
451 {
452 "event_pattern": "stomp_message_received",
453 "handler": {
454 "type": "static",
455 "actions": [{"type": "wait_for_more"}]
456 }
457 }
458 ]
459 }),
460 )
461 }
462}
463
464impl Client for StompClientProtocol {
465 fn connect(
466 &self,
467 ctx: crate::protocol::ConnectContext,
468 ) -> std::pin::Pin<
469 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
470 > {
471 Box::pin(async move {
472 crate::client::stomp::StompClient::connect_with_llm_actions(
473 ctx.remote_addr,
474 ctx.llm_client,
475 ctx.state,
476 ctx.status_tx,
477 ctx.client_id,
478 ctx.startup_params,
479 )
480 .await
481 })
482 }
483
484 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
485 let action_type = action
486 .get("type")
487 .and_then(|v| v.as_str())
488 .context("Missing 'type' field in action")?;
489
490 match action_type {
491 "send_stomp_subscribe" => {
492 let destination = required_str(&action, "destination")?;
493 let id = required_str(&action, "id")?;
494 let ack_mode = action
495 .get("ack_mode")
496 .and_then(|v| v.as_str())
497 .unwrap_or("auto")
498 .to_string();
499 if !matches!(ack_mode.as_str(), "auto" | "client" | "client-individual") {
500 return Err(anyhow::anyhow!(
501 "'ack_mode' must be \"auto\", \"client\" or \"client-individual\"; got \
502 {ack_mode:?}"
503 ));
504 }
505 let mut headers = vec![
506 ("destination".to_string(), destination),
507 ("id".to_string(), id),
508 ("ack".to_string(), ack_mode),
509 ];
510 push_receipt(&mut headers, &action);
511 Ok(ClientActionResult::SendData(
512 StompFrame::new("SUBSCRIBE", headers, Vec::new()).encode(),
513 ))
514 }
515
516 "send_stomp_send" => {
517 let destination = required_str(&action, "destination")?;
518 let body = decode_body(&action)?;
519 let mut headers = vec![("destination".to_string(), destination)];
520 if let Some(content_type) = action.get("content_type").and_then(|v| v.as_str()) {
521 headers.push(("content-type".to_string(), content_type.to_string()));
522 }
523 headers.extend(extra_headers(&action)?);
524 push_receipt(&mut headers, &action);
525 Ok(ClientActionResult::SendData(
526 StompFrame::new("SEND", headers, body).encode(),
527 ))
528 }
529
530 "send_stomp_unsubscribe" => {
531 let id = required_str(&action, "id")?;
532 let mut headers = vec![("id".to_string(), id)];
533 push_receipt(&mut headers, &action);
534 Ok(ClientActionResult::SendData(
535 StompFrame::new("UNSUBSCRIBE", headers, Vec::new()).encode(),
536 ))
537 }
538
539 "send_stomp_ack" | "send_stomp_nack" => {
540 let id = required_str(&action, "id")?;
541 let command = if action_type == "send_stomp_ack" {
542 "ACK"
543 } else {
544 "NACK"
545 };
546 let mut headers = vec![("id".to_string(), id)];
547 push_receipt(&mut headers, &action);
548 Ok(ClientActionResult::SendData(
549 StompFrame::new(command, headers, Vec::new()).encode(),
550 ))
551 }
552
553 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
554
555 "disconnect" => Ok(ClientActionResult::SendData(
561 StompFrame::new(
562 "DISCONNECT",
563 vec![("receipt".to_string(), DISCONNECT_RECEIPT_ID.to_string())],
564 Vec::new(),
565 )
566 .encode(),
567 )),
568
569 other => Err(anyhow::anyhow!("Unknown STOMP client action: {other}")),
570 }
571 }
572}
573
574fn required_str(action: &serde_json::Value, key: &str) -> Result<String> {
577 action
578 .get(key)
579 .and_then(|v| v.as_str())
580 .map(|s| s.to_string())
581 .with_context(|| format!("Missing or non-string '{key}' parameter"))
582}
583
584fn push_receipt(headers: &mut Vec<(String, String)>, action: &serde_json::Value) {
587 if let Some(receipt) = action.get("receipt").and_then(|v| v.as_str()) {
588 if !receipt.is_empty() {
589 headers.push(("receipt".to_string(), receipt.to_string()));
590 }
591 }
592}
593
594fn decode_body(action: &serde_json::Value) -> Result<Vec<u8>> {
601 let body = action.get("body").and_then(|v| v.as_str()).unwrap_or("");
602 match action
603 .get("encoding")
604 .and_then(|v| v.as_str())
605 .unwrap_or("utf8")
606 {
607 "utf8" => Ok(body.as_bytes().to_vec()),
608 "hex" => {
609 let cleaned: String = body
610 .trim()
611 .trim_start_matches("0x")
612 .chars()
613 .filter(|c| !c.is_whitespace() && *c != ':')
614 .collect();
615 hex::decode(&cleaned).map_err(|e| {
616 anyhow::anyhow!("'body' is not valid hex although encoding is \"hex\": {e}")
617 })
618 }
619 other => Err(anyhow::anyhow!(
620 "Invalid 'encoding' {other:?}; valid values are \"utf8\" (default) and \"hex\""
621 )),
622 }
623}
624
625fn extra_headers(action: &serde_json::Value) -> Result<Vec<(String, String)>> {
635 let Some(map) = action.get("headers").and_then(|v| v.as_object()) else {
636 return Ok(Vec::new());
637 };
638 let mut out = Vec::new();
639 for (k, v) in map {
640 if matches!(
641 k.as_str(),
642 "content-length" | "destination" | "content-type" | "receipt"
643 ) {
644 continue;
645 }
646 let value = match v {
647 serde_json::Value::String(s) => s.clone(),
648 serde_json::Value::Number(n) => n.to_string(),
649 serde_json::Value::Bool(b) => b.to_string(),
650 serde_json::Value::Null => continue,
651 _ => {
652 return Err(anyhow::anyhow!(
653 "header {k:?} must be a string, number or boolean; a STOMP header cannot \
654 carry a nested object or array"
655 ))
656 }
657 };
658 out.push((k.clone(), value));
659 }
660 Ok(out)
661}
662
663fn send_stomp_subscribe_action() -> ActionDefinition {
666 ActionDefinition {
667 name: "send_stomp_subscribe".to_string(),
668 description: "Subscribe to a destination. The 'id' is chosen by this client, not the \
669 broker, and every MESSAGE delivered for this subscription quotes it back \
670 in stomp_message_received's 'subscription' field - so pick one you can \
671 recognise and reuse it for send_stomp_unsubscribe."
672 .to_string(),
673 parameters: vec![
674 Parameter {
675 name: "destination".to_string(),
676 type_hint: "string".to_string(),
677 description: "Destination to subscribe to, e.g. /queue/test or /topic/news"
678 .to_string(),
679 required: true,
680 },
681 Parameter {
682 name: "id".to_string(),
683 type_hint: "string".to_string(),
684 description: "Subscription id, unique within this connection".to_string(),
685 required: true,
686 },
687 Parameter {
688 name: "ack_mode".to_string(),
689 type_hint: "string".to_string(),
690 description: "\"auto\" (default - the broker considers a message delivered as \
691 soon as it is sent), \"client\" (cumulative: an ACK also \
692 acknowledges everything before it) or \"client-individual\". \
693 Anything but \"auto\" means you must answer each delivery with \
694 send_stomp_ack or send_stomp_nack."
695 .to_string(),
696 required: false,
697 },
698 Parameter {
699 name: "receipt".to_string(),
700 type_hint: "string".to_string(),
701 description: "Ask the broker to confirm this frame with a RECEIPT carrying this \
702 id, delivered as a stomp_receipt_received event"
703 .to_string(),
704 required: false,
705 },
706 ],
707 example: json!({
708 "type": "send_stomp_subscribe",
709 "destination": "/queue/test",
710 "id": "sub-0",
711 "ack_mode": "auto"
712 }),
713 log_template: None,
714 }
715}
716
717fn send_stomp_send_action() -> ActionDefinition {
718 ActionDefinition {
719 name: "send_stomp_send".to_string(),
720 description: "Publish a message to a destination. The body's encoding is explicit and \
721 is never guessed: pass a stomp_message_received event's 'body' and \
722 'body_encoding' straight through to republish the exact bytes."
723 .to_string(),
724 parameters: vec![
725 Parameter {
726 name: "destination".to_string(),
727 type_hint: "string".to_string(),
728 description: "Destination to publish to, e.g. /queue/replies".to_string(),
729 required: true,
730 },
731 Parameter {
732 name: "body".to_string(),
733 type_hint: "string".to_string(),
734 description: "Message body, interpreted according to 'encoding'".to_string(),
735 required: false,
736 },
737 Parameter {
738 name: "encoding".to_string(),
739 type_hint: "string".to_string(),
740 description: "\"utf8\" (default) or \"hex\" for binary bodies. There is no \
741 sniffing - \"48656c6c6f\" is valid as both."
742 .to_string(),
743 required: false,
744 },
745 Parameter {
746 name: "content_type".to_string(),
747 type_hint: "string".to_string(),
748 description: "Value for the content-type header, e.g. text/plain".to_string(),
749 required: false,
750 },
751 Parameter {
752 name: "headers".to_string(),
753 type_hint: "object".to_string(),
754 description: "Extra headers as a flat string map. content-length is computed \
755 and cannot be set here; destination, content-type and receipt \
756 have their own parameters."
757 .to_string(),
758 required: false,
759 },
760 Parameter {
761 name: "receipt".to_string(),
762 type_hint: "string".to_string(),
763 description: "Ask the broker to confirm this publish with a RECEIPT carrying \
764 this id"
765 .to_string(),
766 required: false,
767 },
768 ],
769 example: json!({
770 "type": "send_stomp_send",
771 "destination": "/queue/replies",
772 "body": "hello from netget",
773 "encoding": "utf8",
774 "content_type": "text/plain"
775 }),
776 log_template: None,
777 }
778}
779
780fn send_stomp_unsubscribe_action() -> ActionDefinition {
781 ActionDefinition {
782 name: "send_stomp_unsubscribe".to_string(),
783 description: "Cancel a subscription. Takes the same id that was passed to \
784 send_stomp_subscribe; the broker stops delivering for it."
785 .to_string(),
786 parameters: vec![
787 Parameter {
788 name: "id".to_string(),
789 type_hint: "string".to_string(),
790 description: "Subscription id to cancel".to_string(),
791 required: true,
792 },
793 Parameter {
794 name: "receipt".to_string(),
795 type_hint: "string".to_string(),
796 description: "Ask the broker to confirm with a RECEIPT carrying this id"
797 .to_string(),
798 required: false,
799 },
800 ],
801 example: json!({"type": "send_stomp_unsubscribe", "id": "sub-0"}),
802 log_template: None,
803 }
804}
805
806fn send_stomp_ack_action() -> ActionDefinition {
807 ActionDefinition {
808 name: "send_stomp_ack".to_string(),
809 description: "Acknowledge a delivered message. Only meaningful when the subscription \
810 was made with ack_mode \"client\" or \"client-individual\". The 'id' is \
811 the delivered frame's own \"ack\" header, which arrives inside the \
812 stomp_message_received event's 'headers' map - it is not the message_id."
813 .to_string(),
814 parameters: vec![
815 Parameter {
816 name: "id".to_string(),
817 type_hint: "string".to_string(),
818 description: "The delivered MESSAGE frame's \"ack\" header value".to_string(),
819 required: true,
820 },
821 Parameter {
822 name: "receipt".to_string(),
823 type_hint: "string".to_string(),
824 description: "Ask the broker to confirm with a RECEIPT carrying this id"
825 .to_string(),
826 required: false,
827 },
828 ],
829 example: json!({"type": "send_stomp_ack", "id": "ack-1"}),
830 log_template: None,
831 }
832}
833
834fn send_stomp_nack_action() -> ActionDefinition {
835 ActionDefinition {
836 name: "send_stomp_nack".to_string(),
837 description: "Refuse a delivered message, telling the broker it was not processed. \
838 Same 'id' as send_stomp_ack - the delivered frame's \"ack\" header. What \
839 the broker then does with the message (redeliver, dead-letter, drop) is \
840 its own policy."
841 .to_string(),
842 parameters: vec![
843 Parameter {
844 name: "id".to_string(),
845 type_hint: "string".to_string(),
846 description: "The delivered MESSAGE frame's \"ack\" header value".to_string(),
847 required: true,
848 },
849 Parameter {
850 name: "receipt".to_string(),
851 type_hint: "string".to_string(),
852 description: "Ask the broker to confirm with a RECEIPT carrying this id"
853 .to_string(),
854 required: false,
855 },
856 ],
857 example: json!({"type": "send_stomp_nack", "id": "ack-1"}),
858 log_template: None,
859 }
860}
861
862fn wait_for_more_action() -> ActionDefinition {
863 ActionDefinition {
864 name: "wait_for_more".to_string(),
865 description: "Send nothing and keep the session open, waiting for the next frame. This \
866 is a real answer, distinct from failing to answer."
867 .to_string(),
868 parameters: vec![],
869 example: json!({"type": "wait_for_more"}),
870 log_template: None,
871 }
872}
873
874fn disconnect_action() -> ActionDefinition {
875 ActionDefinition {
876 name: "disconnect".to_string(),
877 description: "End the session the way the specification defines it: a DISCONNECT frame \
878 carrying a receipt, then the connection closes once the broker's RECEIPT \
879 comes back. That acknowledgement is what tells a publisher the broker \
880 durably took everything it sent, so this is not the same as hanging up."
881 .to_string(),
882 parameters: vec![],
883 example: json!({"type": "disconnect"}),
884 log_template: None,
885 }
886}