Skip to content

Commit

Permalink
Merge pull request #109 from gadget-inc/accepts-undefined
Browse files Browse the repository at this point in the history
Add support for acceptsUndefined: false to safe references
  • Loading branch information
airhorns authored Oct 17, 2024
2 parents 1a3b837 + 5488410 commit c59b402
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 100 deletions.
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gadgetinc/mobx-quick-tree",
"version": "0.7.7",
"version": "0.7.8",
"description": "A mirror of mobx-state-tree's API to construct fast, read-only instances that share all the same views",
"source": "src/index.ts",
"main": "dist/src/index.js",
Expand Down Expand Up @@ -65,8 +65,5 @@
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"yargs": "^17.7.2"
},
"volta": {
"node": "18.12.1"
}
}
316 changes: 233 additions & 83 deletions spec/reference.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { IAnyClassModelType, Instance, SnapshotOrInstance } from "../src";
import { ClassModel, register, types } from "../src";
import { TestClassModel } from "./fixtures/TestClassModel";
import { TestModel, TestModelSnapshot } from "./fixtures/TestModel";
import { create } from "./helpers";

const Referrable = types.model("Referenced", {
key: types.identifier,
Expand Down Expand Up @@ -32,107 +33,256 @@ const Root = types.model("Reference Model", {
});

describe("references", () => {
test("can resolve valid references", () => {
const root = Root.createReadOnly({
model: {
ref: "item-a",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
describe.each([
["read-only", true],
["observable", false],
])("%s", (_name, readOnly) => {
test("can resolve valid references", () => {
const root = create(
Root,
{
model: {
ref: "item-a",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
},
readOnly,
);

expect(root.model.ref).toEqual(
expect.objectContaining({
key: "item-a",
count: 12,
}),
);
});

expect(root.model.ref).toEqual(
expect.objectContaining({
key: "item-a",
count: 12,
}),
);
});
test("can resolve valid safe references", () => {
const root = create(
Root,
{
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
},
readOnly,
);

expect(root.model.safeRef).toEqual(
expect.objectContaining({
key: "item-b",
count: 523,
}),
);
});

test("throws for invalid refs", () => {
const createRoot = () =>
Root.createReadOnly({
model: {
ref: "item-c",
test("does not throw for invalid safe references", () => {
const root = create(
Root,
{
model: {
ref: "item-a",
safeRef: "item-c",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
readOnly,
);

expect(root.model.safeRef).toBeUndefined();
});

test("safe references marked with allowUndefined false are non-nullable in types-style arrays", () => {
const Referencer = types.model("Referencer", {
safeRefs: types.array(types.safeReference(Referrable, { acceptsUndefined: false })),
});

expect(createRoot).toThrow();
});
const Root = types.model("Reference Model", {
refs: types.array(Referrable),
model: Referencer,
});
const root = create(
Root,
{
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
model: {
safeRefs: ["item-a", "item-c"],
},
},
readOnly,
);

test("can resolve valid safe references", () => {
const root = Root.createReadOnly({
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
expect(root.model.safeRefs.map((obj) => obj.key)).toEqual(["item-a"]);

type instanceType = (typeof root.model.safeRefs)[0];
assert<Has<instanceType, undefined>>(false);
assert<Has<instanceType, null>>(false);
});

expect(root.model.safeRef).toEqual(
expect.objectContaining({
key: "item-b",
count: 523,
}),
);
});
test("safe references marked with allowUndefined false are non-nullable in types-style maps", () => {
const Referencer = types.model("Referencer", {
safeRefs: types.map(types.safeReference(Referrable, { acceptsUndefined: false })),
});

const Root = types.model("Reference Model", {
refs: types.array(Referrable),
model: Referencer,
});
const root = create(
Root,
{
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
model: {
safeRefs: {
"item-a": "item-a",
"item-c": "item-c",
},
},
},
readOnly,
);

test("does not throw for invalid safe references", () => {
const root = Root.createReadOnly({
model: {
ref: "item-a",
safeRef: "item-c",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
expect([...root.model.safeRefs.keys()]).toEqual(["item-a"]);
});

expect(root.model.safeRef).toBeUndefined();
});
test("safe references marked with allowUndefined false are non-nullable in class model arrays", () => {
@register
class Referencer extends ClassModel({
safeRefs: types.array(types.safeReference(Referrable, { acceptsUndefined: false })),
}) {}

@register
class Root extends ClassModel({
refs: types.array(Referrable),
model: Referencer,
}) {}

test("references are equal to the instances they refer to", () => {
const root = Root.createReadOnly({
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
const root = create(
Root,
{
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
model: {
safeRefs: ["item-a", "item-c"],
},
},
readOnly,
);

expect(root.model.safeRefs.map((obj) => obj.key)).toEqual(["item-a"]);

type instanceType = (typeof root.model.safeRefs)[0];
assert<Has<instanceType, undefined>>(false);
assert<Has<instanceType, null>>(false);
});

expect(root.model.ref).toBe(root.refs[0]);
expect(root.model.ref).toEqual(root.refs[0]);
expect(root.model.ref).toStrictEqual(root.refs[0]);
});
test("safe references marked with allowUndefined false are non-nullable in class model maps", () => {
@register
class Referencer extends ClassModel({
safeRefs: types.map(types.safeReference(Referrable, { acceptsUndefined: false })),
}) {}

@register
class Root extends ClassModel({
refs: types.array(Referrable),
model: Referencer,
}) {}

test("safe references are equal to the instances they refer to", () => {
const root = Root.createReadOnly({
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
const root = create(
Root,
{
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
model: {
safeRefs: {
"item-a": "item-a",
"item-c": "item-c",
},
},
},
readOnly,
);

expect([...root.model.safeRefs.keys()]).toEqual(["item-a"]);
});

test("references are equal to the instances they refer to", () => {
const root = create(
Root,
{
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
},
readOnly,
);

expect(root.model.ref).toBe(root.refs[0]);
expect(root.model.ref).toEqual(root.refs[0]);
expect(root.model.ref).toStrictEqual(root.refs[0]);
});

expect(root.model.safeRef).toBe(root.refs[1]);
expect(root.model.safeRef).toEqual(root.refs[1]);
expect(root.model.safeRef).toStrictEqual(root.refs[1]);
test("safe references are equal to the instances they refer to", () => {
const root = create(
Root,
{
model: {
ref: "item-a",
safeRef: "item-b",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
},
readOnly,
);

expect(root.model.safeRef).toBe(root.refs[1]);
expect(root.model.safeRef).toEqual(root.refs[1]);
expect(root.model.safeRef).toStrictEqual(root.refs[1]);
});
});

test("throws for invalid refs", () => {
const createRoot = () =>
Root.createReadOnly({
model: {
ref: "item-c",
},
refs: [
{ key: "item-a", count: 12 },
{ key: "item-b", count: 523 },
],
});

expect(createRoot).toThrow();
});

test("instances of a model reference are assignable to instances of the model", () => {
Expand Down
8 changes: 7 additions & 1 deletion src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ensureRegistered } from "./class-model";
import { getSnapshot } from "./snapshot";
import { $context, $parent, $readOnly, $type } from "./symbols";
import type { IAnyStateTreeNode, IAnyType, IArrayType, IMSTArray, IStateTreeNode, Instance, TreeContext } from "./types";
import { SafeReferenceType } from "./reference";

export class QuickArray<T extends IAnyType> extends Array<Instance<T>> implements IMSTArray<T> {
static get [Symbol.species]() {
Expand Down Expand Up @@ -82,8 +83,13 @@ export class ArrayType<T extends IAnyType> extends BaseType<Array<T["InputType"]
instantiate(snapshot: this["InputType"] | undefined, context: TreeContext, parent: IStateTreeNode | null): this["InstanceType"] {
const array = new QuickArray<T>(this, parent, context);
if (snapshot) {
array.push(...snapshot.map((element) => this.childrenType.instantiate(element, context, array)));
let instances = snapshot.map((element) => this.childrenType.instantiate(element, context, array));
if (this.childrenType instanceof SafeReferenceType && this.childrenType.options?.acceptsUndefined === false) {
instances = instances.filter((instance) => instance != null);
}
array.push(...instances);
}

return array as this["InstanceType"];
}

Expand Down
Loading

0 comments on commit c59b402

Please sign in to comment.