JS: Adds termType: "Quad" to quads and adds variable support to RDF/JS implementation

pull/171/head
Tpt 4 years ago
parent f76f0396e8
commit 3ca149fa97
  1. 2
      js/README.md
  2. 4
      js/package.json
  3. 346
      js/src/model.rs
  4. 6
      js/src/store.rs
  5. 17
      js/test/store.js

@ -44,7 +44,7 @@ for (binding of store.query("SELECT ?name WHERE { <http://example/> <http://sche
Oxigraph currently provides a simple JS API. Oxigraph currently provides a simple JS API.
It is centered around the `MemoryStore` class. It is centered around the `MemoryStore` class.
The `NamedNode`, `BlankNode`, `Literal`, `DefaultGraph`, `Quad` and `DataFactory` types The `NamedNode`, `BlankNode`, `Literal`, `DefaultGraph`, `Variable`, `Quad` and `DataFactory` types
are following the [RDF/JS datamodel specification](https://rdf.js.org/data-model-spec/). are following the [RDF/JS datamodel specification](https://rdf.js.org/data-model-spec/).
To import `MemoryStore` using Node: To import `MemoryStore` using Node:

@ -3,8 +3,8 @@
"description": "Oxigraph JS build and tests", "description": "Oxigraph JS build and tests",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"mocha": "^8.0.1", "mocha": "^9.0.0",
"@rdfjs/data-model": "1.1.2", "@rdfjs/data-model": "^1.3.0",
"standard": "^16.0.0" "standard": "^16.0.0"
}, },
"scripts": { "scripts": {

@ -2,7 +2,9 @@ use crate::format_err;
use crate::utils::to_err; use crate::utils::to_err;
use js_sys::{Reflect, UriError}; use js_sys::{Reflect, UriError};
use oxigraph::model::*; use oxigraph::model::*;
use oxigraph::sparql::Variable;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::sync::Arc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_name = DataFactory)] #[wasm_bindgen(js_name = DataFactory)]
@ -57,6 +59,11 @@ impl JsDataFactory {
JsDefaultGraph {} JsDefaultGraph {}
} }
#[wasm_bindgen(js_name = variable)]
pub fn variable(&self, value: String) -> Result<JsVariable, JsValue> {
Ok(Variable::new(value).map_err(to_err)?.into())
}
#[wasm_bindgen(js_name = triple)] #[wasm_bindgen(js_name = triple)]
pub fn triple( pub fn triple(
&self, &self,
@ -64,12 +71,7 @@ impl JsDataFactory {
predicate: &JsValue, predicate: &JsValue,
object: &JsValue, object: &JsValue,
) -> Result<JsQuad, JsValue> { ) -> Result<JsQuad, JsValue> {
Ok(JsQuad { self.quad(subject, predicate, object, &JsValue::UNDEFINED)
subject: self.from_js.to_term(subject)?,
predicate: self.from_js.to_term(predicate)?,
object: self.from_js.to_term(object)?,
graph_name: JsTerm::DefaultGraph(JsDefaultGraph {}),
})
} }
#[wasm_bindgen(js_name = quad)] #[wasm_bindgen(js_name = quad)]
@ -80,26 +82,28 @@ impl JsDataFactory {
object: &JsValue, object: &JsValue,
graph: &JsValue, graph: &JsValue,
) -> Result<JsQuad, JsValue> { ) -> Result<JsQuad, JsValue> {
Ok(JsQuad { Ok(self
subject: self.from_js.to_term(subject)?, .from_js
predicate: self.from_js.to_term(predicate)?, .to_quad_from_parts(subject, predicate, object, graph)?
object: self.from_js.to_term(object)?, .into())
graph_name: if graph.is_undefined() || graph.is_null() {
JsTerm::DefaultGraph(JsDefaultGraph {})
} else {
self.from_js.to_term(graph)?
},
})
} }
#[wasm_bindgen(js_name = fromTerm)] #[wasm_bindgen(js_name = fromTerm)]
pub fn convert_term(&self, original: &JsValue) -> Result<JsValue, JsValue> { pub fn convert_term(&self, original: &JsValue) -> Result<JsValue, JsValue> {
Ok(self.from_js.to_term(original)?.into()) Ok(if original.is_null() {
JsValue::NULL
} else {
self.from_js.to_term(original)?.into()
})
} }
#[wasm_bindgen(js_name = fromQuad)] #[wasm_bindgen(js_name = fromQuad)]
pub fn convert_quad(&self, original: &JsValue) -> Result<JsQuad, JsValue> { pub fn convert_quad(&self, original: &JsValue) -> Result<JsValue, JsValue> {
self.from_js.to_quad(original) Ok(if original.is_null() {
JsValue::NULL
} else {
JsQuad::from(self.from_js.to_quad(original)?).into()
})
} }
} }
@ -316,12 +320,128 @@ impl JsDefaultGraph {
} }
} }
#[wasm_bindgen(js_name = Variable)]
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct JsVariable {
inner: Variable,
}
#[wasm_bindgen(js_class = Variable)]
impl JsVariable {
#[wasm_bindgen(getter = termType)]
pub fn term_type(&self) -> String {
"Variable".to_owned()
}
#[wasm_bindgen(getter)]
pub fn value(&self) -> String {
self.inner.as_str().to_owned()
}
pub fn equals(&self, other: &JsValue) -> bool {
if let Ok(Some(JsTerm::Variable(other))) =
FromJsConverter::default().to_optional_term(&other)
{
self == &other
} else {
false
}
}
}
impl From<Variable> for JsVariable {
fn from(inner: Variable) -> Self {
Self { inner }
}
}
impl From<JsVariable> for Variable {
fn from(node: JsVariable) -> Self {
node.inner
}
}
#[wasm_bindgen(js_name = Quad)]
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct JsQuad {
inner: Quad,
}
#[wasm_bindgen(js_class = Quad)]
impl JsQuad {
#[wasm_bindgen(getter = termType)]
pub fn term_type(&self) -> String {
"Quad".to_owned()
}
#[wasm_bindgen(getter)]
pub fn value(&self) -> String {
"".to_owned()
}
#[wasm_bindgen(getter = subject)]
pub fn subject(&self) -> JsValue {
JsTerm::from(self.inner.subject.clone()).into()
}
#[wasm_bindgen(getter = predicate)]
pub fn predicate(&self) -> JsValue {
JsTerm::from(self.inner.predicate.clone()).into()
}
#[wasm_bindgen(getter = object)]
pub fn object(&self) -> JsValue {
JsTerm::from(self.inner.object.clone()).into()
}
#[wasm_bindgen(getter = graph)]
pub fn graph(&self) -> JsValue {
JsTerm::from(self.inner.graph_name.clone()).into()
}
pub fn equals(&self, other: &JsValue) -> bool {
if let Ok(Some(JsTerm::Quad(other))) = FromJsConverter::default().to_optional_term(&other) {
self == &other
} else {
false
}
}
}
impl From<Triple> for JsQuad {
fn from(inner: Triple) -> Self {
Self {
inner: inner.in_graph(GraphName::DefaultGraph),
}
}
}
impl From<Quad> for JsQuad {
fn from(inner: Quad) -> Self {
Self { inner }
}
}
impl From<JsQuad> for Quad {
fn from(quad: JsQuad) -> Self {
quad.inner
}
}
impl From<JsQuad> for Triple {
fn from(quad: JsQuad) -> Self {
quad.inner.into()
}
}
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum JsTerm { pub enum JsTerm {
NamedNode(JsNamedNode), NamedNode(JsNamedNode),
BlankNode(JsBlankNode), BlankNode(JsBlankNode),
Literal(JsLiteral), Literal(JsLiteral),
DefaultGraph(JsDefaultGraph), DefaultGraph(JsDefaultGraph),
Variable(JsVariable),
Quad(JsQuad),
} }
impl From<JsTerm> for JsValue { impl From<JsTerm> for JsValue {
@ -331,6 +451,8 @@ impl From<JsTerm> for JsValue {
JsTerm::BlankNode(v) => v.into(), JsTerm::BlankNode(v) => v.into(),
JsTerm::Literal(v) => v.into(), JsTerm::Literal(v) => v.into(),
JsTerm::DefaultGraph(v) => v.into(), JsTerm::DefaultGraph(v) => v.into(),
JsTerm::Variable(v) => v.into(),
JsTerm::Quad(v) => v.into(),
} }
} }
} }
@ -367,7 +489,7 @@ impl From<Subject> for JsTerm {
match node { match node {
Subject::NamedNode(node) => node.into(), Subject::NamedNode(node) => node.into(),
Subject::BlankNode(node) => node.into(), Subject::BlankNode(node) => node.into(),
Subject::Triple(_) => unimplemented!(), Subject::Triple(node) => node.into(),
} }
} }
} }
@ -378,7 +500,7 @@ impl From<Term> for JsTerm {
Term::NamedNode(node) => node.into(), Term::NamedNode(node) => node.into(),
Term::BlankNode(node) => node.into(), Term::BlankNode(node) => node.into(),
Term::Literal(literal) => literal.into(), Term::Literal(literal) => literal.into(),
Term::Triple(_) => unimplemented!(), Term::Triple(node) => node.into(),
} }
} }
} }
@ -393,6 +515,30 @@ impl From<GraphName> for JsTerm {
} }
} }
impl From<Variable> for JsTerm {
fn from(variable: Variable) -> Self {
JsTerm::Variable(variable.into())
}
}
impl From<Triple> for JsTerm {
fn from(triple: Triple) -> Self {
JsTerm::Quad(triple.into())
}
}
impl From<Arc<Triple>> for JsTerm {
fn from(triple: Arc<Triple>) -> Self {
triple.as_ref().clone().into()
}
}
impl From<Quad> for JsTerm {
fn from(quad: Quad) -> Self {
JsTerm::Quad(quad.into())
}
}
impl TryFrom<JsTerm> for NamedNode { impl TryFrom<JsTerm> for NamedNode {
type Error = JsValue; type Error = JsValue;
@ -408,6 +554,11 @@ impl TryFrom<JsTerm> for NamedNode {
literal.inner literal.inner
)), )),
JsTerm::DefaultGraph(_) => Err(format_err!("The default graph is not a named node")), JsTerm::DefaultGraph(_) => Err(format_err!("The default graph is not a named node")),
JsTerm::Variable(variable) => Err(format_err!(
"The variable {} is not a named node",
variable.inner
)),
JsTerm::Quad(quad) => Err(format_err!("The quad {} is not a named node", quad.inner)),
} }
} }
} }
@ -423,9 +574,17 @@ impl TryFrom<JsTerm> for NamedOrBlankNode {
"The literal {} is not a possible named or blank node term", "The literal {} is not a possible named or blank node term",
literal.inner literal.inner
)), )),
JsTerm::DefaultGraph(_) => { JsTerm::DefaultGraph(_) => Err(format_err!(
Err(format_err!("The default graph is not a possible RDF term")) "The default graph is not a possible named or blank node term"
} )),
JsTerm::Variable(variable) => Err(format_err!(
"The variable {} is not a possible named or blank node term",
variable.inner
)),
JsTerm::Quad(quad) => Err(format_err!(
"The quad {} is not a possible named or blank node term",
quad.inner
)),
} }
} }
} }
@ -438,12 +597,17 @@ impl TryFrom<JsTerm> for Subject {
JsTerm::NamedNode(node) => Ok(node.into()), JsTerm::NamedNode(node) => Ok(node.into()),
JsTerm::BlankNode(node) => Ok(node.into()), JsTerm::BlankNode(node) => Ok(node.into()),
JsTerm::Literal(literal) => Err(format_err!( JsTerm::Literal(literal) => Err(format_err!(
"The literal {} is not a possible named or blank node term", "The literal {} is not a possible RDF subject",
literal.inner literal.inner
)), )),
JsTerm::DefaultGraph(_) => { JsTerm::DefaultGraph(_) => Err(format_err!(
Err(format_err!("The default graph is not a possible RDF term")) "The default graph is not a possible RDF subject"
} )),
JsTerm::Variable(variable) => Err(format_err!(
"The variable {} is not a possible RDF subject",
variable.inner
)),
JsTerm::Quad(quad) => Ok(Triple::from(quad).into()),
} }
} }
} }
@ -459,6 +623,11 @@ impl TryFrom<JsTerm> for Term {
JsTerm::DefaultGraph(_) => { JsTerm::DefaultGraph(_) => {
Err(format_err!("The default graph is not a possible RDF term")) Err(format_err!("The default graph is not a possible RDF term"))
} }
JsTerm::Variable(variable) => Err(format_err!(
"The variable {} is not a possible RDF term",
variable.inner
)),
JsTerm::Quad(quad) => Ok(Triple::from(quad).into()),
} }
} }
} }
@ -475,72 +644,18 @@ impl TryFrom<JsTerm> for GraphName {
literal.inner literal.inner
)), )),
JsTerm::DefaultGraph(_) => Ok(GraphName::DefaultGraph), JsTerm::DefaultGraph(_) => Ok(GraphName::DefaultGraph),
JsTerm::Variable(variable) => Err(format_err!(
"The variable {} is not a possible RDF term",
variable.inner
)),
JsTerm::Quad(quad) => Err(format_err!(
"The quad {} is not a possible RDF term",
quad.inner
)),
} }
} }
} }
#[wasm_bindgen(js_name = Quad)]
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct JsQuad {
subject: JsTerm,
predicate: JsTerm,
object: JsTerm,
graph_name: JsTerm,
}
#[wasm_bindgen(js_class = Quad)]
impl JsQuad {
#[wasm_bindgen(getter = subject)]
pub fn subject(&self) -> JsValue {
self.subject.clone().into()
}
#[wasm_bindgen(getter = predicate)]
pub fn predicate(&self) -> JsValue {
self.predicate.clone().into()
}
#[wasm_bindgen(getter = object)]
pub fn object(&self) -> JsValue {
self.object.clone().into()
}
#[wasm_bindgen(getter = graph)]
pub fn graph(&self) -> JsValue {
self.graph_name.clone().into()
}
pub fn equals(&self, other: &JsValue) -> bool {
FromJsConverter::default()
.to_quad(other)
.map_or(false, |other| self == &other)
}
}
impl From<Quad> for JsQuad {
fn from(quad: Quad) -> Self {
Self {
subject: quad.subject.into(),
predicate: quad.predicate.into(),
object: quad.object.into(),
graph_name: quad.graph_name.into(),
}
}
}
impl TryFrom<JsQuad> for Quad {
type Error = JsValue;
fn try_from(quad: JsQuad) -> Result<Self, JsValue> {
Ok(Quad {
subject: Subject::try_from(quad.subject)?,
predicate: NamedNode::try_from(quad.predicate)?,
object: Term::try_from(quad.object)?,
graph_name: GraphName::try_from(quad.graph_name)?,
})
}
}
pub struct FromJsConverter { pub struct FromJsConverter {
term_type: JsValue, term_type: JsValue,
value: JsValue, value: JsValue,
@ -608,11 +723,31 @@ impl FromJsConverter {
} }
} }
"DefaultGraph" => Ok(JsTerm::DefaultGraph(JsDefaultGraph {})), "DefaultGraph" => Ok(JsTerm::DefaultGraph(JsDefaultGraph {})),
"Variable" => Ok(Variable::new(
&Reflect::get(&value, &self.value)?
.as_string()
.ok_or_else(|| format_err!("Variable should have a string value"))?,
)
.map_err(to_err)?
.into()),
"Quad" => Ok(self.to_quad(value)?.into()),
_ => Err(format_err!( _ => Err(format_err!(
"The termType {} is not supported by Oxigraph", "The termType {} is not supported by Oxigraph",
term_type term_type
)), )),
} }
} else if term_type.is_undefined() {
// It's a quad without the proper type
if Reflect::has(value, &self.subject)?
&& Reflect::has(value, &self.predicate)?
&& Reflect::has(value, &self.object)?
{
Ok(self.to_quad(value)?.into())
} else {
Err(format_err!(
"RDF term objects should have a termType attribute"
))
}
} else { } else {
Err(format_err!("The object termType field should be a string")) Err(format_err!("The object termType field should be a string"))
} }
@ -626,12 +761,31 @@ impl FromJsConverter {
} }
} }
pub fn to_quad(&self, value: &JsValue) -> Result<JsQuad, JsValue> { pub fn to_quad(&self, value: &JsValue) -> Result<Quad, JsValue> {
Ok(JsQuad { self.to_quad_from_parts(
subject: self.to_term(&Reflect::get(value, &self.subject)?)?, &Reflect::get(&value, &self.subject)?,
predicate: self.to_term(&Reflect::get(value, &self.predicate)?)?, &Reflect::get(&value, &self.predicate)?,
object: self.to_term(&Reflect::get(value, &self.object)?)?, &Reflect::get(&value, &self.object)?,
graph_name: self.to_term(&Reflect::get(value, &self.graph)?)?, &Reflect::get(&value, &self.graph)?,
)
}
pub fn to_quad_from_parts(
&self,
subject: &JsValue,
predicate: &JsValue,
object: &JsValue,
graph_name: &JsValue,
) -> Result<Quad, JsValue> {
Ok(Quad {
subject: Subject::try_from(self.to_term(subject)?)?,
predicate: NamedNode::try_from(self.to_term(predicate)?)?,
object: Term::try_from(self.to_term(object)?)?,
graph_name: if graph_name.is_undefined() {
GraphName::DefaultGraph
} else {
GraphName::try_from(self.to_term(&graph_name)?)?
},
}) })
} }
} }

