Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:05.927Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

command_json.rs

208 lines · 6.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.