From ac3a70f71860f414ee8b3d3549b1f656c4c605ca Mon Sep 17 00:00:00 2001 From: Jacobus Geluk Date: Wed, 4 Dec 2024 01:24:06 +0000 Subject: [PATCH] feat: `cargo run` works with free main components (load, analyze, publish) in place (nothing producing just yet) --- Cargo.toml | 13 +++++ grapharch.code-workspace | 3 +- src/lib.rs | 10 ++++ src/main.rs | 47 +++++++++++++++++- src/model/doc_model.rs | 21 ++++++++ src/model/mod.rs | 1 + src/output/mod.rs | 1 + src/output/typst.rs | 22 +++++++++ src/source/mod.rs | 1 + src/source/owl.rs | 99 +++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 4 ++ src/util/rdf_load.rs | 102 +++++++++++++++++++++++++++++++++++++++ src/util/tracing.rs | 15 ++++++ 13 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/model/doc_model.rs create mode 100644 src/model/mod.rs create mode 100644 src/output/mod.rs create mode 100644 src/output/typst.rs create mode 100644 src/source/mod.rs create mode 100644 src/source/owl.rs create mode 100644 src/util/mod.rs create mode 100644 src/util/rdf_load.rs create mode 100644 src/util/tracing.rs diff --git a/Cargo.toml b/Cargo.toml index 96b5379..a818c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,16 @@ edition = "2024" rust-version = "1.85" [dependencies] +oxigraph = "0.4.4" +oxrdf = "0.2.3" +oxrdfio = { version = "0.1.3", features = ["async-tokio"] } +oxttl = { version = "0.1.3", features = ["async-tokio"] } +anyhow = "1.0.41" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +console = "0.15" +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["json", "stream"] } +url = "2.4.0" +tokio-util = "0.7.12" +futures = "0.3" diff --git a/grapharch.code-workspace b/grapharch.code-workspace index 9af23e0..ada2924 100644 --- a/grapharch.code-workspace +++ b/grapharch.code-workspace @@ -13,6 +13,7 @@ "git.pullBeforeCheckout": true, "evenBetterToml.taplo.bundled": true, "editor.formatOnSave": true, - "github.gitProtocol": "ssh" + "github.gitProtocol": "ssh", + "testExplorer.showOnRun": true } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a9ad874 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +mod model; +mod output; +mod source; +mod util; +pub use { + model::doc_model::DocumentationModel, + output::typst::TypstGenerator, + source::owl::OWLSource, + util::{rdf_load, setup_tracing}, +}; diff --git a/src/main.rs b/src/main.rs index e7a11a9..cf54a08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,46 @@ -fn main() { - println!("Hello, world!"); +use { + console::style, + grapharch::{ + DocumentationModel, + OWLSource, + TypstGenerator, + setup_tracing, + }, + tracing::{error, info}, +}; + +async fn run() -> anyhow::Result<()> { + setup_tracing()?; + + // Read the OWL file + let owl_url = "https://ekgf.github.io/dprod/dprod.ttl"; + let mut owl_source = OWLSource::new(owl_url).map_err(|e| { + error!( + "{}: {}. URL: {}", + style("Failed to initialize OWLSource").red(), + e, + owl_url + ); + + anyhow::Error::msg(format!("{}: URL: {}", e, owl_url)) + })?; + + // Process the OWL file + let mut doc_model = DocumentationModel::new()?; + owl_source.analyze(&mut doc_model).await?; + + // Generate output + let typst_gen = TypstGenerator::new("output"); + typst_gen.generate(doc_model.get_store())?; + + info!("Documentation generation completed successfully."); + Ok(()) +} + +#[tokio::main] +async fn main() { + if let Err(e) = run().await { + error!("Application error: {}", e); + std::process::exit(1); + } } diff --git a/src/model/doc_model.rs b/src/model/doc_model.rs new file mode 100644 index 0000000..e47a7cc --- /dev/null +++ b/src/model/doc_model.rs @@ -0,0 +1,21 @@ +use oxigraph::{model::Quad, store::Store}; + +pub struct DocumentationModel { + store: Store, +} + +impl DocumentationModel { + pub fn new() -> anyhow::Result { + Ok(Self { store: Store::new()? }) + } + + pub async fn add_documentable_item( + &mut self, + quad: Quad, + ) -> anyhow::Result<()> { + self.store.insert(&quad)?; + Ok(()) + } + + pub fn get_store(&self) -> &Store { &self.store } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..7c324c7 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1 @@ +pub mod doc_model; diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000..2fc2405 --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1 @@ +pub mod typst; diff --git a/src/output/typst.rs b/src/output/typst.rs new file mode 100644 index 0000000..ac87a95 --- /dev/null +++ b/src/output/typst.rs @@ -0,0 +1,22 @@ +use {oxigraph::store::Store, std::path::Path}; + +pub struct TypstGenerator { + #[allow(dead_code)] + output_dir: String, +} + +impl TypstGenerator { + pub fn new>(output_dir: P) -> Self { + Self { + output_dir: output_dir + .as_ref() + .to_string_lossy() + .into_owned(), + } + } + + pub fn generate(&self, _store: &Store) -> anyhow::Result<()> { + // TODO: Query the store and generate Typst documentation + Ok(()) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs new file mode 100644 index 0000000..3e08068 --- /dev/null +++ b/src/source/mod.rs @@ -0,0 +1 @@ +pub mod owl; diff --git a/src/source/owl.rs b/src/source/owl.rs new file mode 100644 index 0000000..2089ade --- /dev/null +++ b/src/source/owl.rs @@ -0,0 +1,99 @@ +use { + crate::{model::doc_model::DocumentationModel, rdf_load}, + console::style, + oxigraph::{model::*, store::Store}, + std::path::Path, + tracing::{error, info}, +}; + +pub struct OWLSource { + /// A temporary store that just holds the data from the source + store: Store, + file_path: String, + graph: GraphName, + base_iri: String, +} + +impl OWLSource { + pub fn new>(path: P) -> anyhow::Result { + info!( + "{}", + style("Starting OWL source...").green().bold() + ); + Ok(Self { + store: Store::new()?, + file_path: path.as_ref().to_string_lossy().into_owned(), + graph: NamedNodeRef::new("http://example.com/g2")? + .into(), + base_iri: "http://example.com".to_string(), + }) + } + + /// Load the data from the source into the store. + /// Note that this is not the same store as the store held by the + /// documentation model. This store is temporary and is used to + /// hold the data from the source. + async fn load(&mut self) -> anyhow::Result<()> { + let new_store = Store::new()?; + self.store = rdf_load( + std::mem::replace(&mut self.store, new_store), + &self.file_path, + &self.base_iri, + self.graph.as_ref(), + ) + .await?; + + Ok(()) + } + + /// Analyze the ontology and give the documentation items to the + /// given DocumentationModel. + pub async fn analyze( + &mut self, + doc_model: &mut DocumentationModel, + ) -> anyhow::Result<()> { + info!( + "{}", + style("Analyzing ontology...").green().bold() + ); + self.load().await?; + + let mut documentable_items = Vec::new(); + for quad_result in self.store.quads_for_pattern( + None, + None, + None, + Some(self.graph.as_ref()), + ) { + match quad_result { + Ok(quad) => { + if self.is_documentable(&quad) { + documentable_items.push(quad); + } + }, + Err(e) => { + error!( + "{}: {}", + style("Error processing quad").red().bold(), + e + ); + continue; + }, + } + } + // Store in documentation model + for item in documentable_items { + doc_model.add_documentable_item(item).await?; + } + + Ok(()) + } + + fn is_documentable(&self, _quad: &Quad) -> bool { + // TODO: Implement logic to determine if a + // triple represents something we should + // document For example: Classes, + // Properties, Labels, Comments, etc. + true + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..c4a2e8e --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,4 @@ +mod rdf_load; +mod tracing; + +pub use {rdf_load::rdf_load, tracing::setup_tracing}; diff --git a/src/util/rdf_load.rs b/src/util/rdf_load.rs new file mode 100644 index 0000000..d61a923 --- /dev/null +++ b/src/util/rdf_load.rs @@ -0,0 +1,102 @@ +use { + console::style, + futures::TryStreamExt, + oxigraph::store::Store, + oxrdf::{GraphName, GraphNameRef}, + oxrdfio::{RdfFormat, RdfParser}, + reqwest::Client, + std::path::Path, + tokio::{fs::File as AsyncFile, io::AsyncReadExt, task}, + tracing::info, + url::Url, +}; + +pub async fn rdf_load<'a>( + mut store: Store, + file_path: &String, + base_iri: &String, + graph: GraphNameRef<'a>, +) -> anyhow::Result { + info!( + "{}", + style("Loading ontology from source...").green().bold() + ); + + if let Ok(url) = Url::parse(&file_path) { + // Handle URL + let client = Client::new(); + let response = client.get(url).send().await.map_err(|e| { + anyhow::Error::new(e).context("Failed to send request") + })?; + if !response.status().is_success() { + return Err(anyhow::Error::msg(format!( + "Failed to fetch URL: {}", + response.status() + ))); + } + let mut stream = response.bytes_stream().map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e) + }); + let mut buffer = Vec::new(); + while let Some(chunk) = stream.try_next().await? { + buffer.extend_from_slice(&chunk); + } + + let base_iri = base_iri.clone(); + let graph = GraphName::from(graph); + store = + task::spawn_blocking(move || -> anyhow::Result { + let reader = std::io::Cursor::new(buffer); + store + .load_from_reader( + RdfParser::from_format(RdfFormat::Turtle) + .with_base_iri(base_iri) + .map_err(anyhow::Error::msg)? + .without_named_graphs() + .with_default_graph(graph), + reader, + ) + .map_err(anyhow::Error::msg)?; + Ok(store) + }) + .await??; + } else { + // Handle file path + let path = Path::new(&file_path); + if !path.exists() { + return Err(anyhow::Error::msg(format!( + "File not found: {}", + file_path + ))); + } + let mut file = AsyncFile::open(path).await?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).await?; + + let base_iri = base_iri.clone(); + let graph = GraphName::from(graph); + store = + task::spawn_blocking(move || -> anyhow::Result { + let reader = std::io::Cursor::new(buffer); + store + .load_from_reader( + RdfParser::from_format(RdfFormat::Turtle) + .with_base_iri(base_iri) + .map_err(anyhow::Error::msg)? + .without_named_graphs() + .with_default_graph(graph), + reader, + ) + .map_err(anyhow::Error::msg)?; + Ok(store) + }) + .await??; + } + + info!( + "{}", + style("Ontology loaded successfully.").green().bold() + ); + + Ok(store) +} diff --git a/src/util/tracing.rs b/src/util/tracing.rs new file mode 100644 index 0000000..706ac7b --- /dev/null +++ b/src/util/tracing.rs @@ -0,0 +1,15 @@ +use tracing_subscriber::{EnvFilter, fmt}; + +/// Initialize tracing with custom format +pub fn setup_tracing() -> anyhow::Result<()> { + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); // Default to "info" if RUST_LOG is not set + + fmt() + .with_env_filter(env_filter) + .without_time() + .with_target(false) + .init(); + + Ok(()) +}