0a64db6925
Check go code / build-and-release (push) Successful in 1m9s
Reviewed-on: #5 Co-authored-by: Julien Fastré <julien.fastre@champs-libres.coop> Co-committed-by: Julien Fastré <julien.fastre@champs-libres.coop>
224 lines
6.6 KiB
Rust
224 lines
6.6 KiB
Rust
use cl_cli::cli::Issue2Work as Issue2WorkCli;
|
|
use cl_cli::config::build_config;
|
|
use cl_cli::error::GeneralError;
|
|
use cl_cli::planning::issue2work::handle_issue2work;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::io::{self, Read, Write};
|
|
use std::path::PathBuf;
|
|
use std::process;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
enum InputMessage {
|
|
Issue2Work(Issue2WorkWebExtMessage),
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(tag = "type")]
|
|
enum OutputContent {
|
|
Issue2Work(OutputIssue2Work),
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(tag = "result", content = "data")]
|
|
enum OutputMessage {
|
|
Ok(OutputContent),
|
|
Error(String),
|
|
}
|
|
|
|
/// Input message for creating an issue.
|
|
#[derive(Debug, Deserialize)]
|
|
struct Issue2WorkWebExtMessage {
|
|
url: String,
|
|
project: String,
|
|
}
|
|
|
|
/// Output message returned to the web extension.
|
|
#[derive(Debug, Serialize)]
|
|
struct OutputIssue2Work {
|
|
created: String,
|
|
}
|
|
|
|
/// Reads a *raw* message from stdin (Native Messaging format).
|
|
/// - first reads 4 bytes (length)
|
|
/// - then reads exactly `len` bytes into a Vec<u8>
|
|
/// Returns Ok(None) if EOF (no more data).
|
|
fn read_message_bytes(stdin: &mut io::StdinLock<'_>) -> io::Result<Option<Vec<u8>>> {
|
|
let mut len_buf = [0u8; 4];
|
|
|
|
// Lire les 4 octets de longueur
|
|
match stdin.read_exact(&mut len_buf) {
|
|
Ok(()) => {}
|
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
|
// Plus de données : fin normale
|
|
return Ok(None);
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
|
|
// Longueur annoncée (endianness native, comme dans la spec)
|
|
let len = u32::from_ne_bytes(len_buf) as usize;
|
|
|
|
// Ici, on peut recevoir un *nombre arbitraire d'octets* (dans la limite de u32).
|
|
let mut buf = vec![0u8; len];
|
|
stdin.read_exact(&mut buf)?;
|
|
|
|
Ok(Some(buf))
|
|
}
|
|
|
|
/// Write a JSON message to stdout with a header of 4 bytes.
|
|
fn write_json_message<T: serde::Serialize>(
|
|
stdout: &mut io::StdoutLock<'_>,
|
|
msg: &T,
|
|
) -> io::Result<()> {
|
|
let data =
|
|
serde_json::to_vec(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
|
|
|
let len = data.len() as u32;
|
|
let len_buf = len.to_ne_bytes();
|
|
|
|
stdout.write_all(&len_buf)?;
|
|
stdout.write_all(&data)?;
|
|
stdout.flush()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// "Logique métier" : construit une URL "created" à partir de l'entrée.
|
|
async fn handle_business_logic(input: InputMessage) -> Result<OutputContent, GeneralError> {
|
|
let mut default_config_path = PathBuf::new();
|
|
default_config_path.push(simple_home_dir::home_dir().unwrap());
|
|
default_config_path.push(".config/cl-cli/config.toml");
|
|
// during dev: default_config_path.push("/tmp/cl-cli/config.toml");
|
|
let config = match build_config(&default_config_path) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
|
|
let result = match input {
|
|
InputMessage::Issue2Work(i2w) => {
|
|
let arg = Issue2WorkCli {
|
|
issue_url: i2w.url,
|
|
project_id: i2w.project,
|
|
assign_to_me: false,
|
|
};
|
|
let result = handle_issue2work(config, &arg).await?;
|
|
|
|
OutputContent::Issue2Work(OutputIssue2Work {
|
|
created: result.work_package_url,
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
eprintln!("Native host Rust démarré.");
|
|
|
|
let stdin = io::stdin();
|
|
let mut stdin_lock = stdin.lock();
|
|
let stdout = io::stdout();
|
|
let mut stdout_lock = stdout.lock();
|
|
|
|
loop {
|
|
match read_message_bytes(&mut stdin_lock) {
|
|
Ok(Some(raw_bytes)) => {
|
|
eprintln!("Reçu {} octets depuis l'extension.", raw_bytes.len());
|
|
|
|
// On parse le JSON *après* avoir lu tous les octets.
|
|
let input: InputMessage = match serde_json::from_slice(&raw_bytes) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("JSON invalide reçu: {e}");
|
|
// Option : renvoyer une erreur JSON au lieu d'abandonner
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// Logique métier -> OutputMessage
|
|
let output = match handle_business_logic(input).await {
|
|
Ok(v) => OutputMessage::Ok(v),
|
|
Err(e) => OutputMessage::Error(e.description),
|
|
};
|
|
|
|
if let Err(e) = write_json_message(&mut stdout_lock, &output) {
|
|
eprintln!("Erreur lors de l'écriture de la réponse: {e}");
|
|
// Si on ne sait plus écrire, on termine.
|
|
process::exit(1);
|
|
}
|
|
}
|
|
Ok(None) => {
|
|
eprintln!("EOF sur stdin, arrêt du host.");
|
|
break;
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Erreur de lecture: {e}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn deserializes_issue2work_variant() {
|
|
// Given: a JSON with the external tag "type" matching the enum variant name
|
|
let payload = json!({
|
|
"type": "Issue2Work",
|
|
"url": "https://example.com/issues/123",
|
|
"project": "test"
|
|
});
|
|
|
|
// When: deserializing to InputMessage
|
|
let msg: InputMessage = serde_json::from_value(payload).expect("valid enum JSON");
|
|
|
|
// Then: we get the correct variant and fields
|
|
match msg {
|
|
InputMessage::Issue2Work(inner) => {
|
|
assert_eq!(inner.url, "https://example.com/issues/123");
|
|
assert_eq!(inner.project, "test");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fails_on_unknown_type_tag() {
|
|
let payload = json!({
|
|
"type": "Unknown",
|
|
"url": "https://example.com/issues/123",
|
|
"project": "test"
|
|
});
|
|
|
|
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
|
// Basic sanity check that it's a data error mentioning the unrecognized variant
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("unknown variant") || msg.contains("unknown") || msg.contains("expected"),
|
|
"unexpected error message: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fails_when_missing_required_fields() {
|
|
// Missing "url" and "project"
|
|
let payload = json!({
|
|
"type": "Issue2Work"
|
|
});
|
|
|
|
let err = serde_json::from_value::<InputMessage>(payload).unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("missing field"),
|
|
"unexpected error message: {msg}"
|
|
);
|
|
}
|
|
}
|