Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial setup work for content types #56

Merged
merged 8 commits into from
Jun 15, 2023
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
42 changes: 42 additions & 0 deletions example/src/test_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { EncodedContent } from "../../src/XMTP.types";
import { CodecError } from "../../src/lib/CodecError";
import { ContentTypeID } from "../../src/lib/ContentTypeID";

export class NumberCodec {
contentType: {
id(): string;
authorityID: string;
typeID: string;
versionMajor: number;
versionMinor: number;
};

constructor() {
this.contentType = new ContentTypeID({
authorityID: "example.com",
typeID: "number",
versionMajor: 1,
versionMinor: 1,
});
}

encode(content: Uint8Array) {
const encodedContent = {
type: this.contentType,
content: Buffer.from(JSON.stringify(content)),
fallback: "fallbackText",
};

return encodedContent;
}

decode(encodedContent: EncodedContent) {
try {
const contentToDecode = encodedContent.content.toString();
const decodedContent = JSON.parse(contentToDecode);
return decodedContent;
} catch {
throw new CodecError("invalidContent");
}
}
}
60 changes: 58 additions & 2 deletions example/src/tests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Wallet } from "ethers";
import { Client } from "xmtp-react-native-sdk";
import * as XMTP from "../../src/index";
import {
CodecRegistry,
ContentCodecInterface,
} from "../../src/lib/CodecRegistry";
import { CodecError } from "../../src/lib/CodecError";

import { NumberCodec } from "./test_utils";

function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand Down Expand Up @@ -63,3 +68,54 @@ test("canMessage", async () => {
const canMessage = await bob.canMessage(alice.address);
return canMessage;
});

test("can register, encode, and decode a number codec", async () => {
const numberCodec = new NumberCodec();

const registry = new CodecRegistry();
registry.register(numberCodec as ContentCodecInterface);

const id = numberCodec.contentType.id();
const codec = registry.find(id);

const encodedContent = codec.encode(3.14);
const decodedContent = codec.decode(encodedContent);

return decodedContent === 3.14;
});

test("throws an error if codec is not found in registry", async () => {
const numberCodec = new NumberCodec();
const registry = new CodecRegistry();
registry.register(numberCodec as ContentCodecInterface);

try {
const id = "invalidId";
registry.find(id);
} catch (e) {
return (e as CodecError).message === "codecNotFound";
}
return false;
});

test("throws an error if codec is invalid when decoding", async () => {
const numberCodec = new NumberCodec();
const registry = new CodecRegistry();
registry.register(numberCodec as ContentCodecInterface);

try {
const id = numberCodec.contentType.id();
const codec = registry.find(id);

const encodedContent = codec.encode(3.14);
const invalidContentToDecode = {
...encodedContent,
content: { key1: "This cannot be parsed" },
};
// @ts-ignore
codec.decode(invalidContentToDecode);
} catch (e) {
return (e as CodecError).message === "invalidContent";
}
return false;
});
18 changes: 18 additions & 0 deletions src/XMTP.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { ContentTypeID } from "./lib/ContentTypeID";

export type ChangeEventPayload = {
value: string;
};

export type XMTPViewProps = {
name: string;
};

/*
*
* Represents encoded content and its metadata.
*
* @param {ContentTypeID} type - The content type ID for this content.
* @param {Uint8Array} content - The encoded content data.
* @param {string} fallback - A fallback representation of the content, if any.
*/

export type EncodedContent = {
type: ContentTypeID;
daria-github marked this conversation as resolved.
Show resolved Hide resolved
parameters: { [key: string]: [value: string] }? ,
content: Uint8Array;
fallback?: string;
};
13 changes: 13 additions & 0 deletions src/lib/CodecError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
*
* An error class for codec-related errors when searching the registry.
*
* @param {string} message - The error message.
*/

export class CodecError extends Error {
constructor(message: "invalidContent" | "codecNotFound") {
super(message);
this.name = message;
}
}
62 changes: 62 additions & 0 deletions src/lib/CodecRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { EncodedContent } from "../XMTP.types";
import { CodecError } from "./CodecError";
import { ContentTypeID } from "./ContentTypeID";

/*
*
* Codecs encode and decode content, and this is the interface for ContentCodecs within this app.
*
* @param {ContentTypeID} contentType - The content type this codec handles.
* @param {<T>} encode(content) - Encodes the given content and returns an EncodedContent.
* @param {<T>} decode(content) - Decodes the given EncodedContent and returns the original content.
*/

export interface ContentCodecInterface {
contentType: ContentTypeID;
encode<T>(content: T): EncodedContent;
decode<T>(content: EncodedContent): T;
}

/*
* CodecRegistry
*
* A registry for content codecs. Allows registering codecs by id
* and looking them up.
*
*/
export class CodecRegistry {
codecs: { [key: string]: ContentCodecInterface };

constructor() {
/*
* An object mapping content type IDs to
* codec instances.
*/
this.codecs = {};
}

/*
* Registers a content codec.
*
* @param {ContentCodecInterface} codec - This is the codec instance to register.
*/
register(codec: ContentCodecInterface) {
const contentType = codec.contentType.id();
this.codecs[contentType] = codec;
}

/*
* Finds a registered codec by content type ID.
*
* @param {string} contentTypeID - The id to look up.
* @returns {ContentCodecInterface} The found codec, or an error is thrown if not found.
*/
find(contentTypeID: string) {
const codec = this.codecs[contentTypeID];
if (!codec) {
throw new CodecError("codecNotFound");
} else {
return codec;
}
}
}
29 changes: 29 additions & 0 deletions src/lib/ContentTypeID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
*
* This ContentTypeID class represents a content type identifier.
*
* @param {string} authorityID - The authority that defined the content type.
* @param {string} typeID - The unique ID for the content type within the authority, e.g. "number"
* @param {number} versionMajor - The major version number of the content type.
* @param {number} versionMinor - The minor version number of the content type.
*
* @returns {string} The full content type ID in the format:
* <authorityID>:<typeID>
*/

export class ContentTypeID {
authorityID: string;
typeID: string;
versionMajor: number;
versionMinor: number;
constructor({ authorityID, typeID, versionMajor, versionMinor }) {
this.authorityID = authorityID;
this.typeID = typeID;
this.versionMajor = versionMajor;
this.versionMinor = versionMinor;
}

id() {
return `${this.authorityID}:${this.typeID}`;
}
}