#![allow(dead_code)]
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct JsonLdNode {
pub id: String,
pub types: Vec<String>,
pub properties: BTreeMap<String, String>,
}
impl JsonLdNode {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
types: Vec::new(),
properties: BTreeMap::new(),
}
}
pub fn add_type(&mut self, t: impl Into<String>) {
self.types.push(t.into());
}
pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.properties.insert(key.into(), value.into());
}
pub fn get_property(&self, key: &str) -> Option<&str> {
self.properties.get(key).map(String::as_str)
}
}
#[derive(Debug, Clone)]
pub struct JsonLdDocument {
pub context: BTreeMap<String, String>,
pub nodes: Vec<JsonLdNode>,
}
impl Default for JsonLdDocument {
fn default() -> Self {
let mut ctx = BTreeMap::new();
ctx.insert("schema".into(), "https://schema.org/".into());
Self {
context: ctx,
nodes: Vec::new(),
}
}
}
impl JsonLdDocument {
pub fn add_node(&mut self, node: JsonLdNode) {
self.nodes.push(node);
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn find_node(&self, id: &str) -> Option<&JsonLdNode> {
self.nodes.iter().find(|n| n.id == id)
}
}
pub fn node_to_json(node: &JsonLdNode) -> String {
let mut out = format!(r#"{{"@id":"{}""#, node.id);
if !node.types.is_empty() {
let types: Vec<String> = node.types.iter().map(|t| format!("\"{t}\"")).collect();
out.push_str(&format!(r#","@type":[{}]"#, types.join(",")));
}
for (k, v) in &node.properties {
out.push_str(&format!(r#","{k}":"{v}""#));
}
out.push('}');
out
}
pub fn render_json_ld(doc: &JsonLdDocument) -> String {
let ctx_pairs: Vec<String> = doc
.context
.iter()
.map(|(k, v)| format!(r#""{k}":"{v}""#))
.collect();
let nodes: Vec<String> = doc.nodes.iter().map(node_to_json).collect();
format!(
r#"{{"@context":{{{}}},"@graph":[{}]}}"#,
ctx_pairs.join(","),
nodes.join(",")
)
}
pub fn validate_document(doc: &JsonLdDocument) -> bool {
doc.nodes.iter().all(|n| !n.id.is_empty())
}
pub fn add_schema_context(doc: &mut JsonLdDocument) {
doc.context
.insert("schema".into(), "https://schema.org/".into());
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_node() -> JsonLdNode {
let mut n = JsonLdNode::new("https://example.com/person/1");
n.add_type("schema:Person");
n.set_property("schema:name", "Alice");
n.set_property("schema:email", "alice@example.com");
n
}
#[test]
fn node_count() {
let mut doc = JsonLdDocument::default();
doc.add_node(sample_node());
assert_eq!(doc.node_count(), 1);
}
#[test]
fn find_node_found() {
let mut doc = JsonLdDocument::default();
doc.add_node(sample_node());
assert!(doc.find_node("https://example.com/person/1").is_some());
}
#[test]
fn find_node_missing() {
assert!(JsonLdDocument::default().find_node("nope").is_none());
}
#[test]
fn get_property() {
assert_eq!(sample_node().get_property("schema:name"), Some("Alice"));
}
#[test]
fn node_to_json_contains_id() {
let s = node_to_json(&sample_node());
assert!(s.contains("@id"));
}
#[test]
fn node_to_json_contains_type() {
let s = node_to_json(&sample_node());
assert!(s.contains("@type"));
}
#[test]
fn render_json_ld_context() {
let doc = JsonLdDocument::default();
assert!(render_json_ld(&doc).contains("@context"));
}
#[test]
fn render_json_ld_graph() {
let doc = JsonLdDocument::default();
assert!(render_json_ld(&doc).contains("@graph"));
}
#[test]
fn validate_ok() {
let mut doc = JsonLdDocument::default();
doc.add_node(sample_node());
assert!(validate_document(&doc));
}
#[test]
fn validate_empty_id() {
let mut doc = JsonLdDocument::default();
doc.add_node(JsonLdNode::new(""));
assert!(!validate_document(&doc));
}
}