@ -41,21 +41,21 @@ impl JsStore {
pub fn add(&self, quad: &JsValue) -> Result<(), JsValue> { pub fn add(&self, quad: &JsValue) -> Result<(), JsValue> {
self.store self.store
.insert(&self.from_js.to_quad(quad)?.try_into()?) .insert(&self.from_js.to_quad(quad)?.into())
.map_err(to_err)?; .map_err(to_err)?;
Ok(()) Ok(())
} }
pub fn delete(&self, quad: &JsValue) -> Result<(), JsValue> { pub fn delete(&self, quad: &JsValue) -> Result<(), JsValue> {
self.store self.store
.remove(&self.from_js.to_quad(quad)?.try_into()?) .remove(&self.from_js.to_quad(quad)?)
.map_err(to_err)?; .map_err(to_err)?;
Ok(()) Ok(())
} }
pub fn has(&self, quad: &JsValue) -> Result<bool, JsValue> { pub fn has(&self, quad: &JsValue) -> Result<bool, JsValue> {
self.store self.store
.contains(&self.from_js.to_quad(quad)?.try_into()?) .contains(&self.from_js.to_quad(quad)?)
.map_err(to_err) .map_err(to_err)
} }

@ -5,22 +5,27 @@ const assert = require('assert')
const dataFactory = require('@rdfjs/data-model') const dataFactory = require('@rdfjs/data-model')
const ex = dataFactory.namedNode('http://example.com') const ex = dataFactory.namedNode('http://example.com')
const triple = dataFactory.triple(
dataFactory.blankNode('s'),
dataFactory.namedNode('http://example.com/p'),
dataFactory.literal('o')
)
describe('Store', function () { describe('Store', function () {
describe('#add()', function () { describe('#add()', function () {
it('an added quad should be in the store', function () { it('an added quad should be in the store', function () {
const store = new Store() const store = new Store()
store.add(dataFactory.triple(ex, ex, ex)) store.add(dataFactory.triple(ex, ex, triple))
assert(store.has(dataFactory.triple(ex, ex, ex))) assert(store.has(dataFactory.triple(ex, ex, triple)))
}) })
}) })
describe('#delete()', function () { describe('#delete()', function () {
it('an removed quad should not be in the store anymore', function () { it('an removed quad should not be in the store anymore', function () {
const store = new Store([dataFactory.triple(ex, ex, ex)]) const store = new Store([dataFactory.triple(triple, ex, ex)])
assert(store.has(dataFactory.triple(ex, ex, ex))) assert(store.has(dataFactory.triple(triple, ex, ex)))
store.delete(dataFactory.triple(ex, ex, ex)) store.delete(dataFactory.triple(triple, ex, ex))
assert(!store.has(dataFactory.triple(ex, ex, ex))) assert(!store.has(dataFactory.triple(triple, ex, ex)))
}) })
}) })

Loading…
Cancel
Save