Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Tpt | add1dff458 | 2 years ago |
Tpt | e922d3293b | 2 years ago |
Tpt | 2d7eac932f | 2 years ago |
Tpt | 15819907af | 2 years ago |
Tpt | 6262e02edf | 2 years ago |
@ -1,4 +0,0 @@ |
||||
FROM gcr.io/oss-fuzz-base/base-builder-rust:v1 |
||||
COPY . $SRC/oxigraph |
||||
WORKDIR oxigraph |
||||
COPY .clusterfuzzlite/build.sh $SRC/ |
@ -1,30 +0,0 @@ |
||||
#!/bin/bash -eu |
||||
shopt -s globstar |
||||
|
||||
function build_seed_corpus() { |
||||
mkdir "/tmp/oxigraph_$1" |
||||
for file in **/*."$2" |
||||
do |
||||
hash=$(sha256sum "$file" | awk '{print $1;}') |
||||
cp "$file" "/tmp/oxigraph_$1/$hash" |
||||
done |
||||
zip "$1_seed_corpus.zip" /tmp/"oxigraph_$1"/* |
||||
rm -r "/tmp/oxigraph_$1" |
||||
} |
||||
|
||||
|
||||
cd "$SRC"/oxigraph |
||||
git submodule init |
||||
git submodule update |
||||
cargo fuzz build -O --debug-assertions |
||||
for TARGET in sparql_eval sparql_results_json sparql_results_tsv sparql_results_xml n3 nquads trig rdf_xml |
||||
do |
||||
cp fuzz/target/x86_64-unknown-linux-gnu/release/$TARGET "$OUT"/ |
||||
done |
||||
build_seed_corpus sparql_results_json srj |
||||
build_seed_corpus sparql_results_tsv tsv |
||||
build_seed_corpus sparql_results_xml srx |
||||
build_seed_corpus n3 n3 |
||||
build_seed_corpus nquads nq |
||||
build_seed_corpus trig trig |
||||
build_seed_corpus rdf_xml rdf |
@ -1 +0,0 @@ |
||||
language: rust |
@ -0,0 +1,21 @@ |
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/rust/.devcontainer/base.Dockerfile |
||||
|
||||
# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye |
||||
ARG VARIANT="bullseye" |
||||
FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} |
||||
|
||||
# [Optional] Uncomment this section to install additional packages. |
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ |
||||
&& apt-get -y install --no-install-recommends \ |
||||
python3 \ |
||||
python3-venv \ |
||||
python-is-python3 \ |
||||
libclang-dev |
||||
|
||||
ENV VIRTUAL_ENV=/opt/venv |
||||
RUN python -m venv $VIRTUAL_ENV |
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH" |
||||
RUN pip install --no-cache-dir -r python/requirements.dev.txt |
||||
|
||||
# Change owner to the devcontainer user |
||||
RUN chown -R 1000:1000 $VIRTUAL_ENV |
@ -0,0 +1,69 @@ |
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: |
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/rust |
||||
{ |
||||
"name": "Rust", |
||||
"build": { |
||||
"dockerfile": "Dockerfile", |
||||
"args": { |
||||
// Use the VARIANT arg to pick a Debian OS version: buster, bullseye |
||||
// Use bullseye when on local on arm64/Apple Silicon. |
||||
"VARIANT": "bullseye" |
||||
} |
||||
}, |
||||
"runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], |
||||
|
||||
// Configure tool-specific properties. |
||||
"customizations": { |
||||
// Configure properties specific to VS Code. |
||||
"vscode": { |
||||
// Set *default* container specific settings.json values on container create. |
||||
"settings": { |
||||
"lldb.executable": "/usr/bin/lldb", |
||||
// VS Code don't watch files under ./target |
||||
"files.watcherExclude": { |
||||
"**/target/**": true |
||||
}, |
||||
"rust-analyzer.checkOnSave.command": "clippy", |
||||
|
||||
"python.defaultInterpreterPath": "/opt/venv/bin/python", |
||||
"python.linting.enabled": true, |
||||
"python.linting.pylintEnabled": true, |
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", |
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black", |
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", |
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit", |
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", |
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", |
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", |
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", |
||||
"python.linting.pylintPath": "/opt/venv/bin/pylint", |
||||
"python.testing.pytestPath": "/opt/venv/bin/pytest" |
||||
}, |
||||
|
||||
// Add the IDs of extensions you want installed when the container is created. |
||||
"extensions": [ |
||||
"vadimcn.vscode-lldb", |
||||
"mutantdino.resourcemonitor", |
||||
"rust-lang.rust-analyzer", |
||||
"tamasfe.even-better-toml", |
||||
"serayuzgur.crates", |
||||
"ms-python.python", |
||||
"ms-python.vscode-pylance", |
||||
"esbenp.prettier-vscode", |
||||
"stardog-union.stardog-rdf-grammars" |
||||
] |
||||
} |
||||
}, |
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally. |
||||
// "forwardPorts": [], |
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created. |
||||
"postCreateCommand": "git submodule update --init && cargo build", |
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. |
||||
"remoteUser": "vscode", |
||||
"features": { |
||||
"python": "3.10" |
||||
} |
||||
} |
@ -1,16 +0,0 @@ |
||||
version: 2 |
||||
updates: |
||||
- package-ecosystem: "github-actions" |
||||
directory: "/" |
||||
schedule: |
||||
interval: weekly |
||||
- package-ecosystem: "pip" |
||||
directory: "/python/" |
||||
versioning-strategy: increase-if-necessary |
||||
schedule: |
||||
interval: weekly |
||||
- package-ecosystem: "npm" |
||||
directory: "/js/" |
||||
versioning-strategy: increase-if-necessary |
||||
schedule: |
||||
interval: weekly |
@ -1,16 +0,0 @@ |
||||
--- |
||||
name: Bug report |
||||
about: Create a report to help us improve |
||||
title: '' |
||||
labels: bug |
||||
assignees: '' |
||||
|
||||
--- |
||||
|
||||
**Describe the bug** |
||||
A clear and concise description of what the bug is. |
||||
|
||||
**To Reproduce** |
||||
Steps to reproduce the behavior: |
||||
1. Which version of Oxigraph are you using? On which platform? |
||||
2. A command-line or a code snippet that triggers the bug. |
@ -1,20 +0,0 @@ |
||||
--- |
||||
name: Feature request |
||||
about: Suggest an idea for this project |
||||
title: '' |
||||
labels: enhancement |
||||
assignees: '' |
||||
|
||||
--- |
||||
|
||||
**Is your feature request related to a problem? Please describe.** |
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
||||
|
||||
**Describe the solution you'd like** |
||||
A clear and concise description of what you want to happen. |
||||
|
||||
**Describe alternatives you've considered** |
||||
A clear and concise description of any alternative solutions or features you've considered. |
||||
|
||||
**Additional context** |
||||
Please link to other systems implementing the feature, specification of it if it exists and/or existing documentation about this feature. |
@ -1,10 +0,0 @@ |
||||
--- |
||||
name: Question |
||||
about: Please don't use issues but the Q&A section of the "discussions" space |
||||
title: '' |
||||
labels: question |
||||
assignees: '' |
||||
|
||||
--- |
||||
|
||||
|
@ -1,27 +0,0 @@ |
||||
name: 'Setup Rust' |
||||
description: 'Setup Rust using Rustup' |
||||
inputs: |
||||
version: |
||||
description: 'Rust version to use. By default latest stable version' |
||||
required: false |
||||
default: 'stable' |
||||
component: |
||||
description: 'Rust extra component to install like clippy' |
||||
required: false |
||||
target: |
||||
description: 'Rust extra target to install like wasm32-unknown-unknown' |
||||
required: false |
||||
runs: |
||||
using: "composite" |
||||
steps: |
||||
- run: rustup update |
||||
shell: bash |
||||
- run: rustup default ${{ inputs.version }} |
||||
shell: bash |
||||
- run: rustup component add ${{ inputs.component }} |
||||
shell: bash |
||||
if: ${{ inputs.component }} |
||||
- run: rustup target add ${{ inputs.target }} |
||||
shell: bash |
||||
if: ${{ inputs.target }} |
||||
- uses: Swatinem/rust-cache@v2 |
@ -1,11 +0,0 @@ |
||||
if [ -f "rocksdb" ] |
||||
then |
||||
cd rocksdb || exit |
||||
else |
||||
git clone https://github.com/facebook/rocksdb.git |
||||
cd rocksdb || exit |
||||
git checkout v8.0.0 |
||||
make shared_lib |
||||
fi |
||||
sudo make install-shared |
||||
sudo ldconfig /usr/local/lib |
@ -1,19 +0,0 @@ |
||||
cd /workdir |
||||
apk add clang-dev |
||||
curl https://static.rust-lang.org/rustup/dist/%arch%-unknown-linux-musl/rustup-init --output rustup-init |
||||
chmod +x rustup-init |
||||
./rustup-init -y --profile minimal |
||||
source "$HOME/.cargo/env" |
||||
export PATH="${PATH}:/opt/python/cp37-cp37m/bin:/opt/python/cp38-cp38/bin:/opt/python/cp39-cp39/bin:/opt/python/cp310-cp310/bin:/opt/python/cp311-cp311/bin" |
||||
cd python |
||||
python3.12 -m venv venv |
||||
source venv/bin/activate |
||||
pip install -r requirements.dev.txt |
||||
maturin develop --release |
||||
python generate_stubs.py pyoxigraph pyoxigraph.pyi --ruff |
||||
maturin build --release --features abi3 --compatibility musllinux_1_2 |
||||
if [ %for_each_version% ]; then |
||||
for VERSION in 8 9 10 11 12; do |
||||
maturin build --release --interpreter "python3.$VERSION" --compatibility musllinux_1_2 |
||||
done |
||||
fi |
@ -0,0 +1,276 @@ |
||||
name: Release artifacts |
||||
|
||||
on: |
||||
release: |
||||
types: [ published ] |
||||
|
||||
jobs: |
||||
push_server_to_docker_registry: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: docker/setup-buildx-action@v2 |
||||
- uses: docker/metadata-action@v4 |
||||
id: docker_meta |
||||
with: |
||||
images: | |
||||
${{ github.repository }} |
||||
ghcr.io/${{ github.repository }} |
||||
- uses: docker/login-action@v2 |
||||
with: |
||||
username: ${{ secrets.DOCKER_USERNAME }} |
||||
password: ${{ secrets.DOCKER_PASSWORD }} |
||||
- uses: docker/login-action@v2 |
||||
with: |
||||
registry: ghcr.io |
||||
username: ${{github.actor}} |
||||
password: ${{secrets.GITHUB_TOKEN}} |
||||
- uses: docker/build-push-action@v3 |
||||
with: |
||||
context: . |
||||
file: server/Dockerfile |
||||
pull: true |
||||
push: true |
||||
tags: ${{ steps.docker_meta.outputs.tags }} |
||||
labels: ${{ steps.docker_meta.outputs.labels }} |
||||
cache-from: type=gha |
||||
cache-to: type=gha,mode=max |
||||
|
||||
publish_crates: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- run: rustup update |
||||
- run: cargo login $CRATES_IO_TOKEN |
||||
env: |
||||
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} |
||||
- run: cargo publish |
||||
working-directory: ./oxrocksdb-sys |
||||
continue-on-error: true |
||||
- run: cargo publish |
||||
working-directory: ./lib/oxrdf |
||||
continue-on-error: true |
||||
- run: sleep 60 |
||||
- run: cargo publish |
||||
working-directory: ./lib/sparesults |
||||
continue-on-error: true |
||||
- run: cargo publish |
||||
working-directory: ./lib/spargebra |
||||
continue-on-error: true |
||||
- run: sleep 60 |
||||
- run: cargo publish |
||||
working-directory: ./lib |
||||
continue-on-error: true |
||||
- run: sleep 60 |
||||
- run: cargo publish |
||||
working-directory: ./server |
||||
|
||||
publish_pypi_linux: |
||||
runs-on: ubuntu-latest |
||||
strategy: |
||||
matrix: |
||||
architecture: [ "x86_64", "aarch64" ] |
||||
continue-on-error: true |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: docker/setup-qemu-action@v2 |
||||
with: |
||||
platforms: linux/${{ matrix.architecture }} |
||||
if: matrix.architecture != 'x86_64' |
||||
- run: sed 's/%arch%/${{ matrix.architecture }}/g' .github/workflows/manylinux_build.sh > .github/workflows/manylinux_build_script.sh |
||||
- run: docker run -v "$(pwd)":/workdir --platform linux/${{ matrix.architecture }} quay.io/pypa/manylinux2014_${{ matrix.architecture }} /bin/bash /workdir/.github/workflows/manylinux_build_script.sh |
||||
- uses: pypa/gh-action-pypi-publish@release/v1 |
||||
with: |
||||
user: __token__ |
||||
password: ${{ secrets.PYPI_PASSWORD }} |
||||
packages_dir: target/wheels |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: target/wheels/*.whl |
||||
|
||||
publish_pypi_mac: |
||||
runs-on: macos-latest |
||||
env: |
||||
DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer' |
||||
SDKROOT: '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' |
||||
MACOSX_DEPLOYMENT_TARGET: '10.14' |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: actions/setup-python@v4 |
||||
with: |
||||
python-version: "3.10" |
||||
- run: rustup update && rustup target add aarch64-apple-darwin |
||||
- run: pip install -r python/requirements.dev.txt |
||||
- run: maturin build --release -m python/Cargo.toml |
||||
- run: pip install --no-index --find-links=target/wheels/ pyoxigraph |
||||
- run: rm -r target/wheels |
||||
- run: python generate_stubs.py pyoxigraph pyoxigraph.pyi --black |
||||
working-directory: ./python |
||||
- run: maturin publish --no-sdist --universal2 -m python/Cargo.toml -u __token__ -p ${{ secrets.PYPI_PASSWORD }} |
||||
- run: maturin publish --no-sdist -m python/Cargo.toml -u __token__ -p ${{ secrets.PYPI_PASSWORD }} |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: target/wheels/*.whl |
||||
|
||||
publish_pypi_windows: |
||||
runs-on: windows-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: actions/setup-python@v4 |
||||
with: |
||||
python-version: "3.10" |
||||
- run: rustup update |
||||
- run: Remove-Item -LiteralPath "C:\msys64\" -Force -Recurse |
||||
- run: pip install -r python/requirements.dev.txt |
||||
- run: maturin build --release -m python/Cargo.toml |
||||
- run: pip install --no-index --find-links=target/wheels/ pyoxigraph |
||||
- run: rm -r target/wheels |
||||
- run: python generate_stubs.py pyoxigraph pyoxigraph.pyi --black |
||||
working-directory: ./python |
||||
- run: maturin publish --no-sdist -m python/Cargo.toml -u __token__ -p ${{ secrets.PYPI_PASSWORD }} |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: target/wheels/*.whl |
||||
|
||||
publish_pypi_stdist: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: actions/setup-python@v4 |
||||
with: |
||||
python-version: "3.10" |
||||
- run: rustup update |
||||
- run: pip install -r python/requirements.dev.txt |
||||
- run: maturin build -m python/Cargo.toml |
||||
- run: pip install --no-index --find-links=target/wheels/ pyoxigraph |
||||
- run: rm -r target/wheels |
||||
- run: python generate_stubs.py pyoxigraph pyoxigraph.pyi --black |
||||
working-directory: ./python |
||||
- run: maturin sdist -m python/Cargo.toml |
||||
- uses: pypa/gh-action-pypi-publish@release/v1 |
||||
with: |
||||
user: __token__ |
||||
password: ${{ secrets.PYPI_PASSWORD }} |
||||
packages_dir: target/wheels |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: target/wheels/*.tar.gz |
||||
|
||||
publish_npm: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: 16 |
||||
registry-url: https://registry.npmjs.org |
||||
- run: rustup update |
||||
- run: cargo install wasm-pack |
||||
- run: npm install |
||||
working-directory: ./js |
||||
- run: npm run release |
||||
working-directory: ./js |
||||
env: |
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
||||
- run: npm run pack |
||||
working-directory: ./js |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: js/*.tgz |
||||
|
||||
publish_full_archive: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- run: | |
||||
zip -r oxigraph_${{ github.event.release.tag_name }}.zip . |
||||
tar -czf /tmp/oxigraph_${{ github.event.release.tag_name }}.tar.gz . |
||||
mv /tmp/oxigraph_${{ github.event.release.tag_name }}.tar.gz . |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: | |
||||
oxigraph_${{ github.event.release.tag_name }}.zip |
||||
oxigraph_${{ github.event.release.tag_name }}.tar.gz |
||||
|
||||
publish_homebrew: |
||||
if: "!contains('-', github.event.release.tag_name)" |
||||
runs-on: ubuntu-latest |
||||
needs: publish_full_archive |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
repository: oxigraph/homebrew-oxigraph |
||||
token: ${{ secrets.FULL_ACCESS_TOKEN }} |
||||
- run: | |
||||
wget "https://github.com/oxigraph/oxigraph/releases/download/${{ github.event.release.tag_name }}/oxigraph_${{ github.event.release.tag_name }}.tar.gz" |
||||
SHA=`shasum -a 256 "oxigraph_${{ github.event.release.tag_name }}.tar.gz" | awk '{ print $1 }'` |
||||
rm "oxigraph_${{ github.event.release.tag_name }}.tar.gz" |
||||
sed -i "s/download\/.*\.tar/download\/${{ github.event.release.tag_name }}\/oxigraph_${{ github.event.release.tag_name }}.tar/g" Formula/oxigraph.rb |
||||
sed -i "s/sha256 \".*\"/sha256 \"$SHA\"/g" Formula/oxigraph.rb |
||||
git config user.name github-actions |
||||
git config user.email github-actions@github.com |
||||
git add . |
||||
git diff-index --quiet HEAD || git commit -m "Upgrades to ${{ github.event.release.tag_name }}" |
||||
git push |
||||
|
||||
publish_binary_linux: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- run: cargo build --release |
||||
working-directory: ./server |
||||
- run: mv target/release/oxigraph_server oxigraph_server_${{ github.event.release.tag_name }}_x86_64_linux_gnu |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: oxigraph_server_${{ github.event.release.tag_name }}_x86_64_linux_gnu |
||||
|
||||
publish_binary_mac: |
||||
runs-on: macos-latest |
||||
env: |
||||
DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer' |
||||
SDKROOT: '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' |
||||
MACOSX_DEPLOYMENT_TARGET: '10.14' |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- run: rustup update |
||||
- run: cargo build --release |
||||
working-directory: ./server |
||||
- run: mv target/release/oxigraph_server oxigraph_server_${{ github.event.release.tag_name }}_x86_64_apple |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: oxigraph_server_${{ github.event.release.tag_name }}_x86_64_apple |
||||
|
||||
publish_binary_windows: |
||||
runs-on: windows-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
submodules: true |
||||
- run: rustup update |
||||
- run: Remove-Item -LiteralPath "C:\msys64\" -Force -Recurse |
||||
- run: cargo build --release |
||||
working-directory: ./server |
||||
- run: mv target/release/oxigraph_server.exe oxigraph_server_${{ github.event.release.tag_name }}_x86_64_windows_msvc.exe |
||||
- uses: softprops/action-gh-release@v1 |
||||
with: |
||||
files: oxigraph_server_${{ github.event.release.tag_name }}_x86_64_windows_msvc.exe |
@ -1,3 +0,0 @@ |
||||
Thomas Tanon <thomas@pellissier-tanon.fr> <thomaspt@hotmail.fr> <Tpt@users.noreply.github.com> |
||||
Thomas Tanon <thomas@pellissier-tanon.fr> |
||||
Thomas Tanon <thomas.pellissier-tanon@helsing.ai> |
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
DATASET_SIZE=100000 |
||||
PARALLELISM=16 |
||||
VERSION="4.2.2" |
||||
TOMCAT_VERSION="9.0.71" |
||||
|
||||
set -eu |
||||
wget -nc -O "rdf4j-${VERSION}.zip" "https://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-${VERSION}-sdk.zip&mirror_id=1" |
||||
wget -nc -O "tomcat-${TOMCAT_VERSION}.zip" "https://dlcdn.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.zip" |
||||
cd bsbm-tools || exit |
||||
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" |
||||
wget -nc -O "rdf4j-${VERSION}.zip" "https://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-${VERSION}-sdk.zip&mirror_id=1" |
||||
unzip ../"rdf4j-${VERSION}.zip" |
||||
unzip ../"tomcat-${TOMCAT_VERSION}.zip" |
||||
CATALINA_HOME="$(pwd)/apache-tomcat-${TOMCAT_VERSION}" |
||||
export CATALINA_HOME |
||||
export JAVA_OPTS="-Dorg.eclipse.rdf4j.appdata.basedir=${CATALINA_HOME}/rdf4j" |
||||
cp "eclipse-rdf4j-${VERSION}"/war/rdf4j-server.war "${CATALINA_HOME}"/webapps/ |
||||
chmod +x "${CATALINA_HOME}"/bin/*.sh |
||||
"${CATALINA_HOME}"/bin/startup.sh |
||||
sleep 30 |
||||
curl -f -X PUT http://localhost:8080/rdf4j-server/repositories/bsbm -H 'Content-Type:text/turtle' -d ' |
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. |
||||
@prefix rep: <http://www.openrdf.org/config/repository#>. |
||||
@prefix sr: <http://www.openrdf.org/config/repository/sail#>. |
||||
@prefix sail: <http://www.openrdf.org/config/sail#>. |
||||
|
||||
[] a rep:Repository ; |
||||
rep:repositoryID "bsbm" ; |
||||
rdfs:label "BSBM" ; |
||||
rep:repositoryImpl [ |
||||
rep:repositoryType "openrdf:SailRepository" ; |
||||
sr:sailImpl [ |
||||
sail:sailType "rdf4j:LmdbStore" |
||||
] |
||||
] . |
||||
' |
||||
sleep 10 |
||||
curl -f -X PUT -H 'Content-Type:application/n-triples' -T "explore-${DATASET_SIZE}.nt" http://localhost:8080/rdf4j-server/repositories/bsbm/statements |
||||
./testdriver -mt ${PARALLELISM} -ucf usecases/explore/sparql.txt -o "../bsbm.explore.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm |
||||
./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm -u http://localhost:8080/rdf4j-server/repositories/bsbm/statements -udataset "explore-update-${DATASET_SIZE}.nt" |
||||
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm |
||||
"${CATALINA_HOME}"/bin/shutdown.sh |
||||
rm -f "explore-${DATASET_SIZE}.nt" |
||||
rm -f "explore-update-${DATASET_SIZE}.nt" |
||||
rm -rf td_data |
||||
rm -rf "eclipse-rdf4j-${VERSION}" |
||||
rm -rf "apache-tomcat-${TOMCAT_VERSION}" |
@ -1,63 +0,0 @@ |
||||
""" |
||||
Converts a SPARQL query JSON explanation file to a flamegraph. |
||||
Usage: python explanation_to_flamegraph.py explanation.json flamegraph.svg |
||||
""" |
||||
import json |
||||
import subprocess |
||||
from argparse import ArgumentParser |
||||
from pathlib import Path |
||||
from shutil import which |
||||
from tempfile import NamedTemporaryFile |
||||
|
||||
parser = ArgumentParser( |
||||
prog='OxigraphFlamegraph', |
||||
description='Builds a flamegraph from the Oxigraph query explanation JSON format', |
||||
epilog='Text at the bottom of help') |
||||
parser.add_argument('json_explanation', type=Path) |
||||
parser.add_argument('flamegraph_svg', type=Path) |
||||
args = parser.parse_args() |
||||
|
||||
|
||||
def trace_line(label: str, value: float): |
||||
return f"{label} {int(value * 1_000_000)}" |
||||
|
||||
|
||||
with args.json_explanation.open('rt') as fp: |
||||
explanation = json.load(fp) |
||||
trace = [] |
||||
if "parsing duration in seconds" in explanation: |
||||
trace.append(trace_line("parsing", explanation['parsing duration in seconds'])) |
||||
if "planning duration in seconds" in explanation: |
||||
trace.append(trace_line("planning", explanation['planning duration in seconds'])) |
||||
already_used_names = {} |
||||
|
||||
|
||||
def add_to_trace(node, path): |
||||
path = f"{path};{node['name'].replace(' ', '`')}" |
||||
if path in already_used_names: |
||||
already_used_names[path] += 1 |
||||
path = f"{path}`{already_used_names[path]}" |
||||
else: |
||||
already_used_names[path] = 0 |
||||
samples = node['duration in seconds'] - sum(child['duration in seconds'] for child in node.get("children", ())) |
||||
if int(samples * 1_000_000) > 0: |
||||
trace.append(trace_line(path, samples)) |
||||
for i, child in enumerate(node.get("children", ())): |
||||
add_to_trace(child, path) |
||||
|
||||
|
||||
add_to_trace(explanation["plan"], 'eval') |
||||
inferno = which('inferno-flamegraph') |
||||
flamegraph_pl = which('flamegraph.pl') |
||||
if inferno: |
||||
args.flamegraph_svg.write_text( |
||||
subprocess.run([inferno], input='\n'.join(trace), stdout=subprocess.PIPE, text=True).stdout) |
||||
elif flamegraph_pl: |
||||
with NamedTemporaryFile('w+t') as fp: |
||||
fp.write('\n'.join(trace)) |
||||
fp.flush() |
||||
args.flamegraph_svg.write_text( |
||||
subprocess.run([flamegraph_pl, fp.name], stdout=subprocess.PIPE, text=True).stdout) |
||||
else: |
||||
raise Exception( |
||||
'This script requires either the inferno-flamegraph from https://github.com/jonhoo/inferno either the flamegraph.pl script from https://github.com/brendangregg/FlameGraph to be installed and be in $PATH.') |
@ -1,52 +0,0 @@ |
||||
""" |
||||
Converts a SPARQL query JSON explanation file to a tracing event file compatible with Chrome. |
||||
Usage: python explanation_to_trace.py explanation.json trace.json |
||||
""" |
||||
import json |
||||
from argparse import ArgumentParser |
||||
from pathlib import Path |
||||
|
||||
parser = ArgumentParser( |
||||
prog='OxigraphTracing', |
||||
description='Builds a Trace Event Format file from the Oxigraph query explanation JSON format') |
||||
parser.add_argument('json_explanation', type=Path) |
||||
parser.add_argument('json_trace_event', type=Path) |
||||
args = parser.parse_args() |
||||
|
||||
with args.json_explanation.open('rt') as fp: |
||||
explanation = json.load(fp) |
||||
trace = [] |
||||
|
||||
|
||||
def trace_element(name: str, cat: str, start_s: float, duration_s: float): |
||||
return { |
||||
"name": name, |
||||
"cat": cat, |
||||
"ph": "X", |
||||
"ts": int(start_s * 1_000_000), |
||||
"dur": int(duration_s * 1_000_000), |
||||
"pid": 1 |
||||
} |
||||
|
||||
|
||||
def add_to_trace(node, path, start_time: float): |
||||
path = f"{path};{node['name'].replace(' ', '`')}" |
||||
trace.append(trace_element(node["name"], node["name"].split("(")[0], start_time, node["duration in seconds"])) |
||||
for child in node.get("children", ()): |
||||
add_to_trace(child, path, start_time) |
||||
start_time += child["duration in seconds"] |
||||
|
||||
|
||||
current_time = 0 |
||||
if "parsing duration in seconds" in explanation: |
||||
d = explanation["parsing duration in seconds"] |
||||
trace.append(trace_element(f"parsing", "parsing", current_time, d)) |
||||
current_time += d |
||||
if "planning duration in seconds" in explanation: |
||||
d = explanation["planning duration in seconds"] |
||||
trace.append(trace_element(f"planning", "planning", current_time, d)) |
||||
current_time += d |
||||
add_to_trace(explanation["plan"], 'eval', current_time) |
||||
|
||||
with args.json_trace_event.open("wt") as fp: |
||||
json.dump(trace, fp) |
@ -1,4 +1,4 @@ |
||||
avoid-breaking-exported-api = false |
||||
avoid-breaking-exported-api = true |
||||
cognitive-complexity-threshold = 50 |
||||
too-many-arguments-threshold = 10 |
||||
type-complexity-threshold = 500 |
Before Width: | Height: | Size: 4.6 KiB |
@ -1,35 +0,0 @@ |
||||
+------------------+ +----------------+ +-----------------+ |
||||
+ oxigraph CLI {r} + + pyoxigraph {p} + + oxigraph JS {j} + |
||||
+------------------+ +----------------+ +-----------------+ |
||||
|
||||
+---------------------------------------------------------------------------+ |
||||
+ oxigraph (Rust) {r} + |
||||
+---------------------------------------------------------------------------+ |
||||
|
||||
+----------------------------+ +-------------+ |
||||
+ oxrdfio {r} + + sparopt {r} + |
||||
+----------------------------+ +-------------+ |
||||
|
||||
+-----------+ +--------------+ +-----------------+ +----------------+ |
||||
+ oxttl {r} + + oxrdfxml {r} + + spargebra {r} + + sparesults {r} + |
||||
+-----------+ +--------------+ +-----------------+ +----------------+ |
||||
|
||||
+-----------------------------------------------------------------------+ |
||||
+ oxrdf {r} + |
||||
+-----------------------------------------------------------------------+ |
||||
|
||||
+------------------+ |
||||
+ oxsdatatypes {r} + |
||||
+------------------+ |
||||
|
||||
|
||||
# Legend: |
||||
r = { |
||||
fill: papayawhip; |
||||
} |
||||
p = { |
||||
fill: lightyellow; |
||||
} |
||||
j = { |
||||
fill: lightgreen; |
||||
} |
@ -1,28 +0,0 @@ |
||||
#![no_main] |
||||
|
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxttl::N3Parser; |
||||
|
||||
fuzz_target!(|data: &[u8]| { |
||||
let mut quads = Vec::new(); |
||||
let mut parser = N3Parser::new() |
||||
.with_base_iri("http://example.com/") |
||||
.unwrap() |
||||
.parse(); |
||||
for chunk in data.split(|c| *c == 0xFF) { |
||||
parser.extend_from_slice(chunk); |
||||
while let Some(result) = parser.read_next() { |
||||
if let Ok(quad) = result { |
||||
quads.push(quad); |
||||
} |
||||
} |
||||
} |
||||
parser.end(); |
||||
while let Some(result) = parser.read_next() { |
||||
if let Ok(quad) = result { |
||||
quads.push(quad); |
||||
} |
||||
} |
||||
assert!(parser.is_end()); |
||||
//TODO: serialize
|
||||
}); |
@ -1,84 +0,0 @@ |
||||
#![no_main] |
||||
|
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxrdf::Quad; |
||||
use oxttl::{NQuadsParser, NQuadsSerializer}; |
||||
|
||||
fn parse<'a>( |
||||
chunks: impl IntoIterator<Item = &'a [u8]>, |
||||
unchecked: bool, |
||||
) -> (Vec<Quad>, Vec<String>) { |
||||
let mut quads = Vec::new(); |
||||
let mut errors = Vec::new(); |
||||
let mut parser = NQuadsParser::new().with_quoted_triples(); |
||||
if unchecked { |
||||
parser = parser.unchecked(); |
||||
} |
||||
let mut reader = parser.parse(); |
||||
for chunk in chunks { |
||||
reader.extend_from_slice(chunk); |
||||
while let Some(result) = reader.read_next() { |
||||
match result { |
||||
Ok(quad) => quads.push(quad), |
||||
Err(error) => errors.push(error.to_string()), |
||||
} |
||||
} |
||||
} |
||||
reader.end(); |
||||
while let Some(result) = reader.read_next() { |
||||
match result { |
||||
Ok(quad) => quads.push(quad), |
||||
Err(error) => errors.push(error.to_string()), |
||||
} |
||||
} |
||||
assert!(reader.is_end()); |
||||
(quads, errors) |
||||
} |
||||
|
||||
fuzz_target!(|data: &[u8]| { |
||||
// We parse with splitting
|
||||
let (quads, errors) = parse(data.split(|c| *c == 0xFF), false); |
||||
// We parse without splitting
|
||||
let (quads_without_split, errors_without_split) = parse( |
||||
[data |
||||
.iter() |
||||
.copied() |
||||
.filter(|c| *c != 0xFF) |
||||
.collect::<Vec<_>>() |
||||
.as_slice()], |
||||
false, |
||||
); |
||||
assert_eq!(quads, quads_without_split); |
||||
assert_eq!(errors, errors_without_split); |
||||
|
||||
// We test also unchecked if valid
|
||||
if errors.is_empty() { |
||||
let (quads_unchecked, errors_unchecked) = parse(data.split(|c| *c == 0xFF), true); |
||||
assert!(errors_unchecked.is_empty()); |
||||
assert_eq!(quads, quads_unchecked); |
||||
} |
||||
|
||||
// We serialize
|
||||
let mut writer = NQuadsSerializer::new().serialize_to_write(Vec::new()); |
||||
for quad in &quads { |
||||
writer.write_quad(quad).unwrap(); |
||||
} |
||||
let new_serialization = writer.finish(); |
||||
|
||||
// We parse the serialization
|
||||
let new_quads = NQuadsParser::new() |
||||
.with_quoted_triples() |
||||
.parse_read(new_serialization.as_slice()) |
||||
.collect::<Result<Vec<_>, _>>() |
||||
.map_err(|e| { |
||||
format!( |
||||
"Error on {:?} from {quads:?} based on {:?}: {e}", |
||||
String::from_utf8_lossy(&new_serialization), |
||||
String::from_utf8_lossy(data) |
||||
) |
||||
}) |
||||
.unwrap(); |
||||
|
||||
// We check the roundtrip has not changed anything
|
||||
assert_eq!(new_quads, quads); |
||||
}); |
@ -1,35 +0,0 @@ |
||||
#![no_main] |
||||
|
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxrdfxml::{RdfXmlParser, RdfXmlSerializer}; |
||||
|
||||
fuzz_target!(|data: &[u8]| { |
||||
// We parse
|
||||
let triples = RdfXmlParser::new() |
||||
.parse_read(data) |
||||
.flatten() |
||||
.collect::<Vec<_>>(); |
||||
|
||||
// We serialize
|
||||
let mut writer = RdfXmlSerializer::new().serialize_to_write(Vec::new()); |
||||
for triple in &triples { |
||||
writer.write_triple(triple).unwrap(); |
||||
} |
||||
let new_serialization = writer.finish().unwrap(); |
||||
|
||||
// We parse the serialization
|
||||
let new_triples = RdfXmlParser::new() |
||||
.parse_read(new_serialization.as_slice()) |
||||
.collect::<Result<Vec<_>, _>>() |
||||
.map_err(|e| { |
||||
format!( |
||||
"Error on {:?} from {triples:?} based on {:?}: {e}", |
||||
String::from_utf8_lossy(&new_serialization), |
||||
String::from_utf8_lossy(data) |
||||
) |
||||
}) |
||||
.unwrap(); |
||||
|
||||
// We check the roundtrip has not changed anything
|
||||
assert_eq!(new_triples, triples); |
||||
}); |
@ -1,61 +0,0 @@ |
||||
#![no_main] |
||||
|
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxigraph::io::RdfFormat; |
||||
use oxigraph::sparql::{Query, QueryOptions, QueryResults, QuerySolutionIter}; |
||||
use oxigraph::store::Store; |
||||
use std::sync::OnceLock; |
||||
|
||||
fuzz_target!(|data: sparql_smith::Query| { |
||||
static STORE: OnceLock<Store> = OnceLock::new(); |
||||
let store = STORE.get_or_init(|| { |
||||
let store = Store::new().unwrap(); |
||||
store |
||||
.load_from_read(RdfFormat::TriG, sparql_smith::DATA_TRIG.as_bytes()) |
||||
.unwrap(); |
||||
store |
||||
}); |
||||
|
||||
let query_str = data.to_string(); |
||||
if let Ok(query) = Query::parse(&query_str, None) { |
||||
let options = QueryOptions::default(); |
||||
let with_opt = store.query_opt(query.clone(), options.clone()).unwrap(); |
||||
let without_opt = store |
||||
.query_opt(query, options.without_optimizations()) |
||||
.unwrap(); |
||||
match (with_opt, without_opt) { |
||||
(QueryResults::Solutions(with_opt), QueryResults::Solutions(without_opt)) => { |
||||
assert_eq!( |
||||
query_solutions_key(with_opt, query_str.contains(" REDUCED ")), |
||||
query_solutions_key(without_opt, query_str.contains(" REDUCED ")) |
||||
) |
||||
} |
||||
(QueryResults::Graph(_), QueryResults::Graph(_)) => unimplemented!(), |
||||
(QueryResults::Boolean(with_opt), QueryResults::Boolean(without_opt)) => { |
||||
assert_eq!(with_opt, without_opt) |
||||
} |
||||
_ => panic!("Different query result types"), |
||||
} |
||||
} |
||||
}); |
||||
|
||||
fn query_solutions_key(iter: QuerySolutionIter, is_reduced: bool) -> String { |
||||
// TODO: ordering
|
||||
let mut b = iter |
||||
.into_iter() |
||||
.map(|t| { |
||||
let mut b = t |
||||
.unwrap() |
||||
.iter() |
||||
.map(|(var, val)| format!("{var}: {val}")) |
||||
.collect::<Vec<_>>(); |
||||
b.sort_unstable(); |
||||
b.join(" ") |
||||
}) |
||||
.collect::<Vec<_>>(); |
||||
b.sort_unstable(); |
||||
if is_reduced { |
||||
b.dedup(); |
||||
} |
||||
b.join("\n") |
||||
} |
@ -1,7 +1,10 @@ |
||||
#![no_main] |
||||
use libfuzzer_sys::fuzz_target; |
||||
use spargebra::Query; |
||||
use std::str; |
||||
|
||||
fuzz_target!(|data: &str| { |
||||
let _ = Query::parse(data, None); |
||||
fuzz_target!(|data: &[u8]| { |
||||
if let Ok(data) = str::from_utf8(data) { |
||||
Query::parse(data, None); |
||||
} |
||||
}); |
||||
|
@ -1,6 +1,15 @@ |
||||
#![no_main] |
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxigraph_fuzz::result_format::fuzz_result_format; |
||||
use sparesults::QueryResultsFormat; |
||||
use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader}; |
||||
|
||||
fuzz_target!(|data: &[u8]| fuzz_result_format(QueryResultsFormat::Json, data)); |
||||
fuzz_target!(|data: &[u8]| { |
||||
let parser = QueryResultsParser::from_format(QueryResultsFormat::Json); |
||||
if let Ok(QueryResultsReader::Solutions(solutions)) = parser.read_results(data) { |
||||
for s in solutions { |
||||
if s.is_err() { |
||||
// TODO: avoid infinite loop of errors
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
|
@ -1,6 +1,10 @@ |
||||
#![no_main] |
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxigraph_fuzz::result_format::fuzz_result_format; |
||||
use sparesults::QueryResultsFormat; |
||||
use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader}; |
||||
|
||||
fuzz_target!(|data: &[u8]| fuzz_result_format(QueryResultsFormat::Tsv, data)); |
||||
fuzz_target!(|data: &[u8]| { |
||||
let parser = QueryResultsParser::from_format(QueryResultsFormat::Tsv); |
||||
if let Ok(QueryResultsReader::Solutions(solutions)) = parser.read_results(data) { |
||||
for _ in solutions {} |
||||
} |
||||
}); |
||||
|
@ -1,6 +1,10 @@ |
||||
#![no_main] |
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxigraph_fuzz::result_format::fuzz_result_format; |
||||
use sparesults::QueryResultsFormat; |
||||
use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader}; |
||||
|
||||
fuzz_target!(|data: &[u8]| fuzz_result_format(QueryResultsFormat::Xml, data)); |
||||
fuzz_target!(|data: &[u8]| { |
||||
let parser = QueryResultsParser::from_format(QueryResultsFormat::Xml); |
||||
if let Ok(QueryResultsReader::Solutions(solutions)) = parser.read_results(data) { |
||||
for _ in solutions {} |
||||
} |
||||
}); |
||||
|
@ -1,166 +0,0 @@ |
||||
#![no_main] |
||||
|
||||
use libfuzzer_sys::fuzz_target; |
||||
use oxrdf::graph::CanonicalizationAlgorithm; |
||||
use oxrdf::{Dataset, GraphName, Quad, Subject, Term, Triple}; |
||||
use oxttl::{TriGParser, TriGSerializer}; |
||||
|
||||
fn parse<'a>( |
||||
chunks: impl IntoIterator<Item = &'a [u8]>, |
||||
unchecked: bool, |
||||
) -> (Vec<Quad>, Vec<String>, Vec<(String, String)>) { |
||||
let mut quads = Vec::new(); |
||||
let mut errors = Vec::new(); |
||||
let mut parser = TriGParser::new() |
||||
.with_quoted_triples() |
||||
.with_base_iri("http://example.com/") |
||||
.unwrap(); |
||||
if unchecked { |
||||
parser = parser.unchecked(); |
||||
} |
||||
let mut reader = parser.parse(); |
||||
for chunk in chunks { |
||||
reader.extend_from_slice(chunk); |
||||
while let Some(result) = reader.read_next() { |
||||
match result { |
||||
Ok(quad) => quads.push(quad), |
||||
Err(error) => errors.push(error.to_string()), |
||||
} |
||||
} |
||||
} |
||||
reader.end(); |
||||
while let Some(result) = reader.read_next() { |
||||
match result { |
||||
Ok(quad) => quads.push(quad), |
||||
Err(error) => errors.push(error.to_string()), |
||||
} |
||||
} |
||||
assert!(reader.is_end()); |
||||
( |
||||
quads, |
||||
errors, |
||||
reader |
||||
.prefixes() |
||||
.map(|(k, v)| (k.to_owned(), v.to_owned())) |
||||
.collect(), |
||||
) |
||||
} |
||||
|
||||
fn count_triple_blank_nodes(triple: &Triple) -> usize { |
||||
(match &triple.subject { |
||||
Subject::BlankNode(_) => 1, |
||||
Subject::Triple(t) => count_triple_blank_nodes(t), |
||||
_ => 0, |
||||
}) + (match &triple.object { |
||||
Term::BlankNode(_) => 1, |
||||
Term::Triple(t) => count_triple_blank_nodes(t), |
||||
_ => 0, |
||||
}) |
||||
} |
||||
|
||||
fn count_quad_blank_nodes(quad: &Quad) -> usize { |
||||
(match &quad.subject { |
||||
Subject::BlankNode(_) => 1, |
||||
Subject::Triple(t) => count_triple_blank_nodes(t), |
||||
_ => 0, |
||||
}) + (match &quad.object { |
||||
Term::BlankNode(_) => 1, |
||||
Term::Triple(t) => count_triple_blank_nodes(t), |
||||
_ => 0, |
||||
}) + usize::from(matches!(quad.graph_name, GraphName::BlankNode(_))) |
||||
} |
||||
|
||||
fn serialize_quads(quads: &[Quad], prefixes: Vec<(String, String)>) -> Vec<u8> { |
||||
let mut serializer = TriGSerializer::new(); |
||||
for (prefix_name, prefix_iri) in prefixes { |
||||
serializer = serializer.with_prefix(prefix_name, prefix_iri).unwrap(); |
||||
} |
||||
let mut writer = serializer.serialize_to_write(Vec::new()); |
||||
for quad in quads { |
||||
writer.write_quad(quad).unwrap(); |
||||
} |
||||
writer.finish().unwrap() |
||||
} |
||||
|
||||
fuzz_target!(|data: &[u8]| { |
||||
// We parse with splitting
|
||||
let (quads, errors, prefixes) = parse(data.split(|c| *c == 0xFF), false); |
||||
// We parse without splitting
|
||||
let (quads_without_split, errors_without_split, _) = parse( |
||||
[data |
||||
.iter() |
||||
.copied() |
||||
.filter(|c| *c != 0xFF) |
||||
.collect::<Vec<_>>() |
||||
.as_slice()], |
||||
false, |
||||
); |
||||
let (quads_unchecked, errors_unchecked, _) = parse(data.split(|c| *c == 0xFF), true); |
||||
if errors.is_empty() { |
||||
assert!(errors_unchecked.is_empty()); |
||||
} |
||||
|
||||
let bnodes_count = quads.iter().map(count_quad_blank_nodes).sum::<usize>(); |
||||
if bnodes_count == 0 { |
||||
assert_eq!( |
||||
quads, |
||||
quads_without_split, |
||||
"With split:\n{}\nWithout split:\n{}", |
||||
String::from_utf8_lossy(&serialize_quads(&quads, Vec::new())), |
||||
String::from_utf8_lossy(&serialize_quads(&quads_without_split, Vec::new())) |
||||
); |
||||
if errors.is_empty() { |
||||
assert_eq!( |
||||
quads, |
||||
quads_unchecked, |
||||
"Validating:\n{}\nUnchecked:\n{}", |
||||
String::from_utf8_lossy(&serialize_quads(&quads, Vec::new())), |
||||
String::from_utf8_lossy(&serialize_quads(&quads_unchecked, Vec::new())) |
||||
); |
||||
} |
||||
} else if bnodes_count <= 4 { |
||||
let mut dataset_with_split = quads.iter().collect::<Dataset>(); |
||||
let mut dataset_without_split = quads_without_split.iter().collect::<Dataset>(); |
||||
dataset_with_split.canonicalize(CanonicalizationAlgorithm::Unstable); |
||||
dataset_without_split.canonicalize(CanonicalizationAlgorithm::Unstable); |
||||
assert_eq!( |
||||
dataset_with_split, |
||||
dataset_without_split, |
||||
"With split:\n{}\nWithout split:\n{}", |
||||
String::from_utf8_lossy(&serialize_quads(&quads, Vec::new())), |
||||
String::from_utf8_lossy(&serialize_quads(&quads_without_split, Vec::new())) |
||||
); |
||||
if errors.is_empty() { |
||||
let mut dataset_unchecked = quads_unchecked.iter().collect::<Dataset>(); |
||||
dataset_unchecked.canonicalize(CanonicalizationAlgorithm::Unstable); |
||||
assert_eq!( |
||||
dataset_with_split, |
||||
dataset_unchecked, |
||||
"Validating:\n{}\nUnchecked:\n{}", |
||||
String::from_utf8_lossy(&serialize_quads(&quads, Vec::new())), |
||||
String::from_utf8_lossy(&serialize_quads(&quads_unchecked, Vec::new())) |
||||
); |
||||
} |
||||
} |
||||
assert_eq!(errors, errors_without_split); |
||||
|
||||
// We serialize
|
||||
let new_serialization = serialize_quads(&quads, prefixes); |
||||
|
||||
// We parse the serialization
|
||||
let new_quads = TriGParser::new() |
||||
.with_quoted_triples() |
||||
.parse_read(new_serialization.as_slice()) |
||||
.collect::<Result<Vec<_>, _>>() |
||||
.map_err(|e| { |
||||
format!( |
||||
"Error on {:?} from {quads:?} based on {:?}: {e}", |
||||
String::from_utf8_lossy(&new_serialization), |
||||
String::from_utf8_lossy(data) |
||||
) |
||||
}) |
||||
.unwrap(); |
||||
|
||||
// We check the roundtrip has not changed anything
|
||||
assert_eq!(new_quads, quads); |
||||
}); |
@ -1 +0,0 @@ |
||||
pub mod result_format; |
@ -1,63 +0,0 @@ |
||||
use anyhow::Context; |
||||
use sparesults::{ |
||||
FromReadQueryResultsReader, QueryResultsFormat, QueryResultsParser, QueryResultsSerializer, |
||||
}; |
||||
|
||||
pub fn fuzz_result_format(format: QueryResultsFormat, data: &[u8]) { |
||||
let parser = QueryResultsParser::from_format(format); |
||||
let serializer = QueryResultsSerializer::from_format(format); |
||||
|
||||
let Ok(reader) = parser.parse_read(data) else { |
||||
return; |
||||
}; |
||||
match reader { |
||||
FromReadQueryResultsReader::Solutions(solutions) => { |
||||
let Ok(solutions) = solutions.collect::<Result<Vec<_>, _>>() else { |
||||
return; |
||||
}; |
||||
|
||||
// We try to write again
|
||||
let mut writer = serializer |
||||
.serialize_solutions_to_write( |
||||
Vec::new(), |
||||
solutions |
||||
.first() |
||||
.map_or_else(Vec::new, |s| s.variables().to_vec()), |
||||
) |
||||
.unwrap(); |
||||
for solution in &solutions { |
||||
writer.write(solution).unwrap(); |
||||
} |
||||
let serialized = String::from_utf8(writer.finish().unwrap()).unwrap(); |
||||
|
||||
// And to parse again
|
||||
if let FromReadQueryResultsReader::Solutions(roundtrip_solutions) = parser |
||||
.parse_read(serialized.as_bytes()) |
||||
.with_context(|| format!("Parsing {serialized:?}")) |
||||
.unwrap() |
||||
{ |
||||
assert_eq!( |
||||
roundtrip_solutions |
||||
.collect::<Result<Vec<_>, _>>() |
||||
.with_context(|| format!("Parsing {serialized:?}")) |
||||
.unwrap(), |
||||
solutions |
||||
) |
||||
} |
||||
} |
||||
FromReadQueryResultsReader::Boolean(value) => { |
||||
// We try to write again
|
||||
let mut serialized = Vec::new(); |
||||
serializer |
||||
.serialize_boolean_to_write(&mut serialized, value) |
||||
.unwrap(); |
||||
|
||||
// And to parse again
|
||||
if let FromReadQueryResultsReader::Boolean(roundtrip_value) = |
||||
parser.parse_read(serialized.as_slice()).unwrap() |
||||
{ |
||||
assert_eq!(roundtrip_value, value) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,26 +1,23 @@ |
||||
[package] |
||||
name = "oxigraph-js" |
||||
version.workspace = true |
||||
authors.workspace = true |
||||
license.workspace = true |
||||
name = "oxigraph_js" |
||||
version = "0.4.0-alpha" |
||||
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||
license = "MIT OR Apache-2.0" |
||||
readme = "README.md" |
||||
keywords = ["RDF", "N-Triples", "Turtle", "XML", "SPARQL"] |
||||
keywords = ["RDF", "N-Triples", "Turtle", "RDF/XML", "SPARQL"] |
||||
repository = "https://github.com/oxigraph/oxigraph/tree/main/js" |
||||
description = "JavaScript bindings of Oxigraph" |
||||
edition.workspace = true |
||||
rust-version.workspace = true |
||||
publish = false |
||||
edition = "2021" |
||||
|
||||
[lib] |
||||
crate-type = ["cdylib"] |
||||
name = "oxigraph" |
||||
doc = false |
||||
|
||||
[dependencies] |
||||
console_error_panic_hook.workspace = true |
||||
js-sys.workspace = true |
||||
oxigraph = { workspace = true, features = ["js"] } |
||||
wasm-bindgen.workspace = true |
||||
oxigraph = { version = "0.4.0-alpha", path="../lib" } |
||||
wasm-bindgen = "0.2" |
||||
js-sys = "0.3" |
||||
console_error_panic_hook = "0.1" |
||||
|
||||
[lints] |
||||
workspace = true |
||||
[dev-dependencies] |
||||
wasm-bindgen-test = "0.3" |
||||
|
@ -1,14 +0,0 @@ |
||||
{ |
||||
"$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", |
||||
"formatter": { |
||||
"indentStyle": "space", |
||||
"indentWidth": 4, |
||||
"lineWidth": 100 |
||||
}, |
||||
"linter": { |
||||
"ignore": ["pkg"] |
||||
}, |
||||
"organizeImports": { |
||||
"enabled": true |
||||
} |
||||
} |
@ -1,19 +1,31 @@ |
||||
#! /usr/bin/env node
|
||||
|
||||
const fs = require("node:fs"); |
||||
const pkg = JSON.parse(fs.readFileSync("./pkg/package.json")); |
||||
pkg.name = "oxigraph"; |
||||
pkg.main = "node.js"; |
||||
pkg.browser = "web.js"; |
||||
pkg.files = ["*.{js,wasm,d.ts}"]; |
||||
pkg.homepage = "https://github.com/oxigraph/oxigraph/tree/main/js"; |
||||
const fs = require('fs') |
||||
|
||||
// We copy file to the new directory
|
||||
fs.mkdirSync('pkg') |
||||
for (const file of fs.readdirSync('./pkg-web')) { |
||||
fs.copyFileSync('./pkg-web/' + file, './pkg/' + file) |
||||
} |
||||
for (const file of fs.readdirSync('./pkg-node')) { |
||||
fs.copyFileSync('./pkg-node/' + file, './pkg/' + file) |
||||
} |
||||
|
||||
const pkg = JSON.parse(fs.readFileSync('./pkg/package.json')) |
||||
pkg.name = 'oxigraph' |
||||
pkg.main = 'node.js' |
||||
pkg.browser = 'web.js' |
||||
pkg.files = [ |
||||
'*.{js,wasm,d.ts}' |
||||
] |
||||
pkg.homepage = 'https://github.com/oxigraph/oxigraph/tree/main/js' |
||||
pkg.bugs = { |
||||
url: "https://github.com/oxigraph/oxigraph/issues", |
||||
}; |
||||
pkg.collaborators = undefined; |
||||
url: 'https://github.com/oxigraph/oxigraph/issues' |
||||
} |
||||
pkg.collaborators = undefined |
||||
pkg.repository = { |
||||
type: "git", |
||||
url: "https://github.com/oxigraph/oxigraph.git", |
||||
directory: "js", |
||||
}; |
||||
fs.writeFileSync("./pkg/package.json", JSON.stringify(pkg, null, 2)); |
||||
type: 'git', |
||||
url: 'https://github.com/oxigraph/oxigraph.git', |
||||
directory: 'js' |
||||
} |
||||
fs.writeFileSync('./pkg/package.json', JSON.stringify(pkg, null, 2)) |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,52 +1,38 @@ |
||||
/* global describe, it */ |
||||
|
||||
import assert from "node:assert"; |
||||
import runTests from "../node_modules/@rdfjs/data-model/test/index.js"; |
||||
import oxigraph from "../pkg/oxigraph.js"; |
||||
import oxigraph from '../pkg/oxigraph.js' |
||||
import assert from 'assert' |
||||
import runTests from '../node_modules/@rdfjs/data-model/test/index.js' |
||||
|
||||
runTests({ factory: oxigraph }); |
||||
runTests({ factory: oxigraph }) |
||||
|
||||
describe("DataModel", () => { |
||||
describe("#toString()", () => { |
||||
it("namedNode().toString() should return SPARQL compatible syntax", () => { |
||||
assert.strictEqual( |
||||
"<http://example.com>", |
||||
oxigraph.namedNode("http://example.com").toString(), |
||||
); |
||||
}); |
||||
describe('DataModel', function () { |
||||
describe('#toString()', function () { |
||||
it('namedNode().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual('<http://example.com>', oxigraph.namedNode('http://example.com').toString()) |
||||
}) |
||||
|
||||
it("blankNode().toString() should return SPARQL compatible syntax", () => { |
||||
assert.strictEqual("_:a", oxigraph.blankNode("a").toString()); |
||||
}); |
||||
it('blankNode().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual('_:a', oxigraph.blankNode('a').toString()) |
||||
}) |
||||
|
||||
it("literal().toString() should return SPARQL compatible syntax", () => { |
||||
assert.strictEqual('"a\\"b"@en', oxigraph.literal('a"b', "en").toString()); |
||||
}); |
||||
it('literal().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual('"a\\"b"@en', oxigraph.literal('a"b', 'en').toString()) |
||||
}) |
||||
|
||||
it("defaultGraph().toString() should return SPARQL compatible syntax", () => { |
||||
assert.strictEqual("DEFAULT", oxigraph.defaultGraph().toString()); |
||||
}); |
||||
it('defaultGraph().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual('DEFAULT', oxigraph.defaultGraph().toString()) |
||||
}) |
||||
|
||||
it("variable().toString() should return SPARQL compatible syntax", () => { |
||||
assert.strictEqual("?a", oxigraph.variable("a").toString()); |
||||
}); |
||||
it('variable().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual('?a', oxigraph.variable('a').toString()) |
||||
}) |
||||
|
||||
it("quad().toString() should return SPARQL compatible syntax", () => { |
||||
it('quad().toString() should return SPARQL compatible syntax', function () { |
||||
assert.strictEqual( |
||||
"<http://example.com/s> <http://example.com/p> <<<http://example.com/s1> <http://example.com/p1> <http://example.com/o1>>> <http://example.com/g>", |
||||
oxigraph |
||||
.quad( |
||||
oxigraph.namedNode("http://example.com/s"), |
||||
oxigraph.namedNode("http://example.com/p"), |
||||
oxigraph.quad( |
||||
oxigraph.namedNode("http://example.com/s1"), |
||||
oxigraph.namedNode("http://example.com/p1"), |
||||
oxigraph.namedNode("http://example.com/o1"), |
||||
), |
||||
oxigraph.namedNode("http://example.com/g"), |
||||
'<http://example.com/s> <http://example.com/p> <<<http://example.com/s1> <http://example.com/p1> <http://example.com/o1>>> <http://example.com/g>', |
||||
oxigraph.quad(oxigraph.namedNode('http://example.com/s'), oxigraph.namedNode('http://example.com/p'), oxigraph.quad(oxigraph.namedNode('http://example.com/s1'), oxigraph.namedNode('http://example.com/p1'), oxigraph.namedNode('http://example.com/o1')), oxigraph.namedNode('http://example.com/g')).toString() |
||||
) |
||||
.toString(), |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -1,208 +1,161 @@ |
||||
/* global describe, it */ |
||||
|
||||
import assert from "node:assert"; |
||||
import dataModel from "@rdfjs/data-model"; |
||||
import { Store } from "../pkg/oxigraph.js"; |
||||
import { Store } from '../pkg/oxigraph.js' |
||||
import assert from 'assert' |
||||
import dataModel from '@rdfjs/data-model' |
||||
|
||||
const ex = dataModel.namedNode("http://example.com"); |
||||
const ex = dataModel.namedNode('http://example.com') |
||||
const triple = dataModel.quad( |
||||
dataModel.blankNode("s"), |
||||
dataModel.namedNode("http://example.com/p"), |
||||
dataModel.literal("o"), |
||||
); |
||||
|
||||
describe("Store", () => { |
||||
describe("#add()", () => { |
||||
it("an added quad should be in the store", () => { |
||||
const store = new Store(); |
||||
store.add(dataModel.quad(ex, ex, triple)); |
||||
assert(store.has(dataModel.quad(ex, ex, triple))); |
||||
}); |
||||
}); |
||||
|
||||
describe("#delete()", () => { |
||||
it("an removed quad should not be in the store anymore", () => { |
||||
const store = new Store([dataModel.quad(triple, ex, ex)]); |
||||
assert(store.has(dataModel.quad(triple, ex, ex))); |
||||
store.delete(dataModel.quad(triple, ex, ex)); |
||||
assert(!store.has(dataModel.quad(triple, ex, ex))); |
||||
}); |
||||
}); |
||||
|
||||
describe("#has()", () => { |
||||
it("an added quad should be in the store", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
assert(store.has(dataModel.quad(ex, ex, ex))); |
||||
}); |
||||
}); |
||||
|
||||
describe("#size()", () => { |
||||
it("A store with one quad should have 1 for size", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
assert.strictEqual(1, store.size); |
||||
}); |
||||
}); |
||||
|
||||
describe("#match_quads()", () => { |
||||
it("blank pattern should return all quads", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
const results = store.match(); |
||||
assert.strictEqual(1, results.length); |
||||
assert(dataModel.quad(ex, ex, ex).equals(results[0])); |
||||
}); |
||||
}); |
||||
|
||||
describe("#query()", () => { |
||||
it("ASK true", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
assert.strictEqual(true, store.query("ASK { ?s ?s ?s }")); |
||||
}); |
||||
|
||||
it("ASK false", () => { |
||||
const store = new Store(); |
||||
assert.strictEqual(false, store.query("ASK { FILTER(false)}")); |
||||
}); |
||||
|
||||
it("CONSTRUCT", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
const results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"); |
||||
assert.strictEqual(1, results.length); |
||||
assert(dataModel.quad(ex, ex, ex).equals(results[0])); |
||||
}); |
||||
|
||||
it("SELECT", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
const results = store.query("SELECT ?s WHERE { ?s ?p ?o }"); |
||||
assert.strictEqual(1, results.length); |
||||
assert(ex.equals(results[0].get("s"))); |
||||
}); |
||||
|
||||
it("SELECT with NOW()", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
const results = store.query( |
||||
"SELECT * WHERE { FILTER(2022 <= YEAR(NOW()) && YEAR(NOW()) <= 2100) }", |
||||
); |
||||
assert.strictEqual(1, results.length); |
||||
}); |
||||
|
||||
it("SELECT with RAND()", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
const results = store.query("SELECT (RAND() AS ?y) WHERE {}"); |
||||
assert.strictEqual(1, results.length); |
||||
}); |
||||
|
||||
it("SELECT with base IRI", () => { |
||||
const store = new Store(); |
||||
const results = store.query("SELECT * WHERE { BIND(<t> AS ?t) }", { |
||||
base_iri: "http://example.com/", |
||||
}); |
||||
assert.strictEqual(1, results.length); |
||||
}); |
||||
|
||||
it("SELECT with union graph", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]); |
||||
const results = store.query("SELECT * WHERE { ?s ?p ?o }", { |
||||
use_default_graph_as_union: true, |
||||
}); |
||||
assert.strictEqual(1, results.length); |
||||
}); |
||||
}); |
||||
|
||||
describe("#update()", () => { |
||||
it("INSERT DATA", () => { |
||||
const store = new Store(); |
||||
store.update( |
||||
"INSERT DATA { <http://example.com> <http://example.com> <http://example.com> }", |
||||
); |
||||
assert.strictEqual(1, store.size); |
||||
}); |
||||
|
||||
it("DELETE DATA", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
store.update( |
||||
"DELETE DATA { <http://example.com> <http://example.com> <http://example.com> }", |
||||
); |
||||
assert.strictEqual(0, store.size); |
||||
}); |
||||
|
||||
it("DELETE WHERE", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]); |
||||
store.update("DELETE WHERE { ?v ?v ?v }"); |
||||
assert.strictEqual(0, store.size); |
||||
}); |
||||
}); |
||||
|
||||
describe("#load()", () => { |
||||
it("load NTriples in the default graph", () => { |
||||
const store = new Store(); |
||||
store.load( |
||||
"<http://example.com> <http://example.com> <http://example.com> .", |
||||
"application/n-triples", |
||||
); |
||||
assert(store.has(dataModel.quad(ex, ex, ex))); |
||||
}); |
||||
|
||||
it("load NTriples in an other graph", () => { |
||||
const store = new Store(); |
||||
store.load( |
||||
"<http://example.com> <http://example.com> <http://example.com> .", |
||||
"application/n-triples", |
||||
null, |
||||
ex, |
||||
); |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))); |
||||
}); |
||||
|
||||
it("load Turtle with a base IRI", () => { |
||||
const store = new Store(); |
||||
store.load( |
||||
"<http://example.com> <http://example.com> <> .", |
||||
"text/turtle", |
||||
"http://example.com", |
||||
); |
||||
assert(store.has(dataModel.quad(ex, ex, ex))); |
||||
}); |
||||
|
||||
it("load NQuads", () => { |
||||
const store = new Store(); |
||||
store.load( |
||||
"<http://example.com> <http://example.com> <http://example.com> <http://example.com> .", |
||||
"application/n-quads", |
||||
); |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))); |
||||
}); |
||||
|
||||
it("load TriG with a base IRI", () => { |
||||
const store = new Store(); |
||||
store.load( |
||||
"GRAPH <> { <http://example.com> <http://example.com> <> }", |
||||
"application/trig", |
||||
"http://example.com", |
||||
); |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))); |
||||
}); |
||||
}); |
||||
|
||||
describe("#dump()", () => { |
||||
it("dump dataset content", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]); |
||||
assert.strictEqual( |
||||
"<http://example.com> <http://example.com> <http://example.com> <http://example.com> .\n", |
||||
store.dump("application/n-quads"), |
||||
); |
||||
}); |
||||
|
||||
it("dump named graph content", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]); |
||||
assert.strictEqual( |
||||
"<http://example.com> <http://example.com> <http://example.com> .\n", |
||||
store.dump("application/n-triples", ex), |
||||
); |
||||
}); |
||||
|
||||
it("dump default graph content", () => { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]); |
||||
assert.strictEqual("", store.dump("application/n-triples", dataModel.defaultGraph())); |
||||
}); |
||||
}); |
||||
}); |
||||
dataModel.blankNode('s'), |
||||
dataModel.namedNode('http://example.com/p'), |
||||
dataModel.literal('o') |
||||
) |
||||
|
||||
describe('Store', function () { |
||||
describe('#add()', function () { |
||||
it('an added quad should be in the store', function () { |
||||
const store = new Store() |
||||
store.add(dataModel.quad(ex, ex, triple)) |
||||
assert(store.has(dataModel.quad(ex, ex, triple))) |
||||
}) |
||||
}) |
||||
|
||||
describe('#delete()', function () { |
||||
it('an removed quad should not be in the store anymore', function () { |
||||
const store = new Store([dataModel.quad(triple, ex, ex)]) |
||||
assert(store.has(dataModel.quad(triple, ex, ex))) |
||||
store.delete(dataModel.quad(triple, ex, ex)) |
||||
assert(!store.has(dataModel.quad(triple, ex, ex))) |
||||
}) |
||||
}) |
||||
|
||||
describe('#has()', function () { |
||||
it('an added quad should be in the store', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
assert(store.has(dataModel.quad(ex, ex, ex))) |
||||
}) |
||||
}) |
||||
|
||||
describe('#size()', function () { |
||||
it('A store with one quad should have 1 for size', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
assert.strictEqual(1, store.size) |
||||
}) |
||||
}) |
||||
|
||||
describe('#match_quads()', function () { |
||||
it('blank pattern should return all quads', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
const results = store.match() |
||||
assert.strictEqual(1, results.length) |
||||
assert(dataModel.quad(ex, ex, ex).equals(results[0])) |
||||
}) |
||||
}) |
||||
|
||||
describe('#query()', function () { |
||||
it('ASK true', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
assert.strictEqual(true, store.query('ASK { ?s ?s ?s }')) |
||||
}) |
||||
|
||||
it('ASK false', function () { |
||||
const store = new Store() |
||||
assert.strictEqual(false, store.query('ASK { FILTER(false)}')) |
||||
}) |
||||
|
||||
it('CONSTRUCT', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
const results = store.query('CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }') |
||||
assert.strictEqual(1, results.length) |
||||
assert(dataModel.quad(ex, ex, ex).equals(results[0])) |
||||
}) |
||||
|
||||
it('SELECT', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
const results = store.query('SELECT ?s WHERE { ?s ?p ?o }') |
||||
assert.strictEqual(1, results.length) |
||||
assert(ex.equals(results[0].get('s'))) |
||||
}) |
||||
|
||||
it('SELECT with NOW()', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
const results = store.query('SELECT (YEAR(NOW()) AS ?y) WHERE {}') |
||||
assert.strictEqual(1, results.length) |
||||
}) |
||||
|
||||
it('SELECT with RAND()', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
const results = store.query('SELECT (RAND() AS ?y) WHERE {}') |
||||
assert.strictEqual(1, results.length) |
||||
}) |
||||
}) |
||||
|
||||
describe('#update()', function () { |
||||
it('INSERT DATA', function () { |
||||
const store = new Store() |
||||
store.update('INSERT DATA { <http://example.com> <http://example.com> <http://example.com> }') |
||||
assert.strictEqual(1, store.size) |
||||
}) |
||||
|
||||
it('DELETE DATA', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
store.update('DELETE DATA { <http://example.com> <http://example.com> <http://example.com> }') |
||||
assert.strictEqual(0, store.size) |
||||
}) |
||||
|
||||
it('DELETE WHERE', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex)]) |
||||
store.update('DELETE WHERE { ?v ?v ?v }') |
||||
assert.strictEqual(0, store.size) |
||||
}) |
||||
}) |
||||
|
||||
describe('#load()', function () { |
||||
it('load NTriples in the default graph', function () { |
||||
const store = new Store() |
||||
store.load('<http://example.com> <http://example.com> <http://example.com> .', 'application/n-triples') |
||||
assert(store.has(dataModel.quad(ex, ex, ex))) |
||||
}) |
||||
|
||||
it('load NTriples in an other graph', function () { |
||||
const store = new Store() |
||||
store.load('<http://example.com> <http://example.com> <http://example.com> .', 'application/n-triples', null, ex) |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))) |
||||
}) |
||||
|
||||
it('load Turtle with a base IRI', function () { |
||||
const store = new Store() |
||||
store.load('<http://example.com> <http://example.com> <> .', 'text/turtle', 'http://example.com') |
||||
assert(store.has(dataModel.quad(ex, ex, ex))) |
||||
}) |
||||
|
||||
it('load NQuads', function () { |
||||
const store = new Store() |
||||
store.load('<http://example.com> <http://example.com> <http://example.com> <http://example.com> .', 'application/n-quads') |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))) |
||||
}) |
||||
|
||||
it('load TriG with a base IRI', function () { |
||||
const store = new Store() |
||||
store.load('GRAPH <> { <http://example.com> <http://example.com> <> }', 'application/trig', 'http://example.com') |
||||
assert(store.has(dataModel.quad(ex, ex, ex, ex))) |
||||
}) |
||||
}) |
||||
|
||||
describe('#dump()', function () { |
||||
it('dump dataset content', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]) |
||||
assert.strictEqual('<http://example.com> <http://example.com> <http://example.com> <http://example.com> .\n', store.dump('application/n-quads')) |
||||
}) |
||||
|
||||
it('dump named graph content', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]) |
||||
assert.strictEqual('<http://example.com> <http://example.com> <http://example.com> .\n', store.dump('application/n-triples', ex)) |
||||
}) |
||||
|
||||
it('dump default graph content', function () { |
||||
const store = new Store([dataModel.quad(ex, ex, ex, ex)]) |
||||
assert.strictEqual('', store.dump('application/n-triples')) |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1,63 @@ |
||||
[package] |
||||
name = "oxigraph" |
||||
version = "0.4.0-alpha" |
||||
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||
license = "MIT OR Apache-2.0" |
||||
readme = "README.md" |
||||
keywords = ["RDF", "SPARQL", "graph-database", "database"] |
||||
categories = ["database-implementations"] |
||||
repository = "https://github.com/oxigraph/oxigraph/tree/main/lib" |
||||
homepage = "https://oxigraph.org/" |
||||
description = """ |
||||
a SPARQL database and RDF toolkit |
||||
""" |
||||
edition = "2021" |
||||
|
||||
[package.metadata.docs.rs] |
||||
all-features = true |
||||
|
||||
[features] |
||||
default = [] |
||||
http_client = ["oxhttp", "oxhttp/rustls"] |
||||
|
||||
[dependencies] |
||||
rand = "0.8" |
||||
md-5 = "0.10" |
||||
sha-1 = "0.10" |
||||
sha2 = "0.10" |
||||
digest = "0.10" |
||||
regex = "1" |
||||
oxilangtag = "0.1" |
||||
oxiri = "0.2" |
||||
rio_api = "0.7" |
||||
rio_turtle = "0.7" |
||||
rio_xml = "0.7" |
||||
hex = "0.4" |
||||
nom = "7" |
||||
siphasher = "0.3" |
||||
lazy_static = "1" |
||||
sysinfo = "0.26" |
||||
oxrdf = { version = "0.1.0", path="oxrdf", features = ["rdf-star"] } |
||||
spargebra = { version = "0.3.0-alpha", path="spargebra", features = ["rdf-star", "ex-lateral"] } |
||||
sparesults = { version = "0.1.1", path="sparesults", features = ["rdf-star"] } |
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] |
||||
libc = "0.2" |
||||
oxrocksdb-sys = { version = "0.3.7", path="../oxrocksdb-sys" } |
||||
oxhttp = { version = "0.1", optional = true } |
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies] |
||||
js-sys = "0.3" |
||||
getrandom = {version="0.2", features=["js"]} |
||||
|
||||
[dev-dependencies] |
||||
criterion = "0.4" |
||||
oxhttp = "0.1" |
||||
zstd = "0.11" |
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies] |
||||
wasm-bindgen-test = "0.3" |
||||
|
||||
[[bench]] |
||||
name = "store" |
||||
harness = false |
@ -1,13 +1,72 @@ |
||||
Oxigraph Rust crates |
||||
==================== |
||||
|
||||
Oxigraph is implemented in Rust. |
||||
It is composed on a main library, [`oxigraph`](./oxigraph) and a set of smaller crates used by the `oxigraph` crate: |
||||
* [`oxrdf`](./oxrdf), datastructures encoding RDF basic concepts (the `model` module of the `oxigraph` crate). |
||||
* [`oxrdfio`](./oxrdfio), a unified parser and serializer API for RDF formats (the `io` module of the `oxigraph` crate). It itself relies on: |
||||
* [`oxttl`](./oxttl), N-Triple, N-Quad, Turtle, TriG and N3 parsing and serialization. |
||||
* [`oxrdfxml`](./oxrdfxml), RDF/XML parsing and serialization. |
||||
* [`spargebra`](./spargebra), a SPARQL parser. |
||||
* [`sparesults`](./sparesults), parsers and serializers for SPARQL result formats (the `sparql::results` module of the `oxigraph` crate). |
||||
* [`sparopt`](./sparesults), a SPARQL optimizer. |
||||
* [`oxsdatatypes`](./oxsdatatypes), an implementation of some XML Schema datatypes. |
||||
Oxigraph |
||||
======== |
||||
|
||||
[![Latest Version](https://img.shields.io/crates/v/oxigraph.svg)](https://crates.io/crates/oxigraph) |
||||
[![Released API docs](https://docs.rs/oxigraph/badge.svg)](https://docs.rs/oxigraph) |
||||
[![Crates.io downloads](https://img.shields.io/crates/d/oxigraph)](https://crates.io/crates/oxigraph) |
||||
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) |
||||
|
||||
Oxigraph is a graph database library implementing the [SPARQL](https://www.w3.org/TR/sparql11-overview/) standard. |
||||
|
||||
Its goal is to provide a compliant, safe and fast on-disk graph database. |
||||
It also provides a set of utility functions for reading, writing, and processing RDF files. |
||||
|
||||
Oxigraph is in heavy development and SPARQL query evaluation has not been optimized yet. |
||||
|
||||
Oxigraph also provides [a standalone HTTP server](https://crates.io/crates/oxigraph_server) and [a Python library](https://pyoxigraph.readthedocs.io/) based on this library. |
||||
|
||||
|
||||
Oxigraph implements the following specifications: |
||||
* [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/), [SPARQL 1.1 Update](https://www.w3.org/TR/sparql11-update/), and [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/). |
||||
* [Turtle](https://www.w3.org/TR/turtle/), [TriG](https://www.w3.org/TR/trig/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/), and [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/) RDF serialization formats for both data ingestion and retrieval using the [Rio library](https://github.com/oxigraph/rio). |
||||
* [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/), [SPARQL 1.1 Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) and [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/). |
||||
|
||||
A preliminary benchmark [is provided](../bench/README.md). Oxigraph internal design [is described on the wiki](https://github.com/oxigraph/oxigraph/wiki/Architecture). |
||||
|
||||
The main entry point of Oxigraph is the [`Store`](store::Store) struct: |
||||
```rust |
||||
use oxigraph::store::Store; |
||||
use oxigraph::model::*; |
||||
use oxigraph::sparql::QueryResults; |
||||
|
||||
let store = Store::new().unwrap(); |
||||
|
||||
// insertion |
||||
let ex = NamedNode::new("http://example.com").unwrap(); |
||||
let quad = Quad::new(ex.clone(), ex.clone(), ex.clone(), GraphName::DefaultGraph); |
||||
store.insert(&quad).unwrap(); |
||||
|
||||
// quad filter |
||||
let results = store.quads_for_pattern(Some(ex.as_ref().into()), None, None, None).collect::<Result<Vec<Quad>,_>>().unwrap(); |
||||
assert_eq!(vec![quad], results); |
||||
|
||||
// SPARQL query |
||||
if let QueryResults::Solutions(mut solutions) = store.query("SELECT ?s WHERE { ?s ?p ?o }").unwrap() { |
||||
assert_eq!(solutions.next().unwrap().unwrap().get("s"), Some(&ex.into())); |
||||
} |
||||
``` |
||||
|
||||
Some parts of this library are available as standalone crates: |
||||
* [`oxrdf`](https://crates.io/crates/oxrdf) provides datastructures encoding RDF basic concepts (the `oxigraph::model` module). |
||||
* [`spargebra`](https://crates.io/crates/spargebra) provides a SPARQL parser. |
||||
* [`sparesults`](https://crates.io/crates/sparesults) provides parsers and serializers for SPARQL result formats. |
||||
|
||||
To build the library, don't forget to clone the submodules using `git clone --recursive https://github.com/oxigraph/oxigraph.git` to clone the repository including submodules or `git submodule update --init` to add submodules to the already cloned repository. |
||||
|
||||
|
||||
## License |
||||
|
||||
This project is licensed under either of |
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or |
||||
`<http://www.apache.org/licenses/LICENSE-2.0>`) |
||||
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or |
||||
`<http://opensource.org/licenses/MIT>`) |
||||
|
||||
at your option. |
||||
|
||||
|
||||
### Contribution |
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. |
||||
|
@ -0,0 +1,208 @@ |
||||
use criterion::{criterion_group, criterion_main, Criterion, Throughput}; |
||||
use oxhttp::model::{Method, Request, Status}; |
||||
use oxigraph::io::GraphFormat; |
||||
use oxigraph::model::GraphNameRef; |
||||
use oxigraph::sparql::{Query, QueryResults, Update}; |
||||
use oxigraph::store::Store; |
||||
use rand::random; |
||||
use std::env::temp_dir; |
||||
use std::fs::{remove_dir_all, File}; |
||||
use std::io::{BufRead, BufReader, Cursor, Read}; |
||||
use std::path::{Path, PathBuf}; |
||||
|
||||
fn store_load(c: &mut Criterion) { |
||||
{ |
||||
let mut data = Vec::new(); |
||||
read_data("explore-1000.nt.zst") |
||||
.read_to_end(&mut data) |
||||
.unwrap(); |
||||
|
||||
let mut group = c.benchmark_group("store load"); |
||||
group.throughput(Throughput::Bytes(data.len() as u64)); |
||||
group.sample_size(10); |
||||
group.bench_function("load BSBM explore 1000 in memory", |b| { |
||||
b.iter(|| { |
||||
let store = Store::new().unwrap(); |
||||
do_load(&store, &data); |
||||
}) |
||||
}); |
||||
group.bench_function("load BSBM explore 1000 in on disk", |b| { |
||||
b.iter(|| { |
||||
let path = TempDir::default(); |
||||
let store = Store::open(&path.0).unwrap(); |
||||
do_load(&store, &data); |
||||
}) |
||||
}); |
||||
group.bench_function("load BSBM explore 1000 in on disk with bulk load", |b| { |
||||
b.iter(|| { |
||||
let path = TempDir::default(); |
||||
let store = Store::open(&path.0).unwrap(); |
||||
do_bulk_load(&store, &data); |
||||
}) |
||||
}); |
||||
} |
||||
|
||||
{ |
||||
let mut data = Vec::new(); |
||||
read_data("explore-10000.nt.zst") |
||||
.read_to_end(&mut data) |
||||
.unwrap(); |
||||
|
||||
let mut group = c.benchmark_group("store load large"); |
||||
group.throughput(Throughput::Bytes(data.len() as u64)); |
||||
group.sample_size(10); |
||||
group.bench_function("load BSBM explore 10000 in on disk with bulk load", |b| { |
||||
b.iter(|| { |
||||
let path = TempDir::default(); |
||||
let store = Store::open(&path.0).unwrap(); |
||||
do_bulk_load(&store, &data); |
||||
}) |
||||
}); |
||||
} |
||||
} |
||||
|
||||
fn do_load(store: &Store, data: &[u8]) { |
||||
store |
||||
.load_graph( |
||||
Cursor::new(&data), |
||||
GraphFormat::NTriples, |
||||
GraphNameRef::DefaultGraph, |
||||
None, |
||||
) |
||||
.unwrap(); |
||||
store.optimize().unwrap(); |
||||
} |
||||
|
||||
fn do_bulk_load(store: &Store, data: &[u8]) { |
||||
store |
||||
.bulk_loader() |
||||
.load_graph( |
||||
Cursor::new(&data), |
||||
GraphFormat::NTriples, |
||||
GraphNameRef::DefaultGraph, |
||||
None, |
||||
) |
||||
.unwrap(); |
||||
store.optimize().unwrap(); |
||||
} |
||||
|
||||
fn store_query_and_update(c: &mut Criterion) { |
||||
let mut data = Vec::new(); |
||||
read_data("explore-1000.nt.zst") |
||||
.read_to_end(&mut data) |
||||
.unwrap(); |
||||
|
||||
let operations = read_data("mix-exploreAndUpdate-1000.tsv.zst") |
||||
.lines() |
||||
.map(|l| { |
||||
let l = l.unwrap(); |
||||
let mut parts = l.trim().split('\t'); |
||||
let kind = parts.next().unwrap(); |
||||
let operation = parts.next().unwrap(); |
||||
match kind { |
||||
"query" => Operation::Query(Query::parse(operation, None).unwrap()), |
||||
"update" => Operation::Update(Update::parse(operation, None).unwrap()), |
||||
_ => panic!("Unexpected operation kind {}", kind), |
||||
} |
||||
}) |
||||
.collect::<Vec<_>>(); |
||||
let query_operations = operations |
||||
.iter() |
||||
.filter(|o| matches!(o, Operation::Query(_))) |
||||
.cloned() |
||||
.collect::<Vec<_>>(); |
||||
|
||||
let mut group = c.benchmark_group("store operations"); |
||||
group.throughput(Throughput::Elements(operations.len() as u64)); |
||||
group.sample_size(10); |
||||
|
||||
{ |
||||
let memory_store = Store::new().unwrap(); |
||||
do_bulk_load(&memory_store, &data); |
||||
group.bench_function("BSBM explore 1000 query in memory", |b| { |
||||
b.iter(|| run_operation(&memory_store, &query_operations)) |
||||
}); |
||||
group.bench_function("BSBM explore 1000 queryAndUpdate in memory", |b| { |
||||
b.iter(|| run_operation(&memory_store, &operations)) |
||||
}); |
||||
} |
||||
|
||||
{ |
||||
let path = TempDir::default(); |
||||
let disk_store = Store::open(&path.0).unwrap(); |
||||
do_bulk_load(&disk_store, &data); |
||||
group.bench_function("BSBM explore 1000 query on disk", |b| { |
||||
b.iter(|| run_operation(&disk_store, &query_operations)) |
||||
}); |
||||
group.bench_function("BSBM explore 1000 queryAndUpdate on disk", |b| { |
||||
b.iter(|| run_operation(&disk_store, &operations)) |
||||
}); |
||||
} |
||||
} |
||||
|
||||
fn run_operation(store: &Store, operations: &[Operation]) { |
||||
for operation in operations { |
||||
match operation { |
||||
Operation::Query(q) => match store.query(q.clone()).unwrap() { |
||||
QueryResults::Boolean(_) => (), |
||||
QueryResults::Solutions(s) => { |
||||
for s in s { |
||||
s.unwrap(); |
||||
} |
||||
} |
||||
QueryResults::Graph(g) => { |
||||
for t in g { |
||||
t.unwrap(); |
||||
} |
||||
} |
||||
}, |
||||
Operation::Update(u) => store.update(u.clone()).unwrap(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
criterion_group!(store, store_query_and_update, store_load); |
||||
|
||||
criterion_main!(store); |
||||
|
||||
fn read_data(file: &str) -> impl BufRead { |
||||
if !Path::new(file).exists() { |
||||
let mut client = oxhttp::Client::new(); |
||||
client.set_redirection_limit(5); |
||||
let url = format!( |
||||
"https://github.com/Tpt/bsbm-tools/releases/download/v0.2/{}", |
||||
file |
||||
); |
||||
let request = Request::builder(Method::GET, url.parse().unwrap()).build(); |
||||
let response = client.request(request).unwrap(); |
||||
assert_eq!( |
||||
response.status(), |
||||
Status::OK, |
||||
"{}", |
||||
response.into_body().to_string().unwrap() |
||||
); |
||||
std::io::copy(&mut response.into_body(), &mut File::create(file).unwrap()).unwrap(); |
||||
} |
||||
BufReader::new(zstd::Decoder::new(File::open(file).unwrap()).unwrap()) |
||||
} |
||||
|
||||
#[allow(clippy::large_enum_variant)] |
||||
#[derive(Clone)] |
||||
enum Operation { |
||||
Query(Query), |
||||
Update(Update), |
||||
} |
||||
|
||||
struct TempDir(PathBuf); |
||||
|
||||
impl Default for TempDir { |
||||
fn default() -> Self { |
||||
Self(temp_dir().join(format!("oxigraph-bench-{}", random::<u128>()))) |
||||
} |
||||
} |
||||
|
||||
impl Drop for TempDir { |
||||
fn drop(&mut self) { |
||||
remove_dir_all(&self.0).unwrap() |
||||
} |
||||
} |
@ -1,59 +0,0 @@ |
||||
[package] |
||||
name = "oxigraph" |
||||
version.workspace = true |
||||
authors.workspace = true |
||||
license.workspace = true |
||||
readme = "README.md" |
||||
keywords = ["RDF", "SPARQL", "graph-database", "database"] |
||||
categories = ["database-implementations"] |
||||
repository = "https://github.com/oxigraph/oxigraph/tree/main/lib/oxigraph" |
||||
homepage = "https://oxigraph.org/" |
||||
documentation = "https://docs.rs/oxigraph" |
||||
description = """ |
||||
a SPARQL database and RDF toolkit |
||||
""" |
||||
edition.workspace = true |
||||
rust-version.workspace = true |
||||
|
||||
[features] |
||||
js = ["getrandom/js", "oxsdatatypes/js", "js-sys"] |
||||
|
||||
|
||||
[dependencies] |
||||
digest.workspace = true |
||||
hex.workspace = true |
||||
json-event-parser.workspace = true |
||||
md-5.workspace = true |
||||
oxilangtag.workspace = true |
||||
oxiri.workspace = true |
||||
oxrdf = { workspace = true, features = ["rdf-star", "oxsdatatypes"] } |
||||
oxrdfio = { workspace = true, features = ["rdf-star"] } |
||||
oxsdatatypes.workspace = true |
||||
rand.workspace = true |
||||
regex.workspace = true |
||||
sha1.workspace = true |
||||
sha2.workspace = true |
||||
siphasher.workspace = true |
||||
sparesults = { workspace = true, features = ["rdf-star"] } |
||||
spargebra = { workspace = true, features = ["rdf-star", "sep-0002", "sep-0006"] } |
||||
sparopt = { workspace = true, features = ["rdf-star", "sep-0002", "sep-0006"] } |
||||
thiserror.workspace = true |
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies] |
||||
libc = "0.2" |
||||
rocksdb.workspace = true |
||||
|
||||
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] |
||||
getrandom.workspace = true |
||||
js-sys = { workspace = true, optional = true } |
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] |
||||
codspeed-criterion-compat.workspace = true |
||||
zstd.workspace = true |
||||
|
||||
[lints] |
||||
workspace = true |
||||
|
||||
[package.metadata.docs.rs] |
||||
rustdoc-args = ["--cfg", "docsrs"] |
||||
|
@ -1,82 +0,0 @@ |
||||
Oxigraph |
||||
======== |
||||
|
||||
[![Latest Version](https://img.shields.io/crates/v/oxigraph.svg)](https://crates.io/crates/oxigraph) |
||||
[![Released API docs](https://docs.rs/oxigraph/badge.svg)](https://docs.rs/oxigraph) |
||||
[![Crates.io downloads](https://img.shields.io/crates/d/oxigraph)](https://crates.io/crates/oxigraph) |
||||
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community) |
||||
|
||||
Oxigraph is a graph database library implementing the [SPARQL](https://www.w3.org/TR/sparql11-overview/) standard. |
||||
|
||||
Its goal is to provide a compliant, safe and fast on-disk graph database. |
||||
It also provides a set of utility functions for reading, writing, and processing RDF files. |
||||
|
||||
Oxigraph is in heavy development and SPARQL query evaluation has not been optimized yet. |
||||
|
||||
Oxigraph also provides [a CLI tool](https://crates.io/crates/oxigraph-cli) and [a Python library](https://pyoxigraph.readthedocs.io/) based on this library. |
||||
|
||||
|
||||
Oxigraph implements the following specifications: |
||||
* [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/), [SPARQL 1.1 Update](https://www.w3.org/TR/sparql11-update/), and [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/). |
||||
* [Turtle](https://www.w3.org/TR/turtle/), [TriG](https://www.w3.org/TR/trig/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/), and [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) RDF serialization formats for both data ingestion and retrieval. |
||||
* [SPARQL Query Results XML Format](https://www.w3.org/TR/rdf-sparql-XMLres/), [SPARQL 1.1 Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) and [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/). |
||||
|
||||
A preliminary benchmark [is provided](../bench/README.md). Oxigraph internal design [is described on the wiki](https://github.com/oxigraph/oxigraph/wiki/Architecture). |
||||
|
||||
The main entry point of Oxigraph is the [`Store`](store::Store) struct: |
||||
```rust |
||||
use oxigraph::store::Store; |
||||
use oxigraph::model::*; |
||||
use oxigraph::sparql::QueryResults; |
||||
|
||||
let store = Store::new().unwrap(); |
||||
|
||||
// insertion |
||||
let ex = NamedNode::new("http://example.com").unwrap(); |
||||
let quad = Quad::new(ex.clone(), ex.clone(), ex.clone(), GraphName::DefaultGraph); |
||||
store.insert(&quad).unwrap(); |
||||
|
||||
// quad filter |
||||
let results = store.quads_for_pattern(Some(ex.as_ref().into()), None, None, None).collect::<Result<Vec<Quad>,_>>().unwrap(); |
||||
assert_eq!(vec![quad], results); |
||||
|
||||
// SPARQL query |
||||
if let QueryResults::Solutions(mut solutions) = store.query("SELECT ?s WHERE { ?s ?p ?o }").unwrap() { |
||||
assert_eq!(solutions.next().unwrap().unwrap().get("s"), Some(&ex.into())); |
||||
} |
||||
``` |
||||
|
||||
It is based on these crates that can be used separately: |
||||
* [`oxrdf`](https://crates.io/crates/oxrdf), datastructures encoding RDF basic concepts (the [`oxigraph::model`](crate::model) module). |
||||
* [`oxrdfio`](https://crates.io/crates/oxrdfio), a unified parser and serializer API for RDF formats (the [`oxigraph::io`](crate::io) module). It itself relies on: |
||||
* [`oxttl`](https://crates.io/crates/oxttl), N-Triple, N-Quad, Turtle, TriG and N3 parsing and serialization. |
||||
* [`oxrdfxml`](https://crates.io/crates/oxrdfxml), RDF/XML parsing and serialization. |
||||
* [`spargebra`](https://crates.io/crates/spargebra), a SPARQL parser. |
||||
* [`sparesults`](https://crates.io/crates/sparesults), parsers and serializers for SPARQL result formats (the [`oxigraph::sparql::results`](crate::sparql::results) module). |
||||
* [`sparopt`](https://crates.io/crates/sparesults), a SPARQL optimizer. |
||||
* [`oxsdatatypes`](https://crates.io/crates/oxsdatatypes), an implementation of some XML Schema datatypes. |
||||
|
||||
To build the library locally, don't forget to clone the submodules using `git clone --recursive https://github.com/oxigraph/oxigraph.git` to clone the repository including submodules or `git submodule update --init` to add submodules to the already cloned repository. |
||||
|
||||
It is possible to disable the RocksDB storage backend to only use the in-memory fallback by disabling the `rocksdb` default feature: |
||||
```toml |
||||
oxigraph = { version = "*", default-features = false } |
||||
``` |
||||
This is the default behavior when compiling Oxigraph to WASM. |
||||
|
||||
## License |
||||
|
||||
This project is licensed under either of |
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or |
||||
`<http://www.apache.org/licenses/LICENSE-2.0>`) |
||||
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or |
||||
`<http://opensource.org/licenses/MIT>`) |
||||
|
||||
at your option. |
||||
|
||||
|
||||
### Contribution |
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. |
@ -1,39 +0,0 @@ |
||||
//! Utilities to read and write RDF graphs and datasets using [OxRDF I/O](https://crates.io/crates/oxrdfio).
|
||||
//!
|
||||
//! The entry points of this module are the two [`RdfParser`] and [`RdfSerializer`] structs.
|
||||
//!
|
||||
//! Usage example converting a Turtle file to a N-Triples file:
|
||||
//! ```
|
||||
//! use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
|
||||
//!
|
||||
//! let turtle_file = b"@base <http://example.com/> .
|
||||
//! @prefix schema: <http://schema.org/> .
|
||||
//! <foo> a schema:Person ;
|
||||
//! schema:name \"Foo\" .
|
||||
//! <bar> a schema:Person ;
|
||||
//! schema:name \"Bar\" .";
|
||||
//!
|
||||
//! let ntriples_file = b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
|
||||
//! <http://example.com/foo> <http://schema.org/name> \"Foo\" .
|
||||
//! <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
|
||||
//! <http://example.com/bar> <http://schema.org/name> \"Bar\" .
|
||||
//! ";
|
||||
//!
|
||||
//! let mut writer = RdfSerializer::from_format(RdfFormat::NTriples).serialize_to_write(Vec::new());
|
||||
//! for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(turtle_file.as_ref()) {
|
||||
//! writer.write_quad(&quad.unwrap()).unwrap();
|
||||
//! }
|
||||
//! assert_eq!(writer.finish().unwrap(), ntriples_file);
|
||||
//! ```
|
||||
|
||||
mod format; |
||||
pub mod read; |
||||
pub mod write; |
||||
|
||||
#[allow(deprecated)] |
||||
pub use self::format::{DatasetFormat, GraphFormat}; |
||||
#[allow(deprecated)] |
||||
pub use self::read::{DatasetParser, GraphParser}; |
||||
#[allow(deprecated)] |
||||
pub use self::write::{DatasetSerializer, GraphSerializer}; |
||||
pub use oxrdfio::*; |
@ -1,199 +0,0 @@ |
||||
#![allow(deprecated)] |
||||
|
||||
//! Utilities to read RDF graphs and datasets.
|
||||
|
||||
use crate::io::{DatasetFormat, GraphFormat}; |
||||
use crate::model::*; |
||||
use oxrdfio::{FromReadQuadReader, RdfParseError, RdfParser}; |
||||
use std::io::Read; |
||||
|
||||
/// Parsers for RDF graph serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N-Triples](https://www.w3.org/TR/n-triples/) ([`GraphFormat::NTriples`])
|
||||
/// * [Turtle](https://www.w3.org/TR/turtle/) ([`GraphFormat::Turtle`])
|
||||
/// * [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) ([`GraphFormat::RdfXml`])
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{GraphFormat, GraphParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = GraphParser::from_format(GraphFormat::NTriples);
|
||||
/// let triples = parser
|
||||
/// .read_triples(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(triples.len(), 1);
|
||||
/// assert_eq!(triples[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[deprecated(note = "use RdfParser instead", since = "0.4.0")] |
||||
pub struct GraphParser { |
||||
inner: RdfParser, |
||||
} |
||||
|
||||
impl GraphParser { |
||||
/// Builds a parser for the given format.
|
||||
#[inline] |
||||
pub fn from_format(format: GraphFormat) -> Self { |
||||
Self { |
||||
inner: RdfParser::from_format(format.into()) |
||||
.without_named_graphs() |
||||
.rename_blank_nodes(), |
||||
} |
||||
} |
||||
|
||||
/// Provides an IRI that could be used to resolve the file relative IRIs.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{GraphFormat, GraphParser};
|
||||
///
|
||||
/// let file = "</s> </p> </o> .";
|
||||
///
|
||||
/// let parser =
|
||||
/// GraphParser::from_format(GraphFormat::Turtle).with_base_iri("http://example.com")?;
|
||||
/// let triples = parser
|
||||
/// .read_triples(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(triples.len(), 1);
|
||||
/// assert_eq!(triples[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn with_base_iri(self, base_iri: impl Into<String>) -> Result<Self, IriParseError> { |
||||
Ok(Self { |
||||
inner: self.inner.with_base_iri(base_iri)?, |
||||
}) |
||||
} |
||||
|
||||
/// Executes the parsing itself on a [`Read`] implementation and returns an iterator of triples.
|
||||
pub fn read_triples<R: Read>(self, reader: R) -> TripleReader<R> { |
||||
TripleReader { |
||||
parser: self.inner.parse_read(reader), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator yielding read triples.
|
||||
/// Could be built using a [`GraphParser`].
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{GraphFormat, GraphParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = GraphParser::from_format(GraphFormat::NTriples);
|
||||
/// let triples = parser
|
||||
/// .read_triples(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(triples.len(), 1);
|
||||
/// assert_eq!(triples[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct TripleReader<R: Read> { |
||||
parser: FromReadQuadReader<R>, |
||||
} |
||||
|
||||
impl<R: Read> Iterator for TripleReader<R> { |
||||
type Item = Result<Triple, RdfParseError>; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
Some(self.parser.next()?.map(Into::into).map_err(Into::into)) |
||||
} |
||||
} |
||||
|
||||
/// A parser for RDF dataset serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N-Quads](https://www.w3.org/TR/n-quads/) ([`DatasetFormat::NQuads`])
|
||||
/// * [TriG](https://www.w3.org/TR/trig/) ([`DatasetFormat::TriG`])
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{DatasetFormat, DatasetParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .";
|
||||
///
|
||||
/// let parser = DatasetParser::from_format(DatasetFormat::NQuads);
|
||||
/// let quads = parser.read_quads(file.as_bytes()).collect::<Result<Vec<_>,_>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[deprecated(note = "use RdfParser instead", since = "0.4.0")] |
||||
pub struct DatasetParser { |
||||
inner: RdfParser, |
||||
} |
||||
|
||||
impl DatasetParser { |
||||
/// Builds a parser for the given format.
|
||||
#[inline] |
||||
pub fn from_format(format: DatasetFormat) -> Self { |
||||
Self { |
||||
inner: RdfParser::from_format(format.into()).rename_blank_nodes(), |
||||
} |
||||
} |
||||
|
||||
/// Provides an IRI that could be used to resolve the file relative IRIs.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{DatasetFormat, DatasetParser};
|
||||
///
|
||||
/// let file = "<g> { </s> </p> </o> }";
|
||||
///
|
||||
/// let parser =
|
||||
/// DatasetParser::from_format(DatasetFormat::TriG).with_base_iri("http://example.com")?;
|
||||
/// let triples = parser
|
||||
/// .read_quads(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(triples.len(), 1);
|
||||
/// assert_eq!(triples[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn with_base_iri(self, base_iri: impl Into<String>) -> Result<Self, IriParseError> { |
||||
Ok(Self { |
||||
inner: self.inner.with_base_iri(base_iri)?, |
||||
}) |
||||
} |
||||
|
||||
/// Executes the parsing itself on a [`Read`] implementation and returns an iterator of quads.
|
||||
pub fn read_quads<R: Read>(self, reader: R) -> QuadReader<R> { |
||||
QuadReader { |
||||
parser: self.inner.parse_read(reader), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator yielding read quads.
|
||||
/// Could be built using a [`DatasetParser`].
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{DatasetFormat, DatasetParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .";
|
||||
///
|
||||
/// let parser = DatasetParser::from_format(DatasetFormat::NQuads);
|
||||
/// let quads = parser.read_quads(file.as_bytes()).collect::<Result<Vec<_>,_>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct QuadReader<R: Read> { |
||||
parser: FromReadQuadReader<R>, |
||||
} |
||||
|
||||
impl<R: Read> Iterator for QuadReader<R> { |
||||
type Item = Result<Quad, RdfParseError>; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
Some(self.parser.next()?.map_err(Into::into)) |
||||
} |
||||
} |
@ -1,185 +0,0 @@ |
||||
#![allow(deprecated)] |
||||
|
||||
//! Utilities to write RDF graphs and datasets.
|
||||
|
||||
use crate::io::{DatasetFormat, GraphFormat}; |
||||
use crate::model::*; |
||||
use oxrdfio::{RdfSerializer, ToWriteQuadWriter}; |
||||
use std::io::{self, Write}; |
||||
|
||||
/// A serializer for RDF graph serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N-Triples](https://www.w3.org/TR/n-triples/) ([`GraphFormat::NTriples`])
|
||||
/// * [Turtle](https://www.w3.org/TR/turtle/) ([`GraphFormat::Turtle`])
|
||||
/// * [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) ([`GraphFormat::RdfXml`])
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{GraphFormat, GraphSerializer};
|
||||
/// use oxigraph::model::*;
|
||||
///
|
||||
/// let mut buffer = Vec::new();
|
||||
/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer);
|
||||
/// writer.write(&Triple {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// })?;
|
||||
/// writer.finish()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// buffer.as_slice(),
|
||||
/// "<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n".as_bytes()
|
||||
/// );
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[deprecated(note = "use RdfSerializer instead", since = "0.4.0")] |
||||
pub struct GraphSerializer { |
||||
inner: RdfSerializer, |
||||
} |
||||
|
||||
impl GraphSerializer { |
||||
/// Builds a serializer for the given format
|
||||
#[inline] |
||||
pub fn from_format(format: GraphFormat) -> Self { |
||||
Self { |
||||
inner: RdfSerializer::from_format(format.into()), |
||||
} |
||||
} |
||||
|
||||
/// Returns a [`TripleWriter`] allowing writing triples into the given [`Write`] implementation
|
||||
pub fn triple_writer<W: Write>(self, write: W) -> TripleWriter<W> { |
||||
TripleWriter { |
||||
writer: self.inner.serialize_to_write(write), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Allows writing triples.
|
||||
/// Could be built using a [`GraphSerializer`].
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](TripleWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{GraphFormat, GraphSerializer};
|
||||
/// use oxigraph::model::*;
|
||||
///
|
||||
/// let mut buffer = Vec::new();
|
||||
/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer);
|
||||
/// writer.write(&Triple {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// })?;
|
||||
/// writer.finish()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// buffer.as_slice(),
|
||||
/// "<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n".as_bytes()
|
||||
/// );
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct TripleWriter<W: Write> { |
||||
writer: ToWriteQuadWriter<W>, |
||||
} |
||||
|
||||
impl<W: Write> TripleWriter<W> { |
||||
/// Writes a triple
|
||||
pub fn write<'a>(&mut self, triple: impl Into<TripleRef<'a>>) -> io::Result<()> { |
||||
self.writer.write_triple(triple) |
||||
} |
||||
|
||||
/// Writes the last bytes of the file
|
||||
pub fn finish(self) -> io::Result<()> { |
||||
self.writer.finish()?.flush() |
||||
} |
||||
} |
||||
|
||||
/// A serializer for RDF graph serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N-Quads](https://www.w3.org/TR/n-quads/) ([`DatasetFormat::NQuads`])
|
||||
/// * [TriG](https://www.w3.org/TR/trig/) ([`DatasetFormat::TriG`])
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{DatasetFormat, DatasetSerializer};
|
||||
/// use oxigraph::model::*;
|
||||
///
|
||||
/// let mut buffer = Vec::new();
|
||||
/// let mut writer = DatasetSerializer::from_format(DatasetFormat::NQuads).quad_writer(&mut buffer);
|
||||
/// writer.write(&Quad {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// graph_name: NamedNode::new("http://example.com/g")?.into(),
|
||||
/// })?;
|
||||
/// writer.finish()?;
|
||||
///
|
||||
/// assert_eq!(buffer.as_slice(), "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n".as_bytes());
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[deprecated(note = "use RdfSerializer instead", since = "0.4.0")] |
||||
pub struct DatasetSerializer { |
||||
inner: RdfSerializer, |
||||
} |
||||
|
||||
impl DatasetSerializer { |
||||
/// Builds a serializer for the given format
|
||||
#[inline] |
||||
pub fn from_format(format: DatasetFormat) -> Self { |
||||
Self { |
||||
inner: RdfSerializer::from_format(format.into()), |
||||
} |
||||
} |
||||
|
||||
/// Returns a [`QuadWriter`] allowing writing triples into the given [`Write`] implementation
|
||||
pub fn quad_writer<W: Write>(self, write: W) -> QuadWriter<W> { |
||||
QuadWriter { |
||||
writer: self.inner.serialize_to_write(write), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Allows writing triples.
|
||||
/// Could be built using a [`DatasetSerializer`].
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](QuadWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::{DatasetFormat, DatasetSerializer};
|
||||
/// use oxigraph::model::*;
|
||||
///
|
||||
/// let mut buffer = Vec::new();
|
||||
/// let mut writer = DatasetSerializer::from_format(DatasetFormat::NQuads).quad_writer(&mut buffer);
|
||||
/// writer.write(&Quad {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// graph_name: NamedNode::new("http://example.com/g")?.into(),
|
||||
/// })?;
|
||||
/// writer.finish()?;
|
||||
///
|
||||
/// assert_eq!(buffer.as_slice(), "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n".as_bytes());
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct QuadWriter<W: Write> { |
||||
writer: ToWriteQuadWriter<W>, |
||||
} |
||||
|
||||
impl<W: Write> QuadWriter<W> { |
||||
/// Writes a quad
|
||||
pub fn write<'a>(&mut self, quad: impl Into<QuadRef<'a>>) -> io::Result<()> { |
||||
self.writer.write_quad(quad) |
||||
} |
||||
|
||||
/// Writes the last bytes of the file
|
||||
pub fn finish(self) -> io::Result<()> { |
||||
self.writer.finish()?.flush() |
||||
} |
||||
} |
@ -1,12 +0,0 @@ |
||||
#![doc = include_str!("../README.md")] |
||||
#![doc(test(attr(deny(warnings))))] |
||||
#![doc(test(attr(allow(deprecated))))] |
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))] |
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] |
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] |
||||
|
||||
pub mod io; |
||||
pub mod model; |
||||
pub mod sparql; |
||||
mod storage; |
||||
pub mod store; |
@ -1,22 +0,0 @@ |
||||
//! Implements data structures for [RDF 1.1 Concepts](https://www.w3.org/TR/rdf11-concepts/) using [OxRDF](https://crates.io/crates/oxrdf).
|
||||
//!
|
||||
//! Usage example:
|
||||
//!
|
||||
//! ```
|
||||
//! use oxigraph::model::*;
|
||||
//!
|
||||
//! let mut graph = Graph::default();
|
||||
//!
|
||||
//! // insertion
|
||||
//! let ex = NamedNodeRef::new("http://example.com").unwrap();
|
||||
//! let triple = TripleRef::new(ex, ex, ex);
|
||||
//! graph.insert(triple);
|
||||
//!
|
||||
//! // simple filter
|
||||
//! let results: Vec<_> = graph.triples_for_subject(ex).collect();
|
||||
//! assert_eq!(vec![triple], results);
|
||||
//! ```
|
||||
|
||||
pub use oxrdf::*; |
||||
|
||||
pub use spargebra::term::GroundQuad; |
@ -1,84 +0,0 @@ |
||||
use crate::io::RdfParseError; |
||||
use crate::model::NamedNode; |
||||
use crate::sparql::results::QueryResultsParseError as ResultsParseError; |
||||
use crate::sparql::SparqlSyntaxError; |
||||
use crate::storage::StorageError; |
||||
use std::convert::Infallible; |
||||
use std::error::Error; |
||||
use std::io; |
||||
|
||||
/// A SPARQL evaluation error.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
#[non_exhaustive] |
||||
pub enum EvaluationError { |
||||
/// An error in SPARQL parsing.
|
||||
#[error(transparent)] |
||||
Parsing(#[from] SparqlSyntaxError), |
||||
/// An error from the storage.
|
||||
#[error(transparent)] |
||||
Storage(#[from] StorageError), |
||||
/// An error while parsing an external RDF file.
|
||||
#[error(transparent)] |
||||
GraphParsing(#[from] RdfParseError), |
||||
/// An error while parsing an external result file (likely from a federated query).
|
||||
#[error(transparent)] |
||||
ResultsParsing(#[from] ResultsParseError), |
||||
/// An error returned during results serialization.
|
||||
#[error(transparent)] |
||||
ResultsSerialization(#[from] io::Error), |
||||
/// Error during `SERVICE` evaluation
|
||||
#[error("{0}")] |
||||
Service(#[source] Box<dyn Error + Send + Sync + 'static>), |
||||
/// Error when `CREATE` tries to create an already existing graph
|
||||
#[error("The graph {0} already exists")] |
||||
GraphAlreadyExists(NamedNode), |
||||
/// Error when `DROP` or `CLEAR` tries to remove a not existing graph
|
||||
#[error("The graph {0} does not exist")] |
||||
GraphDoesNotExist(NamedNode), |
||||
/// The variable storing the `SERVICE` name is unbound
|
||||
#[error("The variable encoding the service name is unbound")] |
||||
UnboundService, |
||||
/// The given `SERVICE` is not supported
|
||||
#[error("The service {0} is not supported")] |
||||
UnsupportedService(NamedNode), |
||||
/// The given content media type returned from an HTTP response is not supported (`SERVICE` and `LOAD`)
|
||||
#[error("The content media type {0} is not supported")] |
||||
UnsupportedContentType(String), |
||||
/// The `SERVICE` call has not returns solutions
|
||||
#[error("The service is not returning solutions but a boolean or a graph")] |
||||
ServiceDoesNotReturnSolutions, |
||||
/// The results are not a RDF graph
|
||||
#[error("The query results are not a RDF graph")] |
||||
NotAGraph, |
||||
} |
||||
|
||||
impl From<Infallible> for EvaluationError { |
||||
#[inline] |
||||
fn from(error: Infallible) -> Self { |
||||
match error {} |
||||
} |
||||
} |
||||
|
||||
impl From<EvaluationError> for io::Error { |
||||
#[inline] |
||||
fn from(error: EvaluationError) -> Self { |
||||
match error { |
||||
EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error), |
||||
EvaluationError::GraphParsing(error) => error.into(), |
||||
EvaluationError::ResultsParsing(error) => error.into(), |
||||
EvaluationError::ResultsSerialization(error) => error, |
||||
EvaluationError::Storage(error) => error.into(), |
||||
EvaluationError::Service(error) => match error.downcast() { |
||||
Ok(error) => *error, |
||||
Err(error) => Self::new(io::ErrorKind::Other, error), |
||||
}, |
||||
EvaluationError::GraphAlreadyExists(_) |
||||
| EvaluationError::GraphDoesNotExist(_) |
||||
| EvaluationError::UnboundService |
||||
| EvaluationError::UnsupportedService(_) |
||||
| EvaluationError::UnsupportedContentType(_) |
||||
| EvaluationError::ServiceDoesNotReturnSolutions |
||||
| EvaluationError::NotAGraph => Self::new(io::ErrorKind::InvalidInput, error), |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@ |
||||
#[cfg(not(feature = "http-client"))] |
||||
mod dummy; |
||||
#[cfg(feature = "http-client")] |
||||
mod simple; |
||||
|
||||
#[cfg(not(feature = "http-client"))] |
||||
pub use dummy::Client; |
||||
#[cfg(feature = "http-client")] |
||||
pub use simple::Client; |
@ -1,328 +0,0 @@ |
||||
//! [SPARQL](https://www.w3.org/TR/sparql11-overview/) implementation.
|
||||
//!
|
||||
//! Stores execute SPARQL. See [`Store`](crate::store::Store::query()) for an example.
|
||||
|
||||
mod algebra; |
||||
mod dataset; |
||||
mod error; |
||||
mod eval; |
||||
mod http; |
||||
mod model; |
||||
pub mod results; |
||||
mod service; |
||||
mod update; |
||||
|
||||
use crate::model::{NamedNode, Term}; |
||||
pub use crate::sparql::algebra::{Query, QueryDataset, Update}; |
||||
use crate::sparql::dataset::DatasetView; |
||||
pub use crate::sparql::error::EvaluationError; |
||||
use crate::sparql::eval::{EvalNodeWithStats, SimpleEvaluator, Timer}; |
||||
pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter}; |
||||
pub use crate::sparql::service::ServiceHandler; |
||||
use crate::sparql::service::{EmptyServiceHandler, ErrorConversionServiceHandler}; |
||||
pub(crate) use crate::sparql::update::evaluate_update; |
||||
use crate::storage::StorageReader; |
||||
use json_event_parser::{JsonEvent, ToWriteJsonWriter}; |
||||
pub use oxrdf::{Variable, VariableNameParseError}; |
||||
use oxsdatatypes::{DayTimeDuration, Float}; |
||||
pub use spargebra::SparqlSyntaxError; |
||||
use sparopt::algebra::GraphPattern; |
||||
use sparopt::Optimizer; |
||||
use std::collections::HashMap; |
||||
use std::rc::Rc; |
||||
use std::sync::Arc; |
||||
use std::time::Duration; |
||||
use std::{fmt, io}; |
||||
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
pub(crate) fn evaluate_query( |
||||
reader: StorageReader, |
||||
query: impl TryInto<Query, Error = impl Into<EvaluationError>>, |
||||
options: QueryOptions, |
||||
run_stats: bool, |
||||
) -> Result<(Result<QueryResults, EvaluationError>, QueryExplanation), EvaluationError> { |
||||
let query = query.try_into().map_err(Into::into)?; |
||||
let dataset = DatasetView::new(reader, &query.dataset); |
||||
let start_planning = Timer::now(); |
||||
let (results, plan_node_with_stats, planning_duration) = match query.inner { |
||||
spargebra::Query::Select { |
||||
pattern, base_iri, .. |
||||
} => { |
||||
let mut pattern = GraphPattern::from(&pattern); |
||||
if !options.without_optimizations { |
||||
pattern = Optimizer::optimize_graph_pattern(pattern); |
||||
} |
||||
let planning_duration = start_planning.elapsed(); |
||||
let (results, explanation) = SimpleEvaluator::new( |
||||
Rc::new(dataset), |
||||
base_iri.map(Rc::new), |
||||
options.service_handler(), |
||||
Arc::new(options.custom_functions), |
||||
run_stats, |
||||
) |
||||
.evaluate_select(&pattern); |
||||
(Ok(results), explanation, planning_duration) |
||||
} |
||||
spargebra::Query::Ask { |
||||
pattern, base_iri, .. |
||||
} => { |
||||
let mut pattern = GraphPattern::from(&pattern); |
||||
if !options.without_optimizations { |
||||
pattern = Optimizer::optimize_graph_pattern(GraphPattern::Reduced { |
||||
inner: Box::new(pattern), |
||||
}); |
||||
} |
||||
let planning_duration = start_planning.elapsed(); |
||||
let (results, explanation) = SimpleEvaluator::new( |
||||
Rc::new(dataset), |
||||
base_iri.map(Rc::new), |
||||
options.service_handler(), |
||||
Arc::new(options.custom_functions), |
||||
run_stats, |
||||
) |
||||
.evaluate_ask(&pattern); |
||||
(results, explanation, planning_duration) |
||||
} |
||||
spargebra::Query::Construct { |
||||
template, |
||||
pattern, |
||||
base_iri, |
||||
.. |
||||
} => { |
||||
let mut pattern = GraphPattern::from(&pattern); |
||||
if !options.without_optimizations { |
||||
pattern = Optimizer::optimize_graph_pattern(GraphPattern::Reduced { |
||||
inner: Box::new(pattern), |
||||
}); |
||||
} |
||||
let planning_duration = start_planning.elapsed(); |
||||
let (results, explanation) = SimpleEvaluator::new( |
||||
Rc::new(dataset), |
||||
base_iri.map(Rc::new), |
||||
options.service_handler(), |
||||
Arc::new(options.custom_functions), |
||||
run_stats, |
||||
) |
||||
.evaluate_construct(&pattern, &template); |
||||
(Ok(results), explanation, planning_duration) |
||||
} |
||||
spargebra::Query::Describe { |
||||
pattern, base_iri, .. |
||||
} => { |
||||
let mut pattern = GraphPattern::from(&pattern); |
||||
if !options.without_optimizations { |
||||
pattern = Optimizer::optimize_graph_pattern(GraphPattern::Reduced { |
||||
inner: Box::new(pattern), |
||||
}); |
||||
} |
||||
let planning_duration = start_planning.elapsed(); |
||||
let (results, explanation) = SimpleEvaluator::new( |
||||
Rc::new(dataset), |
||||
base_iri.map(Rc::new), |
||||
options.service_handler(), |
||||
Arc::new(options.custom_functions), |
||||
run_stats, |
||||
) |
||||
.evaluate_describe(&pattern); |
||||
(Ok(results), explanation, planning_duration) |
||||
} |
||||
}; |
||||
let explanation = QueryExplanation { |
||||
inner: plan_node_with_stats, |
||||
with_stats: run_stats, |
||||
parsing_duration: query.parsing_duration, |
||||
planning_duration, |
||||
}; |
||||
Ok((results, explanation)) |
||||
} |
||||
|
||||
/// Options for SPARQL query evaluation.
|
||||
///
|
||||
///
|
||||
/// If the `"http-client"` optional feature is enabled,
|
||||
/// a simple HTTP 1.1 client is used to execute [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/) SERVICE calls.
|
||||
///
|
||||
/// Usage example disabling the federated query support:
|
||||
/// ```
|
||||
/// use oxigraph::sparql::QueryOptions;
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// store.query_opt(
|
||||
/// "SELECT * WHERE { SERVICE <https://query.wikidata.org/sparql> {} }",
|
||||
/// QueryOptions::default().without_service_handler(),
|
||||
/// )?;
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[derive(Clone, Default)] |
||||
pub struct QueryOptions { |
||||
service_handler: Option<Arc<dyn ServiceHandler<Error = EvaluationError>>>, |
||||
custom_functions: CustomFunctionRegistry, |
||||
http_timeout: Option<Duration>, |
||||
http_redirection_limit: usize, |
||||
without_optimizations: bool, |
||||
} |
||||
|
||||
pub(crate) type CustomFunctionRegistry = |
||||
HashMap<NamedNode, Arc<dyn (Fn(&[Term]) -> Option<Term>) + Send + Sync>>; |
||||
|
||||
impl QueryOptions { |
||||
/// Use a given [`ServiceHandler`] to execute [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/) SERVICE calls.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn with_service_handler(mut self, service_handler: impl ServiceHandler + 'static) -> Self { |
||||
self.service_handler = Some(Arc::new(ErrorConversionServiceHandler::wrap( |
||||
service_handler, |
||||
))); |
||||
self |
||||
} |
||||
|
||||
/// Disables the `SERVICE` calls
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn without_service_handler(mut self) -> Self { |
||||
self.service_handler = Some(Arc::new(EmptyServiceHandler)); |
||||
self |
||||
} |
||||
|
||||
/// Sets a timeout for HTTP requests done during SPARQL evaluation.
|
||||
#[cfg(feature = "http-client")] |
||||
#[inline] |
||||
#[must_use] |
||||
pub fn with_http_timeout(mut self, timeout: Duration) -> Self { |
||||
self.http_timeout = Some(timeout); |
||||
self |
||||
} |
||||
|
||||
/// Sets an upper bound of the number of HTTP redirection followed per HTTP request done during SPARQL evaluation.
|
||||
///
|
||||
/// By default this value is `0`.
|
||||
#[cfg(feature = "http-client")] |
||||
#[inline] |
||||
#[must_use] |
||||
pub fn with_http_redirection_limit(mut self, redirection_limit: usize) -> Self { |
||||
self.http_redirection_limit = redirection_limit; |
||||
self |
||||
} |
||||
|
||||
/// Adds a custom SPARQL evaluation function.
|
||||
///
|
||||
/// Example with a function serializing terms to N-Triples:
|
||||
/// ```
|
||||
/// use oxigraph::model::*;
|
||||
/// use oxigraph::sparql::{QueryOptions, QueryResults};
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
///
|
||||
/// if let QueryResults::Solutions(mut solutions) = store.query_opt(
|
||||
/// "SELECT (<http://www.w3.org/ns/formats/N-Triples>(1) AS ?nt) WHERE {}",
|
||||
/// QueryOptions::default().with_custom_function(
|
||||
/// NamedNode::new("http://www.w3.org/ns/formats/N-Triples")?,
|
||||
/// |args| args.get(0).map(|t| Literal::from(t.to_string()).into()),
|
||||
/// ),
|
||||
/// )? {
|
||||
/// assert_eq!(
|
||||
/// solutions.next().unwrap()?.get("nt"),
|
||||
/// Some(&Literal::from("\"1\"^^<http://www.w3.org/2001/XMLSchema#integer>").into())
|
||||
/// );
|
||||
/// }
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn with_custom_function( |
||||
mut self, |
||||
name: NamedNode, |
||||
evaluator: impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static, |
||||
) -> Self { |
||||
self.custom_functions.insert(name, Arc::new(evaluator)); |
||||
self |
||||
} |
||||
|
||||
fn service_handler(&self) -> Arc<dyn ServiceHandler<Error = EvaluationError>> { |
||||
self.service_handler.clone().unwrap_or_else(|| { |
||||
if cfg!(feature = "http-client") { |
||||
Arc::new(service::SimpleServiceHandler::new( |
||||
self.http_timeout, |
||||
self.http_redirection_limit, |
||||
)) |
||||
} else { |
||||
Arc::new(EmptyServiceHandler) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
#[doc(hidden)] |
||||
#[inline] |
||||
#[must_use] |
||||
pub fn without_optimizations(mut self) -> Self { |
||||
self.without_optimizations = true; |
||||
self |
||||
} |
||||
} |
||||
|
||||
/// Options for SPARQL update evaluation.
|
||||
#[derive(Clone, Default)] |
||||
pub struct UpdateOptions { |
||||
query_options: QueryOptions, |
||||
} |
||||
|
||||
impl From<QueryOptions> for UpdateOptions { |
||||
#[inline] |
||||
fn from(query_options: QueryOptions) -> Self { |
||||
Self { query_options } |
||||
} |
||||
} |
||||
|
||||
/// The explanation of a query.
|
||||
#[derive(Clone)] |
||||
pub struct QueryExplanation { |
||||
inner: Rc<EvalNodeWithStats>, |
||||
with_stats: bool, |
||||
parsing_duration: Option<DayTimeDuration>, |
||||
planning_duration: Option<DayTimeDuration>, |
||||
} |
||||
|
||||
impl QueryExplanation { |
||||
/// Writes the explanation as JSON.
|
||||
pub fn write_in_json(&self, write: impl io::Write) -> io::Result<()> { |
||||
let mut writer = ToWriteJsonWriter::new(write); |
||||
writer.write_event(JsonEvent::StartObject)?; |
||||
if let Some(parsing_duration) = self.parsing_duration { |
||||
writer.write_event(JsonEvent::ObjectKey("parsing duration in seconds".into()))?; |
||||
writer.write_event(JsonEvent::Number( |
||||
parsing_duration.as_seconds().to_string().into(), |
||||
))?; |
||||
} |
||||
if let Some(planning_duration) = self.planning_duration { |
||||
writer.write_event(JsonEvent::ObjectKey("planning duration in seconds".into()))?; |
||||
writer.write_event(JsonEvent::Number( |
||||
planning_duration.as_seconds().to_string().into(), |
||||
))?; |
||||
} |
||||
writer.write_event(JsonEvent::ObjectKey("plan".into()))?; |
||||
self.inner.json_node(&mut writer, self.with_stats)?; |
||||
writer.write_event(JsonEvent::EndObject) |
||||
} |
||||
} |
||||
|
||||
impl fmt::Debug for QueryExplanation { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
let mut obj = f.debug_struct("QueryExplanation"); |
||||
if let Some(parsing_duration) = self.parsing_duration { |
||||
obj.field( |
||||
"parsing duration in seconds", |
||||
&f32::from(Float::from(parsing_duration.as_seconds())), |
||||
); |
||||
} |
||||
if let Some(planning_duration) = self.planning_duration { |
||||
obj.field( |
||||
"planning duration in seconds", |
||||
&f32::from(Float::from(planning_duration.as_seconds())), |
||||
); |
||||
} |
||||
obj.field("tree", &self.inner); |
||||
obj.finish_non_exhaustive() |
||||
} |
||||
} |
@ -1,371 +0,0 @@ |
||||
use crate::io::{RdfFormat, RdfSerializer}; |
||||
use crate::model::*; |
||||
use crate::sparql::error::EvaluationError; |
||||
use crate::sparql::results::{ |
||||
FromReadQueryResultsReader, FromReadSolutionsReader, QueryResultsFormat, |
||||
QueryResultsParseError, QueryResultsParser, QueryResultsSerializer, |
||||
}; |
||||
pub use sparesults::QuerySolution; |
||||
use std::io::{Read, Write}; |
||||
use std::sync::Arc; |
||||
|
||||
/// Results of a [SPARQL query](https://www.w3.org/TR/sparql11-query/).
|
||||
pub enum QueryResults { |
||||
/// Results of a [SELECT](https://www.w3.org/TR/sparql11-query/#select) query.
|
||||
Solutions(QuerySolutionIter), |
||||
/// Result of a [ASK](https://www.w3.org/TR/sparql11-query/#ask) query.
|
||||
Boolean(bool), |
||||
/// Results of a [CONSTRUCT](https://www.w3.org/TR/sparql11-query/#construct) or [DESCRIBE](https://www.w3.org/TR/sparql11-query/#describe) query.
|
||||
Graph(QueryTripleIter), |
||||
} |
||||
|
||||
impl QueryResults { |
||||
/// Reads a SPARQL query results serialization.
|
||||
pub fn read( |
||||
read: impl Read + 'static, |
||||
format: QueryResultsFormat, |
||||
) -> Result<Self, QueryResultsParseError> { |
||||
Ok(QueryResultsParser::from_format(format) |
||||
.parse_read(read)? |
||||
.into()) |
||||
} |
||||
|
||||
/// Writes the query results (solutions or boolean).
|
||||
///
|
||||
/// This method fails if it is called on the `Graph` results.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::store::Store;
|
||||
/// use oxigraph::model::*;
|
||||
/// use oxigraph::sparql::results::QueryResultsFormat;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// let ex = NamedNodeRef::new("http://example.com")?;
|
||||
/// store.insert(QuadRef::new(ex, ex, ex, GraphNameRef::DefaultGraph))?;
|
||||
///
|
||||
/// let results = store.query("SELECT ?s WHERE { ?s ?p ?o }")?;
|
||||
/// assert_eq!(
|
||||
/// results.write(Vec::new(), QueryResultsFormat::Json)?,
|
||||
/// r#"{"head":{"vars":["s"]},"results":{"bindings":[{"s":{"type":"uri","value":"http://example.com"}}]}}"#.as_bytes()
|
||||
/// );
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub fn write<W: Write>( |
||||
self, |
||||
write: W, |
||||
format: QueryResultsFormat, |
||||
) -> Result<W, EvaluationError> { |
||||
let serializer = QueryResultsSerializer::from_format(format); |
||||
match self { |
||||
Self::Boolean(value) => serializer.serialize_boolean_to_write(write, value), |
||||
Self::Solutions(solutions) => { |
||||
let mut writer = serializer |
||||
.serialize_solutions_to_write(write, solutions.variables().to_vec()) |
||||
.map_err(EvaluationError::ResultsSerialization)?; |
||||
for solution in solutions { |
||||
writer |
||||
.write(&solution?) |
||||
.map_err(EvaluationError::ResultsSerialization)?; |
||||
} |
||||
writer.finish() |
||||
} |
||||
Self::Graph(triples) => { |
||||
let s = VariableRef::new_unchecked("subject"); |
||||
let p = VariableRef::new_unchecked("predicate"); |
||||
let o = VariableRef::new_unchecked("object"); |
||||
let mut writer = serializer |
||||
.serialize_solutions_to_write( |
||||
write, |
||||
vec![s.into_owned(), p.into_owned(), o.into_owned()], |
||||
) |
||||
.map_err(EvaluationError::ResultsSerialization)?; |
||||
for triple in triples { |
||||
let triple = triple?; |
||||
writer |
||||
.write([ |
||||
(s, &triple.subject.into()), |
||||
(p, &triple.predicate.into()), |
||||
(o, &triple.object), |
||||
]) |
||||
.map_err(EvaluationError::ResultsSerialization)?; |
||||
} |
||||
writer.finish() |
||||
} |
||||
} |
||||
.map_err(EvaluationError::ResultsSerialization) |
||||
} |
||||
|
||||
/// Writes the graph query results.
|
||||
///
|
||||
/// This method fails if it is called on the `Solution` or `Boolean` results.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::io::RdfFormat;
|
||||
/// use oxigraph::model::*;
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let graph = "<http://example.com> <http://example.com> <http://example.com> .\n";
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// store.load_graph(
|
||||
/// graph.as_bytes(),
|
||||
/// RdfFormat::NTriples,
|
||||
/// GraphName::DefaultGraph,
|
||||
/// None,
|
||||
/// )?;
|
||||
///
|
||||
/// let results = store.query("CONSTRUCT WHERE { ?s ?p ?o }")?;
|
||||
/// assert_eq!(
|
||||
/// results.write_graph(Vec::new(), RdfFormat::NTriples)?,
|
||||
/// graph.as_bytes()
|
||||
/// );
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub fn write_graph<W: Write>( |
||||
self, |
||||
write: W, |
||||
format: impl Into<RdfFormat>, |
||||
) -> Result<W, EvaluationError> { |
||||
if let Self::Graph(triples) = self { |
||||
let mut writer = RdfSerializer::from_format(format.into()).serialize_to_write(write); |
||||
for triple in triples { |
||||
writer |
||||
.write_triple(&triple?) |
||||
.map_err(EvaluationError::ResultsSerialization)?; |
||||
} |
||||
writer |
||||
.finish() |
||||
.map_err(EvaluationError::ResultsSerialization) |
||||
} else { |
||||
Err(EvaluationError::NotAGraph) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<QuerySolutionIter> for QueryResults { |
||||
#[inline] |
||||
fn from(value: QuerySolutionIter) -> Self { |
||||
Self::Solutions(value) |
||||
} |
||||
} |
||||
|
||||
impl<R: Read + 'static> From<FromReadQueryResultsReader<R>> for QueryResults { |
||||
fn from(reader: FromReadQueryResultsReader<R>) -> Self { |
||||
match reader { |
||||
FromReadQueryResultsReader::Solutions(s) => Self::Solutions(s.into()), |
||||
FromReadQueryResultsReader::Boolean(v) => Self::Boolean(v), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator over [`QuerySolution`]s.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::sparql::QueryResults;
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// if let QueryResults::Solutions(solutions) = store.query("SELECT ?s WHERE { ?s ?p ?o }")? {
|
||||
/// for solution in solutions {
|
||||
/// println!("{:?}", solution?.get("s"));
|
||||
/// }
|
||||
/// }
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub struct QuerySolutionIter { |
||||
variables: Arc<[Variable]>, |
||||
iter: Box<dyn Iterator<Item = Result<QuerySolution, EvaluationError>>>, |
||||
} |
||||
|
||||
impl QuerySolutionIter { |
||||
/// Construct a new iterator of solution from an ordered list of solution variables and an iterator of solution tuples
|
||||
/// (each tuple using the same ordering as the variable list such that tuple element 0 is the value for the variable 0...)
|
||||
pub fn new( |
||||
variables: Arc<[Variable]>, |
||||
iter: impl Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>> + 'static, |
||||
) -> Self { |
||||
Self { |
||||
variables: Arc::clone(&variables), |
||||
iter: Box::new( |
||||
iter.map(move |t| t.map(|values| (Arc::clone(&variables), values).into())), |
||||
), |
||||
} |
||||
} |
||||
|
||||
/// The variables used in the solutions.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::sparql::{QueryResults, Variable};
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// if let QueryResults::Solutions(solutions) = store.query("SELECT ?s ?o WHERE { ?s ?p ?o }")? {
|
||||
/// assert_eq!(
|
||||
/// solutions.variables(),
|
||||
/// &[Variable::new("s")?, Variable::new("o")?]
|
||||
/// );
|
||||
/// }
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn variables(&self) -> &[Variable] { |
||||
&self.variables |
||||
} |
||||
} |
||||
|
||||
impl<R: Read + 'static> From<FromReadSolutionsReader<R>> for QuerySolutionIter { |
||||
fn from(reader: FromReadSolutionsReader<R>) -> Self { |
||||
Self { |
||||
variables: reader.variables().into(), |
||||
iter: Box::new(reader.map(|t| t.map_err(EvaluationError::from))), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Iterator for QuerySolutionIter { |
||||
type Item = Result<QuerySolution, EvaluationError>; |
||||
|
||||
#[inline] |
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
self.iter.next() |
||||
} |
||||
|
||||
#[inline] |
||||
fn size_hint(&self) -> (usize, Option<usize>) { |
||||
self.iter.size_hint() |
||||
} |
||||
} |
||||
|
||||
/// An iterator over the triples that compose a graph solution.
|
||||
///
|
||||
/// ```
|
||||
/// use oxigraph::sparql::QueryResults;
|
||||
/// use oxigraph::store::Store;
|
||||
///
|
||||
/// let store = Store::new()?;
|
||||
/// if let QueryResults::Graph(triples) = store.query("CONSTRUCT WHERE { ?s ?p ?o }")? {
|
||||
/// for triple in triples {
|
||||
/// println!("{}", triple?);
|
||||
/// }
|
||||
/// }
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub struct QueryTripleIter { |
||||
pub(crate) iter: Box<dyn Iterator<Item = Result<Triple, EvaluationError>>>, |
||||
} |
||||
|
||||
impl Iterator for QueryTripleIter { |
||||
type Item = Result<Triple, EvaluationError>; |
||||
|
||||
#[inline] |
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
self.iter.next() |
||||
} |
||||
|
||||
#[inline] |
||||
fn size_hint(&self) -> (usize, Option<usize>) { |
||||
self.iter.size_hint() |
||||
} |
||||
|
||||
#[inline] |
||||
fn fold<Acc, G>(self, init: Acc, g: G) -> Acc |
||||
where |
||||
G: FnMut(Acc, Self::Item) -> Acc, |
||||
{ |
||||
self.iter.fold(init, g) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
#[allow(clippy::panic_in_result_fn)] |
||||
mod tests { |
||||
use super::*; |
||||
use std::io::Cursor; |
||||
|
||||
#[test] |
||||
fn test_serialization_roundtrip() -> Result<(), EvaluationError> { |
||||
use std::str; |
||||
|
||||
for format in [ |
||||
QueryResultsFormat::Json, |
||||
QueryResultsFormat::Xml, |
||||
QueryResultsFormat::Tsv, |
||||
] { |
||||
let results = vec![ |
||||
QueryResults::Boolean(true), |
||||
QueryResults::Boolean(false), |
||||
QueryResults::Solutions(QuerySolutionIter::new( |
||||
[ |
||||
Variable::new_unchecked("foo"), |
||||
Variable::new_unchecked("bar"), |
||||
] |
||||
.as_ref() |
||||
.into(), |
||||
Box::new( |
||||
vec![ |
||||
Ok(vec![None, None]), |
||||
Ok(vec![ |
||||
Some(NamedNode::new_unchecked("http://example.com").into()), |
||||
None, |
||||
]), |
||||
Ok(vec![ |
||||
None, |
||||
Some(NamedNode::new_unchecked("http://example.com").into()), |
||||
]), |
||||
Ok(vec![ |
||||
Some(BlankNode::new_unchecked("foo").into()), |
||||
Some(BlankNode::new_unchecked("bar").into()), |
||||
]), |
||||
Ok(vec![Some(Literal::new_simple_literal("foo").into()), None]), |
||||
Ok(vec![ |
||||
Some( |
||||
Literal::new_language_tagged_literal_unchecked("foo", "fr") |
||||
.into(), |
||||
), |
||||
None, |
||||
]), |
||||
Ok(vec![ |
||||
Some(Literal::from(1).into()), |
||||
Some(Literal::from(true).into()), |
||||
]), |
||||
Ok(vec![ |
||||
Some(Literal::from(1.33).into()), |
||||
Some(Literal::from(false).into()), |
||||
]), |
||||
Ok(vec![ |
||||
Some( |
||||
Triple::new( |
||||
NamedNode::new_unchecked("http://example.com/s"), |
||||
NamedNode::new_unchecked("http://example.com/p"), |
||||
Triple::new( |
||||
NamedNode::new_unchecked("http://example.com/os"), |
||||
NamedNode::new_unchecked("http://example.com/op"), |
||||
NamedNode::new_unchecked("http://example.com/oo"), |
||||
), |
||||
) |
||||
.into(), |
||||
), |
||||
None, |
||||
]), |
||||
] |
||||
.into_iter(), |
||||
), |
||||
)), |
||||
]; |
||||
|
||||
for ex in results { |
||||
let mut buffer = Vec::new(); |
||||
ex.write(&mut buffer, format)?; |
||||
let ex2 = QueryResults::read(Cursor::new(buffer.clone()), format)?; |
||||
let mut buffer2 = Vec::new(); |
||||
ex2.write(&mut buffer2, format)?; |
||||
assert_eq!( |
||||
str::from_utf8(&buffer).unwrap(), |
||||
str::from_utf8(&buffer2).unwrap() |
||||
); |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
@ -1,44 +0,0 @@ |
||||
//! Utilities to read and write RDF results formats using [sparesults](https://crates.io/crates/sparesults).
|
||||
//!
|
||||
//! It supports [SPARQL Query Results XML Format (Second Edition)](https://www.w3.org/TR/rdf-sparql-XMLres/), [SPARQL 1.1 Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) and [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/).
|
||||
//!
|
||||
//! Usage example converting a JSON result file into a TSV result file:
|
||||
//!
|
||||
//! ```
|
||||
//! use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, FromReadQueryResultsReader, QueryResultsSerializer};
|
||||
//! use std::io::Result;
|
||||
//!
|
||||
//! fn convert_json_to_tsv(json_file: &[u8]) -> Result<Vec<u8>> {
|
||||
//! let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);
|
||||
//! let tsv_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Tsv);
|
||||
//! // We start to read the JSON file and see which kind of results it is
|
||||
//! match json_parser.parse_read(json_file)? {
|
||||
//! FromReadQueryResultsReader::Boolean(value) => {
|
||||
//! // it's a boolean result, we copy it in TSV to the output buffer
|
||||
//! tsv_serializer.serialize_boolean_to_write(Vec::new(), value)
|
||||
//! }
|
||||
//! FromReadQueryResultsReader::Solutions(solutions_reader) => {
|
||||
//! // it's a set of solutions, we create a writer and we write to it while reading in streaming from the JSON file
|
||||
//! let mut serialize_solutions_to_write = tsv_serializer.serialize_solutions_to_write(Vec::new(), solutions_reader.variables().to_vec())?;
|
||||
//! for solution in solutions_reader {
|
||||
//! serialize_solutions_to_write.write(&solution?)?;
|
||||
//! }
|
||||
//! serialize_solutions_to_write.finish()
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Let's test with a boolean
|
||||
//! assert_eq!(
|
||||
//! convert_json_to_tsv(br#"{"boolean":true}"#.as_slice()).unwrap(),
|
||||
//! b"true"
|
||||
//! );
|
||||
//!
|
||||
//! // And with a set of solutions
|
||||
//! assert_eq!(
|
||||
//! convert_json_to_tsv(br#"{"head":{"vars":["foo","bar"]},"results":{"bindings":[{"foo":{"type":"literal","value":"test"}}]}}"#.as_slice()).unwrap(),
|
||||
//! b"?foo\t?bar\n\"test\"\t\n"
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
pub use sparesults::*; |
@ -1,12 +0,0 @@ |
||||
//! A storage backend
|
||||
//! RocksDB is available, if not in memory
|
||||
|
||||
#[cfg(any(target_family = "wasm"))] |
||||
pub use fallback::{ColumnFamily, ColumnFamilyDefinition, Db, Iter, Reader, Transaction}; |
||||
#[cfg(all(not(target_family = "wasm")))] |
||||
pub use oxi_rocksdb::{ColumnFamily, ColumnFamilyDefinition, Db, Iter, Reader, Transaction}; |
||||
|
||||
#[cfg(any(target_family = "wasm"))] |
||||
mod fallback; |
||||
#[cfg(all(not(target_family = "wasm")))] |
||||
mod oxi_rocksdb; |
File diff suppressed because it is too large
Load Diff
@ -1,139 +0,0 @@ |
||||
use crate::io::{RdfFormat, RdfParseError}; |
||||
use crate::storage::numeric_encoder::EncodedTerm; |
||||
use oxiri::IriParseError; |
||||
use oxrdf::TermRef; |
||||
use std::error::Error; |
||||
use std::io; |
||||
|
||||
/// An error related to storage operations (reads, writes...).
|
||||
#[derive(Debug, thiserror::Error)] |
||||
#[non_exhaustive] |
||||
pub enum StorageError { |
||||
/// Error from the OS I/O layer.
|
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
/// Error related to data corruption.
|
||||
#[error(transparent)] |
||||
Corruption(#[from] CorruptionError), |
||||
#[doc(hidden)] |
||||
#[error("{0}")] |
||||
Other(#[source] Box<dyn Error + Send + Sync + 'static>), |
||||
} |
||||
|
||||
impl From<StorageError> for io::Error { |
||||
#[inline] |
||||
fn from(error: StorageError) -> Self { |
||||
match error { |
||||
StorageError::Io(error) => error, |
||||
StorageError::Corruption(error) => error.into(), |
||||
StorageError::Other(error) => Self::new(io::ErrorKind::Other, error), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An error return if some content in the database is corrupted.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
#[error(transparent)] |
||||
pub struct CorruptionError(#[from] CorruptionErrorKind); |
||||
|
||||
/// An error return if some content in the database is corrupted.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
enum CorruptionErrorKind { |
||||
#[error("{0}")] |
||||
Msg(String), |
||||
#[error("{0}")] |
||||
Other(#[source] Box<dyn Error + Send + Sync + 'static>), |
||||
} |
||||
|
||||
impl CorruptionError { |
||||
/// Builds an error from a printable error message.
|
||||
#[inline] |
||||
pub(crate) fn new(error: impl Into<Box<dyn Error + Send + Sync + 'static>>) -> Self { |
||||
Self(CorruptionErrorKind::Other(error.into())) |
||||
} |
||||
|
||||
#[inline] |
||||
pub(crate) fn from_encoded_term(encoded: &EncodedTerm, term: &TermRef<'_>) -> Self { |
||||
// TODO: eventually use a dedicated error enum value
|
||||
Self::msg(format!("Invalid term encoding {encoded:?} for {term}")) |
||||
} |
||||
|
||||
#[inline] |
||||
pub(crate) fn from_missing_column_family_name(name: &'static str) -> Self { |
||||
// TODO: eventually use a dedicated error enum value
|
||||
Self::msg(format!("Column family {name} does not exist")) |
||||
} |
||||
|
||||
/// Builds an error from a printable error message.
|
||||
#[inline] |
||||
pub(crate) fn msg(msg: impl Into<String>) -> Self { |
||||
Self(CorruptionErrorKind::Msg(msg.into())) |
||||
} |
||||
} |
||||
|
||||
impl From<CorruptionError> for io::Error { |
||||
#[inline] |
||||
fn from(error: CorruptionError) -> Self { |
||||
Self::new(io::ErrorKind::InvalidData, error) |
||||
} |
||||
} |
||||
|
||||
/// An error raised while loading a file into a [`Store`](crate::store::Store).
|
||||
#[derive(Debug, thiserror::Error)] |
||||
pub enum LoaderError { |
||||
/// An error raised while reading the file.
|
||||
#[error(transparent)] |
||||
Parsing(#[from] RdfParseError), |
||||
/// An error raised during the insertion in the store.
|
||||
#[error(transparent)] |
||||
Storage(#[from] StorageError), |
||||
/// The base IRI is invalid.
|
||||
#[error("Invalid base IRI '{iri}': {error}")] |
||||
InvalidBaseIri { |
||||
/// The IRI itself.
|
||||
iri: String, |
||||
/// The parsing error.
|
||||
#[source] |
||||
error: IriParseError, |
||||
}, |
||||
} |
||||
|
||||
impl From<LoaderError> for io::Error { |
||||
#[inline] |
||||
fn from(error: LoaderError) -> Self { |
||||
match error { |
||||
LoaderError::Storage(error) => error.into(), |
||||
LoaderError::Parsing(error) => error.into(), |
||||
LoaderError::InvalidBaseIri { .. } => { |
||||
Self::new(io::ErrorKind::InvalidInput, error.to_string()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An error raised while writing a file from a [`Store`](crate::store::Store).
|
||||
#[derive(Debug, thiserror::Error)] |
||||
pub enum SerializerError { |
||||
/// An error raised while writing the content.
|
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
/// An error raised during the lookup in the store.
|
||||
#[error(transparent)] |
||||
Storage(#[from] StorageError), |
||||
/// A format compatible with [RDF dataset](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset) is required.
|
||||
#[error("A RDF format supporting datasets was expected, {0} found")] |
||||
DatasetFormatExpected(RdfFormat), |
||||
} |
||||
|
||||
impl From<SerializerError> for io::Error { |
||||
#[inline] |
||||
fn from(error: SerializerError) -> Self { |
||||
match error { |
||||
SerializerError::Storage(error) => error.into(), |
||||
SerializerError::Io(error) => error, |
||||
SerializerError::DatasetFormatExpected(_) => { |
||||
Self::new(io::ErrorKind::InvalidInput, error.to_string()) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,36 +0,0 @@ |
||||
[package] |
||||
name = "oxrdfio" |
||||
version = "0.1.0-alpha.5" |
||||
authors.workspace = true |
||||
license.workspace = true |
||||
readme = "README.md" |
||||
keywords = ["RDF"] |
||||
repository = "https://github.com/oxigraph/oxigraph/tree/master/lib/oxrdfxml" |
||||
documentation = "https://docs.rs/oxrdfio" |
||||
description = """ |
||||
Parser and serializer for various RDF formats |
||||
""" |
||||
edition.workspace = true |
||||
rust-version.workspace = true |
||||
|
||||
[features] |
||||
default = [] |
||||
async-tokio = ["dep:tokio", "oxrdfxml/async-tokio", "oxttl/async-tokio"] |
||||
rdf-star = ["oxrdf/rdf-star", "oxttl/rdf-star"] |
||||
|
||||
[dependencies] |
||||
oxrdf.workspace = true |
||||
oxrdfxml.workspace = true |
||||
oxttl.workspace = true |
||||
thiserror.workspace = true |
||||
tokio = { workspace = true, optional = true, features = ["io-util"] } |
||||
|
||||
[dev-dependencies] |
||||
tokio = { workspace = true, features = ["rt", "macros"] } |
||||
|
||||
[lints] |
||||
workspace = true |
||||
|
||||
[package.metadata.docs.rs] |
||||
all-features = true |
||||
rustdoc-args = ["--cfg", "docsrs"] |
@ -1,67 +0,0 @@ |
||||
OxRDF I/O |
||||
========= |
||||
|
||||
[![Latest Version](https://img.shields.io/crates/v/oxrdfio.svg)](https://crates.io/crates/oxrdfio) |
||||
[![Released API docs](https://docs.rs/oxrdfio/badge.svg)](https://docs.rs/oxrdfio) |
||||
[![Crates.io downloads](https://img.shields.io/crates/d/oxrdfio)](https://crates.io/crates/oxrdfio) |
||||
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community) |
||||
|
||||
OxRDF I/O is a set of parsers and serializers for RDF. |
||||
|
||||
It supports: |
||||
* [N3](https://w3c.github.io/N3/spec/) using [`oxttl`](https://crates.io/crates/oxttl) |
||||
* [N-Quads](https://www.w3.org/TR/n-quads/) using [`oxttl`](https://crates.io/crates/oxttl) |
||||
* [N-Triples](https://www.w3.org/TR/n-triples/) using [`oxttl`](https://crates.io/crates/oxttl) |
||||
* [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) using [`oxrdfxml`](https://crates.io/crates/oxrdfxml) |
||||
* [TriG](https://www.w3.org/TR/trig/) using [`oxttl`](https://crates.io/crates/oxttl) |
||||
* [Turtle](https://www.w3.org/TR/turtle/) using [`oxttl`](https://crates.io/crates/oxttl) |
||||
|
||||
Support for [SPARQL-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html) is also available behind the `rdf-star`feature for [Turtle-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#turtle-star), [TriG-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#trig-star), [N-Triples-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#n-triples-star) and [N-Quads-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#n-quads-star). |
||||
|
||||
It is designed as a low level parser compatible with both synchronous and asynchronous I/O (behind the `async-tokio` feature). |
||||
|
||||
The entry points of this library are the two [`RdfParser`] and [`RdfSerializer`] structs. |
||||
|
||||
Usage example converting a Turtle file to a N-Triples file: |
||||
```rust |
||||
use oxrdfio::{RdfFormat, RdfParser, RdfSerializer}; |
||||
|
||||
let turtle_file = b"@base <http://example.com/> . |
||||
@prefix schema: <http://schema.org/> . |
||||
<foo> a schema:Person ; |
||||
schema:name \"Foo\" . |
||||
<bar> a schema:Person ; |
||||
schema:name \"Bar\" ."; |
||||
|
||||
let ntriples_file = b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> . |
||||
<http://example.com/foo> <http://schema.org/name> \"Foo\" . |
||||
<http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> . |
||||
<http://example.com/bar> <http://schema.org/name> \"Bar\" . |
||||
"; |
||||
|
||||
let mut writer = RdfSerializer::from_format(RdfFormat::NTriples).serialize_to_write(Vec::new()); |
||||
for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(turtle_file.as_ref()) { |
||||
writer.write_quad(&quad.unwrap()).unwrap(); |
||||
} |
||||
assert_eq!(writer.finish().unwrap(), ntriples_file); |
||||
``` |
||||
|
||||
Parsers for other RDF formats exists in Rust like [graph-rdfa-processor](https://github.com/nbittich/graph-rdfa-processor) for RDFa and [json-ld](https://github.com/timothee-haudebourg/json-ld) for JSON-LD. |
||||
|
||||
|
||||
## License |
||||
|
||||
This project is licensed under either of |
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or |
||||
`<http://www.apache.org/licenses/LICENSE-2.0>`) |
||||
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or |
||||
`<http://opensource.org/licenses/MIT>`) |
||||
|
||||
at your option. |
||||
|
||||
|
||||
### Contribution |
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. |
@ -1,122 +0,0 @@ |
||||
use std::io; |
||||
use std::ops::Range; |
||||
|
||||
/// Error returned during RDF format parsing.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
pub enum RdfParseError { |
||||
/// I/O error during parsing (file not found...).
|
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
/// An error in the file syntax.
|
||||
#[error(transparent)] |
||||
Syntax(#[from] RdfSyntaxError), |
||||
} |
||||
|
||||
impl RdfParseError { |
||||
pub(crate) fn msg(msg: &'static str) -> Self { |
||||
Self::Syntax(RdfSyntaxError(SyntaxErrorKind::Msg(msg))) |
||||
} |
||||
} |
||||
|
||||
impl From<oxttl::TurtleSyntaxError> for RdfSyntaxError { |
||||
#[inline] |
||||
fn from(error: oxttl::TurtleSyntaxError) -> Self { |
||||
Self(SyntaxErrorKind::Turtle(error)) |
||||
} |
||||
} |
||||
|
||||
impl From<oxttl::TurtleParseError> for RdfParseError { |
||||
#[inline] |
||||
fn from(error: oxttl::TurtleParseError) -> Self { |
||||
match error { |
||||
oxttl::TurtleParseError::Syntax(e) => Self::Syntax(e.into()), |
||||
oxttl::TurtleParseError::Io(e) => Self::Io(e), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<oxrdfxml::RdfXmlSyntaxError> for RdfSyntaxError { |
||||
#[inline] |
||||
fn from(error: oxrdfxml::RdfXmlSyntaxError) -> Self { |
||||
Self(SyntaxErrorKind::RdfXml(error)) |
||||
} |
||||
} |
||||
|
||||
impl From<oxrdfxml::RdfXmlParseError> for RdfParseError { |
||||
#[inline] |
||||
fn from(error: oxrdfxml::RdfXmlParseError) -> Self { |
||||
match error { |
||||
oxrdfxml::RdfXmlParseError::Syntax(e) => Self::Syntax(e.into()), |
||||
oxrdfxml::RdfXmlParseError::Io(e) => Self::Io(e), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<RdfParseError> for io::Error { |
||||
#[inline] |
||||
fn from(error: RdfParseError) -> Self { |
||||
match error { |
||||
RdfParseError::Io(error) => error, |
||||
RdfParseError::Syntax(error) => error.into(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An error in the syntax of the parsed file.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
#[error(transparent)] |
||||
pub struct RdfSyntaxError(#[from] SyntaxErrorKind); |
||||
|
||||
/// An error in the syntax of the parsed file.
|
||||
#[derive(Debug, thiserror::Error)] |
||||
enum SyntaxErrorKind { |
||||
#[error(transparent)] |
||||
Turtle(#[from] oxttl::TurtleSyntaxError), |
||||
#[error(transparent)] |
||||
RdfXml(#[from] oxrdfxml::RdfXmlSyntaxError), |
||||
#[error("{0}")] |
||||
Msg(&'static str), |
||||
} |
||||
|
||||
impl RdfSyntaxError { |
||||
/// The location of the error inside of the file.
|
||||
#[inline] |
||||
pub fn location(&self) -> Option<Range<TextPosition>> { |
||||
match &self.0 { |
||||
SyntaxErrorKind::Turtle(e) => { |
||||
let location = e.location(); |
||||
Some( |
||||
TextPosition { |
||||
line: location.start.line, |
||||
column: location.start.column, |
||||
offset: location.start.offset, |
||||
}..TextPosition { |
||||
line: location.end.line, |
||||
column: location.end.column, |
||||
offset: location.end.offset, |
||||
}, |
||||
) |
||||
} |
||||
SyntaxErrorKind::RdfXml(_) | SyntaxErrorKind::Msg(_) => None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<RdfSyntaxError> for io::Error { |
||||
#[inline] |
||||
fn from(error: RdfSyntaxError) -> Self { |
||||
match error.0 { |
||||
SyntaxErrorKind::Turtle(error) => error.into(), |
||||
SyntaxErrorKind::RdfXml(error) => error.into(), |
||||
SyntaxErrorKind::Msg(msg) => Self::new(io::ErrorKind::InvalidData, msg), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// A position in a text i.e. a `line` number starting from 0, a `column` number starting from 0 (in number of code points) and a global file `offset` starting from 0 (in number of bytes).
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Copy)] |
||||
pub struct TextPosition { |
||||
pub line: u64, |
||||
pub column: u64, |
||||
pub offset: u64, |
||||
} |
@ -1,216 +0,0 @@ |
||||
use std::fmt; |
||||
|
||||
/// RDF serialization formats.
|
||||
///
|
||||
/// This enumeration is non exhaustive. New formats like JSON-LD might be added in the future.
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] |
||||
#[non_exhaustive] |
||||
pub enum RdfFormat { |
||||
/// [N3](https://w3c.github.io/N3/spec/)
|
||||
N3, |
||||
/// [N-Quads](https://www.w3.org/TR/n-quads/)
|
||||
NQuads, |
||||
/// [N-Triples](https://www.w3.org/TR/n-triples/)
|
||||
NTriples, |
||||
/// [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/)
|
||||
RdfXml, |
||||
/// [TriG](https://www.w3.org/TR/trig/)
|
||||
TriG, |
||||
/// [Turtle](https://www.w3.org/TR/turtle/)
|
||||
Turtle, |
||||
} |
||||
|
||||
impl RdfFormat { |
||||
/// The format canonical IRI according to the [Unique URIs for file formats registry](https://www.w3.org/ns/formats/).
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// RdfFormat::NTriples.iri(),
|
||||
/// "http://www.w3.org/ns/formats/N-Triples"
|
||||
/// )
|
||||
/// ```
|
||||
#[inline] |
||||
pub const fn iri(self) -> &'static str { |
||||
match self { |
||||
Self::N3 => "http://www.w3.org/ns/formats/N3", |
||||
Self::NQuads => "http://www.w3.org/ns/formats/N-Quads", |
||||
Self::NTriples => "http://www.w3.org/ns/formats/N-Triples", |
||||
Self::RdfXml => "http://www.w3.org/ns/formats/RDF_XML", |
||||
Self::TriG => "http://www.w3.org/ns/formats/TriG", |
||||
Self::Turtle => "http://www.w3.org/ns/formats/Turtle", |
||||
} |
||||
} |
||||
|
||||
/// The format [IANA media type](https://tools.ietf.org/html/rfc2046).
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::NTriples.media_type(), "application/n-triples")
|
||||
/// ```
|
||||
#[inline] |
||||
pub const fn media_type(self) -> &'static str { |
||||
match self { |
||||
Self::N3 => "text/n3", |
||||
Self::NQuads => "application/n-quads", |
||||
Self::NTriples => "application/n-triples", |
||||
Self::RdfXml => "application/rdf+xml", |
||||
Self::TriG => "application/trig", |
||||
Self::Turtle => "text/turtle", |
||||
} |
||||
} |
||||
|
||||
/// The format [IANA-registered](https://tools.ietf.org/html/rfc2046) file extension.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::NTriples.file_extension(), "nt")
|
||||
/// ```
|
||||
#[inline] |
||||
pub const fn file_extension(self) -> &'static str { |
||||
match self { |
||||
Self::N3 => "n3", |
||||
Self::NQuads => "nq", |
||||
Self::NTriples => "nt", |
||||
Self::RdfXml => "rdf", |
||||
Self::TriG => "trig", |
||||
Self::Turtle => "ttl", |
||||
} |
||||
} |
||||
|
||||
/// The format name.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::NTriples.name(), "N-Triples")
|
||||
/// ```
|
||||
#[inline] |
||||
pub const fn name(self) -> &'static str { |
||||
match self { |
||||
Self::N3 => "N3", |
||||
Self::NQuads => "N-Quads", |
||||
Self::NTriples => "N-Triples", |
||||
Self::RdfXml => "RDF/XML", |
||||
Self::TriG => "TriG", |
||||
Self::Turtle => "Turtle", |
||||
} |
||||
} |
||||
|
||||
/// Checks if the formats supports [RDF datasets](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset) and not only [RDF graphs](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-graph).
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::NTriples.supports_datasets(), false);
|
||||
/// assert_eq!(RdfFormat::NQuads.supports_datasets(), true);
|
||||
/// ```
|
||||
#[inline] |
||||
pub const fn supports_datasets(self) -> bool { |
||||
matches!(self, Self::NQuads | Self::TriG) |
||||
} |
||||
|
||||
/// Checks if the formats supports [RDF-star quoted triples](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#dfn-quoted).
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::NTriples.supports_rdf_star(), true);
|
||||
/// assert_eq!(RdfFormat::RdfXml.supports_rdf_star(), false);
|
||||
/// ```
|
||||
#[inline] |
||||
#[cfg(feature = "rdf-star")] |
||||
pub const fn supports_rdf_star(self) -> bool { |
||||
matches!( |
||||
self, |
||||
Self::NTriples | Self::NQuads | Self::Turtle | Self::TriG |
||||
) |
||||
} |
||||
|
||||
/// Looks for a known format from a media type.
|
||||
///
|
||||
/// It supports some media type aliases.
|
||||
/// For example, "application/xml" is going to return `RdfFormat::RdfXml` even if it is not its canonical media type.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// RdfFormat::from_media_type("text/turtle; charset=utf-8"),
|
||||
/// Some(RdfFormat::Turtle)
|
||||
/// )
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn from_media_type(media_type: &str) -> Option<Self> { |
||||
const MEDIA_SUBTYPES: [(&str, RdfFormat); 10] = [ |
||||
("n-quads", RdfFormat::NQuads), |
||||
("n-triples", RdfFormat::NTriples), |
||||
("n3", RdfFormat::N3), |
||||
("nquads", RdfFormat::NQuads), |
||||
("ntriples", RdfFormat::NTriples), |
||||
("plain", RdfFormat::NTriples), |
||||
("rdf+xml", RdfFormat::RdfXml), |
||||
("trig", RdfFormat::TriG), |
||||
("turtle", RdfFormat::Turtle), |
||||
("xml", RdfFormat::RdfXml), |
||||
]; |
||||
|
||||
let (r#type, subtype) = media_type |
||||
.split_once(';') |
||||
.unwrap_or((media_type, "")) |
||||
.0 |
||||
.split_once('/')?; |
||||
let r#type = r#type.trim(); |
||||
if !r#type.eq_ignore_ascii_case("application") && !r#type.eq_ignore_ascii_case("text") { |
||||
return None; |
||||
} |
||||
let subtype = subtype.trim(); |
||||
let subtype = subtype.strip_prefix("x-").unwrap_or(subtype); |
||||
for (candidate_subtype, candidate_id) in MEDIA_SUBTYPES { |
||||
if candidate_subtype.eq_ignore_ascii_case(subtype) { |
||||
return Some(candidate_id); |
||||
} |
||||
} |
||||
None |
||||
} |
||||
|
||||
/// Looks for a known format from an extension.
|
||||
///
|
||||
/// It supports some aliases.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// use oxrdfio::RdfFormat;
|
||||
///
|
||||
/// assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples))
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn from_extension(extension: &str) -> Option<Self> { |
||||
const MEDIA_TYPES: [(&str, RdfFormat); 8] = [ |
||||
("n3", RdfFormat::N3), |
||||
("nq", RdfFormat::NQuads), |
||||
("nt", RdfFormat::NTriples), |
||||
("rdf", RdfFormat::RdfXml), |
||||
("trig", RdfFormat::TriG), |
||||
("ttl", RdfFormat::Turtle), |
||||
("txt", RdfFormat::NTriples), |
||||
("xml", RdfFormat::RdfXml), |
||||
]; |
||||
for (candidate_extension, candidate_id) in MEDIA_TYPES { |
||||
if candidate_extension.eq_ignore_ascii_case(extension) { |
||||
return Some(candidate_id); |
||||
} |
||||
} |
||||
None |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for RdfFormat { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
f.write_str(self.name()) |
||||
} |
||||
} |
@ -1,19 +0,0 @@ |
||||
#![doc = include_str!("../README.md")] |
||||
#![doc(test(attr(deny(warnings))))] |
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))] |
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] |
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] |
||||
|
||||
mod error; |
||||
mod format; |
||||
mod parser; |
||||
mod serializer; |
||||
|
||||
pub use error::{RdfParseError, RdfSyntaxError, TextPosition}; |
||||
pub use format::RdfFormat; |
||||
#[cfg(feature = "async-tokio")] |
||||
pub use parser::FromTokioAsyncReadQuadReader; |
||||
pub use parser::{FromReadQuadReader, RdfParser}; |
||||
#[cfg(feature = "async-tokio")] |
||||
pub use serializer::ToTokioAsyncWriteQuadWriter; |
||||
pub use serializer::{RdfSerializer, ToWriteQuadWriter}; |
@ -1,807 +0,0 @@ |
||||
//! Utilities to read RDF graphs and datasets.
|
||||
|
||||
pub use crate::error::RdfParseError; |
||||
use crate::format::RdfFormat; |
||||
use oxrdf::{BlankNode, GraphName, IriParseError, Quad, Subject, Term, Triple}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxrdfxml::FromTokioAsyncReadRdfXmlReader; |
||||
use oxrdfxml::{FromReadRdfXmlReader, RdfXmlParser}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::n3::FromTokioAsyncReadN3Reader; |
||||
use oxttl::n3::{FromReadN3Reader, N3Parser, N3PrefixesIter, N3Quad, N3Term}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::nquads::FromTokioAsyncReadNQuadsReader; |
||||
use oxttl::nquads::{FromReadNQuadsReader, NQuadsParser}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::ntriples::FromTokioAsyncReadNTriplesReader; |
||||
use oxttl::ntriples::{FromReadNTriplesReader, NTriplesParser}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::trig::FromTokioAsyncReadTriGReader; |
||||
use oxttl::trig::{FromReadTriGReader, TriGParser, TriGPrefixesIter}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::turtle::FromTokioAsyncReadTurtleReader; |
||||
use oxttl::turtle::{FromReadTurtleReader, TurtleParser, TurtlePrefixesIter}; |
||||
use std::collections::HashMap; |
||||
use std::io::Read; |
||||
#[cfg(feature = "async-tokio")] |
||||
use tokio::io::AsyncRead; |
||||
|
||||
/// Parsers for RDF serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N3](https://w3c.github.io/N3/spec/) ([`RdfFormat::N3`])
|
||||
/// * [N-Quads](https://www.w3.org/TR/n-quads/) ([`RdfFormat::NQuads`])
|
||||
/// * [N-Triples](https://www.w3.org/TR/n-triples/) ([`RdfFormat::NTriples`])
|
||||
/// * [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) ([`RdfFormat::RdfXml`])
|
||||
/// * [TriG](https://www.w3.org/TR/trig/) ([`RdfFormat::TriG`])
|
||||
/// * [Turtle](https://www.w3.org/TR/turtle/) ([`RdfFormat::Turtle`])
|
||||
///
|
||||
/// Note the useful options:
|
||||
/// - [`with_base_iri`](Self::with_base_iri) to resolve the relative IRIs.
|
||||
/// - [`rename_blank_nodes`](Self::rename_blank_nodes) to rename the blank nodes to auto-generated numbers to avoid conflicts when merging RDF graphs together.
|
||||
/// - [`without_named_graphs`](Self::without_named_graphs) to parse a single graph.
|
||||
/// - [`unchecked`](Self::unchecked) to skip some validations if the file is already known to be valid.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NTriples);
|
||||
/// let quads = parser
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct RdfParser { |
||||
inner: RdfParserKind, |
||||
default_graph: GraphName, |
||||
without_named_graphs: bool, |
||||
rename_blank_nodes: bool, |
||||
} |
||||
|
||||
enum RdfParserKind { |
||||
N3(N3Parser), |
||||
NQuads(NQuadsParser), |
||||
NTriples(NTriplesParser), |
||||
RdfXml(RdfXmlParser), |
||||
TriG(TriGParser), |
||||
Turtle(TurtleParser), |
||||
} |
||||
|
||||
impl RdfParser { |
||||
/// Builds a parser for the given format.
|
||||
#[inline] |
||||
pub fn from_format(format: RdfFormat) -> Self { |
||||
Self { |
||||
inner: match format { |
||||
RdfFormat::N3 => RdfParserKind::N3(N3Parser::new()), |
||||
RdfFormat::NQuads => RdfParserKind::NQuads({ |
||||
#[cfg(feature = "rdf-star")] |
||||
{ |
||||
NQuadsParser::new().with_quoted_triples() |
||||
} |
||||
#[cfg(not(feature = "rdf-star"))] |
||||
{ |
||||
NQuadsParser::new() |
||||
} |
||||
}), |
||||
RdfFormat::NTriples => RdfParserKind::NTriples({ |
||||
#[cfg(feature = "rdf-star")] |
||||
{ |
||||
NTriplesParser::new().with_quoted_triples() |
||||
} |
||||
#[cfg(not(feature = "rdf-star"))] |
||||
{ |
||||
NTriplesParser::new() |
||||
} |
||||
}), |
||||
RdfFormat::RdfXml => RdfParserKind::RdfXml(RdfXmlParser::new()), |
||||
RdfFormat::TriG => RdfParserKind::TriG({ |
||||
#[cfg(feature = "rdf-star")] |
||||
{ |
||||
TriGParser::new().with_quoted_triples() |
||||
} |
||||
#[cfg(not(feature = "rdf-star"))] |
||||
{ |
||||
TriGParser::new() |
||||
} |
||||
}), |
||||
RdfFormat::Turtle => RdfParserKind::Turtle({ |
||||
#[cfg(feature = "rdf-star")] |
||||
{ |
||||
TurtleParser::new().with_quoted_triples() |
||||
} |
||||
#[cfg(not(feature = "rdf-star"))] |
||||
{ |
||||
TurtleParser::new() |
||||
} |
||||
}), |
||||
}, |
||||
default_graph: GraphName::DefaultGraph, |
||||
without_named_graphs: false, |
||||
rename_blank_nodes: false, |
||||
} |
||||
} |
||||
|
||||
/// The format the parser uses.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// RdfParser::from_format(RdfFormat::Turtle).format(),
|
||||
/// RdfFormat::Turtle
|
||||
/// );
|
||||
/// ```
|
||||
pub fn format(&self) -> RdfFormat { |
||||
match &self.inner { |
||||
RdfParserKind::N3(_) => RdfFormat::N3, |
||||
RdfParserKind::NQuads(_) => RdfFormat::NQuads, |
||||
RdfParserKind::NTriples(_) => RdfFormat::NTriples, |
||||
RdfParserKind::RdfXml(_) => RdfFormat::RdfXml, |
||||
RdfParserKind::TriG(_) => RdfFormat::TriG, |
||||
RdfParserKind::Turtle(_) => RdfFormat::Turtle, |
||||
} |
||||
} |
||||
|
||||
/// Provides an IRI that could be used to resolve the file relative IRIs.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "</s> </p> </o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::Turtle).with_base_iri("http://example.com")?;
|
||||
/// let quads = parser
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Result<Self, IriParseError> { |
||||
self.inner = match self.inner { |
||||
RdfParserKind::N3(p) => RdfParserKind::N3(p), |
||||
RdfParserKind::NTriples(p) => RdfParserKind::NTriples(p), |
||||
RdfParserKind::NQuads(p) => RdfParserKind::NQuads(p), |
||||
RdfParserKind::RdfXml(p) => RdfParserKind::RdfXml(p.with_base_iri(base_iri)?), |
||||
RdfParserKind::TriG(p) => RdfParserKind::TriG(p.with_base_iri(base_iri)?), |
||||
RdfParserKind::Turtle(p) => RdfParserKind::Turtle(p.with_base_iri(base_iri)?), |
||||
}; |
||||
Ok(self) |
||||
} |
||||
|
||||
/// Provides the name graph name that should replace the default graph in the returned quads.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdf::NamedNode;
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::Turtle)
|
||||
/// .with_default_graph(NamedNode::new("http://example.com/g")?);
|
||||
/// let quads = parser
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].graph_name.to_string(), "<http://example.com/g>");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn with_default_graph(mut self, default_graph: impl Into<GraphName>) -> Self { |
||||
self.default_graph = default_graph.into(); |
||||
self |
||||
} |
||||
|
||||
/// Sets that the parser must fail if parsing a named graph.
|
||||
///
|
||||
/// This function restricts the parser to only parse a single [RDF graph](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-graph) and not an [RDF dataset](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset).
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NQuads).without_named_graphs();
|
||||
/// assert!(parser.parse_read(file.as_bytes()).next().unwrap().is_err());
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn without_named_graphs(mut self) -> Self { |
||||
self.without_named_graphs = true; |
||||
self |
||||
} |
||||
|
||||
/// Renames the blank nodes ids from the ones set in the serialization to random ids.
|
||||
///
|
||||
/// This allows to avoid id conflicts when merging graphs together.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "_:a <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let result1 = RdfParser::from_format(RdfFormat::NQuads)
|
||||
/// .rename_blank_nodes()
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
/// let result2 = RdfParser::from_format(RdfFormat::NQuads)
|
||||
/// .rename_blank_nodes()
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
/// assert_ne!(result1, result2);
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn rename_blank_nodes(mut self) -> Self { |
||||
self.rename_blank_nodes = true; |
||||
self |
||||
} |
||||
|
||||
/// Assumes the file is valid to make parsing faster.
|
||||
///
|
||||
/// It will skip some validations.
|
||||
///
|
||||
/// Note that if the file is actually not valid, then broken RDF might be emitted by the parser.
|
||||
#[inline] |
||||
pub fn unchecked(mut self) -> Self { |
||||
self.inner = match self.inner { |
||||
RdfParserKind::N3(p) => RdfParserKind::N3(p.unchecked()), |
||||
RdfParserKind::NTriples(p) => RdfParserKind::NTriples(p.unchecked()), |
||||
RdfParserKind::NQuads(p) => RdfParserKind::NQuads(p.unchecked()), |
||||
RdfParserKind::RdfXml(p) => RdfParserKind::RdfXml(p.unchecked()), |
||||
RdfParserKind::TriG(p) => RdfParserKind::TriG(p.unchecked()), |
||||
RdfParserKind::Turtle(p) => RdfParserKind::Turtle(p.unchecked()), |
||||
}; |
||||
self |
||||
} |
||||
|
||||
/// Parses from a [`Read`] implementation and returns an iterator of quads.
|
||||
///
|
||||
/// Reads are buffered.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NTriples);
|
||||
/// let quads = parser
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn parse_read<R: Read>(self, reader: R) -> FromReadQuadReader<R> { |
||||
FromReadQuadReader { |
||||
parser: match self.inner { |
||||
RdfParserKind::N3(p) => FromReadQuadReaderKind::N3(p.parse_read(reader)), |
||||
RdfParserKind::NQuads(p) => FromReadQuadReaderKind::NQuads(p.parse_read(reader)), |
||||
RdfParserKind::NTriples(p) => { |
||||
FromReadQuadReaderKind::NTriples(p.parse_read(reader)) |
||||
} |
||||
RdfParserKind::RdfXml(p) => FromReadQuadReaderKind::RdfXml(p.parse_read(reader)), |
||||
RdfParserKind::TriG(p) => FromReadQuadReaderKind::TriG(p.parse_read(reader)), |
||||
RdfParserKind::Turtle(p) => FromReadQuadReaderKind::Turtle(p.parse_read(reader)), |
||||
}, |
||||
mapper: QuadMapper { |
||||
default_graph: self.default_graph.clone(), |
||||
without_named_graphs: self.without_named_graphs, |
||||
blank_node_map: self.rename_blank_nodes.then(HashMap::new), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// Parses from a Tokio [`AsyncRead`] implementation and returns an async iterator of quads.
|
||||
///
|
||||
/// Reads are buffered.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> Result<(), oxrdfio::RdfParseError> {
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NTriples);
|
||||
/// let mut reader = parser.parse_tokio_async_read(file.as_bytes());
|
||||
/// if let Some(quad) = reader.next().await {
|
||||
/// assert_eq!(quad?.subject.to_string(), "<http://example.com/s>");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "async-tokio")] |
||||
pub fn parse_tokio_async_read<R: AsyncRead + Unpin>( |
||||
self, |
||||
reader: R, |
||||
) -> FromTokioAsyncReadQuadReader<R> { |
||||
FromTokioAsyncReadQuadReader { |
||||
parser: match self.inner { |
||||
RdfParserKind::N3(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::N3(p.parse_tokio_async_read(reader)) |
||||
} |
||||
RdfParserKind::NQuads(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::NQuads(p.parse_tokio_async_read(reader)) |
||||
} |
||||
RdfParserKind::NTriples(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::NTriples(p.parse_tokio_async_read(reader)) |
||||
} |
||||
RdfParserKind::RdfXml(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::RdfXml(p.parse_tokio_async_read(reader)) |
||||
} |
||||
RdfParserKind::TriG(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::TriG(p.parse_tokio_async_read(reader)) |
||||
} |
||||
RdfParserKind::Turtle(p) => { |
||||
FromTokioAsyncReadQuadReaderKind::Turtle(p.parse_tokio_async_read(reader)) |
||||
} |
||||
}, |
||||
mapper: QuadMapper { |
||||
default_graph: self.default_graph.clone(), |
||||
without_named_graphs: self.without_named_graphs, |
||||
blank_node_map: self.rename_blank_nodes.then(HashMap::new), |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<RdfFormat> for RdfParser { |
||||
fn from(format: RdfFormat) -> Self { |
||||
Self::from_format(format) |
||||
} |
||||
} |
||||
|
||||
/// Parses a RDF file from a [`Read`] implementation. Can be built using [`RdfParser::parse_read`].
|
||||
///
|
||||
/// Reads are buffered.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NTriples);
|
||||
/// let quads = parser
|
||||
/// .parse_read(file.as_bytes())
|
||||
/// .collect::<Result<Vec<_>, _>>()?;
|
||||
///
|
||||
/// assert_eq!(quads.len(), 1);
|
||||
/// assert_eq!(quads[0].subject.to_string(), "<http://example.com/s>");
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct FromReadQuadReader<R: Read> { |
||||
parser: FromReadQuadReaderKind<R>, |
||||
mapper: QuadMapper, |
||||
} |
||||
|
||||
enum FromReadQuadReaderKind<R: Read> { |
||||
N3(FromReadN3Reader<R>), |
||||
NQuads(FromReadNQuadsReader<R>), |
||||
NTriples(FromReadNTriplesReader<R>), |
||||
RdfXml(FromReadRdfXmlReader<R>), |
||||
TriG(FromReadTriGReader<R>), |
||||
Turtle(FromReadTurtleReader<R>), |
||||
} |
||||
|
||||
impl<R: Read> Iterator for FromReadQuadReader<R> { |
||||
type Item = Result<Quad, RdfParseError>; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
Some(match &mut self.parser { |
||||
FromReadQuadReaderKind::N3(parser) => match parser.next()? { |
||||
Ok(quad) => self.mapper.map_n3_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromReadQuadReaderKind::NQuads(parser) => match parser.next()? { |
||||
Ok(quad) => self.mapper.map_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromReadQuadReaderKind::NTriples(parser) => match parser.next()? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromReadQuadReaderKind::RdfXml(parser) => match parser.next()? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromReadQuadReaderKind::TriG(parser) => match parser.next()? { |
||||
Ok(quad) => self.mapper.map_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromReadQuadReaderKind::Turtle(parser) => match parser.next()? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl<R: Read> FromReadQuadReader<R> { |
||||
/// The list of IRI prefixes considered at the current step of the parsing.
|
||||
///
|
||||
/// This method returns (prefix name, prefix value) tuples.
|
||||
/// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
|
||||
/// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
|
||||
///
|
||||
/// An empty iterator is return if the format does not support prefixes.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = br#"@base <http://example.com/> .
|
||||
/// @prefix schema: <http://schema.org/> .
|
||||
/// <foo> a schema:Person ;
|
||||
/// schema:name "Foo" ."#;
|
||||
///
|
||||
/// let mut reader = RdfParser::from_format(RdfFormat::Turtle).parse_read(file.as_slice());
|
||||
/// assert!(reader.prefixes().collect::<Vec<_>>().is_empty()); // No prefix at the beginning
|
||||
///
|
||||
/// reader.next().unwrap()?; // We read the first triple
|
||||
/// assert_eq!(
|
||||
/// reader.prefixes().collect::<Vec<_>>(),
|
||||
/// [("schema", "http://schema.org/")]
|
||||
/// ); // There are now prefixes
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub fn prefixes(&self) -> PrefixesIter<'_> { |
||||
PrefixesIter { |
||||
inner: match &self.parser { |
||||
FromReadQuadReaderKind::N3(p) => PrefixesIterKind::N3(p.prefixes()), |
||||
FromReadQuadReaderKind::TriG(p) => PrefixesIterKind::TriG(p.prefixes()), |
||||
FromReadQuadReaderKind::Turtle(p) => PrefixesIterKind::Turtle(p.prefixes()), |
||||
FromReadQuadReaderKind::NQuads(_) |
||||
| FromReadQuadReaderKind::NTriples(_) |
||||
| FromReadQuadReaderKind::RdfXml(_) => PrefixesIterKind::None, /* TODO: implement for RDF/XML */ |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// The base IRI considered at the current step of the parsing.
|
||||
///
|
||||
/// `None` is returned if no base IRI is set or the format does not support base IRIs.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// let file = br#"@base <http://example.com/> .
|
||||
/// @prefix schema: <http://schema.org/> .
|
||||
/// <foo> a schema:Person ;
|
||||
/// schema:name "Foo" ."#;
|
||||
///
|
||||
/// let mut reader = RdfParser::from_format(RdfFormat::Turtle).parse_read(file.as_slice());
|
||||
/// assert!(reader.base_iri().is_none()); // No base at the beginning because none has been given to the parser.
|
||||
///
|
||||
/// reader.next().unwrap()?; // We read the first triple
|
||||
/// assert_eq!(reader.base_iri(), Some("http://example.com/")); // There is now a base IRI.
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub fn base_iri(&self) -> Option<&str> { |
||||
match &self.parser { |
||||
FromReadQuadReaderKind::N3(p) => p.base_iri(), |
||||
FromReadQuadReaderKind::TriG(p) => p.base_iri(), |
||||
FromReadQuadReaderKind::Turtle(p) => p.base_iri(), |
||||
FromReadQuadReaderKind::NQuads(_) |
||||
| FromReadQuadReaderKind::NTriples(_) |
||||
| FromReadQuadReaderKind::RdfXml(_) => None, // TODO: implement for RDF/XML
|
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Parses a RDF file from a Tokio [`AsyncRead`] implementation. Can be built using [`RdfParser::parse_tokio_async_read`].
|
||||
///
|
||||
/// Reads are buffered.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> Result<(), oxrdfio::RdfParseError> {
|
||||
/// let file = "<http://example.com/s> <http://example.com/p> <http://example.com/o> .";
|
||||
///
|
||||
/// let parser = RdfParser::from_format(RdfFormat::NTriples);
|
||||
/// let mut reader = parser.parse_tokio_async_read(file.as_bytes());
|
||||
/// if let Some(quad) = reader.next().await {
|
||||
/// assert_eq!(quad?.subject.to_string(), "<http://example.com/s>");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use] |
||||
#[cfg(feature = "async-tokio")] |
||||
pub struct FromTokioAsyncReadQuadReader<R: AsyncRead + Unpin> { |
||||
parser: FromTokioAsyncReadQuadReaderKind<R>, |
||||
mapper: QuadMapper, |
||||
} |
||||
|
||||
#[cfg(feature = "async-tokio")] |
||||
enum FromTokioAsyncReadQuadReaderKind<R: AsyncRead + Unpin> { |
||||
N3(FromTokioAsyncReadN3Reader<R>), |
||||
NQuads(FromTokioAsyncReadNQuadsReader<R>), |
||||
NTriples(FromTokioAsyncReadNTriplesReader<R>), |
||||
RdfXml(FromTokioAsyncReadRdfXmlReader<R>), |
||||
TriG(FromTokioAsyncReadTriGReader<R>), |
||||
Turtle(FromTokioAsyncReadTurtleReader<R>), |
||||
} |
||||
|
||||
#[cfg(feature = "async-tokio")] |
||||
impl<R: AsyncRead + Unpin> FromTokioAsyncReadQuadReader<R> { |
||||
pub async fn next(&mut self) -> Option<Result<Quad, RdfParseError>> { |
||||
Some(match &mut self.parser { |
||||
FromTokioAsyncReadQuadReaderKind::N3(parser) => match parser.next().await? { |
||||
Ok(quad) => self.mapper.map_n3_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromTokioAsyncReadQuadReaderKind::NQuads(parser) => match parser.next().await? { |
||||
Ok(quad) => self.mapper.map_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromTokioAsyncReadQuadReaderKind::NTriples(parser) => match parser.next().await? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromTokioAsyncReadQuadReaderKind::RdfXml(parser) => match parser.next().await? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromTokioAsyncReadQuadReaderKind::TriG(parser) => match parser.next().await? { |
||||
Ok(quad) => self.mapper.map_quad(quad), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
FromTokioAsyncReadQuadReaderKind::Turtle(parser) => match parser.next().await? { |
||||
Ok(triple) => Ok(self.mapper.map_triple_to_quad(triple)), |
||||
Err(e) => Err(e.into()), |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
/// The list of IRI prefixes considered at the current step of the parsing.
|
||||
///
|
||||
/// This method returns (prefix name, prefix value) tuples.
|
||||
/// It is empty at the beginning of the parsing and gets updated when prefixes are encountered.
|
||||
/// It should be full at the end of the parsing (but if a prefix is overridden, only the latest version will be returned).
|
||||
///
|
||||
/// An empty iterator is return if the format does not support prefixes.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> Result<(), oxttl::TurtleParseError> {
|
||||
/// let file = br#"@base <http://example.com/> .
|
||||
/// @prefix schema: <http://schema.org/> .
|
||||
/// <foo> a schema:Person ;
|
||||
/// schema:name "Foo" ."#;
|
||||
///
|
||||
/// let mut reader = RdfParser::from_format(RdfFormat::Turtle).parse_read(file.as_slice());
|
||||
/// assert_eq!(reader.prefixes().collect::<Vec<_>>(), []); // No prefix at the beginning
|
||||
///
|
||||
/// reader.next().await.unwrap()?; // We read the first triple
|
||||
/// assert_eq!(
|
||||
/// reader.prefixes().collect::<Vec<_>>(),
|
||||
/// [("schema", "http://schema.org/")]
|
||||
/// ); // There are now prefixes
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn prefixes(&self) -> PrefixesIter<'_> { |
||||
PrefixesIter { |
||||
inner: match &self.parser { |
||||
FromTokioAsyncReadQuadReaderKind::N3(p) => PrefixesIterKind::N3(p.prefixes()), |
||||
FromTokioAsyncReadQuadReaderKind::TriG(p) => PrefixesIterKind::TriG(p.prefixes()), |
||||
FromTokioAsyncReadQuadReaderKind::Turtle(p) => { |
||||
PrefixesIterKind::Turtle(p.prefixes()) |
||||
} |
||||
FromTokioAsyncReadQuadReaderKind::NQuads(_) |
||||
| FromTokioAsyncReadQuadReaderKind::NTriples(_) |
||||
| FromTokioAsyncReadQuadReaderKind::RdfXml(_) => PrefixesIterKind::None, /* TODO: implement for RDF/XML */ |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// The base IRI considered at the current step of the parsing.
|
||||
///
|
||||
/// `None` is returned if no base IRI is set or the format does not support base IRIs.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfParser};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> Result<(), oxttl::TurtleParseError> {
|
||||
/// let file = br#"@base <http://example.com/> .
|
||||
/// @prefix schema: <http://schema.org/> .
|
||||
/// <foo> a schema:Person ;
|
||||
/// schema:name "Foo" ."#;
|
||||
///
|
||||
/// let mut reader =
|
||||
/// RdfParser::from_format(RdfFormat::Turtle).parse_tokio_async_read(file.as_slice());
|
||||
/// assert!(reader.base_iri().is_none()); // No base IRI at the beginning
|
||||
///
|
||||
/// reader.next().await.unwrap()?; // We read the first triple
|
||||
/// assert_eq!(reader.base_iri(), Some("http://example.com/")); // There is now a base IRI
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn base_iri(&self) -> Option<&str> { |
||||
match &self.parser { |
||||
FromTokioAsyncReadQuadReaderKind::N3(p) => p.base_iri(), |
||||
FromTokioAsyncReadQuadReaderKind::TriG(p) => p.base_iri(), |
||||
FromTokioAsyncReadQuadReaderKind::Turtle(p) => p.base_iri(), |
||||
FromTokioAsyncReadQuadReaderKind::NQuads(_) |
||||
| FromTokioAsyncReadQuadReaderKind::NTriples(_) |
||||
| FromTokioAsyncReadQuadReaderKind::RdfXml(_) => None, // TODO: implement for RDF/XML
|
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Iterator on the file prefixes.
|
||||
///
|
||||
/// See [`FromReadQuadReader::prefixes`].
|
||||
pub struct PrefixesIter<'a> { |
||||
inner: PrefixesIterKind<'a>, |
||||
} |
||||
|
||||
enum PrefixesIterKind<'a> { |
||||
Turtle(TurtlePrefixesIter<'a>), |
||||
TriG(TriGPrefixesIter<'a>), |
||||
N3(N3PrefixesIter<'a>), |
||||
None, |
||||
} |
||||
|
||||
impl<'a> Iterator for PrefixesIter<'a> { |
||||
type Item = (&'a str, &'a str); |
||||
|
||||
#[inline] |
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
match &mut self.inner { |
||||
PrefixesIterKind::Turtle(iter) => iter.next(), |
||||
PrefixesIterKind::TriG(iter) => iter.next(), |
||||
PrefixesIterKind::N3(iter) => iter.next(), |
||||
PrefixesIterKind::None => None, |
||||
} |
||||
} |
||||
|
||||
#[inline] |
||||
fn size_hint(&self) -> (usize, Option<usize>) { |
||||
match &self.inner { |
||||
PrefixesIterKind::Turtle(iter) => iter.size_hint(), |
||||
PrefixesIterKind::TriG(iter) => iter.size_hint(), |
||||
PrefixesIterKind::N3(iter) => iter.size_hint(), |
||||
PrefixesIterKind::None => (0, Some(0)), |
||||
} |
||||
} |
||||
} |
||||
|
||||
struct QuadMapper { |
||||
default_graph: GraphName, |
||||
without_named_graphs: bool, |
||||
blank_node_map: Option<HashMap<BlankNode, BlankNode>>, |
||||
} |
||||
|
||||
impl QuadMapper { |
||||
fn map_blank_node(&mut self, node: BlankNode) -> BlankNode { |
||||
if let Some(blank_node_map) = &mut self.blank_node_map { |
||||
blank_node_map |
||||
.entry(node) |
||||
.or_insert_with(BlankNode::default) |
||||
.clone() |
||||
} else { |
||||
node |
||||
} |
||||
} |
||||
|
||||
fn map_subject(&mut self, node: Subject) -> Subject { |
||||
match node { |
||||
Subject::NamedNode(node) => node.into(), |
||||
Subject::BlankNode(node) => self.map_blank_node(node).into(), |
||||
#[cfg(feature = "rdf-star")] |
||||
Subject::Triple(triple) => self.map_triple(*triple).into(), |
||||
} |
||||
} |
||||
|
||||
fn map_term(&mut self, node: Term) -> Term { |
||||
match node { |
||||
Term::NamedNode(node) => node.into(), |
||||
Term::BlankNode(node) => self.map_blank_node(node).into(), |
||||
Term::Literal(literal) => literal.into(), |
||||
#[cfg(feature = "rdf-star")] |
||||
Term::Triple(triple) => self.map_triple(*triple).into(), |
||||
} |
||||
} |
||||
|
||||
fn map_triple(&mut self, triple: Triple) -> Triple { |
||||
Triple { |
||||
subject: self.map_subject(triple.subject), |
||||
predicate: triple.predicate, |
||||
object: self.map_term(triple.object), |
||||
} |
||||
} |
||||
|
||||
fn map_graph_name(&mut self, graph_name: GraphName) -> Result<GraphName, RdfParseError> { |
||||
match graph_name { |
||||
GraphName::NamedNode(node) => { |
||||
if self.without_named_graphs { |
||||
Err(RdfParseError::msg("Named graphs are not allowed")) |
||||
} else { |
||||
Ok(node.into()) |
||||
} |
||||
} |
||||
GraphName::BlankNode(node) => { |
||||
if self.without_named_graphs { |
||||
Err(RdfParseError::msg("Named graphs are not allowed")) |
||||
} else { |
||||
Ok(self.map_blank_node(node).into()) |
||||
} |
||||
} |
||||
GraphName::DefaultGraph => Ok(self.default_graph.clone()), |
||||
} |
||||
} |
||||
|
||||
fn map_quad(&mut self, quad: Quad) -> Result<Quad, RdfParseError> { |
||||
Ok(Quad { |
||||
subject: self.map_subject(quad.subject), |
||||
predicate: quad.predicate, |
||||
object: self.map_term(quad.object), |
||||
graph_name: self.map_graph_name(quad.graph_name)?, |
||||
}) |
||||
} |
||||
|
||||
fn map_triple_to_quad(&mut self, triple: Triple) -> Quad { |
||||
self.map_triple(triple).in_graph(self.default_graph.clone()) |
||||
} |
||||
|
||||
fn map_n3_quad(&mut self, quad: N3Quad) -> Result<Quad, RdfParseError> { |
||||
Ok(Quad { |
||||
subject: match quad.subject { |
||||
N3Term::NamedNode(s) => Ok(s.into()), |
||||
N3Term::BlankNode(s) => Ok(self.map_blank_node(s).into()), |
||||
N3Term::Literal(_) => Err(RdfParseError::msg( |
||||
"literals are not allowed in regular RDF subjects", |
||||
)), |
||||
#[cfg(feature = "rdf-star")] |
||||
N3Term::Triple(s) => Ok(self.map_triple(*s).into()), |
||||
N3Term::Variable(_) => Err(RdfParseError::msg( |
||||
"variables are not allowed in regular RDF subjects", |
||||
)), |
||||
}?, |
||||
predicate: match quad.predicate { |
||||
N3Term::NamedNode(p) => Ok(p), |
||||
N3Term::BlankNode(_) => Err(RdfParseError::msg( |
||||
"blank nodes are not allowed in regular RDF predicates", |
||||
)), |
||||
N3Term::Literal(_) => Err(RdfParseError::msg( |
||||
"literals are not allowed in regular RDF predicates", |
||||
)), |
||||
#[cfg(feature = "rdf-star")] |
||||
N3Term::Triple(_) => Err(RdfParseError::msg( |
||||
"quoted triples are not allowed in regular RDF predicates", |
||||
)), |
||||
N3Term::Variable(_) => Err(RdfParseError::msg( |
||||
"variables are not allowed in regular RDF predicates", |
||||
)), |
||||
}?, |
||||
object: match quad.object { |
||||
N3Term::NamedNode(o) => Ok(o.into()), |
||||
N3Term::BlankNode(o) => Ok(self.map_blank_node(o).into()), |
||||
N3Term::Literal(o) => Ok(o.into()), |
||||
#[cfg(feature = "rdf-star")] |
||||
N3Term::Triple(o) => Ok(self.map_triple(*o).into()), |
||||
N3Term::Variable(_) => Err(RdfParseError::msg( |
||||
"variables are not allowed in regular RDF objects", |
||||
)), |
||||
}?, |
||||
graph_name: self.map_graph_name(quad.graph_name)?, |
||||
}) |
||||
} |
||||
} |
@ -1,410 +0,0 @@ |
||||
//! Utilities to write RDF graphs and datasets.
|
||||
|
||||
use crate::format::RdfFormat; |
||||
use oxrdf::{GraphNameRef, IriParseError, QuadRef, TripleRef}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxrdfxml::ToTokioAsyncWriteRdfXmlWriter; |
||||
use oxrdfxml::{RdfXmlSerializer, ToWriteRdfXmlWriter}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::nquads::ToTokioAsyncWriteNQuadsWriter; |
||||
use oxttl::nquads::{NQuadsSerializer, ToWriteNQuadsWriter}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::ntriples::ToTokioAsyncWriteNTriplesWriter; |
||||
use oxttl::ntriples::{NTriplesSerializer, ToWriteNTriplesWriter}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::trig::ToTokioAsyncWriteTriGWriter; |
||||
use oxttl::trig::{ToWriteTriGWriter, TriGSerializer}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use oxttl::turtle::ToTokioAsyncWriteTurtleWriter; |
||||
use oxttl::turtle::{ToWriteTurtleWriter, TurtleSerializer}; |
||||
use std::io::{self, Write}; |
||||
#[cfg(feature = "async-tokio")] |
||||
use tokio::io::AsyncWrite; |
||||
|
||||
/// A serializer for RDF serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
/// * [N3](https://w3c.github.io/N3/spec/) ([`RdfFormat::N3`])
|
||||
/// * [N-Quads](https://www.w3.org/TR/n-quads/) ([`RdfFormat::NQuads`])
|
||||
/// * [canonical](https://www.w3.org/TR/n-triples/#canonical-ntriples) [N-Triples](https://www.w3.org/TR/n-triples/) ([`RdfFormat::NTriples`])
|
||||
/// * [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) ([`RdfFormat::RdfXml`])
|
||||
/// * [TriG](https://www.w3.org/TR/trig/) ([`RdfFormat::TriG`])
|
||||
/// * [Turtle](https://www.w3.org/TR/turtle/) ([`RdfFormat::Turtle`])
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
/// use oxrdf::{Quad, NamedNode};
|
||||
///
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::NQuads).serialize_to_write(Vec::new());
|
||||
/// writer.write_quad(&Quad {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// graph_name: NamedNode::new("http://example.com/g")?.into()
|
||||
/// })?;
|
||||
/// assert_eq!(writer.finish()?, b"<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct RdfSerializer { |
||||
inner: RdfSerializerKind, |
||||
} |
||||
|
||||
enum RdfSerializerKind { |
||||
NQuads(NQuadsSerializer), |
||||
NTriples(NTriplesSerializer), |
||||
RdfXml(RdfXmlSerializer), |
||||
TriG(TriGSerializer), |
||||
Turtle(TurtleSerializer), |
||||
} |
||||
|
||||
impl RdfSerializer { |
||||
/// Builds a serializer for the given format
|
||||
#[inline] |
||||
pub fn from_format(format: RdfFormat) -> Self { |
||||
Self { |
||||
inner: match format { |
||||
RdfFormat::NQuads => RdfSerializerKind::NQuads(NQuadsSerializer::new()), |
||||
RdfFormat::NTriples => RdfSerializerKind::NTriples(NTriplesSerializer::new()), |
||||
RdfFormat::RdfXml => RdfSerializerKind::RdfXml(RdfXmlSerializer::new()), |
||||
RdfFormat::TriG => RdfSerializerKind::TriG(TriGSerializer::new()), |
||||
RdfFormat::Turtle | RdfFormat::N3 => { |
||||
RdfSerializerKind::Turtle(TurtleSerializer::new()) |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// The format the serializer serializes to.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// RdfSerializer::from_format(RdfFormat::Turtle).format(),
|
||||
/// RdfFormat::Turtle
|
||||
/// );
|
||||
/// ```
|
||||
pub fn format(&self) -> RdfFormat { |
||||
match &self.inner { |
||||
RdfSerializerKind::NQuads(_) => RdfFormat::NQuads, |
||||
RdfSerializerKind::NTriples(_) => RdfFormat::NTriples, |
||||
RdfSerializerKind::RdfXml(_) => RdfFormat::RdfXml, |
||||
RdfSerializerKind::TriG(_) => RdfFormat::TriG, |
||||
RdfSerializerKind::Turtle(_) => RdfFormat::Turtle, |
||||
} |
||||
} |
||||
|
||||
/// If the format supports it, sets a prefix.
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdf::vocab::rdf;
|
||||
/// use oxrdf::{NamedNodeRef, TripleRef};
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
///
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::Turtle)
|
||||
/// .with_prefix("schema", "http://schema.org/")?
|
||||
/// .serialize_to_write(Vec::new());
|
||||
/// writer.write_triple(TripleRef {
|
||||
/// subject: NamedNodeRef::new("http://example.com/s")?.into(),
|
||||
/// predicate: rdf::TYPE.into(),
|
||||
/// object: NamedNodeRef::new("http://schema.org/Person")?.into(),
|
||||
/// })?;
|
||||
/// assert_eq!(
|
||||
/// writer.finish()?,
|
||||
/// b"@prefix schema: <http://schema.org/> .\n<http://example.com/s> a schema:Person .\n"
|
||||
/// );
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[inline] |
||||
pub fn with_prefix( |
||||
mut self, |
||||
prefix_name: impl Into<String>, |
||||
prefix_iri: impl Into<String>, |
||||
) -> Result<Self, IriParseError> { |
||||
self.inner = match self.inner { |
||||
RdfSerializerKind::NQuads(s) => RdfSerializerKind::NQuads(s), |
||||
RdfSerializerKind::NTriples(s) => RdfSerializerKind::NTriples(s), |
||||
RdfSerializerKind::RdfXml(s) => { |
||||
RdfSerializerKind::RdfXml(s.with_prefix(prefix_name, prefix_iri)?) |
||||
} |
||||
RdfSerializerKind::TriG(s) => { |
||||
RdfSerializerKind::TriG(s.with_prefix(prefix_name, prefix_iri)?) |
||||
} |
||||
RdfSerializerKind::Turtle(s) => { |
||||
RdfSerializerKind::Turtle(s.with_prefix(prefix_name, prefix_iri)?) |
||||
} |
||||
}; |
||||
Ok(self) |
||||
} |
||||
|
||||
/// Writes to a [`Write`] implementation.
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](ToWriteQuadWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// This writer does unbuffered writes. You might want to use [`BufWriter`](io::BufWriter) to avoid that.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
/// use oxrdf::{Quad, NamedNode};
|
||||
///
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::NQuads).serialize_to_write(Vec::new());
|
||||
/// writer.write_quad(&Quad {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// graph_name: NamedNode::new("http://example.com/g")?.into()
|
||||
/// })?;
|
||||
/// assert_eq!(writer.finish()?, b"<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
pub fn serialize_to_write<W: Write>(self, write: W) -> ToWriteQuadWriter<W> { |
||||
ToWriteQuadWriter { |
||||
formatter: match self.inner { |
||||
RdfSerializerKind::NQuads(s) => { |
||||
ToWriteQuadWriterKind::NQuads(s.serialize_to_write(write)) |
||||
} |
||||
RdfSerializerKind::NTriples(s) => { |
||||
ToWriteQuadWriterKind::NTriples(s.serialize_to_write(write)) |
||||
} |
||||
RdfSerializerKind::RdfXml(s) => { |
||||
ToWriteQuadWriterKind::RdfXml(s.serialize_to_write(write)) |
||||
} |
||||
RdfSerializerKind::TriG(s) => { |
||||
ToWriteQuadWriterKind::TriG(s.serialize_to_write(write)) |
||||
} |
||||
RdfSerializerKind::Turtle(s) => { |
||||
ToWriteQuadWriterKind::Turtle(s.serialize_to_write(write)) |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// Writes to a Tokio [`AsyncWrite`] implementation.
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](ToTokioAsyncWriteQuadWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// This writer does unbuffered writes. You might want to use [`BufWriter`](tokio::io::BufWriter) to avoid that.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
/// use oxrdf::{Quad, NamedNode};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> std::io::Result<()> {
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::NQuads).serialize_to_tokio_async_write(Vec::new());
|
||||
/// writer.write_quad(&Quad {
|
||||
/// subject: NamedNode::new_unchecked("http://example.com/s").into(),
|
||||
/// predicate: NamedNode::new_unchecked("http://example.com/p"),
|
||||
/// object: NamedNode::new_unchecked("http://example.com/o").into(),
|
||||
/// graph_name: NamedNode::new_unchecked("http://example.com/g").into()
|
||||
/// }).await?;
|
||||
/// assert_eq!(writer.finish().await?, "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "async-tokio")] |
||||
pub fn serialize_to_tokio_async_write<W: AsyncWrite + Unpin>( |
||||
self, |
||||
write: W, |
||||
) -> ToTokioAsyncWriteQuadWriter<W> { |
||||
ToTokioAsyncWriteQuadWriter { |
||||
formatter: match self.inner { |
||||
RdfSerializerKind::NQuads(s) => { |
||||
ToTokioAsyncWriteQuadWriterKind::NQuads(s.serialize_to_tokio_async_write(write)) |
||||
} |
||||
RdfSerializerKind::NTriples(s) => ToTokioAsyncWriteQuadWriterKind::NTriples( |
||||
s.serialize_to_tokio_async_write(write), |
||||
), |
||||
RdfSerializerKind::RdfXml(s) => { |
||||
ToTokioAsyncWriteQuadWriterKind::RdfXml(s.serialize_to_tokio_async_write(write)) |
||||
} |
||||
RdfSerializerKind::TriG(s) => { |
||||
ToTokioAsyncWriteQuadWriterKind::TriG(s.serialize_to_tokio_async_write(write)) |
||||
} |
||||
RdfSerializerKind::Turtle(s) => { |
||||
ToTokioAsyncWriteQuadWriterKind::Turtle(s.serialize_to_tokio_async_write(write)) |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<RdfFormat> for RdfSerializer { |
||||
fn from(format: RdfFormat) -> Self { |
||||
Self::from_format(format) |
||||
} |
||||
} |
||||
|
||||
/// Writes quads or triples to a [`Write`] implementation.
|
||||
///
|
||||
/// Can be built using [`RdfSerializer::serialize_to_write`].
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](ToWriteQuadWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// This writer does unbuffered writes. You might want to use [`BufWriter`](io::BufWriter) to avoid that.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
/// use oxrdf::{Quad, NamedNode};
|
||||
///
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::NQuads).serialize_to_write(Vec::new());
|
||||
/// writer.write_quad(&Quad {
|
||||
/// subject: NamedNode::new("http://example.com/s")?.into(),
|
||||
/// predicate: NamedNode::new("http://example.com/p")?,
|
||||
/// object: NamedNode::new("http://example.com/o")?.into(),
|
||||
/// graph_name: NamedNode::new("http://example.com/g")?.into(),
|
||||
/// })?;
|
||||
/// assert_eq!(writer.finish()?, b"<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n");
|
||||
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||
/// ```
|
||||
#[must_use] |
||||
pub struct ToWriteQuadWriter<W: Write> { |
||||
formatter: ToWriteQuadWriterKind<W>, |
||||
} |
||||
|
||||
enum ToWriteQuadWriterKind<W: Write> { |
||||
NQuads(ToWriteNQuadsWriter<W>), |
||||
NTriples(ToWriteNTriplesWriter<W>), |
||||
RdfXml(ToWriteRdfXmlWriter<W>), |
||||
TriG(ToWriteTriGWriter<W>), |
||||
Turtle(ToWriteTurtleWriter<W>), |
||||
} |
||||
|
||||
impl<W: Write> ToWriteQuadWriter<W> { |
||||
/// Writes a [`QuadRef`]
|
||||
pub fn write_quad<'a>(&mut self, quad: impl Into<QuadRef<'a>>) -> io::Result<()> { |
||||
match &mut self.formatter { |
||||
ToWriteQuadWriterKind::NQuads(writer) => writer.write_quad(quad), |
||||
ToWriteQuadWriterKind::NTriples(writer) => writer.write_triple(to_triple(quad)?), |
||||
ToWriteQuadWriterKind::RdfXml(writer) => writer.write_triple(to_triple(quad)?), |
||||
ToWriteQuadWriterKind::TriG(writer) => writer.write_quad(quad), |
||||
ToWriteQuadWriterKind::Turtle(writer) => writer.write_triple(to_triple(quad)?), |
||||
} |
||||
} |
||||
|
||||
/// Writes a [`TripleRef`]
|
||||
pub fn write_triple<'a>(&mut self, triple: impl Into<TripleRef<'a>>) -> io::Result<()> { |
||||
self.write_quad(triple.into().in_graph(GraphNameRef::DefaultGraph)) |
||||
} |
||||
|
||||
/// Writes the last bytes of the file
|
||||
///
|
||||
/// Note that this function does not flush the writer. You need to do that if you are using a [`BufWriter`](io::BufWriter).
|
||||
pub fn finish(self) -> io::Result<W> { |
||||
Ok(match self.formatter { |
||||
ToWriteQuadWriterKind::NQuads(writer) => writer.finish(), |
||||
ToWriteQuadWriterKind::NTriples(writer) => writer.finish(), |
||||
ToWriteQuadWriterKind::RdfXml(writer) => writer.finish()?, |
||||
ToWriteQuadWriterKind::TriG(writer) => writer.finish()?, |
||||
ToWriteQuadWriterKind::Turtle(writer) => writer.finish()?, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/// Writes quads or triples to a [`Write`] implementation.
|
||||
///
|
||||
/// Can be built using [`RdfSerializer::serialize_to_write`].
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Do not forget to run the [`finish`](ToWriteQuadWriter::finish()) method to properly write the last bytes of the file.</div>
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// This writer does unbuffered writes. You might want to use [`BufWriter`](io::BufWriter) to avoid that.</div>
|
||||
///
|
||||
/// ```
|
||||
/// use oxrdfio::{RdfFormat, RdfSerializer};
|
||||
/// use oxrdf::{Quad, NamedNode};
|
||||
///
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() -> std::io::Result<()> {
|
||||
/// let mut writer = RdfSerializer::from_format(RdfFormat::NQuads).serialize_to_tokio_async_write(Vec::new());
|
||||
/// writer.write_quad(&Quad {
|
||||
/// subject: NamedNode::new_unchecked("http://example.com/s").into(),
|
||||
/// predicate: NamedNode::new_unchecked("http://example.com/p"),
|
||||
/// object: NamedNode::new_unchecked("http://example.com/o").into(),
|
||||
/// graph_name: NamedNode::new_unchecked("http://example.com/g").into()
|
||||
/// }).await?;
|
||||
/// assert_eq!(writer.finish().await?, "<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use] |
||||
#[cfg(feature = "async-tokio")] |
||||
pub struct ToTokioAsyncWriteQuadWriter<W: AsyncWrite + Unpin> { |
||||
formatter: ToTokioAsyncWriteQuadWriterKind<W>, |
||||
} |
||||
|
||||
#[cfg(feature = "async-tokio")] |
||||
enum ToTokioAsyncWriteQuadWriterKind<W: AsyncWrite + Unpin> { |
||||
NQuads(ToTokioAsyncWriteNQuadsWriter<W>), |
||||
NTriples(ToTokioAsyncWriteNTriplesWriter<W>), |
||||
RdfXml(ToTokioAsyncWriteRdfXmlWriter<W>), |
||||
TriG(ToTokioAsyncWriteTriGWriter<W>), |
||||
Turtle(ToTokioAsyncWriteTurtleWriter<W>), |
||||
} |
||||
|
||||
#[cfg(feature = "async-tokio")] |
||||
impl<W: AsyncWrite + Unpin> ToTokioAsyncWriteQuadWriter<W> { |
||||
/// Writes a [`QuadRef`]
|
||||
pub async fn write_quad<'a>(&mut self, quad: impl Into<QuadRef<'a>>) -> io::Result<()> { |
||||
match &mut self.formatter { |
||||
ToTokioAsyncWriteQuadWriterKind::NQuads(writer) => writer.write_quad(quad).await, |
||||
ToTokioAsyncWriteQuadWriterKind::NTriples(writer) => { |
||||
writer.write_triple(to_triple(quad)?).await |
||||
} |
||||
ToTokioAsyncWriteQuadWriterKind::RdfXml(writer) => { |
||||
writer.write_triple(to_triple(quad)?).await |
||||
} |
||||
ToTokioAsyncWriteQuadWriterKind::TriG(writer) => writer.write_quad(quad).await, |
||||
ToTokioAsyncWriteQuadWriterKind::Turtle(writer) => { |
||||
writer.write_triple(to_triple(quad)?).await |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Writes a [`TripleRef`]
|
||||
pub async fn write_triple<'a>(&mut self, triple: impl Into<TripleRef<'a>>) -> io::Result<()> { |
||||
self.write_quad(triple.into().in_graph(GraphNameRef::DefaultGraph)) |
||||
.await |
||||
} |
||||
|
||||
/// Writes the last bytes of the file
|
||||
///
|
||||
/// Note that this function does not flush the writer. You need to do that if you are using a [`BufWriter`](io::BufWriter).
|
||||
pub async fn finish(self) -> io::Result<W> { |
||||
Ok(match self.formatter { |
||||
ToTokioAsyncWriteQuadWriterKind::NQuads(writer) => writer.finish(), |
||||
ToTokioAsyncWriteQuadWriterKind::NTriples(writer) => writer.finish(), |
||||
ToTokioAsyncWriteQuadWriterKind::RdfXml(writer) => writer.finish().await?, |
||||
ToTokioAsyncWriteQuadWriterKind::TriG(writer) => writer.finish().await?, |
||||
ToTokioAsyncWriteQuadWriterKind::Turtle(writer) => writer.finish().await?, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
fn to_triple<'a>(quad: impl Into<QuadRef<'a>>) -> io::Result<TripleRef<'a>> { |
||||
let quad = quad.into(); |
||||
if quad.graph_name.is_default_graph() { |
||||
Ok(quad.into()) |
||||
} else { |
||||
Err(io::Error::new( |
||||
io::ErrorKind::InvalidInput, |
||||
"Only quads in the default graph can be serialized to a RDF graph format", |
||||
)) |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue