Skip to content

Commit

Permalink
feat: get and set functions now also available as schemaLeaf.get()/.s…
Browse files Browse the repository at this point in the history
…et()
  • Loading branch information
kpietraszko committed Jan 4, 2025
1 parent 67d9bf9 commit bdace03
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ This is sometimes referred to as a message with *indexed keys*.

**Schemind** helps you create and read such messages, if your (de)serializer doesn't support this technique.


*Note that this format obviously has some drawbacks: [recommended reading about the pros and cons](https://github.com/MessagePack-CSharp/MessagePack-CSharp#use-indexed-keys-instead-of-string-keys-contractless).*

## Installation
Expand Down
2 changes: 1 addition & 1 deletion src/convenient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ function populateIndexedKeysMessage<TSchema extends ValidIndexedKeysMessageSchem
populateIndexedKeysMessage(messageToPopulate, leafValueOrSubObject as PlainObjectOfSchema<TSchema>, nestedNode)
}
}
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export { validateSchema, withIndex, InvalidSchemaError } from './indexedKeysSche
export { get, set } from "./raw";
export { toPlainObject, toIndexedKeysMessage } from "./convenient";

export type { ValidIndexedKeysMessageSchema } from './indexedKeysSchema'
export type { ValidIndexedKeysMessageSchema } from './indexedKeysSchema';
30 changes: 18 additions & 12 deletions src/indexedKeysSchema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { NonNegativeInteger } from "type-fest";
import { get, set } from "./raw";

export const indexesPathReversed = Symbol("indexesPathReversed");
const fieldType = Symbol("fieldType");
const isSchemaLeafTag = Symbol("isSchemaLeaf");
const isValidSchemaLeaf = Symbol("isValidSchemaLeaf");
const schemaRoot = Symbol("schemaRoot");
const isValidSchemaLeafTag = Symbol("isValidSchemaLeaf");

