Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
16 changes: 12 additions & 4 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 36 additions & 8 deletions packages/core/src/__tests__/Uri.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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");
});
});
113 changes: 74 additions & 39 deletions packages/core/src/types/Uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<UriConfig, Error> /* $ */ {
if (!uri) {
return ResultErr(Error("The provided URI is empty"));
public static parseUri(input: string): Result<UriConfig, Error> /* $ */ {
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,
});
}

Expand Down
12 changes: 3 additions & 9 deletions packages/core/src/types/WrapError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CleanResolutionStep } from "../algorithms";
import { RegExpGroups } from "../utils";

export type ErrorSource = Readonly<{
file?: string;
Expand Down Expand Up @@ -52,12 +53,6 @@ export interface WrapErrorOptions {
innerError?: WrapError;
}

type RegExpGroups<T extends string> =
| (RegExpExecArray & {
groups?: { [name in T]: string | undefined } | { [key: string]: string };
})
| null;

export class WrapError extends Error {
readonly name: string = "WrapError";
readonly code: WrapErrorCode;
Expand Down Expand Up @@ -174,7 +169,7 @@ export class WrapError extends Error {
| "resolutionStack"
| "cause"
> = WrapError.re.exec(error);
if (!result) {
if (!result || !result.groups) {
return undefined;
}
const {
Expand All @@ -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);

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utils/RegExpGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type RegExpGroups<T extends string> =
| (RegExpExecArray & {
groups?: { [name in T]: string | undefined } | { [key: string]: string };
})
| null;
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./combinePaths";
export * from "./getEnvFromResolutionPath";
export * from "./is-buffer";
export * from "./typesHandler";
export * from "./RegExpGroups";
8 changes: 1 addition & 7 deletions packages/plugin/src/utils/getErrorSource.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { ErrorSource } from "@polywrap/core-js";

type RegExpGroups<T extends string> =
| (RegExpExecArray & {
groups?: { [name in T]: string | undefined } | { [key: string]: string };
})
| null;
import { ErrorSource, RegExpGroups } from "@polywrap/core-js";

const re = /\((?<file>.*):(?<row>\d+):(?<col>\d+)\)$/;

Expand Down