Skip to content

Commit

Permalink
refactor!: strictly require URIs
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Dec 1, 2023
1 parent 5dfbbc0 commit 8b4642d
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 63 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-jobs-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rdf-transform-graph-imports": minor
---

Require that all URIs in stream are absolute
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.js
node_modules/
coverage/
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ it through the transform. The transform will output a stream of quads where `cod
are removed and replaced with the imported graphs. The latter can also contain import statements which
will be recursively resolved.

It is required that all imports are absolute URIs in the upstream. If you parse document's yourself,
make sure to provide a base IRI so that relative URIs are resolved correctly. The easiest way is to
use the libraries as shown below.

### From remote resource

The `rdf.fetch` method ensures that relative URIs in the response are parsed against the URL of the
document itself by default.

```javascript
import rdf from '@zazuko/env-node'
import imports from 'rdf-transform-graph-imports'
Expand All @@ -65,17 +72,17 @@ const dataset = await rdf.dataset().import(stream.pipe(imports(rdf)))

### From local file

When streaming a local file, it may be necessary to provide a `basePath` to resolve relative URIs.
When streaming a local file, you must explicitly provide a base IRI to the parser or use the option
to use the file's path as base IRI. This is not the default behaviour because many prior users relied
on the parser to return relative URIs as-is.

```javascript
import rdf from '@zazuko/env-node'
import imports from 'rdf-transform-graph-imports'

const stream = rdf.fromFile('/path/to/shape.ttl')
const stream = rdf.fromFile('/path/to/shape.ttl', { implicitBaseIRI: true })

const dataset = await rdf.dataset().import(stream.pipe(imports(rdf, {
basePath: '/path/to/shape.ttl'
})))
const dataset = await rdf.dataset().import(stream.pipe(imports(rdf)))
```

### Reusing imports for local and remote documents
Expand Down
10 changes: 3 additions & 7 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { resolveImport } from './lib/path.js'
import Environment from './lib/env.js'
import fetchImport from './lib/fetchImport.js'

interface Options {
basePath?: string | URL
}

function transform(env: Environment, { basePath }: Options = {}) {
function transform(env: Environment) {
const code = env.namespace('https://code.described.at/')

const importStatements: AnyPointer = env.clownface()
Expand All @@ -27,12 +23,12 @@ function transform(env: Environment, { basePath }: Options = {}) {
.map(Import => {
const importPath = Import.out(code.imports).term!
const extension = Import.out(code.extension).value
return resolveImport(importPath, { basePath, extension })
return resolveImport(importPath, { extension })
})

for (const importTarget of imports) {
const fetchStream = await fetchImport(env, importTarget)
const importStream = fetchStream.pipe(transform(env, { basePath: importTarget }))
const importStream = fetchStream.pipe(transform(env))

for await (const importedQuad of importStream) {
this.push(importedQuad)
Expand Down
2 changes: 1 addition & 1 deletion lib/fetchImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Environment from './env.js'

export default async function (env: Environment, importTarget: string | URL) {
if (typeof importTarget === 'string') {
return env.fromFile(importTarget)
return env.fromFile(importTarget, { implicitBaseIRI: true })
}

const response = await env.fetch(importTarget.toString())
Expand Down
24 changes: 14 additions & 10 deletions lib/path.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import * as url from 'url'
import type { Term } from '@rdfjs/types'
import isURI from 'is-uri'

interface Options {
basePath?: string | URL
extension?: string
}

export function resolveImport(importNode: Term, { basePath, extension }: Options = {}) {
export function resolveImport(importNode: Term, { extension }: Options = {}) {
if (importNode.termType !== 'NamedNode') {
throw new Error(`Import target must be a NamedNode, got ${importNode.termType}`)
throw new Error(`Import target must be a NamedNode. Got ${importNode.termType}`)
}

if (isURI(importNode.value)) {
return new URL(importNode.value)
}
try {
const targetUri = new URL(importNode.value)
if (targetUri.protocol === 'http:') {
return targetUri
}

const base = typeof basePath === 'string' ? url.pathToFileURL(basePath) : basePath
if (extension) {
targetUri.pathname += `.${extension}`
}

const filePath = extension ? `${importNode.value}.${extension}` : importNode.value
return url.fileURLToPath(new URL(filePath, base))
return url.fileURLToPath(targetUri)
} catch (e: unknown) {
throw new Error(`Import target must be a valid URI. Got: ${importNode.value}`)
}
}
35 changes: 3 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
},
"homepage": "https://github.com/zazuko/rdf-merge-stream#readme",
"dependencies": {
"is-uri": "^1.2.6",
"readable-stream": "3 - 4",
"through2": "^4.0.2"
},
Expand Down
30 changes: 23 additions & 7 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ describe('rdf-merge-stream', () => {
it('merges file stream by relative path', async () => {
// given
const path = new URL('./resources/import-relative.ttl', import.meta.url)
const root = rdf.fromFile(path)
const root = rdf.fromFile(path, { implicitBaseIRI: true })

// when
const merged = await rdf.dataset().import(root.pipe(transform(rdf, {
basePath: path,
})))
const merged = await rdf.dataset().import(root.pipe(transform(rdf)))

// then
expect(await turtle(merged)).toMatchSnapshot()
Expand All @@ -40,9 +38,7 @@ describe('rdf-merge-stream', () => {
const root = rdf.fromFile(path)

// when
const merged = await rdf.dataset().import(root.pipe(transform(rdf, {
basePath: path,
})))
const merged = await rdf.dataset().import(root.pipe(transform(rdf)))

// then
expect(await turtle(merged)).toMatchSnapshot()
Expand All @@ -69,6 +65,26 @@ describe('rdf-merge-stream', () => {
await expect(rdf.dataset().import(root.pipe(transform(rdf))))
.to.be.eventually.rejectedWith('Failed to fetch: Not Found')
})

it('fails when local import is not a valid file: URI', async () => {
// given
const path = new URL('./resources/import-relative.ttl', import.meta.url)
const root = rdf.fromFile(path)

// then
await expect(rdf.dataset().import(root.pipe(transform(rdf))))
.to.be.eventually.rejectedWith('Import target must be a valid URI')
})

it('fails when import is literal', async () => {
// given
const path = new URL('./resources/import-literal.ttl', import.meta.url)
const root = rdf.fromFile(path)

// then
await expect(rdf.dataset().import(root.pipe(transform(rdf))))
.to.be.eventually.rejectedWith('Import target must be a NamedNode')
})
})

async function turtle(merged: Dataset) {
Expand Down
6 changes: 6 additions & 0 deletions test/resources/import-literal.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PREFIX code: <https://code.described.at/>

[
code:imports "/property/identifier" ;
code:extension "ttl" ;
] .

0 comments on commit 8b4642d

Please sign in to comment.