use serde_json::Value;
use std::collections::HashMap;
use crate::client::NeutralIpcClient;
use crate::constants::*;
use crate::error::{NeutralIpcError, Result};
pub struct NeutralIpcTemplate {
template: String,
tpl_type: u8,
schema_type: u8,
schema: Vec<u8>,
pub(crate) result: HashMap<String, Value>,
}
impl NeutralIpcTemplate {
pub fn new() -> Result<Self> {
Ok(Self {
template: "".to_string(),
tpl_type: CONTENT_PATH,
schema_type: CONTENT_JSON,
schema: b"{}".to_vec(),
result: HashMap::new(),
})
}
pub fn from_file_value(template: &str, schema: Value) -> Result<Self> {
let schema_str = if schema.is_string() {
schema.as_str().unwrap().to_string()
} else {
serde_json::to_string(&schema)?
};
Ok(Self {
template: template.to_string(),
tpl_type: CONTENT_PATH,
schema_type: CONTENT_JSON,
schema: schema_str.into_bytes(),
result: HashMap::new(),
})
}
pub fn from_src_value(template: &str, schema: Value) -> Result<Self> {
let schema_str = if schema.is_string() {
schema.as_str().unwrap().to_string()
} else {
serde_json::to_string(&schema)?
};
Ok(Self {
template: template.to_string(),
tpl_type: CONTENT_TEXT,
schema_type: CONTENT_JSON,
schema: schema_str.into_bytes(),
result: HashMap::new(),
})
}
pub fn from_file_msgpack(template: &str, schema: &[u8]) -> Result<Self> {
Ok(Self {
template: template.to_string(),
tpl_type: CONTENT_PATH,
schema_type: CONTENT_MSGPACK,
schema: schema.to_vec(),
result: HashMap::new(),
})
}
pub fn from_src_msgpack(template: &str, schema: &[u8]) -> Result<Self> {
Ok(Self {
template: template.to_string(),
tpl_type: CONTENT_TEXT,
schema_type: CONTENT_MSGPACK,
schema: schema.to_vec(),
result: HashMap::new(),
})
}
pub fn render(&mut self) -> Result<String> {
let mut client = NeutralIpcClient::new(
CTRL_PARSE_TEMPLATE,
self.schema_type,
self.schema.as_slice(),
self.tpl_type,
&self.template
);
let result = client.start()?;
let status = result.get("control")
.and_then(|v| v.as_u64())
.ok_or(NeutralIpcError::InvalidResponse)? as u8;
let content1 = result.get("content-1")
.and_then(|v| v.as_str())
.ok_or(NeutralIpcError::InvalidResponse)?;
let content2 = result.get("content-2")
.and_then(|v| v.as_str())
.ok_or(NeutralIpcError::InvalidResponse)?;
let result_data: Value = serde_json::from_str(content1)?;
self.result = HashMap::new();
self.result.insert("status".to_string(), Value::Number(status.into()));
self.result.insert("result".to_string(), result_data);
self.result.insert("content".to_string(), Value::String(content2.to_string()));
Ok(content2.to_string())
}
pub fn set_path(&mut self, path: &str) {
self.tpl_type = CONTENT_PATH;
self.template = path.to_string();
}
pub fn set_source(&mut self, source: &str) {
self.tpl_type = CONTENT_TEXT;
self.template = source.to_string();
}
pub fn merge_schema(&mut self, schema: Value) -> Result<()> {
let current_schema: Value = match self.schema_type {
CONTENT_MSGPACK => rmp_serde::from_slice(&self.schema)?,
_ => serde_json::from_slice(&self.schema)?,
};
let new_schema = if schema.is_string() {
serde_json::from_str(schema.as_str().unwrap())?
} else {
schema
};
let merged = Self::deep_merge(current_schema, new_schema);
self.schema = match self.schema_type {
CONTENT_MSGPACK => rmp_serde::to_vec(&merged)?,
_ => serde_json::to_vec(&merged)?,
};
Ok(())
}
pub fn set_schema_msgpack(&mut self, schema: &[u8]) {
self.schema_type = CONTENT_MSGPACK;
self.schema = schema.to_vec();
}
pub fn has_error(&self) -> bool {
if let Some(status) = self.result.get("status").and_then(|v| v.as_u64()) {
if status != 0 {
return true;
}
}
if let Some(result) = self.result.get("result") {
if let Some(has_error) = result.get("has_error").and_then(|v| v.as_bool()) {
return has_error;
}
}
false
}
pub fn get_status_code(&self) -> &str {
self.result.get("result")
.and_then(|r| r.get("status_code"))
.and_then(|v| v.as_str())
.unwrap_or("")
}
pub fn get_status_text(&self) -> &str {
self.result.get("result")
.and_then(|r| r.get("status_text"))
.and_then(|v| v.as_str())
.unwrap_or("")
}
pub fn get_status_param(&self) -> &str {
self.result.get("result")
.and_then(|r| r.get("status_param"))
.and_then(|v| v.as_str())
.unwrap_or("")
}
pub fn get_result(&self) -> Option<&Value> {
self.result.get("result")
}
fn deep_merge(a: Value, b: Value) -> Value {
match (a, b) {
(Value::Object(mut map_a), Value::Object(map_b)) => {
for (key, value_b) in map_b {
if let Some(value_a) = map_a.get_mut(&key) {
*value_a = Self::deep_merge(value_a.clone(), value_b);
} else {
map_a.insert(key, value_b);
}
}
Value::Object(map_a)
}
(_, b) => b,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmp_serde::from_slice;
use serde_json::json;
use crate::client::is_server_available;
fn skip_if_server_unavailable() {
if !is_server_available() {
panic!("Neutral TS server not available - skipping test");
}
}
#[test]
fn test_template_src() {
skip_if_server_unavailable();
let schema = json!({
"data": {
"text": "Hello!",
"number": 123
}
});
let mut template = NeutralIpcTemplate::from_src_value("Rust IPC client: {:;text:} {:;number:}", schema).unwrap();
let result = template.render().unwrap();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
assert!(!template.has_error());
assert_eq!(status_code, "200");
assert_eq!(status_text, "OK");
assert_eq!(status_param, "");
assert_eq!(result, "Rust IPC client: Hello! 123");
}
#[test]
fn test_template_file() {
skip_if_server_unavailable();
let schema = json!({
"data": {
"text": "Hello!",
"number": 123
}
});
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let tpl_file = format!("{}/tests/template.ntpl", manifest_dir);
let mut template = NeutralIpcTemplate::from_file_value(&tpl_file, schema).unwrap();
let result = template.render().unwrap();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
assert!(!template.has_error());
assert_eq!(status_code, "200");
assert_eq!(status_text, "OK");
assert_eq!(status_param, "");
assert_eq!(result, "Rust IPC client: Hello! 123");
}
#[test]
fn test_template_merge_schema() {
skip_if_server_unavailable();
let schema = json!({
"data": {
"text": "Hello!",
"number": 123
}
});
let schema_merge = json!({
"data": {
"text": "Hello! (merged)"
}
});
let mut template = NeutralIpcTemplate::from_src_value("Rust IPC client: {:;text:} {:;number:}", schema).unwrap();
let _ = template.merge_schema(schema_merge).unwrap();
let result = template.render().unwrap();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
assert!(!template.has_error());
assert_eq!(status_code, "200");
assert_eq!(status_text, "OK");
assert_eq!(status_param, "");
assert_eq!(result, "Rust IPC client: Hello! (merged) 123");
}
#[test]
fn test_template_404() {
skip_if_server_unavailable();
let schema = json!({
"data": {
"text": "Hello!",
"number": 123
}
});
let mut template = NeutralIpcTemplate::from_src_value("Rust IPC client: {:exit; 404 :}", schema).unwrap();
let result = template.render().unwrap();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
assert!(!template.has_error());
assert_eq!(status_code, "404");
assert_eq!(status_text, "Not Found");
assert_eq!(status_param, "");
assert_eq!(result, "404 Not Found");
}
#[test]
fn test_template_redirect() {
skip_if_server_unavailable();
let schema = json!({
"data": {
"text": "Hello!",
"number": 123
}
});
let mut template = NeutralIpcTemplate::from_src_value("Rust IPC client: {:redirect; 301 >> https://crates.io/crates/neutralts :}", schema).unwrap();
let result = template.render().unwrap();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
assert!(!template.has_error());
assert_eq!(status_code, "301");
assert_eq!(status_text, "Moved Permanently");
assert_eq!(status_param, "https://crates.io/crates/neutralts");
assert_eq!(result, "301 Moved Permanently\nhttps://crates.io/crates/neutralts");
}
#[test]
fn test_from_src_msgpack_and_merge_schema() {
let schema = json!({
"data": {
"text": "Hello!"
}
});
let msgpack = rmp_serde::to_vec(&schema).unwrap();
let mut template = NeutralIpcTemplate::from_src_msgpack("tpl", &msgpack).unwrap();
template.merge_schema(json!({"data": {"number": 123}})).unwrap();
assert_eq!(template.schema_type, CONTENT_MSGPACK);
let merged: Value = from_slice(&template.schema).unwrap();
assert_eq!(merged["data"]["text"], "Hello!");
assert_eq!(merged["data"]["number"], 123);
}
#[test]
fn test_set_schema_msgpack_switches_type() {
let schema = json!({"data": {"value": 1}});
let msgpack = rmp_serde::to_vec(&schema).unwrap();
let mut template = NeutralIpcTemplate::new().unwrap();
template.set_schema_msgpack(&msgpack);
assert_eq!(template.schema_type, CONTENT_MSGPACK);
let decoded: Value = from_slice(&template.schema).unwrap();
assert_eq!(decoded["data"]["value"], 1);
}
}