use std::path::Path;
use base64::Engine as _;
use greentic_types::ChannelMessageEnvelope;
use serde_json::json;
use crate::domains::Domain;
use crate::ingress_dispatch::build_injected_config;
use crate::messaging_app as app;
use crate::messaging_dto::ProviderPayloadV1;
use crate::messaging_egress as egress;
use crate::operator_log;
use crate::runner_host::{DemoRunnerHost, OperatorContext};
pub(super) fn route_messaging_envelopes(
bundle: &Path,
runner_host: &DemoRunnerHost,
provider: &str,
ctx: &OperatorContext,
envelopes: Vec<ChannelMessageEnvelope>,
) -> anyhow::Result<()> {
let team = ctx.team.as_deref();
let app_pack_path = app::resolve_app_pack_path(bundle, &ctx.tenant, team, None)
.context("resolve app pack for messaging pipeline")?;
let pack_info = app::load_app_pack_info(&app_pack_path).context("load app pack manifest")?;
let flow = app::select_app_flow(&pack_info).context("select app default flow")?;
operator_log::debug(
module_path!(),
format!(
"[demo messaging] routing {} envelope(s) through app flow={} pack={}",
envelopes.len(),
flow.id,
pack_info.pack_id
),
);
for envelope in &envelopes {
let outputs = if let Some(route_to_card) = envelope.metadata.get("routeToCardId") {
match read_card_from_pack(&app_pack_path, route_to_card) {
Some(mut card_json) => {
operator_log::info(
module_path!(),
format!(
"[demo messaging] card routing: {} -> card asset found",
route_to_card
),
);
let from_id = envelope.from.as_ref().map(|f| f.id.as_str()).unwrap_or("?");
crate::flow_log::log(
"CARD",
&format!(
"pack={} routeToCardId={} tenant={} from={}",
pack_info.pack_id, route_to_card, ctx.tenant, from_id
),
);
let locale = envelope
.metadata
.get("locale")
.map(String::as_str)
.unwrap_or("en");
resolve_i18n_tokens(&mut card_json, &app_pack_path, locale);
resolve_placeholders(&mut card_json, &envelope.metadata);
carry_form_data_to_actions(&mut card_json, &envelope.metadata);
let mut reply = envelope.clone();
reply.metadata.insert(
"adaptive_card".to_string(),
serde_json::to_string(&card_json).unwrap_or_default(),
);
reply.text = None;
vec![reply]
}
None => {
operator_log::warn(
module_path!(),
format!(
"[demo messaging] card routing: {} -> card asset NOT found, using app flow",
route_to_card
),
);
run_app_flow_safe(
runner_host,
bundle,
ctx,
&app_pack_path,
&pack_info,
flow,
envelope,
)
}
}
} else {
run_app_flow_safe(
runner_host,
bundle,
ctx,
&app_pack_path,
&pack_info,
flow,
envelope,
)
};
for mut out_envelope in outputs {
if let Some(team) = &ctx.team {
out_envelope
.metadata
.entry("team".to_string())
.or_insert_with(|| team.clone());
}
ensure_card_i18n_resolved(&mut out_envelope, &app_pack_path);
let message_value = serde_json::to_value(&out_envelope)?;
let has_adaptive_card = message_value
.get("metadata")
.and_then(|m| m.get("adaptive_card"))
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
operator_log::debug(
module_path!(),
format!(
"[demo messaging] pre-encode adaptive_card={} text_present={} session_id={} route={} tenant={} metadata_keys={}",
has_adaptive_card,
message_value
.get("text")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false),
message_value
.get("session_id")
.and_then(|v| v.as_str())
.unwrap_or(""),
message_value
.get("metadata")
.and_then(|m| m.get("route"))
.and_then(|v| v.as_str())
.unwrap_or(""),
message_value
.get("metadata")
.and_then(|m| m.get("tenant"))
.and_then(|v| v.as_str())
.unwrap_or(""),
message_value
.get("metadata")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect::<Vec<_>>().join(","))
.unwrap_or_default()
),
);
let plan = match egress::render_plan(runner_host, ctx, provider, message_value.clone())
{
Ok(plan) => plan,
Err(err) => {
operator_log::warn(
module_path!(),
format!("[demo messaging] render_plan failed: {err}; using empty plan"),
);
json!({})
}
};
let payload = match egress::encode_payload(
runner_host,
ctx,
provider,
message_value.clone(),
plan,
) {
Ok(payload) => payload,
Err(err) => {
operator_log::warn(
module_path!(),
format!("[demo messaging] encode failed: {err}; using fallback payload"),
);
let body_bytes = serde_json::to_vec(&message_value)?;
ProviderPayloadV1 {
content_type: "application/json".to_string(),
body_b64: base64::engine::general_purpose::STANDARD.encode(&body_bytes),
metadata_json: Some(serde_json::to_string(&message_value)?),
metadata: None,
}
}
};
let provider_type = runner_host.canonical_provider_type(Domain::Messaging, provider);
let config = build_injected_config(runner_host, Domain::Messaging, provider, ctx)?
.map(decode_injected_config_for_provider);
let send_input = egress::build_send_payload(
payload,
&provider_type,
&ctx.tenant,
ctx.team.clone(),
config,
);
let send_bytes = serde_json::to_vec(&send_input)?;
let outcome = runner_host.invoke_provider_op(
Domain::Messaging,
provider,
"send_payload",
&send_bytes,
ctx,
)?;
let provider_ok = outcome
.output
.as_ref()
.and_then(|v| v.get("ok"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if outcome.success && provider_ok {
operator_log::debug(
module_path!(),
format!(
"[demo messaging] send succeeded provider={} envelope_id={}",
provider, out_envelope.id
),
);
} else {
let provider_msg = outcome
.output
.as_ref()
.and_then(|v| v.get("message"))
.and_then(|v| v.as_str())
.unwrap_or("");
let err_msg = outcome
.error
.clone()
.unwrap_or_else(|| provider_msg.to_string());
operator_log::error(
module_path!(),
format!(
"[demo messaging] send failed provider={} provider_ok={} err={}",
provider, provider_ok, err_msg
),
);
}
}
}
Ok(())
}
fn decode_injected_config_for_provider(config: serde_json::Value) -> serde_json::Value {
let Some(obj) = config.as_object() else {
return config;
};
let mut decoded = serde_json::Map::new();
for (key, value) in obj {
if let Some(raw_key) = key.strip_suffix("_b64")
&& let Some(text) = value.as_str()
&& let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(text)
&& let Ok(decoded_text) = String::from_utf8(bytes)
{
decoded.insert(raw_key.to_string(), serde_json::Value::String(decoded_text));
continue;
}
decoded.insert(key.clone(), value.clone());
}
serde_json::Value::Object(decoded)
}
fn read_card_from_pack(pack_path: &Path, card_key: &str) -> Option<serde_json::Value> {
let file = std::fs::File::open(pack_path).ok()?;
let mut archive = zip::ZipArchive::new(file).ok()?;
let asset_path = format!("assets/cards/{card_key}.json");
let mut entry = archive.by_name(&asset_path).ok()?;
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut buf).ok()?;
serde_json::from_slice(&buf).ok()
}
fn run_app_flow_safe(
runner_host: &DemoRunnerHost,
bundle: &Path,
ctx: &OperatorContext,
app_pack_path: &Path,
pack_info: &app::AppPackInfo,
flow: &app::AppFlowInfo,
envelope: &ChannelMessageEnvelope,
) -> Vec<ChannelMessageEnvelope> {
match app::run_app_flow(
runner_host,
bundle,
ctx,
app_pack_path,
&pack_info.pack_id,
&flow.id,
envelope,
) {
Ok(outputs) => outputs,
Err(err) => {
operator_log::error(
module_path!(),
format!("[demo messaging] app flow failed: {err}"),
);
vec![envelope.clone()]
}
}
}
use anyhow::Context;
const ROUTING_META_KEYS: &[&str] = &[
"routeToCardId",
"toCardId",
"action_id",
"adaptive_card",
"locale",
"autoStart",
"mcp_wizard",
"mcp_operation",
];
fn carry_form_data_to_actions(
card: &mut serde_json::Value,
metadata: &std::collections::BTreeMap<String, String>,
) {
let form_fields: Vec<(String, String)> = metadata
.iter()
.filter(|(k, _)| !ROUTING_META_KEYS.contains(&k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if form_fields.is_empty() {
return;
}
inject_form_data_recursive(card, &form_fields);
}
fn inject_form_data_recursive(value: &mut serde_json::Value, fields: &[(String, String)]) {
match value {
serde_json::Value::Object(map) => {
if map.get("type").and_then(|v| v.as_str()) == Some("Action.Submit")
&& let Some(data) = map.get_mut("data").and_then(|d| d.as_object_mut())
{
for (k, v) in fields {
if !data.contains_key(k) {
data.insert(k.clone(), serde_json::Value::String(v.clone()));
}
}
}
for val in map.values_mut() {
inject_form_data_recursive(val, fields);
}
}
serde_json::Value::Array(items) => {
for item in items {
inject_form_data_recursive(item, fields);
}
}
_ => {}
}
}
fn resolve_placeholders(
value: &mut serde_json::Value,
metadata: &std::collections::BTreeMap<String, String>,
) {
match value {
serde_json::Value::String(text) if text.contains("${") => {
let mut output = String::with_capacity(text.len());
let mut rest = text.as_str();
loop {
let Some(start) = rest.find("${") else {
output.push_str(rest);
break;
};
output.push_str(&rest[..start]);
let after = &rest[start + 2..];
let Some(end) = after.find('}') else {
output.push_str(&rest[start..]);
break;
};
let key = after[..end].trim();
if let Some(val) = metadata.get(key) {
output.push_str(val);
} else {
output.push_str(&rest[start..start + 2 + end + 1]);
}
rest = &after[end + 1..];
}
*text = output;
}
serde_json::Value::Array(items) => {
for item in items {
resolve_placeholders(item, metadata);
}
}
serde_json::Value::Object(map) => {
for val in map.values_mut() {
resolve_placeholders(val, metadata);
}
}
_ => {}
}
}
fn resolve_i18n_tokens(card: &mut serde_json::Value, pack_path: &Path, locale: &str) {
let bundle = read_i18n_bundle(pack_path, locale).or_else(|| read_i18n_bundle(pack_path, "en"));
let Some(bundle) = bundle else { return };
replace_tokens_recursive(card, &bundle);
}
fn read_i18n_bundle(
pack_path: &Path,
locale: &str,
) -> Option<std::collections::HashMap<String, String>> {
let file = std::fs::File::open(pack_path).ok()?;
let mut archive = zip::ZipArchive::new(file).ok()?;
let asset_path = format!("assets/i18n/{locale}.json");
let mut entry = archive.by_name(&asset_path).ok()?;
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut buf).ok()?;
serde_json::from_slice(&buf).ok()
}
fn replace_tokens_recursive(
value: &mut serde_json::Value,
bundle: &std::collections::HashMap<String, String>,
) {
match value {
serde_json::Value::String(text) if text.contains("{{i18n:") => {
let mut output = String::with_capacity(text.len());
let mut rest = text.as_str();
loop {
let Some(start) = rest.find("{{i18n:") else {
output.push_str(rest);
break;
};
output.push_str(&rest[..start]);
let token_start = start + "{{i18n:".len();
let after = &rest[token_start..];
let Some(end) = after.find("}}") else {
output.push_str(&rest[start..]);
break;
};
let key = after[..end].trim();
output.push_str(bundle.get(key).map(String::as_str).unwrap_or(key));
rest = &after[end + 2..];
}
*text = output;
}
serde_json::Value::Array(items) => {
for item in items {
replace_tokens_recursive(item, bundle);
}
}
serde_json::Value::Object(map) => {
for val in map.values_mut() {
replace_tokens_recursive(val, bundle);
}
}
_ => {}
}
}
fn translate_string_fields_recursive(
value: &mut serde_json::Value,
en_to_target: &std::collections::HashMap<String, String>,
) {
match value {
serde_json::Value::String(text) => {
if let Some(translated) = en_to_target.get(text.as_str()) {
*text = translated.clone();
}
}
serde_json::Value::Array(items) => {
for item in items {
translate_string_fields_recursive(item, en_to_target);
}
}
serde_json::Value::Object(map) => {
for val in map.values_mut() {
translate_string_fields_recursive(val, en_to_target);
}
}
_ => {}
}
}
fn ensure_card_i18n_resolved(envelope: &mut ChannelMessageEnvelope, pack_path: &Path) {
let Some(ac_str) = envelope.metadata.get("adaptive_card") else {
return;
};
let Ok(mut card) = serde_json::from_str::<serde_json::Value>(ac_str) else {
return;
};
let locale = envelope
.metadata
.get("locale")
.map(String::as_str)
.unwrap_or("en");
if locale != "en"
&& locale != "en-GB"
&& locale != "en-US"
&& let Some(en_bundle) = read_i18n_bundle(pack_path, "en")
&& let Some(target_bundle) = read_i18n_bundle(pack_path, locale)
{
let mut en_to_target = std::collections::HashMap::<String, String>::new();
for (key, en_value) in &en_bundle {
if let Some(target_value) = target_bundle.get(key)
&& target_value != en_value
{
en_to_target.insert(en_value.clone(), target_value.clone());
}
}
if !en_to_target.is_empty() {
translate_string_fields_recursive(&mut card, &en_to_target);
if let Ok(resolved) = serde_json::to_string(&card) {
envelope
.metadata
.insert("adaptive_card".to_string(), resolved);
}
}
}
let card_id = card
.pointer("/greentic/cardId")
.and_then(serde_json::Value::as_str);
let Some(card_id) = card_id else { return };
let has_empty_text = card
.get("body")
.and_then(serde_json::Value::as_array)
.map(|body| {
body.iter().any(|item| {
item.get("text")
.and_then(serde_json::Value::as_str)
.is_some_and(str::is_empty)
})
})
.unwrap_or(false);
if !has_empty_text {
return;
}
let Some(mut fresh_card) = read_card_from_pack(pack_path, card_id) else {
return;
};
let locale = envelope
.metadata
.get("locale")
.map(String::as_str)
.unwrap_or("en");
resolve_i18n_tokens(&mut fresh_card, pack_path, locale);
if let Ok(resolved) = serde_json::to_string(&fresh_card) {
envelope
.metadata
.insert("adaptive_card".to_string(), resolved);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::messaging_app::{AppFlowInfo, AppPackInfo};
use crate::secrets_gate;
use tempfile::tempdir;
use zip::write::FileOptions;
fn envelope() -> ChannelMessageEnvelope {
serde_json::from_value(json!({
"id": "msg-1",
"tenant": {
"env": "dev",
"tenant": "demo",
"tenant_id": "demo",
"team": "default",
"attempt": 0
},
"channel": "conv-1",
"session_id": "conv-1",
"from": {
"id": "user-1",
"kind": "user"
},
"text": "hello",
"metadata": {}
}))
.expect("envelope")
}
fn write_test_app_pack(pack_path: &Path) {
use greentic_types::pack_manifest::{
PackFlowEntry, PackKind, PackManifest, PackSignatures,
};
use greentic_types::{Flow, FlowId, FlowKind, PackId};
use semver::Version;
let file = std::fs::File::create(pack_path).expect("create pack");
let mut zip = zip::ZipWriter::new(file);
zip.start_file("manifest.cbor", FileOptions::<()>::default())
.expect("start manifest");
let flow = Flow {
schema_version: "flow-v1".to_string(),
id: FlowId::new("default").expect("flow id"),
kind: FlowKind::Messaging,
entrypoints: std::collections::BTreeMap::from([(
"default".to_string(),
serde_json::Value::Null,
)]),
nodes: Default::default(),
metadata: Default::default(),
};
let manifest = PackManifest {
agents: Default::default(),
schema_version: "pack-v1".into(),
pack_id: PackId::new("demo-app").expect("pack id"),
name: Some("demo-app".into()),
version: Version::parse("0.1.0").expect("version"),
kind: PackKind::Application,
publisher: "demo".into(),
components: Vec::new(),
flows: vec![PackFlowEntry {
id: FlowId::new("default").expect("flow id"),
kind: FlowKind::Messaging,
flow,
tags: vec!["default".to_string()],
entrypoints: vec!["default".to_string()],
}],
dependencies: Vec::new(),
capabilities: Vec::new(),
secret_requirements: Vec::new(),
signatures: PackSignatures::default(),
bootstrap: None,
extensions: None,
};
let bytes = greentic_types::encode_pack_manifest(&manifest).expect("encode manifest");
zip.write_all(&bytes).expect("write manifest");
zip.start_file("assets/cards/welcome.json", FileOptions::<()>::default())
.expect("start card");
zip.write_all(br#"{"body":[{"text":"Welcome card"}]}"#)
.expect("write card");
zip.finish().expect("finish pack");
}
#[test]
fn read_card_from_pack_loads_card_assets_and_handles_missing_cards() {
let dir = tempdir().expect("tempdir");
let pack_path = dir.path().join("app.gtpack");
let file = std::fs::File::create(&pack_path).expect("create pack");
let mut zip = zip::ZipWriter::new(file);
zip.start_file("assets/cards/welcome.json", FileOptions::<()>::default())
.expect("start file");
zip.write_all(br#"{"body":[{"text":"Welcome card"}]}"#)
.expect("write card");
zip.finish().expect("finish pack");
let card = read_card_from_pack(&pack_path, "welcome").expect("card");
assert_eq!(card["body"][0]["text"], "Welcome card");
assert!(read_card_from_pack(&pack_path, "missing").is_none());
}
#[test]
fn run_app_flow_safe_falls_back_to_original_envelope_on_errors() {
let dir = tempdir().expect("tempdir");
let discovery = crate::discovery::discover(dir.path()).expect("discovery");
let secrets_handle =
secrets_gate::resolve_secrets_manager(dir.path(), "demo", Some("default"))
.expect("secrets handle");
let runner_host = DemoRunnerHost::new(
dir.path().to_path_buf(),
&discovery,
None,
secrets_handle,
false,
)
.expect("runner host");
let original = envelope();
let outputs = run_app_flow_safe(
&runner_host,
dir.path(),
&OperatorContext {
tenant: "demo".to_string(),
team: Some("default".to_string()),
correlation_id: None,
},
&dir.path().join("missing.gtpack"),
&AppPackInfo {
pack_id: "app-pack".to_string(),
flows: vec![],
},
&AppFlowInfo {
id: "default".to_string(),
kind: "messaging".to_string(),
},
&original,
);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].id, original.id);
assert_eq!(outputs[0].text, original.text);
}
#[test]
fn read_card_from_pack_rejects_invalid_card_json() {
let dir = tempdir().expect("tempdir");
let pack_path = dir.path().join("app.gtpack");
let file = std::fs::File::create(&pack_path).expect("create pack");
let mut zip = zip::ZipWriter::new(file);
zip.start_file("assets/cards/broken.json", FileOptions::<()>::default())
.expect("start file");
zip.write_all(b"{not-json").expect("write broken card");
zip.finish().expect("finish pack");
assert!(read_card_from_pack(&pack_path, "broken").is_none());
}
#[test]
fn route_messaging_envelopes_errors_when_no_app_pack_is_available() {
let dir = tempdir().expect("tempdir");
let discovery = crate::discovery::discover(dir.path()).expect("discovery");
let secrets_handle =
secrets_gate::resolve_secrets_manager(dir.path(), "demo", Some("default"))
.expect("secrets");
let runner_host = DemoRunnerHost::new(
dir.path().to_path_buf(),
&discovery,
None,
secrets_handle,
false,
)
.expect("runner host");
let err = route_messaging_envelopes(
dir.path(),
&runner_host,
"messaging-webchat",
&OperatorContext {
tenant: "demo".to_string(),
team: Some("default".to_string()),
correlation_id: None,
},
vec![envelope()],
)
.unwrap_err();
assert!(err.to_string().contains("resolve app pack"));
}
#[test]
fn route_messaging_envelopes_card_routing_uses_standard_egress_pipeline() {
let dir = tempdir().expect("tempdir");
let packs_dir = dir.path().join("packs");
std::fs::create_dir_all(&packs_dir).expect("packs dir");
let app_pack = packs_dir.join("default.gtpack");
write_test_app_pack(&app_pack);
let discovery = crate::discovery::discover(dir.path()).expect("discovery");
let secrets_handle =
secrets_gate::resolve_secrets_manager(dir.path(), "demo", Some("default"))
.expect("secrets");
let runner_host = DemoRunnerHost::new(
dir.path().to_path_buf(),
&discovery,
None,
secrets_handle,
false,
)
.expect("runner host");
let mut card_routed = envelope();
card_routed
.metadata
.insert("routeToCardId".to_string(), "welcome".to_string());
let result = route_messaging_envelopes(
dir.path(),
&runner_host,
"messaging-webchat",
&OperatorContext {
tenant: "demo".to_string(),
team: Some("default".to_string()),
correlation_id: None,
},
vec![card_routed],
);
assert!(
result.is_err(),
"expected error because no messaging provider pack is available"
);
}
use std::io::Write;
#[test]
fn resolve_placeholders_replaces_known_keys_and_preserves_unknown() {
let mut card = json!({
"body": [
{ "type": "FactSet", "facts": [
{ "title": "Name", "value": "${full_name}" },
{ "title": "Email", "value": "${email}" },
{ "title": "Missing", "value": "${unknown_key}" }
]}
]
});
let mut meta = std::collections::BTreeMap::new();
meta.insert("full_name".to_string(), "Alice".to_string());
meta.insert("email".to_string(), "alice@example.com".to_string());
resolve_placeholders(&mut card, &meta);
assert_eq!(card["body"][0]["facts"][0]["value"], "Alice");
assert_eq!(card["body"][0]["facts"][1]["value"], "alice@example.com");
assert_eq!(card["body"][0]["facts"][2]["value"], "${unknown_key}");
}
#[test]
fn carry_form_data_injects_into_action_submit_and_skips_routing_keys() {
let mut card = json!({
"actions": [
{
"type": "Action.Submit",
"data": { "action_id": "next", "routeToCardId": "success" }
},
{
"type": "Action.OpenUrl",
"url": "https://example.com"
}
]
});
let mut meta = std::collections::BTreeMap::new();
meta.insert("full_name".to_string(), "Alice".to_string());
meta.insert("routeToCardId".to_string(), "review".to_string());
meta.insert("action_id".to_string(), "goto_review".to_string());
carry_form_data_to_actions(&mut card, &meta);
let data = &card["actions"][0]["data"];
assert_eq!(data["full_name"], "Alice");
assert_eq!(data["action_id"], "next"); assert!(card["actions"][1].get("data").is_none());
}
#[test]
fn translate_string_fields_recursive_swaps_matching_strings() {
let mut card = json!({
"type": "AdaptiveCard",
"body": [
{ "type": "TextBlock", "text": "Hello world" },
{ "type": "TextBlock", "text": "Untranslated string" },
{
"type": "Container",
"items": [
{ "type": "Input.Text", "label": "Question", "placeholder": "Type" }
]
}
],
"actions": [
{ "type": "Action.Submit", "title": "Send" }
]
});
let mut map = std::collections::HashMap::new();
map.insert("Hello world".to_string(), "こんにちは世界".to_string());
map.insert("Question".to_string(), "質問".to_string());
map.insert("Send".to_string(), "送信".to_string());
translate_string_fields_recursive(&mut card, &map);
assert_eq!(card["body"][0]["text"], "こんにちは世界");
assert_eq!(card["body"][1]["text"], "Untranslated string");
assert_eq!(card["body"][2]["items"][0]["label"], "質問");
assert_eq!(card["body"][2]["items"][0]["placeholder"], "Type");
assert_eq!(card["actions"][0]["title"], "送信");
assert_eq!(card["type"], "AdaptiveCard");
assert_eq!(card["body"][0]["type"], "TextBlock");
}
#[test]
fn translate_string_fields_recursive_only_substitutes_exact_matches() {
let mut value = json!({
"a": "foo",
"b": "foo bar", "c": "FOO", "d": ["foo", "baz"]
});
let mut map = std::collections::HashMap::new();
map.insert("foo".to_string(), "FOO_TR".to_string());
translate_string_fields_recursive(&mut value, &map);
assert_eq!(value["a"], "FOO_TR");
assert_eq!(value["b"], "foo bar");
assert_eq!(value["c"], "FOO");
assert_eq!(value["d"][0], "FOO_TR");
assert_eq!(value["d"][1], "baz");
}
}