diff options
author | Anna (navi) Figueiredo Gomes <navi@vlhl.dev> | 2023-10-14 10:28:30 +0100 |
---|---|---|
committer | Anna (navi) Figueiredo Gomes <navi@vlhl.dev> | 2023-10-14 10:28:30 +0100 |
commit | fa65142861d256e1c12c225e22ac2e13914b7fbb (patch) | |
tree | 1ca29c523c039cb13c46850bcac17777c0ab0766 /src/context.rs |
Diffstat (limited to 'src/context.rs')
-rw-r--r-- | src/context.rs | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..18b556c --- /dev/null +++ b/src/context.rs @@ -0,0 +1,593 @@ +#![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<Self, Self::Err> { + 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<String>, + default_language: Option<String>, + base_direction: Option<Direction>, + previous_context: Option<Box<Context>>, + terms: HashMap<String, Term> +} + +#[derive(Clone, Default, Debug, PartialEq)] +pub struct Term { + iri_mapping: String, + prefix: bool, + protected: bool, + reverse: bool, + base_iri: Option<String>, + context: Option<Context>, + container: Option<Vec<String>>, + direction: Option<Direction>, + index: Option<String>, + language: Option<String>, + nest: Option<String>, + term_type: Option<String> +} + +#[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<bool>, + propagate: Option<bool>, + validate_scoped: Option<bool> +} + +impl ContextOpts { + fn new(override_protected: Option<bool>, propagate: Option<bool>, validate_scoped: Option<bool>) -> Self { + Self { override_protected, propagate, validate_scoped } + } +} + +#[derive(Default)] +struct TermOpts { + override_protected: Option<bool>, + protected: Option<bool>, + validate_scoped: Option<bool> +} + +impl TermOpts { + fn new(override_protected: Option<bool>, protected: Option<bool>, validate_scoped: Option<bool>) -> Self { + Self { override_protected, protected, validate_scoped } + } +} + +pub impl From<JsonValue> for Context { + +} + +impl Context { + fn parse_context(active_context: &mut Context, local_context: &JsonValue, base_url: &String, + remote_contexts: Option<Vec<Context>>, opts: ContextOpts) -> Result<Context, JsonLdError> { + 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<String, bool> = 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<String, bool>, base_url: &String, remote_contexts: &Vec<Context>, + 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<String, bool>, + document_relative: Option<bool>, vocab: Option<bool>, local_context: Option<&Object>) -> Result<String, JsonLdError> { + 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(()) + } +} |