type IndexesPath = number[];
export type SchemaLeaf<TField> = {
Expand All @@ -14,8 +14,9 @@ export type SchemaLeaf<TField> = {
};

export type ValidSchemaLeaf<TField> = SchemaLeaf<TField> & {
[isValidSchemaLeaf]: true,
[schemaRoot]: ValidIndexedKeysMessageSchema<unknown>
[isValidSchemaLeafTag]: true,
get: (message: unknown[]) => TField,
set: (message: unknown[], value: TField) => void
};

export type IndexedKeysMessageSchema<TSchema> = {
Expand Down Expand Up @@ -54,26 +55,32 @@ export class InvalidSchemaError extends Error {
}

export function validateSchema<TSchema extends IndexedKeysMessageSchema<TSchemaInner>, TSchemaInner>(schema: TSchema) {
validateSchemaRecursively(schema, [], 0);
validateSchemaRecursively(schema, schema, [], 0);
return schema as unknown as ToValidIndexedKeysMessageSchema<TSchema>;
}

function validateSchemaRecursively(
rootSchema: IndexedKeysMessageSchema<unknown>,
schemaNode: IndexedKeysMessageSchema<unknown>,
encounteredIndexesPaths: IndexesPath[],
currentTreeLevel: number){

for (const [_, nestedSchemaNode] of Object.entries(schemaNode)) {
const nestedNode = nestedSchemaNode as IndexedKeysMessageSchema<unknown> | SchemaLeaf<unknown>;
if (isSchemaLeaf(nestedNode)) {
validateSchemaLeaf(nestedNode, encounteredIndexesPaths, currentTreeLevel);
const subschemaOrLeaf = nestedSchemaNode as IndexedKeysMessageSchema<unknown> | SchemaLeaf<unknown>;
if (isSchemaLeaf(subschemaOrLeaf)) {
validateSchemaLeaf(subschemaOrLeaf, encounteredIndexesPaths, currentTreeLevel);

subschemaOrLeaf.get = (message) => get(message, subschemaOrLeaf);
subschemaOrLeaf.set = (message, value) => set(message, subschemaOrLeaf, value);
} else {
validateSchemaRecursively(nestedNode, encounteredIndexesPaths, currentTreeLevel + 1);
validateSchemaRecursively(rootSchema, subschemaOrLeaf, encounteredIndexesPaths, currentTreeLevel + 1);
}
}
}

function validateSchemaLeaf(schemaLeaf: SchemaLeaf<unknown>, encounteredIndexesPaths: IndexesPath[], currentTreeLevel: number){
function validateSchemaLeaf(schemaLeaf: SchemaLeaf<unknown>,
encounteredIndexesPaths: IndexesPath[], currentTreeLevel: number): asserts schemaLeaf is ValidSchemaLeaf<unknown>{

const duplicateIndexesPathDetected = encounteredIndexesPaths.some(encounteredPath =>
encounteredPath.length === schemaLeaf[indexesPathReversed].length &&
encounteredPath.every((pathElement, index) => pathElement === schemaLeaf[indexesPathReversed][index]));
Expand Down Expand Up @@ -109,8 +116,7 @@ export function withIndex<const TIndex extends number>(index: NonNegativeInteger
}

// intentionally not validating that it has the "isValidSchemaLeaf" symbol property, because it actually doesn't - it's just a type trick
export function isSchemaLeaf(value: IndexedKeysMessageSchema<unknown> | ValidSchemaLeaf<unknown>): value is ValidSchemaLeaf<unknown>;
export function isSchemaLeaf(value: IndexedKeysMessageSchema<unknown> | SchemaLeaf<unknown>): value is SchemaLeaf<unknown> {
export function isSchemaLeaf<TLeaf extends SchemaLeaf<unknown>>(value: IndexedKeysMessageSchema<unknown> | TLeaf): value is TLeaf {
return Object.hasOwn(value, isSchemaLeafTag);
}

Expand Down
71 changes: 70 additions & 1 deletion test/indexedKeysSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
set,
toPlainObject,
toIndexedKeysMessage,
validateSchema, type ValidIndexedKeysMessageSchema, InvalidSchemaError
validateSchema,
InvalidSchemaError
} from "../src/index";

const someDate = new Date();
Expand Down Expand Up @@ -105,6 +106,52 @@ describe("get", () => {
});
});

describe("leaf.get", () => {
it("should return value from the index - and of type - specified by the schema", () => {
const schema = createTestSchema();

const r1 = schema.anotherNumber.get(message);
expectTypeOf(r1).toBeNumber();
expect(r1).to.equal(69);

const r2 = schema.someNumber.get(message);
expectTypeOf(r2).toBeNumber();
expect(r2).to.equal(420);

const r3 = schema.someBool.get(message);
expectTypeOf(r3).toBeBoolean();
expect(r3).to.equal(true);

const r4 = schema.someString.get(message);
expectTypeOf(r4).toBeString();
expect(r4).to.equal("nice");

const r5 = schema.someArray.get(message);
expectTypeOf(r5).toEqualTypeOf<string[]>();
expect(r5).to.deep.equal(["quick", "brown", "fox"]);

const r6 = schema.nestedThing.someNestedNumber.get(message);
expectTypeOf(r6).toBeNumber();
expect(r6).to.equal(1234567891234567);

const r7 = schema.nestedThing.someNestedDate.get(message);
expectTypeOf(r7).toEqualTypeOf<Date>();
expect(r7).to.equal(someDate);

const r8 = schema.nestedThing.evenMoreNestedThing.moreNestedNumber.get(message);
expectTypeOf(r8).toBeNumber();
expect(r8).to.equal(2138);

const r9 = schema.nestedThing.evenMoreNestedThing.moreNestedBool.get(message);
expectTypeOf(r9).toBeBoolean();
expect(r9).to.equal(false);

const r10 = schema.nestedThing.evenMoreNestedThing.moreNestedArray.get(message);
expectTypeOf(r10).toEqualTypeOf<number[]>();
expect(r10).to.deep.equal([2, 3, 5, 8]);
});
});

describe("set", () => {
it("should place values at indexes specified by the schema", () => {
const schema = createTestSchema();
Expand All @@ -127,6 +174,28 @@ describe("set", () => {
});
});

describe("leaf.set", () => {
it("should place values at indexes specified by the schema", () => {
const schema = createTestSchema();

const newMessage = [] as unknown[];

schema.someNumber.set(newMessage, 420);
schema.someString.set(newMessage, "nice");
schema.anotherNumber.set(newMessage, 69);
schema.someBool.set(newMessage, true);
schema.someArray.set(newMessage, ["quick", "brown", "fox"]);
schema.nestedThing.someNestedDate.set(newMessage, someDate);
schema.nestedThing.someNestedNumber.set(newMessage, 1234567891234567);
schema.nestedThing.evenMoreNestedThing.moreNestedBool.set(newMessage, false);
schema.nestedThing.evenMoreNestedThing.moreNestedNumber.set(newMessage, 2138);
schema.nestedThing.evenMoreNestedThing.moreNestedArray.set(newMessage, [2, 3, 5, 8]);

const expectedMessage = message;
expect(newMessage).to.deep.equal(expectedMessage);
});
});

describe("toPlainObject", () => {
it("should convert a message to properly typed plain object", () => {
const schema = createTestSchema();
Expand Down

0 comments on commit bdace03

Please sign in to comment.