#![allow(clippy::wildcard_imports)]
use crate::*;
pub(crate) async fn parse_context(parts: &[String]) -> Result<serde_json::Value> {
let mut context = serde_json::Value::Object(serde_json::Map::new());
for part in parts {
let value = parse_context_part(part).await?;
merge_context(&mut context, value)?;
}
Ok(context)
}
pub(crate) async fn parse_context_part(part: &str) -> Result<serde_json::Value> {
if let Some(path) = part.strip_prefix('@') {
if path.is_empty() {
return Err(RototoError::new("context file path must not be empty"));
}
let text = tokio::fs::read_to_string(path).await.map_err(|err| {
RototoError::new(format!("failed to read context file {path}: {err}"))
})?;
return parse_context_json(&text);
}
if part.trim_start().starts_with('{') {
return parse_context_json(part);
}
if let Some((path, value)) = part.split_once('=') {
if path.is_empty() {
return Err(RototoError::new(
"context assignment path must not be empty",
));
}
return context_assignment(path, value);
}
parse_context_json(part)
}
pub(crate) fn parse_context_json(text: &str) -> Result<serde_json::Value> {
let context: serde_json::Value = serde_json::from_str(text)
.map_err(|err| RototoError::new(format!("failed to parse context JSON: {err}")))?;
if !context.is_object() {
return Err(RototoError::new("context JSON must be an object"));
}
Ok(context)
}
pub(crate) fn context_assignment(path: &str, value: &str) -> Result<serde_json::Value> {
let value =
serde_json::from_str(value).unwrap_or_else(|_| serde_json::Value::String(value.to_owned()));
let mut root = serde_json::Map::new();
insert_context_path(&mut root, path, value)?;
Ok(serde_json::Value::Object(root))
}
pub(crate) fn insert_context_path(
object: &mut serde_json::Map<String, serde_json::Value>,
path: &str,
value: serde_json::Value,
) -> Result<()> {
let mut segments = path.split('.').peekable();
let mut current = object;
while let Some(segment) = segments.next() {
if segment.is_empty() {
return Err(RototoError::new(format!("invalid context path: {path}")));
}
if segments.peek().is_none() {
current.insert(segment.to_owned(), value);
return Ok(());
}
let entry = current
.entry(segment.to_owned())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if !entry.is_object() {
*entry = serde_json::Value::Object(serde_json::Map::new());
}
current = entry.as_object_mut().expect("object inserted above");
}
Err(RototoError::new(
"context assignment path must not be empty",
))
}
pub(crate) fn merge_context(
target: &mut serde_json::Value,
source: serde_json::Value,
) -> Result<()> {
let (Some(target), Some(source)) = (target.as_object_mut(), source.as_object()) else {
return Err(RototoError::new("context must be a JSON object"));
};
merge_context_objects(target, source);
Ok(())
}
pub(crate) fn merge_context_objects(
target: &mut serde_json::Map<String, serde_json::Value>,
source: &serde_json::Map<String, serde_json::Value>,
) {
for (key, value) in source {
match (target.get_mut(key), value) {
(Some(existing), serde_json::Value::Object(source_object)) if existing.is_object() => {
merge_context_objects(
existing.as_object_mut().expect("checked above"),
source_object,
);
}
_ => {
target.insert(key.clone(), value.clone());
}
}
}
}