use crate::engine::assertion::Value;
use crate::engine::{AgentInfo, CallState, sip_user_part};
use rhai::{Array, Dynamic, EvalAltResult, Map};
use ringo_core::baresip::Account;
pub(super) fn to_value(d: &Dynamic) -> Value {
if d.is_unit() {
Value::Unit
} else if let Some(c) = d.clone().try_cast::<CallState>() {
Value::State(c)
} else if let Ok(b) = d.as_bool() {
Value::Bool(b)
} else if let Ok(i) = d.as_int() {
Value::Int(i)
} else if let Some(arr) = d.clone().try_cast::<Array>() {
Value::List(arr.iter().map(to_value).collect())
} else if let Some(map) = d.clone().try_cast::<Map>() {
Value::Map(
map.iter()
.map(|(k, v)| (k.to_string(), to_value(v)))
.collect(),
)
} else if let Ok(s) = d.clone().into_string() {
Value::Str(s)
} else {
Value::Str(d.to_string())
}
}
pub(super) fn to_dynamic(v: &Value) -> Dynamic {
match v {
Value::Unit => Dynamic::UNIT,
Value::Bool(b) => (*b).into(),
Value::Int(i) => (*i).into(),
Value::Str(s) => s.clone().into(),
Value::State(c) => Dynamic::from(*c),
Value::List(items) => Dynamic::from_array(items.iter().map(to_dynamic).collect()),
Value::Map(pairs) => {
let mut m = Map::new();
for (k, v) in pairs {
m.insert(k.as_str().into(), to_dynamic(v));
}
Dynamic::from_map(m)
}
}
}
pub(super) fn json_to_dynamic(v: &serde_json::Value) -> Dynamic {
use serde_json::Value as J;
match v {
J::Null => Dynamic::UNIT,
J::Bool(b) => (*b).into(),
J::Number(n) => {
if let Some(i) = n.as_i64() {
i.into()
} else if let Some(f) = n.as_f64() {
f.into()
} else {
n.to_string().into()
}
}
J::String(s) => s.clone().into(),
J::Array(a) => {
let arr: Array = a.iter().map(json_to_dynamic).collect();
Dynamic::from_array(arr)
}
J::Object(o) => {
let mut m = Map::new();
for (k, val) in o {
m.insert(k.as_str().into(), json_to_dynamic(val));
}
Dynamic::from_map(m)
}
}
}
pub(super) fn info_to_map(i: &AgentInfo) -> Dynamic {
let mut m = Map::new();
m.insert("name".into(), i.name.clone().into());
m.insert("aor".into(), i.aor.clone().into());
m.insert("registered".into(), i.registered.into());
m.insert("state".into(), i.state.to_string().into());
m.insert("reason".into(), opt_to_dynamic(i.reason.clone()));
m.insert(
"status_code".into(),
match i.status_code {
Some(c) => (c as i64).into(),
None => Dynamic::UNIT,
},
);
m.insert("peer".into(), peer_to_map(i.peer.clone()));
m.insert("calls".into(), (i.calls as i64).into());
Dynamic::from_map(m)
}
pub(super) fn peer_to_map(p: Option<(String, Option<String>)>) -> Dynamic {
match p {
Some((uri, name)) => {
let mut m = Map::new();
m.insert("number".into(), sip_user_part(&uri).into());
m.insert("uri".into(), uri.into());
m.insert("name".into(), opt_to_dynamic(name));
Dynamic::from_map(m)
}
None => Dynamic::UNIT,
}
}
pub(super) fn headers_to_map(headers: Vec<(String, String)>) -> Dynamic {
let mut m = Map::new();
for (k, v) in headers {
m.insert(k.as_str().into(), v.into());
}
Dynamic::from_map(m)
}
pub(super) fn opt_to_dynamic(v: Option<String>) -> Dynamic {
match v {
Some(s) => s.into(),
None => Dynamic::UNIT,
}
}
pub(super) fn account_from_map(name: &str, map: &Map) -> Result<Account, Box<EvalAltResult>> {
let get = |k: &str| map.get(k).and_then(|d| d.clone().into_string().ok());
let req = |k: &str| get(k).ok_or_else(|| format!("agent `{name}`: `{k}` is required"));
Ok(Account {
username: req("username")?,
domain: req("domain")?,
password: get("password").unwrap_or_default(),
display_name: get("display_name"),
transport: get("transport"),
auth_user: get("auth_user"),
outbound: get("outbound"),
stun_server: get("stun_server"),
media_enc: get("media_enc"),
regint: map
.get("regint")
.and_then(|d| d.as_int().ok())
.map(|i| i as u32),
mwi: map
.get("mwi")
.and_then(|d| d.as_bool().ok())
.unwrap_or(false),
dtmf_mode: get("dtmf_mode"),
})
}
pub(super) fn headers_from_map(map: &Map) -> Result<Vec<(String, String)>, Box<EvalAltResult>> {
let Some(h) = map.get("headers").and_then(|d| d.clone().try_cast::<Map>()) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for (k, v) in h.iter() {
if !is_header_token(k) {
return Err(format!("`{k}` is not a valid SIP header name").into());
}
if let Ok(val) = v.clone().into_string() {
out.push((k.to_string(), val));
}
}
Ok(out)
}
fn is_header_token(s: &str) -> bool {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"-.!%*_+`'~".contains(&b))
}
pub(super) fn body_to_string(d: &Dynamic) -> Option<String> {
match d.clone().try_cast::<Map>() {
Some(map) => Some(rhai::format_map_as_json(&map)),
None => d.clone().into_string().ok(),
}
}
#[cfg(test)]
mod tests {
use super::{account_from_map, body_to_string, headers_from_map, json_to_dynamic};
use rhai::{Dynamic, Map};
#[test]
fn json_to_dynamic_maps_types() {
use serde_json::json;
assert!(json_to_dynamic(&json!(null)).is_unit());
assert_eq!(json_to_dynamic(&json!(true)).as_bool(), Ok(true));
assert_eq!(json_to_dynamic(&json!(42)).as_int(), Ok(42));
assert_eq!(
json_to_dynamic(&json!("hi")).into_string(),
Ok("hi".to_string())
);
let m = json_to_dynamic(&json!({"id": 7}))
.try_cast::<Map>()
.unwrap();
assert_eq!(m.get("id").unwrap().as_int(), Ok(7));
let a = json_to_dynamic(&json!(["a", "b"]))
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(a.len(), 2);
}
#[test]
fn account_required_and_optional_fields() {
let mut m = Map::new();
m.insert("username".into(), Dynamic::from("alice"));
m.insert("domain".into(), Dynamic::from("example.com"));
m.insert("stun_server".into(), Dynamic::from("stun:x"));
let acc = account_from_map("A", &m).unwrap();
assert_eq!(acc.username, "alice");
assert_eq!(acc.domain, "example.com");
assert_eq!(acc.stun_server.as_deref(), Some("stun:x"));
assert_eq!(acc.password, "");
let mut bad = Map::new();
bad.insert("username".into(), Dynamic::from("alice"));
assert!(account_from_map("A", &bad).is_err());
}
#[test]
fn headers_collected_from_submap() {
let mut hdrs = Map::new();
hdrs.insert("X-Foo".into(), Dynamic::from("bar"));
let mut m = Map::new();
m.insert("headers".into(), Dynamic::from(hdrs));
assert_eq!(
headers_from_map(&m).unwrap(),
vec![("X-Foo".to_string(), "bar".to_string())]
);
assert!(headers_from_map(&Map::new()).unwrap().is_empty());
let mut bad_hdrs = Map::new();
bad_hdrs.insert("X-Bad\r\nInjected".into(), Dynamic::from("v"));
let mut bad = Map::new();
bad.insert("headers".into(), Dynamic::from(bad_hdrs));
assert!(headers_from_map(&bad).is_err());
}
#[test]
fn body_accepts_string_or_map() {
assert_eq!(
body_to_string(&Dynamic::from("raw")).as_deref(),
Some("raw")
);
let mut m = Map::new();
m.insert("announcement".into(), Dynamic::from(false));
assert_eq!(
body_to_string(&Dynamic::from(m)).as_deref(),
Some(r#"{"announcement":false}"#)
);
}
}