parent
5108c35a1d
commit
9060998f79
@ -0,0 +1,24 @@ |
|||||||
|
[package] |
||||||
|
name = "oxigraph_js" |
||||||
|
version = "0.0.1" |
||||||
|
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||||
|
license = "MIT/Apache-2.0" |
||||||
|
readme = "../README.md" |
||||||
|
keywords = ["RDF", "N-Triples", "Turtle", "RDF/XML", "SPARQL"] |
||||||
|
repository = "https://github.com/oxigraph/oxigraph/tree/master/js" |
||||||
|
description = "JavaScript bindings of Oxigraph" |
||||||
|
edition = "2018" |
||||||
|
|
||||||
|
|
||||||
|
[lib] |
||||||
|
crate-type = ["cdylib", "rlib"] |
||||||
|
name = "oxigraph" |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
oxigraph = {path = "../lib"} |
||||||
|
wasm-bindgen = "0.2" |
||||||
|
js-sys = "0.3" |
||||||
|
console_error_panic_hook = "0.1" |
||||||
|
|
||||||
|
[dev-dependencies] |
||||||
|
wasm-bindgen-test = "0.2" |
@ -0,0 +1,142 @@ |
|||||||
|
Oxigraph for JavaScript |
||||||
|
======================= |
||||||
|
|
||||||
|
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||||
|
[![npm](https://img.shields.io/npm/v/oxigraph)](https://www.npmjs.com/package/oxigraph) |
||||||
|
|
||||||
|
This package provides a JavaScript API on top of Oxigraph compiled with WebAssembly. |
||||||
|
|
||||||
|
Oxigraph is a work in progress graph database written in Rust implementing the [SPARQL](https://www.w3.org/TR/sparql11-overview/) standard. |
||||||
|
|
||||||
|
It is a work in progress and currently offers a simple in-memory store with [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/) capabilities. |
||||||
|
|
||||||
|
It is distributed using a [a NPM package](https://www.npmjs.com/package/oxigraph) that should work with nodeJS. |
||||||
|
|
||||||
|
```bash |
||||||
|
npm install oxigraph |
||||||
|
``` |
||||||
|
|
||||||
|
```js |
||||||
|
const oxigraph = require('oxigraph'); |
||||||
|
``` |
||||||
|
|
||||||
|
## API |
||||||
|
|
||||||
|
Oxigraph currently provides a simple JS API. |
||||||
|
It is centered around the `MemoryStore` class. |
||||||
|
|
||||||
|
The `NamedNode`, `BlankNode`, `Literal`, `DefaultGraph`, `Quad` and `DataFactory` types |
||||||
|
are following the [RDF/JS datamodel specification](https://rdf.js.org/data-model-spec/). |
||||||
|
|
||||||
|
To import `MemoryStore` using Node: |
||||||
|
```js |
||||||
|
const { MemoryStore } = require('oxigraph'); |
||||||
|
``` |
||||||
|
|
||||||
|
### `MemoryStore` |
||||||
|
|
||||||
|
#### `MemoryStore(optional sequence<Quad>? quads)` (constructor) |
||||||
|
```js |
||||||
|
const store = new MemoryStore(); |
||||||
|
``` |
||||||
|
|
||||||
|
If provided, the `MemoryStore` will be initialized with a sequence of quads. |
||||||
|
|
||||||
|
#### `MemoryStore.dataFactory` |
||||||
|
Returns a `DataFactory` following [RDF/JS datamodel specification](https://rdf.js.org/data-model-spec/). |
||||||
|
|
||||||
|
Example: |
||||||
|
```js |
||||||
|
const store = new MemoryStore(); |
||||||
|
const ex = store.dataFactory.namedNode("http://example.com"); |
||||||
|
const blank = store.dataFactory.blankNode(); |
||||||
|
const foo = store.dataFactory.literal("foo"); |
||||||
|
const quad = store.dataFactory.quad(blank, ex, foo); |
||||||
|
``` |
||||||
|
|
||||||
|
#### `MemoryStore.prototype.add(Quad quad)` |
||||||
|
Inserts a quad in the store. |
||||||
|
|
||||||
|
Example: |
||||||
|
```js |
||||||
|
store.add(quad); |
||||||
|
``` |
||||||
|
|
||||||
|
#### `MemoryStore.prototype.delete(Quad quad)` |
||||||
|
Removes a quad from the store. |
||||||
|
|
||||||
|
Example: |
||||||
|
```js |
||||||
|
store.delete(quad); |
||||||
|
``` |
||||||
|
|
||||||
|
#### `MemoryStore.prototype.has(Quad quad)` |
||||||
|
Returns a boolean stating if the store contains the quad. |
||||||
|
|
||||||
|
Example: |
||||||
|
```js |
||||||
|
store.has(quad); |
||||||
|
``` |
||||||
|
|
||||||
|
#### `MemoryStore.prototype.match(optional Term? subject, optional Term? predicate, optional Term? object, optional Term? graph)` |
||||||
|
Returns an array with all the quads matching a given quad pattern. |
||||||
|
|
||||||
|
Example to get all quads in the default graph with `ex` for subject: |
||||||
|
```js |
||||||
|
store.match(ex, null, null, store.dataFactory.defaultGraph()); |
||||||
|
``` |
||||||
|
|
||||||
|
Example to get all quads: |
||||||
|
```js |
||||||
|
store.match(); |
||||||
|
``` |
||||||
|
|
||||||
|
#### `MemoryStore.prototype.query(String query)` |
||||||
|
Executes a [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/). |
||||||
|
For `SELECT` queries the return type is an array of `Map` which keys are the bound variables and values are the values the result is bound to. |
||||||
|
For `CONSTRUCT` and `ÐESCRIBE` queries the return type is an array of `Quad`. |
||||||
|
For `ASK` queries the return type is a boolean. |
||||||
|
|
||||||
|
Example of SELECT query: |
||||||
|
```js |
||||||
|
for (binding of store.query("SELECT DISTINCT ?s WHERE { ?s ?p ?o }")) { |
||||||
|
console.log(binding.get("s").value); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Example of CONSTRUCT query: |
||||||
|
```js |
||||||
|
const filteredStore = new MemoryStore(store.query("CONSTRUCT { <http:/example.com/> ?p ?o } WHERE { <http:/example.com/> ?p ?o }")); |
||||||
|
``` |
||||||
|
|
||||||
|
Example of ASK query: |
||||||
|
```js |
||||||
|
if (store.query("ASK { ?s ?s ?s }")) { |
||||||
|
console.log("there is a triple with same subject, predicate and object"); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
## Example |
||||||
|
|
||||||
|
Insert the triple `<http://example/> <http://schema.org/name> "example"` and log the name of `<http://example/>` in SPARQL: |
||||||
|
```js |
||||||
|
const { MemoryStore } = require('oxigraph'); |
||||||
|
const store = new MemoryStore(); |
||||||
|
const dataFactory = store.dataFactory; |
||||||
|
const ex = dataFactory.namedNode("http://example/"); |
||||||
|
const schemaName = dataFactory.namedNode("http://schema.org/name"); |
||||||
|
store.add(dataFactory.triple(ex, schemaName, dataFactory.literal("example"))); |
||||||
|
for (binding of store.query("SELECT ?name WHERE { <http://example/> <http://schema.org/name> ?name }")) { |
||||||
|
console.log(binding.get("name").value); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
## How to contribute |
||||||
|
|
||||||
|
The Oxigraph bindings are written in Rust using [the Rust WASM toolkit](https://rustwasm.github.io/docs.html). |
||||||
|
|
||||||
|
The [The Rust Wasm Book](https://rustwasm.github.io/docs/book/) is a great tutorial to get started. |
||||||
|
|
||||||
|
To build the JavaScript bindings, just run `wasm-pack build`, to run the tests of the JS bindings written in JS just do a usual `npm test`. |
@ -0,0 +1,14 @@ |
|||||||
|
{ |
||||||
|
"name": "oxigraph_tests", |
||||||
|
"description": "Build a tests for Oxigraph JS", |
||||||
|
"private": true, |
||||||
|
"devDependencies": { |
||||||
|
"mocha": "^5.2.0", |
||||||
|
"@rdfjs/data-model": "^1.1.2" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"test": "wasm-pack build --dev --target nodejs && mocha", |
||||||
|
"build": "wasm-pack build --release --target nodejs && sed -i 's/oxigraph_js/oxigraph/g' pkg/package.json", |
||||||
|
"release": "wasm-pack pack && wasm-pack publish" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
mod model; |
||||||
|
mod store; |
||||||
|
mod utils; |
@ -0,0 +1,583 @@ |
|||||||
|
use crate::format_err; |
||||||
|
use crate::utils::to_err; |
||||||
|
use js_sys::{Reflect, UriError}; |
||||||
|
use oxigraph::model::*; |
||||||
|
use std::convert::TryFrom; |
||||||
|
use wasm_bindgen::prelude::*; |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = DataFactory)] |
||||||
|
#[derive(Default)] |
||||||
|
pub struct JsDataFactory { |
||||||
|
from_js: FromJsConverter, |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = DataFactory)] |
||||||
|
impl JsDataFactory { |
||||||
|
#[wasm_bindgen(js_name = namedNode)] |
||||||
|
pub fn named_node(&self, value: String) -> Result<JsNamedNode, JsValue> { |
||||||
|
NamedNode::parse(value) |
||||||
|
.map(|v| v.into()) |
||||||
|
.map_err(|v| UriError::new(&v.to_string()).into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = blankNode)] |
||||||
|
pub fn blank_node(&self, value: Option<String>) -> Result<JsBlankNode, JsValue> { |
||||||
|
Ok(if let Some(value) = value { |
||||||
|
BlankNode::new_from_unique_id(u128::from_str_radix(&value, 16).map_err(|_| { |
||||||
|
format_err!("Oxigraph only supports BlankNode created with Oxigraph DataFactory") |
||||||
|
})?) |
||||||
|
} else { |
||||||
|
BlankNode::default() |
||||||
|
} |
||||||
|
.into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen] |
||||||
|
pub fn literal( |
||||||
|
&self, |
||||||
|
value: Option<String>, |
||||||
|
language_or_datatype: &JsValue, |
||||||
|
) -> Result<JsLiteral, JsValue> { |
||||||
|
if language_or_datatype.is_null() || language_or_datatype.is_undefined() { |
||||||
|
Ok(Literal::new_simple_literal(value.unwrap_or_else(String::new)).into()) |
||||||
|
} else if language_or_datatype.is_string() { |
||||||
|
Ok(Literal::new_language_tagged_literal( |
||||||
|
value.unwrap_or_else(String::new), |
||||||
|
language_or_datatype.as_string().unwrap_or_else(String::new), |
||||||
|
) |
||||||
|
.map_err(to_err)? |
||||||
|
.into()) |
||||||
|
} else if let JsTerm::NamedNode(datatype) = self.from_js.to_term(language_or_datatype)? { |
||||||
|
Ok(Literal::new_typed_literal(value.unwrap_or_else(String::new), datatype).into()) |
||||||
|
} else { |
||||||
|
Err(format_err!("The literal datatype should be a NamedNode")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = defaultGraph)] |
||||||
|
pub fn default_graph(&self) -> JsDefaultGraph { |
||||||
|
JsDefaultGraph {} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = triple)] |
||||||
|
pub fn triple( |
||||||
|
&self, |
||||||
|
subject: &JsValue, |
||||||
|
predicate: &JsValue, |
||||||
|
object: &JsValue, |
||||||
|
) -> Result<JsQuad, JsValue> { |
||||||
|
Ok(JsQuad { |
||||||
|
subject: self.from_js.to_term(subject)?, |
||||||
|
predicate: self.from_js.to_term(predicate)?, |
||||||
|
object: self.from_js.to_term(object)?, |
||||||
|
graph: JsTerm::DefaultGraph(JsDefaultGraph {}), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = quad)] |
||||||
|
pub fn quad( |
||||||
|
&self, |
||||||
|
subject: &JsValue, |
||||||
|
predicate: &JsValue, |
||||||
|
object: &JsValue, |
||||||
|
graph: &JsValue, |
||||||
|
) -> Result<JsQuad, JsValue> { |
||||||
|
Ok(JsQuad { |
||||||
|
subject: self.from_js.to_term(subject)?, |
||||||
|
predicate: self.from_js.to_term(predicate)?, |
||||||
|
object: self.from_js.to_term(object)?, |
||||||
|
graph: if graph.is_undefined() || graph.is_null() { |
||||||
|
JsTerm::DefaultGraph(JsDefaultGraph {}) |
||||||
|
} else { |
||||||
|
self.from_js.to_term(&graph)? |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = fromTerm)] |
||||||
|
pub fn convert_term(&self, original: &JsValue) -> Result<JsValue, JsValue> { |
||||||
|
Ok(self.from_js.to_term(original)?.into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = fromQuad)] |
||||||
|
pub fn convert_quad(&self, original: &JsValue) -> Result<JsQuad, JsValue> { |
||||||
|
Ok(self.from_js.to_quad(original)?.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = NamedNode)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct JsNamedNode { |
||||||
|
inner: NamedNode, |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = NamedNode)] |
||||||
|
impl JsNamedNode { |
||||||
|
#[wasm_bindgen(getter = termType)] |
||||||
|
pub fn term_type(&self) -> String { |
||||||
|
"NamedNode".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::NamedNode(other))) = |
||||||
|
FromJsConverter::default().to_optional_term(&other) |
||||||
|
{ |
||||||
|
self == &other |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<NamedNode> for JsNamedNode { |
||||||
|
fn from(inner: NamedNode) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsNamedNode> for NamedNode { |
||||||
|
fn from(node: JsNamedNode) -> Self { |
||||||
|
node.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsNamedNode> for NamedOrBlankNode { |
||||||
|
fn from(node: JsNamedNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsNamedNode> for Term { |
||||||
|
fn from(node: JsNamedNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = BlankNode)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct JsBlankNode { |
||||||
|
inner: BlankNode, |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = BlankNode)] |
||||||
|
impl JsBlankNode { |
||||||
|
#[wasm_bindgen(getter = termType)] |
||||||
|
pub fn term_type(&self) -> String { |
||||||
|
"BlankNode".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::BlankNode(other))) = |
||||||
|
FromJsConverter::default().to_optional_term(&other) |
||||||
|
{ |
||||||
|
self == &other |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<BlankNode> for JsBlankNode { |
||||||
|
fn from(inner: BlankNode) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsBlankNode> for BlankNode { |
||||||
|
fn from(node: JsBlankNode) -> Self { |
||||||
|
node.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsBlankNode> for NamedOrBlankNode { |
||||||
|
fn from(node: JsBlankNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsBlankNode> for Term { |
||||||
|
fn from(node: JsBlankNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = Literal)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct JsLiteral { |
||||||
|
inner: Literal, |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = Literal)] |
||||||
|
impl JsLiteral { |
||||||
|
#[wasm_bindgen(getter = termType)] |
||||||
|
pub fn term_type(&self) -> String { |
||||||
|
"Literal".to_owned() |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(getter)] |
||||||
|
pub fn value(&self) -> String { |
||||||
|
self.inner.value().to_owned() |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(getter)] |
||||||
|
pub fn language(&self) -> String { |
||||||
|
self.inner.language().unwrap_or("").to_owned() |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(getter)] |
||||||
|
pub fn datatype(&self) -> JsNamedNode { |
||||||
|
self.inner.datatype().clone().into() |
||||||
|
} |
||||||
|
|
||||||
|
pub fn equals(&self, other: &JsValue) -> bool { |
||||||
|
if let Ok(Some(JsTerm::Literal(other))) = |
||||||
|
FromJsConverter::default().to_optional_term(&other) |
||||||
|
{ |
||||||
|
self == &other |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Literal> for JsLiteral { |
||||||
|
fn from(inner: Literal) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsLiteral> for Literal { |
||||||
|
fn from(node: JsLiteral) -> Self { |
||||||
|
node.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsLiteral> for Term { |
||||||
|
fn from(node: JsLiteral) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = DefaultGraph)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct JsDefaultGraph {} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = DefaultGraph)] |
||||||
|
impl JsDefaultGraph { |
||||||
|
#[wasm_bindgen(getter = termType)] |
||||||
|
pub fn term_type(&self) -> String { |
||||||
|
"DefaultGraph".to_owned() |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(getter)] |
||||||
|
pub fn value(&self) -> String { |
||||||
|
"".to_owned() |
||||||
|
} |
||||||
|
|
||||||
|
pub fn equals(&self, other: &JsValue) -> bool { |
||||||
|
if let Ok(Some(JsTerm::DefaultGraph(other))) = |
||||||
|
FromJsConverter::default().to_optional_term(&other) |
||||||
|
{ |
||||||
|
self == &other |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub enum JsTerm { |
||||||
|
NamedNode(JsNamedNode), |
||||||
|
BlankNode(JsBlankNode), |
||||||
|
Literal(JsLiteral), |
||||||
|
DefaultGraph(JsDefaultGraph), |
||||||
|
} |
||||||
|
|
||||||
|
impl From<JsTerm> for JsValue { |
||||||
|
fn from(value: JsTerm) -> Self { |
||||||
|
match value { |
||||||
|
JsTerm::NamedNode(v) => v.into(), |
||||||
|
JsTerm::BlankNode(v) => v.into(), |
||||||
|
JsTerm::Literal(v) => v.into(), |
||||||
|
JsTerm::DefaultGraph(v) => v.into(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<NamedNode> for JsTerm { |
||||||
|
fn from(node: NamedNode) -> Self { |
||||||
|
JsTerm::NamedNode(node.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<BlankNode> for JsTerm { |
||||||
|
fn from(node: BlankNode) -> Self { |
||||||
|
JsTerm::BlankNode(node.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Literal> for JsTerm { |
||||||
|
fn from(literal: Literal) -> Self { |
||||||
|
JsTerm::Literal(literal.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<NamedOrBlankNode> for JsTerm { |
||||||
|
fn from(node: NamedOrBlankNode) -> Self { |
||||||
|
match node { |
||||||
|
NamedOrBlankNode::NamedNode(node) => node.into(), |
||||||
|
NamedOrBlankNode::BlankNode(node) => node.into(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Term> for JsTerm { |
||||||
|
fn from(term: Term) -> Self { |
||||||
|
match term { |
||||||
|
Term::NamedNode(node) => node.into(), |
||||||
|
Term::BlankNode(node) => node.into(), |
||||||
|
Term::Literal(literal) => literal.into(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<JsTerm> for NamedNode { |
||||||
|
type Error = JsValue; |
||||||
|
|
||||||
|
fn try_from(value: JsTerm) -> Result<Self, JsValue> { |
||||||
|
match value { |
||||||
|
JsTerm::NamedNode(node) => Ok(node.into()), |
||||||
|
JsTerm::BlankNode(node) => Err(format_err!( |
||||||
|
"The blank node {} is not a named node", |
||||||
|
node.inner |
||||||
|
)), |
||||||
|
JsTerm::Literal(literal) => Err(format_err!( |
||||||
|
"The literal {} is not a named node", |
||||||
|
literal.inner |
||||||
|
)), |
||||||
|
JsTerm::DefaultGraph(_) => Err(format_err!("The default graph is not a named node")), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<JsTerm> for NamedOrBlankNode { |
||||||
|
type Error = JsValue; |
||||||
|
|
||||||
|
fn try_from(value: JsTerm) -> Result<Self, JsValue> { |
||||||
|
match value { |
||||||
|
JsTerm::NamedNode(node) => Ok(node.into()), |
||||||
|
JsTerm::BlankNode(node) => Ok(node.into()), |
||||||
|
JsTerm::Literal(literal) => Err(format_err!( |
||||||
|
"The variable {} is not a possible named or blank node term", |
||||||
|
literal.inner |
||||||
|
)), |
||||||
|
JsTerm::DefaultGraph(_) => { |
||||||
|
Err(format_err!("The default graph is not a possible RDF term")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<JsTerm> for Term { |
||||||
|
type Error = JsValue; |
||||||
|
|
||||||
|
fn try_from(value: JsTerm) -> Result<Self, JsValue> { |
||||||
|
match value { |
||||||
|
JsTerm::NamedNode(node) => Ok(node.into()), |
||||||
|
JsTerm::BlankNode(node) => Ok(node.into()), |
||||||
|
JsTerm::Literal(literal) => Ok(literal.into()), |
||||||
|
JsTerm::DefaultGraph(_) => { |
||||||
|
Err(format_err!("The default graph is not a possible RDF term")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = Quad)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct JsQuad { |
||||||
|
subject: JsTerm, |
||||||
|
predicate: JsTerm, |
||||||
|
object: JsTerm, |
||||||
|
graph: 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.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 { |
||||||
|
let (s, p, o, g) = quad.destruct(); |
||||||
|
Self { |
||||||
|
subject: s.into(), |
||||||
|
predicate: p.into(), |
||||||
|
object: o.into(), |
||||||
|
graph: if let Some(g) = g { |
||||||
|
g.into() |
||||||
|
} else { |
||||||
|
JsTerm::DefaultGraph(JsDefaultGraph {}) |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<JsQuad> for Quad { |
||||||
|
type Error = JsValue; |
||||||
|
|
||||||
|
fn try_from(quad: JsQuad) -> Result<Self, JsValue> { |
||||||
|
Ok(Quad::new( |
||||||
|
NamedOrBlankNode::try_from(quad.subject)?, |
||||||
|
NamedNode::try_from(quad.predicate)?, |
||||||
|
Term::try_from(quad.object)?, |
||||||
|
match quad.graph { |
||||||
|
JsTerm::NamedNode(node) => Some(NamedOrBlankNode::from(NamedNode::from(node))), |
||||||
|
JsTerm::BlankNode(node) => Some(NamedOrBlankNode::from(BlankNode::from(node))), |
||||||
|
JsTerm::Literal(literal) => { |
||||||
|
return Err(format_err!( |
||||||
|
"The variable ?{} is not a valid graph name", |
||||||
|
literal.inner |
||||||
|
)) |
||||||
|
} |
||||||
|
JsTerm::DefaultGraph(_) => None, |
||||||
|
}, |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub struct FromJsConverter { |
||||||
|
term_type: JsValue, |
||||||
|
value: JsValue, |
||||||
|
language: JsValue, |
||||||
|
datatype: JsValue, |
||||||
|
subject: JsValue, |
||||||
|
predicate: JsValue, |
||||||
|
object: JsValue, |
||||||
|
graph: JsValue, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for FromJsConverter { |
||||||
|
fn default() -> Self { |
||||||
|
Self { |
||||||
|
term_type: JsValue::from_str("termType"), |
||||||
|
value: JsValue::from_str("value"), |
||||||
|
language: JsValue::from_str("language"), |
||||||
|
datatype: JsValue::from_str("datatype"), |
||||||
|
subject: JsValue::from_str("subject"), |
||||||
|
predicate: JsValue::from_str("predicate"), |
||||||
|
object: JsValue::from_str("object"), |
||||||
|
graph: JsValue::from_str("graph"), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromJsConverter { |
||||||
|
pub fn to_term(&self, value: &JsValue) -> Result<JsTerm, JsValue> { |
||||||
|
let term_type = Reflect::get(&value, &self.term_type)?; |
||||||
|
if let Some(term_type) = term_type.as_string() { |
||||||
|
match term_type.as_str() { |
||||||
|
"NamedNode" => Ok(NamedNode::parse( |
||||||
|
Reflect::get(&value, &self.value)? |
||||||
|
.as_string() |
||||||
|
.ok_or_else(|| format_err!("NamedNode should have a string value"))?, |
||||||
|
) |
||||||
|
.map_err(|v| UriError::new(&v.to_string()))? |
||||||
|
.into()), |
||||||
|
"BlankNode" => Ok(BlankNode::new_from_unique_id( |
||||||
|
u128::from_str_radix( |
||||||
|
&Reflect::get(&value, &self.value)? |
||||||
|
.as_string() |
||||||
|
.ok_or_else(|| format_err!("BlankNode should have a string value"))?, |
||||||
|
16, |
||||||
|
) |
||||||
|
.map_err(|_| { |
||||||
|
format_err!( |
||||||
|
"Oxigraph only supports BlankNode created with Oxigraph DataFactory" |
||||||
|
) |
||||||
|
})?, |
||||||
|
) |
||||||
|
.into()), |
||||||
|
"Literal" => { |
||||||
|
if let JsTerm::NamedNode(datatype) = |
||||||
|
self.to_term(&Reflect::get(&value, &self.datatype)?)? |
||||||
|
{ |
||||||
|
let datatype = NamedNode::from(datatype); |
||||||
|
let literal_value = Reflect::get(&value, &self.value)? |
||||||
|
.as_string() |
||||||
|
.ok_or_else(|| format_err!("Literal should have a string value"))?; |
||||||
|
Ok(match datatype.as_str() { |
||||||
|
"http://www.w3.org/2001/XMLSchema#string" => Literal::new_simple_literal(literal_value), |
||||||
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#langString" => Literal::new_language_tagged_literal(literal_value, Reflect::get(&value, &self.language)?.as_string().ok_or_else( |
||||||
|
|| format_err!("Literal with rdf:langString datatype should have a language"), |
||||||
|
)?).map_err(to_err)?, |
||||||
|
_ => Literal::new_typed_literal(literal_value, datatype) |
||||||
|
}.into()) |
||||||
|
} else { |
||||||
|
Err(format_err!( |
||||||
|
"Literal should have a datatype that is a NamedNode" |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
"DefaultGraph" => Ok(JsTerm::DefaultGraph(JsDefaultGraph {})), |
||||||
|
_ => Err(format_err!( |
||||||
|
"The termType {} is not supported by Oxigraph", |
||||||
|
term_type |
||||||
|
)), |
||||||
|
} |
||||||
|
} else { |
||||||
|
Err(format_err!("The object termType field should be a string")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_optional_term(&self, value: &JsValue) -> Result<Option<JsTerm>, JsValue> { |
||||||
|
if value.is_null() || value.is_undefined() { |
||||||
|
Ok(None) |
||||||
|
} else { |
||||||
|
self.to_term(value).map(Some) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_quad(&self, value: &JsValue) -> Result<JsQuad, JsValue> { |
||||||
|
Ok(JsQuad { |
||||||
|
subject: self.to_term(&Reflect::get(&value, &self.subject)?)?, |
||||||
|
predicate: self.to_term(&Reflect::get(&value, &self.predicate)?)?, |
||||||
|
object: self.to_term(&Reflect::get(&value, &self.object)?)?, |
||||||
|
graph: self.to_term(&Reflect::get(&value, &self.graph)?)?, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
use crate::format_err; |
||||||
|
use crate::model::*; |
||||||
|
use crate::utils::to_err; |
||||||
|
use js_sys::{Array, Map}; |
||||||
|
use oxigraph::sparql::{PreparedQuery, QueryOptions, QueryResult}; |
||||||
|
use oxigraph::{Error, MemoryRepository, Repository, RepositoryConnection}; |
||||||
|
use std::convert::TryInto; |
||||||
|
use wasm_bindgen::prelude::*; |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = MemoryStore)] |
||||||
|
#[derive(Default)] |
||||||
|
pub struct JsMemoryStore { |
||||||
|
store: MemoryRepository, |
||||||
|
from_js: FromJsConverter, |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = MemoryStore)] |
||||||
|
impl JsMemoryStore { |
||||||
|
#[wasm_bindgen(constructor)] |
||||||
|
pub fn new(quads: Option<Box<[JsValue]>>) -> Result<JsMemoryStore, JsValue> { |
||||||
|
console_error_panic_hook::set_once(); |
||||||
|
|
||||||
|
let this = Self::default(); |
||||||
|
if let Some(quads) = quads { |
||||||
|
for quad in quads.iter() { |
||||||
|
this.add(quad)?; |
||||||
|
} |
||||||
|
} |
||||||
|
Ok(this) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = dataFactory, getter)] |
||||||
|
pub fn data_factory(&self) -> JsDataFactory { |
||||||
|
JsDataFactory::default() |
||||||
|
} |
||||||
|
|
||||||
|
pub fn add(&self, quad: &JsValue) -> Result<(), JsValue> { |
||||||
|
self.store |
||||||
|
.connection() |
||||||
|
.map_err(to_err)? |
||||||
|
.insert(&self.from_js.to_quad(quad)?.try_into()?) |
||||||
|
.map_err(to_err) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn delete(&self, quad: &JsValue) -> Result<(), JsValue> { |
||||||
|
self.store |
||||||
|
.connection() |
||||||
|
.map_err(to_err)? |
||||||
|
.remove(&self.from_js.to_quad(quad)?.try_into()?) |
||||||
|
.map_err(to_err) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn has(&self, quad: &JsValue) -> Result<bool, JsValue> { |
||||||
|
self.store |
||||||
|
.connection() |
||||||
|
.map_err(to_err)? |
||||||
|
.contains(&self.from_js.to_quad(quad)?.try_into()?) |
||||||
|
.map_err(to_err) |
||||||
|
} |
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = match)] |
||||||
|
pub fn match_quads( |
||||||
|
&self, |
||||||
|
subject: &JsValue, |
||||||
|
predicate: &JsValue, |
||||||
|
object: &JsValue, |
||||||
|
graph: &JsValue, |
||||||
|
) -> Result<Box<[JsValue]>, JsValue> { |
||||||
|
Ok(self |
||||||
|
.store |
||||||
|
.connection() |
||||||
|
.map_err(to_err)? |
||||||
|
.quads_for_pattern( |
||||||
|
match self.from_js.to_optional_term(subject)? { |
||||||
|
Some(JsTerm::NamedNode(node)) => Some(node.into()), |
||||||
|
Some(JsTerm::BlankNode(node)) => Some(node.into()), |
||||||
|
Some(_) => { |
||||||
|
return Err(format_err!( |
||||||
|
"The match subject parameter should be a named or a blank node", |
||||||
|
)) |
||||||
|
} |
||||||
|
None => None, |
||||||
|
}.as_ref(), |
||||||
|
match self.from_js.to_optional_term(predicate)? { |
||||||
|
Some(JsTerm::NamedNode(node)) => Some(node.into()), |
||||||
|
Some(_) => { |
||||||
|
return Err(format_err!( |
||||||
|
"The match predicate parameter should be a named node", |
||||||
|
)) |
||||||
|
} |
||||||
|
None => None, |
||||||
|
}.as_ref(), |
||||||
|
match self.from_js.to_optional_term(object)? { |
||||||
|
Some(JsTerm::NamedNode(node)) => Some(node.into()), |
||||||
|
Some(JsTerm::BlankNode(node)) => Some(node.into()), |
||||||
|
Some(JsTerm::Literal(literal)) => Some(literal.into()), |
||||||
|
Some(_) => { |
||||||
|
return Err(format_err!( |
||||||
|
"The match object parameter should be a named or a blank node or a literal", |
||||||
|
)) |
||||||
|
} |
||||||
|
None => None, |
||||||
|
}.as_ref(), |
||||||
|
match self.from_js.to_optional_term(graph)? { |
||||||
|
Some(JsTerm::NamedNode(node)) => Some(Some(node.into())), |
||||||
|
Some(JsTerm::BlankNode(node)) => Some(Some(node.into())), |
||||||
|
Some(JsTerm::DefaultGraph(_)) => Some(None), |
||||||
|
Some(_) => { |
||||||
|
return Err(format_err!( |
||||||
|
"The match subject parameter should be a named or a blank node or the default graph", |
||||||
|
)) |
||||||
|
} |
||||||
|
None => None, |
||||||
|
}.as_ref().map(|v| v.as_ref()), |
||||||
|
).map(|v| v.map(|v| JsQuad::from(v).into())).collect::<Result<Vec<_>,Error>>().map_err(to_err)?.into_boxed_slice()) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn query(&self, query: &str) -> Result<JsValue, JsValue> { |
||||||
|
let query = self |
||||||
|
.store |
||||||
|
.connection() |
||||||
|
.map_err(to_err)? |
||||||
|
.prepare_query(query, QueryOptions::default()) |
||||||
|
.map_err(to_err)?; |
||||||
|
let results = query.exec().map_err(to_err)?; |
||||||
|
let output = match results { |
||||||
|
QueryResult::Bindings(bindings) => { |
||||||
|
let (variables, iter) = bindings.destruct(); |
||||||
|
let variables: Vec<JsValue> = variables |
||||||
|
.into_iter() |
||||||
|
.map(|v| v.name().unwrap().into()) |
||||||
|
.collect(); |
||||||
|
let results = Array::new(); |
||||||
|
for values in iter { |
||||||
|
let values = values.map_err(to_err)?; |
||||||
|
let result = Map::new(); |
||||||
|
for (variable, value) in variables.iter().zip(values) { |
||||||
|
if let Some(value) = value { |
||||||
|
result.set(variable, &JsTerm::from(value).into()); |
||||||
|
} |
||||||
|
} |
||||||
|
results.push(&result.into()); |
||||||
|
} |
||||||
|
results.into() |
||||||
|
} |
||||||
|
QueryResult::Graph(quads) => { |
||||||
|
let results = Array::new(); |
||||||
|
for quad in quads { |
||||||
|
results.push(&JsQuad::from(quad.map_err(to_err)?.in_graph(None)).into()); |
||||||
|
} |
||||||
|
results.into() |
||||||
|
} |
||||||
|
QueryResult::Boolean(b) => b.into(), |
||||||
|
}; |
||||||
|
Ok(output) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
use js_sys::Error; |
||||||
|
use wasm_bindgen::JsValue; |
||||||
|
|
||||||
|
#[macro_export] |
||||||
|
macro_rules! format_err { |
||||||
|
($msg:literal $(,)?) => { |
||||||
|
::wasm_bindgen::JsValue::from(::js_sys::Error::new($msg)) |
||||||
|
}; |
||||||
|
($fmt:literal, $($arg:tt)*) => { |
||||||
|
::wasm_bindgen::JsValue::from(::js_sys::Error::new(&format!($fmt, $($arg)*))) |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_err(e: impl ToString) -> JsValue { |
||||||
|
JsValue::from(Error::new(&e.to_string())) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
const { MemoryStore } = require('../pkg/oxigraph.js'); |
||||||
|
const assert = require('assert'); |
||||||
|
require('../node_modules/@rdfjs/data-model/test/index.js')((new MemoryStore()).dataFactory); |
@ -0,0 +1,66 @@ |
|||||||
|
const { MemoryStore } = require('../pkg/oxigraph.js'); |
||||||
|
const assert = require('assert'); |
||||||
|
const dataFactory = require('@rdfjs/data-model'); |
||||||
|
|
||||||
|
const ex = dataFactory.namedNode('http://example.com'); |
||||||
|
|
||||||
|
describe('MemoryStore', function() { |
||||||
|
describe('#add()', function() { |
||||||
|
it('an added quad should be in the store', function() { |
||||||
|
const store = new MemoryStore(); |
||||||
|
store.add(dataFactory.triple(ex, ex, ex)); |
||||||
|
assert(store.has(dataFactory.triple(ex, ex, ex))); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#delete()', function() { |
||||||
|
it('an removed quad should not be in the store anymore', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
assert(store.has(dataFactory.triple(ex, ex, ex))); |
||||||
|
store.delete(dataFactory.triple(ex, ex, ex)) |
||||||
|
assert(!store.has(dataFactory.triple(ex, ex, ex))); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#has()', function() { |
||||||
|
it('an added quad should be in the store', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
assert(store.has(dataFactory.triple(ex, ex, ex))); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#match_quads()', function() { |
||||||
|
it('blank pattern should return all quads', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
const results = store.match(); |
||||||
|
assert.equal(1, results.length); |
||||||
|
assert(dataFactory.triple(ex, ex, ex).equals(results[0])); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#query()', function() { |
||||||
|
it('ASK true', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
assert.equal(true, store.query("ASK { ?s ?s ?s }")); |
||||||
|
}); |
||||||
|
|
||||||
|
it('ASK false', function() { |
||||||
|
const store = new MemoryStore(); |
||||||
|
assert.equal(false, store.query("ASK { FILTER(false)}")); |
||||||
|
}); |
||||||
|
|
||||||
|
it('CONSTRUCT', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
const results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"); |
||||||
|
assert.equal(1, results.length); |
||||||
|
assert(dataFactory.triple(ex, ex, ex).equals(results[0])); |
||||||
|
}); |
||||||
|
|
||||||
|
it('SELECT', function() { |
||||||
|
const store = new MemoryStore([dataFactory.triple(ex, ex, ex)]); |
||||||
|
const results = store.query("SELECT ?s WHERE { ?s ?p ?o }"); |
||||||
|
assert.equal(1, results.length); |
||||||
|
assert(ex.equals(results[0].get("s"))); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue