use std::fs::OpenOptions;
use crate::ShEx2HtmlError;
use minijinja::Template;
use minijinja::{path_loader, Environment};
use prefixmap::{IriRef, PrefixMap};
use shex_ast::{Schema, Shape, ShapeExpr, ShapeExprLabel, TripleExpr};
use tracing::debug;
use super::{
Cardinality, Entry, HtmlSchema, HtmlShape, Name, NodeId, ShEx2HtmlConfig, ValueConstraint,
};
pub struct ShEx2Html {
config: ShEx2HtmlConfig,
current_html: HtmlSchema,
}
impl ShEx2Html {
pub fn new(config: ShEx2HtmlConfig) -> ShEx2Html {
ShEx2Html {
config,
current_html: HtmlSchema::new(),
}
}
pub fn current_html(&self) -> &HtmlSchema {
&self.current_html
}
pub fn export_schema(&self) -> Result<(), ShEx2HtmlError> {
let environment = create_env();
let landing_page = self.config.landing_page();
let template = environment.get_template(self.config.landing_page_name.as_str())?;
let landing_page_name = landing_page.to_string_lossy().to_string();
let out = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(landing_page)
.map_err(|e| ShEx2HtmlError::ErrorCreatingLandingPage {
name: landing_page_name,
error: e,
})?;
let _state = template
.render_to_write(self.current_html.to_landing_html_schema(&self.config), out)?;
let shape_template = environment.get_template("shape.html")?;
for shape in self.current_html.shapes() {
generate_shape_page(shape, &shape_template, &self.config)?;
}
Ok(())
}
pub fn convert(&mut self, shex: &Schema) -> Result<(), ShEx2HtmlError> {
let prefixmap = shex
.prefixmap()
.unwrap_or_default()
.without_rich_qualifying();
if let Some(shapes) = shex.shapes() {
for shape_decl in shapes {
let name = self.shape_label2name(&shape_decl.id, &prefixmap)?;
let node_id = self.current_html.add_label(&name);
let component =
self.shape_expr2htmlshape(&name, &shape_decl.shape_expr, &prefixmap, &node_id)?;
self.current_html.add_component(node_id, component)?;
}
}
Ok(())
}
fn shape_label2name(
&mut self,
label: &ShapeExprLabel,
prefixmap: &PrefixMap,
) -> Result<Name, ShEx2HtmlError> {
match label {
ShapeExprLabel::IriRef { value } => iri_ref2name(value, &self.config, prefixmap),
ShapeExprLabel::BNode { value: _ } => todo!(),
ShapeExprLabel::Start => todo!(),
}
}
fn shape_expr2htmlshape(
&mut self,
name: &Name,
shape_expr: &ShapeExpr,
prefixmap: &PrefixMap,
current_node_id: &NodeId,
) -> Result<HtmlShape, ShEx2HtmlError> {
match shape_expr {
ShapeExpr::Shape(shape) => {
self.shape2htmlshape(name, shape, prefixmap, current_node_id)
}
_ => Err(ShEx2HtmlError::NotImplemented {
msg: "Complex shape expressions are not implemented yet".to_string(),
}),
}
}
fn shape2htmlshape(
&mut self,
name: &Name,
shape: &Shape,
prefixmap: &PrefixMap,
current_node_id: &NodeId,
) -> Result<HtmlShape, ShEx2HtmlError> {
let mut html_shape = HtmlShape::new(name.clone());
if let Some(te) = &shape.expression {
match &te.te {
TripleExpr::EachOf {
id: _,
expressions,
min: _,
max: _,
sem_acts: _,
annotations: _,
} => {
for e in expressions {
match &e.te {
TripleExpr::TripleConstraint {
id: _,
negated: _,
inverse: _,
predicate,
value_expr,
min,
max,
sem_acts: _,
annotations: _,
} => {
let pred_name = iri_ref2name(predicate, &self.config, prefixmap)?;
let card = mk_card(min, max)?;
let value_constraint = if let Some(se) = value_expr {
self.value_expr2value_constraint(
se,
prefixmap,
current_node_id,
&pred_name,
&card,
)?
} else {
ValueConstraint::default()
};
match value_constraint {
ValueConstraint::None => {}
_ => {
let entry = Entry::new(pred_name, value_constraint, card);
html_shape.add_entry(entry)
}
}
}
_ => todo!(),
}
}
}
TripleExpr::OneOf {
id: _,
expressions: _,
min: _,
max: _,
sem_acts: _,
annotations: _,
} => todo!(),
TripleExpr::TripleConstraint {
id: _,
negated: _,
inverse: _,
predicate,
value_expr,
min,
max,
sem_acts: _,
annotations: _,
} => {
let pred_name = iri_ref2name(predicate, &self.config, prefixmap)?;
let card = mk_card(min, max)?;
let value_constraint = if let Some(se) = value_expr {
self.value_expr2value_constraint(
se,
prefixmap,
current_node_id,
&pred_name,
&card,
)?
} else {
ValueConstraint::default()
};
match value_constraint {
ValueConstraint::None => {}
_ => {
let entry = Entry::new(pred_name, value_constraint, card);
html_shape.add_entry(entry)
}
}
}
TripleExpr::TripleExprRef(_) => todo!(),
}
Ok(html_shape)
} else {
Ok(html_shape)
}
}
fn value_expr2value_constraint(
&mut self,
value_expr: &ShapeExpr,
prefixmap: &PrefixMap,
_current_node_id: &NodeId,
_current_predicate: &Name,
_current_card: &Cardinality,
) -> Result<ValueConstraint, ShEx2HtmlError> {
match value_expr {
ShapeExpr::ShapeOr { shape_exprs: _ } => todo!(),
ShapeExpr::ShapeAnd { shape_exprs: _ } => todo!(),
ShapeExpr::ShapeNot { shape_expr: _ } => todo!(),
ShapeExpr::NodeConstraint(nc) => {
if let Some(datatype) = nc.datatype() {
let name = iri_ref2name(&datatype, &self.config, prefixmap)?;
Ok(ValueConstraint::datatype(name))
} else {
todo!()
}
}
ShapeExpr::Shape(_) => todo!(),
ShapeExpr::External => todo!(),
ShapeExpr::Ref(r) => match &r {
ShapeExprLabel::IriRef { value } => {
let _ref_name = iri_ref2name(value, &self.config, prefixmap)?;
Ok(ValueConstraint::None)
}
ShapeExprLabel::BNode { value: _ } => todo!(),
ShapeExprLabel::Start => todo!(),
},
}
}
}
fn iri_ref2name(
iri_ref: &IriRef,
config: &ShEx2HtmlConfig,
prefixmap: &PrefixMap,
) -> Result<Name, ShEx2HtmlError> {
match iri_ref {
IriRef::Iri(iri) => Ok(Name::new(
prefixmap.qualify(iri).as_str(),
Some(iri.as_str()),
config.target_folder().as_path(),
)),
IriRef::Prefixed { prefix: _, local } => {
Ok(Name::new(local, None, config.target_folder().as_path()))
}
}
}
pub fn create_env() -> Environment<'static> {
let mut env = Environment::new();
env.set_loader(path_loader("shapes_converter/default_templates"));
env
}
fn mk_card(min: &Option<i32>, max: &Option<i32>) -> Result<Cardinality, ShEx2HtmlError> {
let min = if let Some(n) = min { *n } else { 1 };
let max = if let Some(n) = max { *n } else { 1 };
match (min, max) {
(1, 1) => Ok(Cardinality::OneOne),
(0, -1) => Ok(Cardinality::Star),
(0, 1) => Ok(Cardinality::Optional),
(1, -1) => Ok(Cardinality::Plus),
(m, n) if m >= 0 && n > m => Ok(Cardinality::Fixed(m)),
(m, n) if m >= 0 && n > m => Ok(Cardinality::Range(m, n)),
_ => Err(ShEx2HtmlError::WrongCardinality { min, max }),
}
}
fn generate_shape_page(
shape: &HtmlShape,
template: &Template,
_config: &ShEx2HtmlConfig,
) -> Result<(), ShEx2HtmlError> {
let name = shape.name();
debug!("Generating shape with name: {name:?}");
if let Some((path, _local_name)) = name.as_local_ref() {
let file_name = path.as_path().display().to_string();
let out_shape = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.map_err(|e| ShEx2HtmlError::ErrorCreatingShapesFile {
name: file_name,
error: e,
})?;
let state = template.render_to_write(shape, out_shape)?;
debug!("Generated state: {state:?}");
Ok(())
} else {
Err(ShEx2HtmlError::NoLocalRefName { name: name.clone() })
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_minininja() {
use minijinja::{context, Environment};
let mut env = Environment::new();
env.add_template("hello", "Hello {{ name }}!").unwrap();
let tmpl = env.get_template("hello").unwrap();
assert_eq!(
tmpl.render(context!(name => "John")).unwrap(),
"Hello John!".to_string()
)
}
}