use crate::{
jsonld::schema::{
JsonLdContext, JsonLdHeader, OneOrMany, SimpleContext, TermDef, TermDetail, TypeOrVec,
},
object::Object,
prelude::DataModel,
tree,
};
use petgraph::Direction;
use std::{
collections::{HashMap, HashSet},
error::Error,
};
pub fn to_json_ld(model: &DataModel, root: Option<&str>) -> Result<JsonLdHeader, Box<dyn Error>> {
let context = create_context(model, root)?;
let config = model.config.clone().unwrap_or_default();
let object = match root {
Some(name) => model
.objects
.iter()
.find(|o| o.name == name)
.ok_or_else(|| format!("Object {name} not found"))?,
None => model.objects.first().ok_or("No objects found in model")?,
};
let object_type = match object.term.clone() {
Some(term) => TypeOrVec::Single(term),
None => TypeOrVec::Single(format!("{}:{}", config.prefix, object.name)),
};
Ok(JsonLdHeader {
context: Some(JsonLdContext::Object(context)),
type_: Some(object_type),
..Default::default()
})
}
fn create_context(model: &DataModel, root: Option<&str>) -> Result<SimpleContext, String> {
let mut context = SimpleContext::default();
let config = model.config.clone().unwrap_or_default();
let model_id = if !config.prefix.is_empty() {
config.prefix
} else if config.id.is_some() {
config.id.unwrap()
} else {
"model".to_string()
};
context
.terms
.insert(model_id.clone(), TermDef::Simple(config.repo.clone()));
if let Some(prefixes) = &config.prefixes {
for (prefix, uri) in prefixes {
context
.terms
.insert(prefix.clone(), TermDef::Simple(uri.clone()));
}
}
let mut context_cache: HashMap<String, SimpleContext> = HashMap::new();
let root_name = match root {
Some(name) => name.to_string(),
None => model
.objects
.first()
.ok_or("No objects found in model")?
.name
.clone(),
};
let graph = tree::object_graph(model, &root_name)?;
if let Some(root_idx) = graph
.node_indices()
.find(|&idx| graph[idx].name == root_name)
{
let obj_context = build_object_context(&graph, root_idx, &model_id, &mut context_cache);
for (term_name, term_def) in obj_context.terms {
context.terms.insert(term_name, term_def);
}
}
Ok(context)
}
fn build_object_context(
graph: &petgraph::graph::DiGraph<Object, ()>,
node_idx: petgraph::graph::NodeIndex,
model_id: &str,
cache: &mut HashMap<String, SimpleContext>,
) -> SimpleContext {
let object = &graph[node_idx];
if let Some(cached) = cache.get(&object.name) {
return cached.clone();
}
let mut context = SimpleContext::default();
let object_names: HashSet<String> = graph
.node_indices()
.map(|idx| graph[idx].name.clone())
.collect();
for attr in &object.attributes {
let has_nested = attr.dtypes.iter().any(|dt| object_names.contains(dt));
let term_def = if has_nested || attr.is_array {
build_detailed_term_def(graph, node_idx, attr, model_id, has_nested, cache)
} else {
let term_id = get_attr_term_id(attr, object, model_id);
TermDef::Simple(term_id)
};
context.terms.insert(attr.name.clone(), term_def);
}
cache.insert(object.name.clone(), context.clone());
context
}
fn build_detailed_term_def(
graph: &petgraph::graph::DiGraph<Object, ()>,
node_idx: petgraph::graph::NodeIndex,
attr: &crate::attribute::Attribute,
model_id: &str,
has_nested: bool,
cache: &mut HashMap<String, SimpleContext>,
) -> TermDef {
let object_type = attr
.dtypes
.first()
.and_then(|dtype| find_sub_object(graph, dtype))
.map(|_idx| "@id".to_string());
let mut detail = TermDetail {
type_: object_type,
container: if attr.is_array {
Some(OneOrMany::One("@set".to_string()))
} else {
None
},
..Default::default()
};
if has_nested {
let nested = build_nested_for_attr(graph, node_idx, attr, model_id, cache);
if !nested.terms.is_empty() {
detail.context = Some(Box::new(JsonLdContext::Object(nested)));
}
}
TermDef::Detailed(detail)
}
fn find_sub_object(
graph: &petgraph::graph::DiGraph<Object, ()>,
dtype: &str,
) -> Option<petgraph::graph::NodeIndex> {
graph.node_indices().find(|&idx| graph[idx].name == dtype)
}
fn get_attr_term_id(attr: &crate::attribute::Attribute, object: &Object, model_id: &str) -> String {
match &attr.term {
Some(term) => term.clone(),
None => format!("{}:{}/{}", model_id, object.name, attr.name),
}
}
fn get_object_term_or_default(term: &Option<String>, model_id: &str, object_name: &str) -> String {
match term {
Some(term) => term.clone(),
None => format!("{}:{}", model_id, object_name),
}
}
fn build_nested_for_attr(
graph: &petgraph::graph::DiGraph<Object, ()>,
parent_idx: petgraph::graph::NodeIndex,
attr: &crate::attribute::Attribute,
model_id: &str,
cache: &mut HashMap<String, SimpleContext>,
) -> SimpleContext {
let mut context = SimpleContext::default();
for neighbor_idx in graph.neighbors_directed(parent_idx, Direction::Outgoing) {
let neighbor_obj = &graph[neighbor_idx];
if attr.dtypes.contains(&neighbor_obj.name) {
let nested_context = build_object_context(graph, neighbor_idx, model_id, cache);
let term = get_object_term_or_default(&neighbor_obj.term, model_id, &neighbor_obj.name);
context.type_ = Some(TypeOrVec::Single(term));
for (key, value) in nested_context.terms {
context.terms.insert(key, value);
}
}
}
context
}