summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml10
-rw-r--r--src/context.rs593
-rw-r--r--src/json.rs70
-rw-r--r--src/lib.rs2
5 files changed, 677 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4fffb2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..f583e6f
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "linked"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+curl = "0.4.44"
+json = "0.12.4"
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(())
+ }
+}
diff --git a/src/json.rs b/src/json.rs
new file mode 100644
index 0000000..8151212
--- /dev/null
+++ b/src/json.rs
@@ -0,0 +1,70 @@
+#![allow(dead_code)]
+use std::collections::HashMap;
+
+pub struct Object(HashMap<String, Json>);
+pub struct Array(Vec<Json>);
+
+pub enum Json {
+ Bool(bool),
+ Object(Object),
+ Array(Array),
+ Number(f64),
+ String(String),
+ Null
+}
+
+pub enum JsonError<'a> {
+ InvalidTokenErr(&'a str),
+ ExpectedValueErr(&'a str),
+ ObjectExpectedValueErr(&'a str),
+ ObjectInvalidKeyErr(&'a str),
+ MaxDepthErr(&'a str),
+ InvalidNumberErr(&'a str),
+ InvalidStringErr(&'a str),
+ StringInvalidEscapeErr(&'a str),
+ StringInvalidUnicodeErr(&'a str),
+ StringInvalidErr(&'a str),
+}
+
+fn skip_ws(raw_json: &str) -> &str {
+ let non_ws = raw_json.find(|chr: char| !chr.is_whitespace()).unwrap_or(raw_json.len());
+ &raw_json[non_ws..]
+}
+
+impl Json {
+ pub fn parse(raw_json: &str) -> Result<Json, JsonError> {
+ todo!()
+ }
+
+ fn parse_value(raw: &str) -> Result<(&str, Json), JsonError> {
+ let raw = skip_ws(raw);
+ match raw.chars().next() {
+ Some('"') => Json::parse_string(raw),
+ Some('{') => Json::parse_object(raw),
+ Some('[') => Json::parse_array(raw),
+ Some(_) => Json::parse_literal(raw),
+ None => return Err(JsonError::StringInvalidErr(raw))
+ }
+ }
+
+ fn parse_object(raw: &str) -> Result<(&str, Json), JsonError> {
+ let mut empty = true;
+ let raw = skip_ws(raw);
+
+ 'outer: loop {
+ for chr in raw.chars() {
+ }
+ }
+
+ todo!()
+ }
+ fn parse_array(raw: &str) -> Result<(&str, Json), JsonError> {
+ todo!()
+ }
+ fn parse_string(raw: &str) -> Result<(&str, Json), JsonError> {
+ todo!()
+ }
+ fn parse_literal(raw: &str) -> Result<(&str, Json), JsonError> {
+ todo!()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..0534d42
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,2 @@
+mod context;
+mod json;