diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d98ff945..82800e079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Polywrap Origin (0.11.0) ## Features +**`@polywrap/core-js`:** +* [PR-6](https://github.com/polywrap/javascript-client/pull/6) **Improved URI Inference** + * Non-wrap URI schemes can now be used (ex: `https://domain.com/path`). The non-wrap scheme will be used as the authority, and all other contents will be shifted into the path. + * Examples: + * `https://domain.com/path` into `wrap://https/domain.com/path` + * `ipfs://QmHASH` into `wrap://ipfs/QmHASH` + **`@polywrap/client-config-builder-js`:** * [PR-45](https://github.com/polywrap/javascript-client/pull/45) **Modular Config Bundles** * The `DefaultBundle` has been broken apart into two separate bundles: `sys` and `web3`. diff --git a/packages/core/README.md b/packages/core/README.md index f122bcdaa..8498960e3 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -339,15 +339,23 @@ export interface UriConfig { ```ts /** * A Polywrap URI. Some examples of valid URIs are: + * wrap://https/domain.com * wrap://ipfs/QmHASH - * wrap://ens/sub.dimain.eth - * wrap://fs/directory/file.txt - * wrap://uns/domain.crypto + * wrap://ens/sub.domain.eth + * wrap://file/directory/file.txt + * + * Some example short-hand URIs (utilizing inference): + * ipfs/QmHASH -> wrap://ipfs/QmHASH + * https://domain.com -> wrap://https/domain.com + * + * URI inference is performed in the following ways: + * 1. If wrap:// is missing, it will be added. + * 2. If non-wrap schema exists, it becomes the authority. * * Breaking down the various parts of the URI, as it applies * to [the URI standard](https://tools.ietf.org/html/rfc3986#section-3): * **wrap://** - URI Scheme: differentiates Polywrap URIs. - * **ipfs/** - URI Authority: allows the Polywrap URI resolution algorithm to determine an authoritative URI resolver. + * **ens/** - URI Authority: allows the Polywrap URI resolution algorithm to determine an authoritative URI resolver. * **sub.domain.eth** - URI Path: tells the Authority where the Wrapper resides. */ export class Uri { diff --git a/packages/core/src/__tests__/Uri.spec.ts b/packages/core/src/__tests__/Uri.spec.ts index f38bfa7f1..4e832db94 100644 --- a/packages/core/src/__tests__/Uri.spec.ts +++ b/packages/core/src/__tests__/Uri.spec.ts @@ -15,17 +15,11 @@ describe("Uri", () => { }); it("Fails if an authority is not present", () => { - expect(() => new Uri("wrap://path")).toThrowError(/URI is malformed,/); + expect(() => new Uri("wrap://path")).toThrowError(/URI authority is missing,/); }); it("Fails if a path is not present", () => { - expect(() => new Uri("wrap://authority/")).toThrowError(/URI is malformed,/); - }); - - it("Fails if scheme is not at the beginning", () => { - expect(() => new Uri("path/wrap://something")).toThrowError( - /The wrap:\/\/ scheme must/ - ); + expect(() => new Uri("wrap://authority/")).toThrowError(/URI path is missing,/); }); it("Fails with an empty string", () => { @@ -50,4 +44,38 @@ describe("Uri", () => { path: "uri", }); }); + + it("Infers common URI authorities", () => { + let uri = new Uri("https://domain.com/path/to/thing"); + expect(uri.uri).toEqual("wrap://https/domain.com/path/to/thing"); + expect(uri.authority).toEqual("https"); + expect(uri.path).toEqual("domain.com/path/to/thing"); + + uri = new Uri("http://domain.com/path/to/thing"); + expect(uri.uri).toEqual("wrap://http/domain.com/path/to/thing"); + expect(uri.authority).toEqual("http"); + expect(uri.path).toEqual("domain.com/path/to/thing"); + + uri = new Uri("ipfs://QmaM318ABUXDhc5eZGGbmDxkb2ZgnbLxigm5TyZcCsh1Kw"); + expect(uri.uri).toEqual("wrap://ipfs/QmaM318ABUXDhc5eZGGbmDxkb2ZgnbLxigm5TyZcCsh1Kw"); + expect(uri.authority).toEqual("ipfs"); + expect(uri.path).toEqual("QmaM318ABUXDhc5eZGGbmDxkb2ZgnbLxigm5TyZcCsh1Kw"); + + uri = new Uri("ens://domain.eth"); + expect(uri.uri).toEqual("wrap://ens/domain.eth"); + expect(uri.authority).toEqual("ens"); + expect(uri.path).toEqual("domain.eth"); + + uri = new Uri("ens://domain.eth:pkg@1.0.0"); + expect(uri.uri).toEqual("wrap://ens/domain.eth:pkg@1.0.0"); + expect(uri.authority).toEqual("ens"); + expect(uri.path).toEqual("domain.eth:pkg@1.0.0"); + }); + + it("Handles cases where the scheme delimiter is nested under an authority", () => { + const uri = new Uri("authority/something?uri=wrap://something/something2"); + expect(uri.uri).toEqual("wrap://authority/something?uri=wrap://something/something2"); + expect(uri.authority).toEqual("authority"); + expect(uri.path).toEqual("something?uri=wrap://something/something2"); + }); }); diff --git a/packages/core/src/types/Uri.ts b/packages/core/src/types/Uri.ts index 755e32a39..73d5ccd13 100644 --- a/packages/core/src/types/Uri.ts +++ b/packages/core/src/types/Uri.ts @@ -17,15 +17,23 @@ export interface UriConfig { // $start: Uri /** * A Polywrap URI. Some examples of valid URIs are: + * wrap://https/domain.com * wrap://ipfs/QmHASH - * wrap://ens/sub.dimain.eth - * wrap://fs/directory/file.txt - * wrap://uns/domain.crypto + * wrap://ens/sub.domain.eth + * wrap://file/directory/file.txt + * + * Some example short-hand URIs (utilizing inference): + * ipfs/QmHASH -> wrap://ipfs/QmHASH + * https://domain.com -> wrap://https/domain.com + * + * URI inference is performed in the following ways: + * 1. If wrap:// is missing, it will be added. + * 2. If non-wrap schema exists, it becomes the authority. * * Breaking down the various parts of the URI, as it applies * to [the URI standard](https://tools.ietf.org/html/rfc3986#section-3): * **wrap://** - URI Scheme: differentiates Polywrap URIs. - * **ipfs/** - URI Authority: allows the Polywrap URI resolution algorithm to determine an authoritative URI resolver. + * **ens/** - URI Authority: allows the Polywrap URI resolution algorithm to determine an authoritative URI resolver. * **sub.domain.eth** - URI Path: tells the Authority where the Wrapper resides. */ export class Uri { @@ -107,60 +115,87 @@ export class Uri { * @param uri - a string representation of a wrap URI * @returns A Result containing a UriConfig, if successful, or an error */ - public static parseUri(uri: string): Result /* $ */ { - if (!uri) { - return ResultErr(Error("The provided URI is empty")); + public static parseUri(input: string): Result /* $ */ { + const authorityDelimiter = "/"; + const schemeDelimiter = "://"; + const wrapScheme = "wrap://"; + + const validUriExamples = + "wrap://ipfs/QmHASH\n" + + "wrap://ens/domain.eth\n" + + "ipfs/QmHASH\n" + + "ens/domain.eth\n" + + "https://domain.com/path\n\n"; + + if (!input) { + return ResultErr( + Error( + "The provided URI is empty, here are some examples of valid URIs:\n" + + validUriExamples + ) + ); } - let processed = uri; + let processedUri = input.trim(); - // Trim preceding '/' characters - while (processed[0] === "/") { - processed = processed.substring(1); + // Remove leading "/" + if (processedUri.startsWith(authorityDelimiter)) { + processedUri = processedUri.substring(1); } - // Check for the wrap:// scheme, add if it isn't there - const wrapSchemeIdx = processed.indexOf("wrap://"); - - // If it's missing the wrap:// scheme, add it - if (wrapSchemeIdx === -1) { - processed = "wrap://" + processed; + // Check if the string starts with a non-wrap URI scheme + if (!processedUri.startsWith(wrapScheme)) { + const schemeIndex = processedUri.indexOf(schemeDelimiter); + const authorityIndex = processedUri.indexOf(authorityDelimiter); + if (schemeIndex !== -1) { + // Make sure the string before the scheme doesn't contain an authority + if (!(authorityIndex !== -1 && schemeIndex > authorityIndex)) { + processedUri = + processedUri.substring(0, schemeIndex) + + "/" + + processedUri.substring(schemeIndex + schemeDelimiter.length); + } + } + } else { + processedUri = processedUri.substring(wrapScheme.length); } - // If the wrap:// is not in the beginning, return an error - if (wrapSchemeIdx > -1 && wrapSchemeIdx !== 0) { + // Split the string into parts, using "/" as a delimeter + const parts = processedUri.split(authorityDelimiter); + + if (parts.length < 2) { return ResultErr( - Error("The wrap:// scheme must be at the beginning of the URI string") + Error( + `URI authority is missing, here are some examples of valid URIs:\n` + + validUriExamples + + `Invalid URI Received: ${input}` + ) ); } - // Extract the authoriy & path - const result = processed.match(/wrap:\/\/([a-z][a-z0-9-_]+)\/(.*)/); - let uriParts: string[]; - - // Remove all empty strings - if (result) { - uriParts = result.filter((str) => !!str); - } else { - uriParts = []; - } + // Extract the authority and path + const authority = parts[0]; + const path = parts.slice(1).join("/"); - if (uriParts.length !== 3) { + if (!path) { return ResultErr( Error( - `URI is malformed, here are some examples of valid URIs:\n` + - `wrap://ipfs/QmHASH\n` + - `wrap://ens/domain.eth\n` + - `ens/domain.eth\n\n` + - `Invalid URI Received: ${uri}` + `URI path is missing, here are some examples of valid URIs:\n` + + validUriExamples + + `Invalid URI Received: ${input}` ) ); } + // Add "wrap://" if not already present + if (!processedUri.startsWith("wrap://")) { + processedUri = "wrap://" + processedUri; + } + return ResultOk({ - uri: processed, - authority: uriParts[1], - path: uriParts[2], + uri: processedUri, + authority, + path, }); } diff --git a/packages/core/src/types/WrapError.ts b/packages/core/src/types/WrapError.ts index 38641858b..f901b7b92 100644 --- a/packages/core/src/types/WrapError.ts +++ b/packages/core/src/types/WrapError.ts @@ -1,4 +1,5 @@ import { CleanResolutionStep } from "../algorithms"; +import { RegExpGroups } from "../utils"; export type ErrorSource = Readonly<{ file?: string; @@ -52,12 +53,6 @@ export interface WrapErrorOptions { innerError?: WrapError; } -type RegExpGroups = - | (RegExpExecArray & { - groups?: { [name in T]: string | undefined } | { [key: string]: string }; - }) - | null; - export class WrapError extends Error { readonly name: string = "WrapError"; readonly code: WrapErrorCode; @@ -174,7 +169,7 @@ export class WrapError extends Error { | "resolutionStack" | "cause" > = WrapError.re.exec(error); - if (!result) { + if (!result || !result.groups) { return undefined; } const { @@ -188,8 +183,7 @@ export class WrapError extends Error { col, resolutionStack: resolutionStackStr, cause, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - } = result.groups!; + } = result.groups; const code = parseInt(codeStr as string); diff --git a/packages/core/src/utils/RegExpGroups.ts b/packages/core/src/utils/RegExpGroups.ts new file mode 100644 index 000000000..35e7fa063 --- /dev/null +++ b/packages/core/src/utils/RegExpGroups.ts @@ -0,0 +1,5 @@ +export type RegExpGroups = + | (RegExpExecArray & { + groups?: { [name in T]: string | undefined } | { [key: string]: string }; + }) + | null; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 8ad8fc58b..075b0c35d 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./combinePaths"; export * from "./getEnvFromResolutionPath"; export * from "./is-buffer"; export * from "./typesHandler"; +export * from "./RegExpGroups"; diff --git a/packages/plugin/src/utils/getErrorSource.ts b/packages/plugin/src/utils/getErrorSource.ts index 8b737f432..f87e497e0 100644 --- a/packages/plugin/src/utils/getErrorSource.ts +++ b/packages/plugin/src/utils/getErrorSource.ts @@ -1,10 +1,4 @@ -import { ErrorSource } from "@polywrap/core-js"; - -type RegExpGroups = - | (RegExpExecArray & { - groups?: { [name in T]: string | undefined } | { [key: string]: string }; - }) - | null; +import { ErrorSource, RegExpGroups } from "@polywrap/core-js"; const re = /\((?.*):(?\d+):(?\d+)\)$/;