Skip to repository content208 lines · 6.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:28:05.927Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
command_json.rs
1use std::process::Output;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use util::command::Command;
6
7use crate::devcontainer_api::DevContainerError;
8
9pub(crate) struct DefaultCommandRunner;
10
11impl DefaultCommandRunner {
12 pub(crate) fn new() -> Self {
13 Self
14 }
15}
16
17#[async_trait]
18impl CommandRunner for DefaultCommandRunner {
19 async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error> {
20 command.output().await
21 }
22}
23
24#[async_trait]
25pub(crate) trait CommandRunner: Send + Sync {
26 async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error>;
27}
28
29pub(crate) async fn evaluate_json_command<T>(
30 mut command: Command,
31) -> Result<Option<T>, DevContainerError>
32where
33 T: for<'de> Deserialize<'de>,
34{
35 let output = command.output().await.map_err(|e| {
36 log::error!("Error running command {:?}: {e}", command);
37 DevContainerError::CommandFailed(command.get_program().display().to_string())
38 })?;
39
40 deserialize_json_output(output).map_err(|e| {
41 log::error!("Error running command {:?}: {e}", command);
42 DevContainerError::CommandFailed(command.get_program().display().to_string())
43 })
44}
45
46pub(crate) async fn evaluate_yaml_command<T>(
47 mut command: Command,
48) -> Result<Option<T>, DevContainerError>
49where
50 T: for<'de> Deserialize<'de>,
51{
52 let output = command.output().await.map_err(|e| {
53 log::error!("Error running command {:?}: {e}", command);
54 DevContainerError::CommandFailed(command.get_program().display().to_string())
55 })?;
56
57 deserialize_yaml_output(output).map_err(|e| {
58 log::error!("Error running command {:?}: {e}", command);
59 DevContainerError::CommandFailed(command.get_program().display().to_string())
60 })
61}
62
63pub(crate) fn deserialize_yaml_output<T>(output: Output) -> Result<Option<T>, String>
64where
65 T: for<'de> Deserialize<'de>,
66{
67 if output.status.success() {
68 let raw = String::from_utf8_lossy(&output.stdout);
69 if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
70 return Ok(None);
71 }
72 serde_yaml::from_str(&raw)
73 .map(Some)
74 .map_err(|e| format!("Error deserializing from raw yaml: {e}"))
75 } else {
76 let std_err = String::from_utf8_lossy(&output.stderr);
77 Err(format!(
78 "Sent non-successful output; cannot deserialize. StdErr: {std_err}"
79 ))
80 }
81}
82
83pub(crate) fn deserialize_json_output<T>(output: Output) -> Result<Option<T>, String>
84where
85 T: for<'de> Deserialize<'de>,
86{
87 if output.status.success() {
88 let raw = String::from_utf8_lossy(&output.stdout);
89 if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
90 return Ok(None);
91 }
92 serde_json_lenient::from_str(&raw)
93 .map_err(|e| format!("Error deserializing from raw json: {e}"))
94 } else {
95 let std_err = String::from_utf8_lossy(&output.stderr);
96 Err(format!(
97 "Sent non-successful output; cannot deserialize. StdErr: {std_err}"
98 ))
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use std::process::ExitStatus;
105
106 use crate::docker::{DockerComposeConfig, DockerComposeServiceBuild};
107
108 use super::*;
109
110 fn success_output(stdout: &str) -> Output {
111 Output {
112 status: ExitStatus::default(),
113 stdout: stdout.as_bytes().to_vec(),
114 stderr: Vec::new(),
115 }
116 }
117
118 #[derive(Debug, Deserialize, PartialEq)]
119 struct TestItem {
120 id: String,
121 }
122
123 #[test]
124 fn test_deserialize_newline_delimited_json_rejected() {
125 // Strict single-value contract: NDJSON must be rejected. Commands that
126 // may legitimately return multiple rows (e.g. `docker ps`) parse their
127 // output themselves rather than routing through this helper.
128 let output = success_output("{\"id\":\"first\"}\n{\"id\":\"second\"}\n");
129 let result: Result<Option<TestItem>, String> = deserialize_json_output(output);
130 assert!(result.is_err(), "expected parse error, got {result:?}");
131 }
132
133 #[test]
134 fn test_deserialize_empty_output() {
135 let output = success_output("");
136 let result: Option<TestItem> = deserialize_json_output(output).unwrap();
137 assert_eq!(result, None);
138 }
139
140 #[test]
141 fn test_deserialize_empty_object() {
142 let output = success_output("{}");
143 let result: Option<TestItem> = deserialize_json_output(output).unwrap();
144 assert_eq!(result, None);
145 }
146
147 #[test]
148 fn test_deserialize_yaml_docker_compose_config() {
149 let yaml = indoc::indoc! {"
150 name: my-project
151 services:
152 app:
153 image: node:18
154 command:
155 - sleep
156 - infinity
157 build:
158 context: .
159 dockerfile: Dockerfile
160 db:
161 image: postgres:15
162 volumes: {}
163 "};
164 let output = success_output(yaml);
165 let result: DockerComposeConfig = deserialize_yaml_output(output)
166 .expect("deserialization should succeed")
167 .expect("result should not be None");
168
169 assert_eq!(result.name, Some("my-project".to_string()));
170 assert_eq!(result.services.len(), 2);
171
172 let app = result
173 .services
174 .get("app")
175 .expect("app service should exist");
176 assert_eq!(app.image, Some("node:18".to_string()));
177 assert_eq!(
178 app.command,
179 vec!["sleep".to_string(), "infinity".to_string()]
180 );
181 assert_eq!(
182 app.build,
183 Some(DockerComposeServiceBuild {
184 context: Some(".".to_string()),
185 dockerfile: Some("Dockerfile".to_string()),
186 ..Default::default()
187 })
188 );
189
190 let db = result.services.get("db").expect("db service should exist");
191 assert_eq!(db.image, Some("postgres:15".to_string()));
192 }
193
194 #[test]
195 fn test_deserialize_yaml_empty_output() {
196 let output = success_output("");
197 let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
198 assert_eq!(result, None);
199 }
200
201 #[test]
202 fn test_deserialize_yaml_empty_object() {
203 let output = success_output("{}");
204 let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
205 assert_eq!(result, None);
206 }
207}
208