@hyperjump/browser
Advanced tools
Comparing version 1.0.0 to 1.1.0
import curry from "just-curry-it"; | ||
import { get as pointerGet, append as pointerAppend } from "@hyperjump/json-pointer"; | ||
import { resolveIri, parseIri, toAbsoluteIri } from "@hyperjump/uri"; | ||
import { parseIri, resolveIri, toAbsoluteIri } from "@hyperjump/uri"; | ||
import { contextUri } from "./context-uri.js"; | ||
import { retrieve } from "../uri-schemes/uri-schemes.js"; | ||
import { parseResponse } from "../media-types/media-types.js"; | ||
import { Reference } from "../jref/index.js"; | ||
import { jrefTypeOf } from "../jref/index.js"; | ||
export const get = async (uri, document = undefined) => { | ||
const baseUri = document ? document.baseUri : contextUri(); | ||
export const get = async (uri, browser = { _cache: {} }) => { | ||
const baseUri = browser.document ? browser.document.baseUri : contextUri(); | ||
uri = resolveIri(uri, baseUri); | ||
const id = toAbsoluteIri(uri); | ||
const { fragment } = parseIri(uri); | ||
let responseDocument; | ||
if (document && toAbsoluteIri(uri) === document.baseUri) { | ||
const { fragment } = parseIri(uri); | ||
responseDocument = { | ||
...document, | ||
cursor: fragment, | ||
_value: pointerGet(document.cursor, document.root) | ||
}; | ||
const cachedDocument = browser._cache[id] ?? browser.document?.embedded?.[id]; | ||
if (cachedDocument) { | ||
browser.document = cachedDocument; | ||
browser.uri = uri; | ||
browser.cursor = browser.document.anchorLocation(fragment); | ||
} else { | ||
try { | ||
const { response, fragment } = await retrieve(uri, document); | ||
responseDocument = await parseResponse(response, fragment); | ||
responseDocument._value = pointerGet(responseDocument.cursor, responseDocument.root); | ||
const response = await retrieve(uri, baseUri); | ||
browser.document = await parseResponse(response); | ||
browser.uri = response.url + (fragment === undefined ? "" : `#${fragment}`); | ||
browser.cursor = browser.document.anchorLocation(fragment); | ||
} catch (error) { | ||
const referencedMessage = document ? ` Referenced from '${document.baseUri}#${document.cursor}'.` : ""; | ||
const referencedMessage = browser.uri ? ` Referenced from '${browser.uri}'.` : ""; | ||
throw new RetrievalError(`Unable to load resource '${uri}'.${referencedMessage}`, error); | ||
} | ||
browser._cache[id] = browser.document; | ||
} | ||
return followReferences(responseDocument); | ||
browser._value = pointerGet(browser.cursor, browser.document.root); | ||
return followReferences(browser); | ||
}; | ||
const followReferences = (document) => document._value instanceof Reference | ||
? get(document._value.href, document) | ||
: document; | ||
const followReferences = (browser) => jrefTypeOf(value(browser)) === "reference" | ||
? get(value(browser).href, browser) | ||
: browser; | ||
export const value = (document) => document._value; | ||
export const value = (browser) => browser._value; | ||
export const step = curry((key, document) => { | ||
export const typeOf = (browser) => jrefTypeOf(browser._value); | ||
export const has = (key, browser) => key in browser._value; | ||
export const length = (browser) => browser._value.length; | ||
export const step = curry((key, browser) => { | ||
return followReferences({ | ||
...document, | ||
cursor: pointerAppend(`${key}`, document.cursor), | ||
_value: document._value[key] | ||
...browser, | ||
cursor: pointerAppend(`${key}`, browser.cursor), | ||
_value: browser._value[key] | ||
}); | ||
}); | ||
export const iter = async function* (document) { | ||
for (let index = 0; index < value(document).length; index++) { | ||
yield step(index, document); | ||
export const iter = async function* (browser) { | ||
for (let index = 0; index < value(browser).length; index++) { | ||
yield step(index, browser); | ||
} | ||
}; | ||
export const keys = function* (document) { | ||
for (const key in value(document)) { | ||
export const keys = function* (browser) { | ||
for (const key in value(browser)) { | ||
yield key; | ||
@@ -62,11 +70,11 @@ } | ||
export const values = async function* (document) { | ||
for (const key in value(document)) { | ||
yield step(key, document); | ||
export const values = async function* (browser) { | ||
for (const key in value(browser)) { | ||
yield step(key, browser); | ||
} | ||
}; | ||
export const entries = async function* (document) { | ||
for (const key in value(document)) { | ||
yield [key, await step(key, document)]; | ||
export const entries = async function* (browser) { | ||
for (const key in value(browser)) { | ||
yield [key, await step(key, browser)]; | ||
} | ||
@@ -73,0 +81,0 @@ }; |
@@ -15,2 +15,5 @@ import { addMediaTypePlugin } from "./media-types/media-types.js"; | ||
value, | ||
typeOf, | ||
has, | ||
length, | ||
step, | ||
@@ -27,3 +30,5 @@ iter, | ||
removeMediaTypePlugin, | ||
setMediaTypeQuality | ||
setMediaTypeQuality, | ||
UnsupportedMediaTypeError, | ||
UnknownMediaTypeError | ||
} from "./media-types/media-types.js"; | ||
@@ -34,3 +39,4 @@ | ||
removeUriSchemePlugin, | ||
retrieve | ||
retrieve, | ||
UnsupportedUriSchemeError | ||
} from "./uri-schemes/uri-schemes.js"; |
import type { Response } from "undici"; | ||
import type { JRef } from "./jref/index.js"; | ||
import type { JRef, JRefType } from "./jref/index.js"; | ||
// Browser | ||
export type Browser<T extends Document = Document> = { | ||
uri: string; | ||
document: T; | ||
cursor: string; | ||
}; | ||
export type Document = { | ||
baseUri: string; | ||
cursor: string; | ||
root: JRef; | ||
anchorLocation: (anchor: string | undefined) => string; | ||
embedded?: Record<string, Document>; | ||
}; | ||
export const get: (uri: string, document?: Document) => Promise<Document>; | ||
export const value: (document: Document) => unknown; | ||
export const step: (key: string, document: Document) => Promise<Document>; | ||
export const iter: (document: Document) => AsyncGenerator<Document>; | ||
export const keys: (document: Document) => Generator<string>; | ||
export const values: (document: Document) => AsyncGenerator<string>; | ||
export const entries: (document: Document) => AsyncGenerator<[string, Document]>; | ||
export const get: <T extends Document>(uri: string, browser?: Browser) => Promise<Browser<T>>; | ||
export const value: <T>(browser: Browser) => T; | ||
export const typeOf: (browser: Browser) => JRefType; | ||
export const has: (key: string, browser: Browser) => boolean; | ||
export const length: (browser: Browser) => number; | ||
export const step: (key: string, browser: Browser) => Promise<Browser>; | ||
export const iter: (browser: Browser) => AsyncGenerator<Browser>; | ||
export const keys: (browser: Browser) => Generator<string>; | ||
export const values: (browser: Browser) => AsyncGenerator<Browser>; | ||
export const entries: (browser: Browser) => AsyncGenerator<[string, Browser]>; | ||
export class RetrievalError extends Error { | ||
public constructor(message: string, cause: Error); | ||
public get cause(): Error; | ||
} | ||
// Media Types | ||
export type MediaTypePlugin = { | ||
parse: (response: Response, fragment: string) => Promise<Document>; | ||
export type MediaTypePlugin<T extends Document = Document> = { | ||
parse: (response: Response) => Promise<T>; | ||
fileMatcher: (path: string) => Promise<boolean>; | ||
@@ -35,2 +46,11 @@ quality?: number; | ||
export class UnsupportedMediaTypeError extends Error { | ||
public constructor(mediaType: string, message?: string); | ||
public get mediaType(): string; | ||
} | ||
export class UnknownMediaTypeError extends Error { | ||
public constructor(message?: string); | ||
} | ||
// URI Schemes | ||
@@ -41,7 +61,9 @@ export type UriSchemePlugin = { | ||
export const retrieve: (uri: string, baseUri?: string) => Promise<Response>; | ||
export const addUriSchemePlugin: (scheme: string, plugin: UriSchemePlugin) => void; | ||
export const removeUriSchemePlugin: (scheme: string) => void; | ||
export const retrieve: (uri: string, document?: Document) => Promise<{ | ||
response: Response; | ||
fragment: string; | ||
}>; | ||
export class UnsupportedUriSchemeError extends Error { | ||
public constructor(scheme: string, message?: string); | ||
public get scheme(): string; | ||
} |
@@ -17,2 +17,5 @@ import { addMediaTypePlugin } from "./media-types/media-types.js"; | ||
value, | ||
typeOf, | ||
has, | ||
length, | ||
step, | ||
@@ -29,3 +32,5 @@ iter, | ||
removeMediaTypePlugin, | ||
setMediaTypeQuality | ||
setMediaTypeQuality, | ||
UnsupportedMediaTypeError, | ||
UnknownMediaTypeError | ||
} from "./media-types/media-types.js"; | ||
@@ -36,3 +41,4 @@ | ||
removeUriSchemePlugin, | ||
retrieve | ||
retrieve, | ||
UnsupportedUriSchemeError | ||
} from "./uri-schemes/uri-schemes.js"; |
@@ -7,3 +7,3 @@ export type JRef = null | boolean | string | number | Reference | JRefObject | JRef[]; | ||
export const parse: (jref: string, reviver?: Reviver) => JRef; | ||
export type Reviver = (key: string, value: unknown) => unknown; | ||
export type Reviver = (key: string, value: JRef) => JRef | undefined; | ||
@@ -13,2 +13,5 @@ export const stringify: (value: JRef, replacer?: (string | number)[] | null | Replacer, space?: string | number) => string; | ||
export type JRefType = "object" | "array" | "string" | "number" | "boolean" | "null" | "reference" | "undefined"; | ||
export const jrefTypeOf: (value: unknown) => JRefType; | ||
export class Reference { | ||
@@ -15,0 +18,0 @@ constructor(href: string, value?: unknown); |
export const parse = (jref, reviver = undefined) => { | ||
return JSON.parse(jref, (key, value) => { | ||
const newValue = value !== null && typeof value.$href === "string" ? new Reference(value.$href) : value; | ||
const newValue = value !== null && typeof value.$ref === "string" ? new Reference(value.$ref) : value; | ||
@@ -17,3 +17,3 @@ return reviver ? reviver(key, newValue) : newValue; | ||
this.#href = href; | ||
this.#value = value ?? { $href: href }; | ||
this.#value = value ?? { $ref: href }; | ||
} | ||
@@ -29,1 +29,28 @@ | ||
} | ||
export const jrefTypeOf = (value) => { | ||
const jsType = typeof value; | ||
switch (jsType) { | ||
case "bigint": | ||
return "number"; | ||
case "number": | ||
case "string": | ||
case "boolean": | ||
case "undefined": | ||
return jsType; | ||
case "object": | ||
if (value instanceof Reference) { | ||
return "reference"; | ||
} else if (Array.isArray(value)) { | ||
return "array"; | ||
} else if (value === null) { | ||
return "null"; | ||
} else if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) { | ||
return "object"; | ||
} | ||
default: | ||
const type = jsType === "object" ? Object.getPrototypeOf(value).constructor.name || "anonymous" : jsType; | ||
throw Error(`Not a JRef compatible type: ${type}`); | ||
} | ||
}; |
@@ -5,7 +5,7 @@ import { parse } from "../jref/index.js"; | ||
export const jrefMediaTypePlugin = { | ||
parse: async (response, fragment) => { | ||
parse: async (response) => { | ||
return { | ||
baseUri: response.url, | ||
cursor: fragment, | ||
root: parse(await response.text()) | ||
root: parse(await response.text()), | ||
anchorLocation: anchorLocation | ||
}; | ||
@@ -15,1 +15,3 @@ }, | ||
}; | ||
const anchorLocation = (fragment) => decodeURI(fragment || ""); |
@@ -18,3 +18,3 @@ import { parse as parseContentType } from "content-type"; | ||
export const parseResponse = (response, fragment) => { | ||
export const parseResponse = (response) => { | ||
const contentTypeText = response.headers.get("content-type"); | ||
@@ -32,8 +32,8 @@ if (contentTypeText === null) { | ||
return mediaTypePlugins[contentType.type].parse(response, fragment); | ||
return mediaTypePlugins[contentType.type].parse(response); | ||
}; | ||
export const getFileMediaType = (path) => { | ||
export const getFileMediaType = async (path) => { | ||
for (const contentType in mediaTypePlugins) { | ||
if (mediaTypePlugins[contentType].fileMatcher(path)) { | ||
if (await mediaTypePlugins[contentType].fileMatcher(path)) { | ||
return contentType; | ||
@@ -68,3 +68,3 @@ } | ||
class UnsupportedMediaTypeError extends Error { | ||
export class UnsupportedMediaTypeError extends Error { | ||
constructor(mediaType, message = undefined) { | ||
@@ -77,3 +77,3 @@ super(message); | ||
class UnknownMediaTypeError extends Error { | ||
export class UnknownMediaTypeError extends Error { | ||
constructor(message = undefined) { | ||
@@ -80,0 +80,0 @@ super(message); |
import { createReadStream } from "node:fs"; | ||
import { readlink, lstat } from "node:fs/promises"; | ||
import { fileURLToPath } from "node:url"; | ||
import { parseIri, parseIriReference, toAbsoluteIri } from "@hyperjump/uri"; | ||
import { parseIri, toAbsoluteIri } from "@hyperjump/uri"; | ||
import { getFileMediaType } from "../media-types/media-types.js"; | ||
const retrieve = async (uri, document) => { | ||
if (document) { | ||
const { scheme } = parseIri(document.baseUri); | ||
const retrieve = async (uri, baseUri) => { | ||
const { scheme } = parseIri(baseUri); | ||
if (baseUri) { | ||
if (scheme !== "file") { | ||
throw Error(`Accessing a file (${uri}) from a non-filesystem document (${document.baseUri}) is not allowed`); | ||
throw Error(`Accessing a file (${uri}) from a non-filesystem document (${baseUri}) is not allowed`); | ||
} | ||
} | ||
const { fragment } = parseIriReference(uri); | ||
let responseUri = toAbsoluteIri(uri); | ||
@@ -31,5 +31,5 @@ | ||
return { response, fragment: fragment ?? "" }; | ||
return response; | ||
}; | ||
export const fileSchemePlugin = { retrieve }; |
@@ -1,2 +0,1 @@ | ||
import { parseIriReference } from "@hyperjump/uri"; | ||
import { acceptableMediaTypes } from "../media-types/media-types.js"; | ||
@@ -8,4 +7,2 @@ | ||
const retrieve = async (uri) => { | ||
const { fragment } = parseIriReference(uri); | ||
const response = await fetch(uri, { headers: { Accept: acceptableMediaTypes() } }); | ||
@@ -21,3 +18,3 @@ | ||
return { response, fragment: fragment ?? "" }; | ||
return response; | ||
}; | ||
@@ -24,0 +21,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { parseIriReference } from "@hyperjump/uri"; | ||
import { parseIriReference, resolveIri } from "@hyperjump/uri"; | ||
@@ -14,3 +14,4 @@ | ||
export const retrieve = (uri, document) => { | ||
export const retrieve = (uri, baseUri) => { | ||
uri = resolveIri(uri, baseUri); | ||
const { scheme } = parseIriReference(uri); | ||
@@ -22,6 +23,6 @@ | ||
return uriSchemePlugins[scheme].retrieve(uri, document); | ||
return uriSchemePlugins[scheme].retrieve(uri, baseUri); | ||
}; | ||
class UnsupportedUriSchemeError extends Error { | ||
export class UnsupportedUriSchemeError extends Error { | ||
constructor(scheme, message = undefined) { | ||
@@ -28,0 +29,0 @@ super(message); |
{ | ||
"name": "@hyperjump/browser", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "Browse JSON-compatible data with hypermedia references", | ||
@@ -18,3 +18,3 @@ "type": "module", | ||
"lint": "eslint lib", | ||
"test": "mocha 'lib/**/*.spec.ts'" | ||
"test": "vitest --watch=false" | ||
}, | ||
@@ -29,3 +29,4 @@ "repository": { | ||
"jref", | ||
"hypermedia" | ||
"hypermedia", | ||
"$ref" | ||
], | ||
@@ -39,15 +40,13 @@ "author": "Jason Desrosiers <jdesrosi@gmail.com>", | ||
"devDependencies": { | ||
"@types/chai": "*", | ||
"@types/mocha": "*", | ||
"@types/node": "*", | ||
"@typescript-eslint/eslint-plugin": "*", | ||
"@typescript-eslint/parser": "*", | ||
"chai": "*", | ||
"eslint": "*", | ||
"eslint-import-resolver-exports": "*", | ||
"eslint-import-resolver-node": "*", | ||
"eslint-import-resolver-typescript": "*", | ||
"eslint-plugin-import": "*", | ||
"mocha": "*", | ||
"ts-node": "*", | ||
"typescript": "*", | ||
"yaml": "*" | ||
"undici": "*", | ||
"vitest": "*" | ||
}, | ||
@@ -61,5 +60,4 @@ "engines": { | ||
"content-type": "^1.0.5", | ||
"just-curry-it": "^5.3.0", | ||
"undici": "^5.23.0" | ||
"just-curry-it": "^5.3.0" | ||
} | ||
} |
@@ -19,18 +19,2 @@ # Hyperjump - Browser | ||
### Browser | ||
When in a browser context, this library is designed to use the browser's `fetch` | ||
implementation instead of a node.js fetch clone. The Webpack bundler does this | ||
properly without any extra configuration, but if you are using the Rollup | ||
bundler you will need to include the `browser: true` option in your Rollup | ||
configuration. | ||
```javascript | ||
plugins: [ | ||
resolve({ | ||
browser: true | ||
}) | ||
] | ||
``` | ||
## JRef Browser | ||
@@ -70,3 +54,3 @@ | ||
* get(uri: string, document?: Document): Promise\<Document> | ||
* get(uri: string, browser?: Browser): Promise\<Browser> | ||
@@ -78,21 +62,32 @@ Retrieve a document located at the given URI. Support for [JRef] is built | ||
how to support other URI schemes. | ||
* value(document: Document) => Json | ||
* value(browser: Browser) => JRef | ||
Get the JSON compatible value the document represents. Any references will | ||
have been followed so you'll never receive a `Reference` type. | ||
* step(key: string | number, document: Document) => Promise\<Document> | ||
Get the JRef compatible value the document represents. | ||
* typeOf(browser: Browser) => JRefType | ||
Move the document cursor by the given "key" value. This is analogous to | ||
Works the same as the `typeof` keyword. It will return one of the JSON types | ||
(null, boolean, number, string, array, object) or "reference". If the value | ||
is not one of these types, it will throw an error. | ||
* has(key: string, browser: Browser) => boolean | ||
Returns whether or not a property is present in the object that the browser | ||
represents. | ||
* length(browser: Browser) => number | ||
Get the length of the array that the browser represents. | ||
* step(key: string | number, browser: Browser) => Promise\<Browser> | ||
Move the browser cursor by the given "key" value. This is analogous to | ||
indexing into an object or array (`foo[key]`). This function supports | ||
curried application. | ||
* **Schema.iter**: (document: Document) => AsyncGenerator\<Document> | ||
* iter(browser: Browser) => AsyncGenerator\<Browser> | ||
Iterate over the items in the array that the Document represents. | ||
* **Schema.entries**: (document: Document) => AsyncGenerator\<[string, Document]> | ||
Iterate over the items in the array that the document represents. | ||
* entries(browser: Browser) => AsyncGenerator\<[string, Browser]> | ||
Similar to `Object.entries`, but yields Documents for values. | ||
* **Schema.values**: (document: Document) => AsyncGenerator\<Document> | ||
Similar to `Object.entries`, but yields Browsers for values. | ||
* values(browser: Browser) => AsyncGenerator\<Browser> | ||
Similar to `Object.values`, but yields Documents for values. | ||
* **Schema.keys**: (document: Document) => Generator\<string> | ||
Similar to `Object.values`, but yields Browsers for values. | ||
* keys(browser: Browser) => Generator\<string> | ||
@@ -115,9 +110,12 @@ Similar to `Object.keys`. | ||
return { | ||
documentValue: YAML.parse(await response.text(), (key, value) => { | ||
return value !== null && typeof value.$href === "string" | ||
? new Reference(value.$href) | ||
baseUri: response.url, | ||
root: (response) => YAML.parse(await response.text(), (key, value) => { | ||
return value !== null && typeof value.$ref === "string" | ||
? new Reference(value.$ref) | ||
: value; | ||
}); | ||
}, | ||
anchorLocation: (fragment) => decodeUri(fragment ?? ""); | ||
}; | ||
} | ||
}, | ||
fileMatcher: (path) => path.endsWith(".jref") | ||
}); | ||
@@ -139,3 +137,3 @@ | ||
* type MediaTypePlugin | ||
* parse: (content: string) => Document | ||
* parse: (response: Response) => Document | ||
* [quality](https://developer.mozilla.org/en-US/docs/Glossary/Quality_values): | ||
@@ -165,3 +163,3 @@ number (defaults to `1`) | ||
addUriSchemePlugin("urn", { | ||
parse: (urn, document) => { | ||
parse: (urn, baseUri) => { | ||
let { nid, nss, query, fragment } = parseUrn(urn); | ||
@@ -178,3 +176,3 @@ nid = nid.toLowerCase(); | ||
return retrieve(uri, document); | ||
return retrieve(uri, baseUri); | ||
} | ||
@@ -195,7 +193,7 @@ }); | ||
* type UriSchemePlugin | ||
* retrieve: (uri: string, document: Document) => Promise\<{ response: Response, fragment: string }> | ||
* retrieve: (uri: string, baseUri?: string) => Promise\<Response> | ||
* removeUriSchemePlugin(scheme: string): void | ||
Remove support for a URI scheme. | ||
* retrieve(uri: string, document: Document) => Promise\<{ response: Response, fragment: string }> | ||
* retrieve(uri: string, baseUri?: string) => Promise\<Response> | ||
@@ -207,15 +205,15 @@ This is used internally, but you may need it if mapping names to locators | ||
Parse and stringify [JRef] values using the same API as the `JSON` built-in | ||
functions including reviver and replacer functions. | ||
`parse` and `stringify` [JRef] values using the same API as the `JSON` built-in | ||
functions including `reviver` and `replacer` functions. | ||
```javascript | ||
import { parse, stringify, Reference } from "@hyperjump/browser/jref"; | ||
import { parse, stringify, jrefTypeOf } from "@hyperjump/browser/jref"; | ||
const blogPostJref = `{ | ||
"title": "Working with JRef", | ||
"author": { "$href": "/author/jdesrosiers" }, | ||
"author": { "$ref": "/author/jdesrosiers" }, | ||
"content": "lorem ipsum dolor sit amet", | ||
}`; | ||
const blogPost = parse(blogPostJref); | ||
blogPost.author instanceof Reference; // => true | ||
jrefTypeOf(blogPost.author) // => "reference" | ||
blogPost.author.href; // => "/author/jdesrosiers" | ||
@@ -226,2 +224,15 @@ | ||
### API | ||
export type Replacer = (key: string, value: unknown) => unknown; | ||
* parse: (jref: string, reviver?: (key: string, value: unknown) => unknown) => JRef; | ||
Same as `JSON.parse`, but converts `{ "$ref": "..." }` to `Reference` | ||
objects. | ||
* stringify: (value: JRef, replacer?: (string | number)[] | null | Replacer, space?: string | number) => string; | ||
Same as `JSON.stringify`, but converts `Reference` objects to `{ "$ref": | ||
"... " }` | ||
* jrefTypeOf: (value: unknown) => "object" | "array" | "string" | "number" | "boolean" | "null" | "reference" | "undefined"; | ||
## Contributing | ||
@@ -228,0 +239,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
24976
4
11
402
245
1
- Removedundici@^5.23.0
- Removed@fastify/busboy@2.1.1(transitive)
- Removedundici@5.28.4(transitive)