#![allow(dead_code, unused_variables, clippy::upper_case_acronyms)] use std::{collections::HashMap, str::FromStr}; use json::{JsonValue, object::Object}; //use curl::Curl; #[derive(Clone, Debug, PartialEq)] pub enum Direction { LTR, RTL, NULL } impl FromStr for Direction { type Err = JsonLdError; fn from_str(s: &str) -> Result { if s == "ltr" { Ok(Direction::LTR) } else if s == "rtl" { Ok(Direction::RTL) } else if s == "null" { Ok(Direction::NULL) } else { Err(JsonLdError::InvalidBaseDirection) } } } #[derive(Clone, Default, Debug, PartialEq)] pub struct Context { remote_iri: String, base_iri: String, original_iri: String, // inverse context vocab: Option, default_language: Option, base_direction: Option, previous_context: Option>, terms: HashMap } #[derive(Clone, Default, Debug, PartialEq)] pub struct Term { iri_mapping: String, prefix: bool, protected: bool, reverse: bool, base_iri: Option, context: Option, container: Option>, direction: Option, index: Option, language: Option, nest: Option, term_type: Option } #[derive(Debug)] pub enum JsonLdError { CollidingKeywords, ConflictingIndexes, ContextOverflow, CyclicIRIMapping, InvalidIdValue, InvalidImportValue, InvalidIncludedValue, InvalidIndexValue, InvalidNestValue, InvalidPrefixValue, InvalidPropagateValue, InvalidProtectedValue, InvalidReverseValue, InvalidVersionValue, InvalidBaseDirection, InvalidBaseIRI, InvalidContainerMapping, InvalidContextEntry, InvalidContextNullification, InvalidDefaultLanguage, InvalidIRIMapping, InvalidJSONLiteral, InvalidKeywordAlias, InvalidLanguageMapValue, InvalidLanguageMapping, InvalidLanguageTaggedString, InvalidLanguageTaggedValue, InvalidLocalContext, InvalidRemoteContext, InvalidReversePropertyMap, InvalidReversePropertyValue, InvalidReverseProperty, InvalidScopedContext, InvalidScriptElement, InvalidSetOrListObject, InvalidTermDefinition, InvalidTypeMapping, InvalidTypeValue, InvalidTypedValue, InvalidValueObjectValue, InvalidValueObject, InvalidVocabMapping, IRIConfusedWithPrefix, KeywordRedefinition, LoadingDocumentFailed, LoadingRemoteContextFailed, MultipleContextLinkHeaders, ProcessingModeConflict, ProtectedTermRedefinition } struct ContextOpts { override_protected: Option, propagate: Option, validate_scoped: Option } impl ContextOpts { fn new(override_protected: Option, propagate: Option, validate_scoped: Option) -> Self { Self { override_protected, propagate, validate_scoped } } } #[derive(Default)] struct TermOpts { override_protected: Option, protected: Option, validate_scoped: Option } impl TermOpts { fn new(override_protected: Option, protected: Option, validate_scoped: Option) -> Self { Self { override_protected, protected, validate_scoped } } } pub impl From for Context { } impl Context { fn parse_context(active_context: &mut Context, local_context: &JsonValue, base_url: &String, remote_contexts: Option>, opts: ContextOpts) -> Result { let mut result = active_context.clone(); let remote_contexts = remote_contexts.unwrap_or(Vec::new()); let override_protected = opts.override_protected.unwrap_or(false); let mut propagate = opts.propagate.unwrap_or(true); let validate_scoped = opts.validate_scoped.unwrap_or(true); let local_context = local_context.clone(); let local_context = match local_context { JsonValue::Array(ctx) => ctx, JsonValue::Object(ref ctx) => { // TODO: can we move this later so we don't need to check for Object(_) twice? if let Some(prop) = ctx.get("@propagate") { if let JsonValue::Boolean(prop) = prop { propagate = *prop; } else { return Err(JsonLdError::InvalidPropagateValue); } } vec![local_context] } _ => vec![local_context] }; if !propagate && result.previous_context.is_none() { result.previous_context = Some(Box::new(active_context.clone())); } for context in local_context { let context = match &context { JsonValue::Null => { if !override_protected { for term in active_context.terms.values() { if term.protected { return Err(JsonLdError::InvalidContextNullification); } } } let old_result = result; result = Context::default(); result.base_iri = base_url.clone(); result.original_iri = base_url.clone(); if !propagate { result.previous_context = Some(Box::new(old_result)); } continue; }, JsonValue::String(value) => { todo!() // Remote context stuff }, JsonValue::Object(definition) => definition, _ => { return Err(JsonLdError::InvalidLocalContext); } }; let mut defined: HashMap = HashMap::new(); if let Some(version) = context.get("@version") { if let JsonValue::String(version) = version { if version != "1.1" { return Err(JsonLdError::InvalidVersionValue); } } else { return Err(JsonLdError::InvalidVersionValue); } } if let Some(import) = context.get("@import") { if let JsonValue::String(import) = import { todo!() } else { return Err(JsonLdError::InvalidImportValue); } } if let Some(base) = context.get("@base") { if !remote_contexts.is_empty() { match base { JsonValue::Null => { result.base_iri.clear(); } JsonValue::String(base) => { if base.contains("://") { result.base_iri = base.to_string(); } else if base.contains(':') { todo!(); } else { return Err(JsonLdError::InvalidBaseIRI); } } _ => return Err(JsonLdError::InvalidBaseIRI) } } } if let Some(vocab) = context.get("@vocab") { match vocab { JsonValue::Null => { result.vocab = None; }, JsonValue::String(vocab) => { if vocab.contains("://") || vocab.starts_with("_:") { Context::iri_expansion(active_context, vocab, &mut defined, Some(true), None, Some(context))?; } }, _ => return Err(JsonLdError::InvalidVocabMapping) } } if let Some(language) = context.get("@language") { match language { JsonValue::Null => { result.default_language = None; }, JsonValue::String(language) => { result.default_language = Some(language.to_string().to_lowercase()); }, _ => return Err(JsonLdError::InvalidDefaultLanguage) } } if let Some(direction) = context.get("@direction") { match direction { JsonValue::Null => { result.base_direction = None; } JsonValue::String(direction) => { result.base_direction = Some(Direction::from_str(direction)?); } _ => return Err(JsonLdError::InvalidBaseDirection) } } if let Some(propagate) = context.get("@propagate") { if !propagate.is_boolean() { return Err(JsonLdError::InvalidPropagateValue); } } let mut protected = false; if let Some(JsonValue::Boolean(prot)) = context.get("@protected") { protected = *prot; } for (key, term) in context.iter() { Context::create_term_definition(&mut result, context, (key, term), &mut defined, base_url, &remote_contexts, TermOpts::new(Some(protected), Some(override_protected), Some(validate_scoped)))?; } } Ok(result) } fn create_term_definition(active_context: &mut Context, local_context: &Object, term: (&str, &JsonValue), defined: &mut HashMap, base_url: &String, remote_contexts: &Vec, opts: TermOpts) -> Result<(), JsonLdError> { let protected = opts.protected.unwrap_or(false); let override_protected = opts.override_protected.unwrap_or(false); let validate_scoped = opts.validate_scoped.unwrap_or(true); let (key, term) = term; if key.starts_with('@') { return Err(JsonLdError::KeywordRedefinition); } if let Some(defined) = defined.get(key) { if *defined { return Ok(()); } else { return Err(JsonLdError::CyclicIRIMapping); } } if key.is_empty() { return Err(JsonLdError::InvalidTermDefinition); } defined.insert(key.to_string(), false); let previous_definition = active_context.terms.remove(key); let value = match term { JsonValue::Null => { let mut obj = Object::new(); obj.insert("@id", term.clone()); obj } JsonValue::String(value) => { let mut obj = Object::new(); obj.insert("@id", JsonValue::String(value.clone())); obj } JsonValue::Object(value) => value.clone(), _ => return Err(JsonLdError::InvalidTermDefinition) }; let mut definition = Term { prefix: false, protected, reverse: false, ..Term::default() }; let mut has_type = false; if let Some(protected) = value.get("@protected") { if let JsonValue::Boolean(protected) = protected { definition.protected = *protected; } else { return Err(JsonLdError::InvalidProtectedValue); } } if let Some(term_type) = value.get("@type") { has_type = true; if let JsonValue::String(term_type) = term_type { definition.term_type = Some(Context::iri_expansion(active_context, term_type, defined, None, None, Some(local_context))?); } else { return Err(JsonLdError::InvalidTypeValue); } } if let Some(reverse) = value.get("@reverse") { if let (Some(_), Some(_)) = (value.get("@id"), value.get("@nest")) { return Err(JsonLdError::InvalidReverseProperty); } if let JsonValue::String(reverse) = reverse { definition.iri_mapping = Context::iri_expansion(active_context, reverse, defined, None, None, Some(local_context))?; definition.reverse = true; if let Some(container) = value.get("@container") { if container != "@set" && container != "@index" && container != "null" { return Err(JsonLdError::InvalidReverseProperty); } } active_context.terms.insert(key.to_string(), definition); defined.insert(key.to_string(), true); return Ok(()); } else { return Err(JsonLdError::InvalidIRIMapping); } } if let Some(id) = value.get("@id") { match id { JsonValue::String(id) if id != key => { let iri = Context::iri_expansion(active_context, id, defined, None, None, Some(local_context))?; if !(iri.contains("://") || iri.starts_with("_:") || iri.starts_with('@')) { return Err(JsonLdError::InvalidIRIMapping); } if iri == "@context" { return Err(JsonLdError::InvalidKeywordAlias); } if key.contains(':') && !key.starts_with(':') && key.contains('/') { defined.insert(key.to_string(), true); let term = Context::iri_expansion(active_context, key, defined, None, None, Some(local_context))?; if term != iri { return Err(JsonLdError::InvalidIRIMapping); } } else if iri == "_" || (iri.contains("://") && iri.ends_with([':', '/', '?', '#', '[', ']', '@'])) { definition.prefix = true; } definition.iri_mapping = iri; }, _ => return Err(JsonLdError::InvalidIRIMapping) } } else if let Some(idx) = key.find(':') { let (prefix, suffix) = key.split_at(idx); if !suffix.starts_with('/') { if let Some(prefix_term) = local_context.get(prefix) { Context::create_term_definition(active_context, local_context, (prefix, prefix_term), defined, base_url, remote_contexts, TermOpts::new(Some(protected), Some(override_protected), Some(validate_scoped)))?; } } } else if key.contains('/') { let iri = Context::iri_expansion(active_context, key, defined, Some(true), Some(true), Some(local_context))?; if !iri.contains("://") { return Err(JsonLdError::InvalidIRIMapping); } definition.iri_mapping = iri; } else if key == "@type" { definition.iri_mapping = "@type".to_string(); } else if let Some(vocab) = &active_context.vocab { definition.iri_mapping = format!("{}{}", vocab, key); } else { return Err(JsonLdError::InvalidIRIMapping); } if let Some(container) = value.get("@container") { todo!() } if let Some(index) = value.get("@index") { if let Some(container) = &definition.container { if let (JsonValue::String(index), true) = (index, container.contains(&"@index".to_string())) { let iri = Context::iri_expansion(active_context, index, defined, None, None, Some(local_context))?; if !iri.contains("://") { return Err(JsonLdError::InvalidTermDefinition); } } else { return Err(JsonLdError::InvalidTermDefinition); } } } if let Some(context) = value.get("@context") { todo!()//Context::parse_context(active_context, context, base_url, Some(remote_contexts.clone()), Some(true), None, None)?; } if let (Some(lang), true) = (value.get("@language"), has_type) { definition.language = match lang { JsonValue::Null => None, JsonValue::String(str) => Some(str.to_owned()), _ => return Err(JsonLdError::InvalidLanguageMapping) } } if let (Some(direction), true) = (value.get("@direction"), has_type) { definition.direction = match direction { JsonValue::Null => Some(Direction::NULL), JsonValue::String(str) => Some(Direction::from_str(str)?), _ => return Err(JsonLdError::InvalidLanguageMapping) } } if let Some(nest) = value.get("@nest") { if let JsonValue::String(nest) = nest { definition.nest = if nest == "@nest" { Some("@nest".to_owned()) } else { return Err(JsonLdError::InvalidNestValue) }; } else { return Err(JsonLdError::InvalidNestValue); } } if let Some(prefix) = value.get("@prefix") { if key.contains(':') || key.contains('/') { return Err(JsonLdError::InvalidTermDefinition); } if let JsonValue::Boolean(prefix) = prefix { if *prefix && definition.iri_mapping.starts_with('@') { return Err(JsonLdError::InvalidTermDefinition); } definition.prefix = *prefix; } } if let (Some(prev), false) = (previous_definition, override_protected) { if prev.protected { if definition != prev { return Err(JsonLdError::ProtectedTermRedefinition); } definition = prev; } } active_context.terms.insert(key.to_string(), definition); defined.insert(key.to_string(), true) ; Ok(()) } fn iri_expansion(active_context: &mut Context, value: &str, defined: &mut HashMap, document_relative: Option, vocab: Option, local_context: Option<&Object>) -> Result { if value.starts_with('@') { return Ok(value.to_string()); } let document_relative = document_relative.unwrap_or(false); let vocab = vocab.unwrap_or(false); if let Some(ctx) = local_context { if let Some(JsonValue::String(value)) = ctx.get(value) { if let Some(is_defined) = defined.get(value) { if !*is_defined { let term = ctx.get(value).unwrap(); Context::create_term_definition(active_context, ctx, (value, term), defined, &String::new(), &Vec::new(), TermOpts::default())?; } } } } if let Some(term) = active_context.terms.get(value) { if term.iri_mapping.starts_with('@') || vocab { return Ok(term.iri_mapping.clone()); } } let split_idx = value.find(':').unwrap_or(0); if split_idx > 0 { let (prefix, suffix) = value.split_at(split_idx); let suffix = &suffix[1..]; if prefix == "_" || suffix.starts_with("//") { return Ok(value.to_string()); } if let Some(ctx) = local_context { if let Some(JsonValue::String(value)) = ctx.get(prefix) { if let Some(is_defined) = defined.get(value) { if !is_defined { let term = ctx.get(value).unwrap(); Context::create_term_definition(active_context, ctx, (value, term), defined, &String::new(), &Vec::new(), TermOpts::default())?; } } } } if let Some(term) = active_context.terms.get(prefix) { if term.prefix { // TODO: concatenate this better return Ok(format!("{}{}", term.iri_mapping, suffix)); } } } if let Some(vocab_map) = &active_context.vocab { if vocab { return Ok(format!("{}{}", vocab_map, value)); } } if document_relative { todo!() } Ok(value.to_string()) } } #[cfg(test)] mod tests { use std::{fs::File, path::Path, io::{Read, self}}; use json::JsonValue; use super::{Context, ContextOpts}; #[test] fn parse_context() -> Result<(), io::Error> { let mut json_context = String::new(); File::open(Path::new("./person.jsonld"))?.read_to_string(&mut json_context)?; let context = match json::parse(json_context.as_str()).expect("Failed to parse example context") { JsonValue::Object(mut context) => context.remove("@context").unwrap(), _ => panic!() }; let context = Context::parse_context(&mut Context::default(), &context, &"https://vlhl.dev".to_owned(), Some(Vec::new()), ContextOpts::new(Some(false), Some(true), Some(false))); println!("context: {:#?}", context); Ok(()) } }