Skip to content

Commit

Permalink
more helpful errors when tracks/clocks/characters/etc are unable to r…
Browse files Browse the repository at this point in the history
…ender

Fixes #81
  • Loading branch information
cwegrzyn committed May 27, 2024
1 parent 49f8b6a commit 6468e23
Show file tree
Hide file tree
Showing 18 changed files with 386 additions and 250 deletions.
123 changes: 34 additions & 89 deletions src/character-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,131 +1,76 @@
import { type CachedMetadata } from "obsidian";
import { updaterWithContext } from "utils/update";
import { z } from "zod";
import {
CharacterLens,
CharacterValidater,
ValidatedCharacter,
characterLens,
} from "./characters/lens";
import { Datastore } from "./datastore";
import { BaseIndexer } from "./indexer/indexer";
import { Either, Left, Right } from "./utils/either";

export class CharacterTracker implements ReadonlyMap<string, CharacterResult> {
constructor(
public readonly index: Map<string, CharacterResult> = new Map(),
) {}

forEach(
callbackfn: (
value: CharacterResult,
key: string,
map: ReadonlyMap<string, CharacterResult>,
) => void,
thisArg?: unknown,
): void {
this.index.forEach(callbackfn, thisArg);
}
get(key: string): CharacterResult | undefined {
return this.index.get(key);
}
has(key: string): boolean {
return this.index.has(key);
}
get size(): number {
return this.index.size;
}
entries(): IterableIterator<[string, CharacterResult]> {
return this.index.entries();
}
keys(): IterableIterator<string> {
return this.index.keys();
}
values(): IterableIterator<CharacterResult> {
return this.index.values();
}
[Symbol.iterator](): IterableIterator<[string, CharacterResult]> {
return this.index[Symbol.iterator]();
}

*validCharacterEntries(): Generator<[string, CharacterContext]> {
for (const [key, val] of this.entries()) {
if (val.isRight()) {
yield [key, val.value];
}
}
}

activeCharacter(): [string, CharacterContext] {
if (this.size == 0) {
throw new MissingCharacterError("no valid characters found");
} else if (this.size > 1) {
throw new MissingCharacterError(
"we don't yet support multiple characters",
);
}

const [[key, val]] = this.entries();
if (val.isLeft()) {
throw val.error;
}

return [key, val.value];
}
}
import { BaseIndexer, IndexOf, IndexUpdate } from "./indexer/indexer";

export class CharacterError extends Error {}

export class MissingCharacterError extends Error {}

export class InvalidCharacterError extends Error {}

export class CharacterIndexer extends BaseIndexer<CharacterResult> {
export class CharacterIndexer extends BaseIndexer<
CharacterContext,
z.ZodError
> {
readonly id: string = "character";

constructor(
tracker: CharacterTracker,
protected readonly dataStore: Datastore,
) {
super(tracker.index);
constructor(protected readonly dataStore: Datastore) {
super();
}

processFile(
path: string,
cache: CachedMetadata,
): CharacterResult | undefined {
): IndexUpdate<CharacterContext, z.ZodError> {
if (cache.frontmatter == null) {
throw new Error("missing frontmatter cache");
}
const { validater, lens } = characterLens(this.dataStore.ruleset);
try {
const result = validater(cache.frontmatter);
return Right.create(new CharacterContext(result, lens, validater));
} catch (e) {
return Left.create(
e instanceof Error ? e : new Error("unexpected error", { cause: e }),
);
}
return validater(cache.frontmatter).map(
(character) => new CharacterContext(character, lens, validater),
);
}
}

// TODO: this type is really weird. should a validatedcharacter carry around all of the context
// used to produce it here? Or should that be coming from the datastore as needed?
export type CharacterResult<E extends Error = Error> = Either<
E,
CharacterContext
>;

export class CharacterContext {
constructor(
public readonly character: ValidatedCharacter,
public readonly lens: CharacterLens,
public readonly validater: (data: unknown) => ValidatedCharacter,
public readonly validater: CharacterValidater,
) {}

get updater() {
return updaterWithContext<ValidatedCharacter, CharacterContext>(
(data) => this.validater(data),
(data) => this.validater(data).unwrap(),
(character) => character.raw,
this,
);
}
}

export type CharacterTracker = IndexOf<CharacterIndexer>;

export function activeCharacter(
characters: CharacterTracker,
): [string, CharacterContext] {
if (characters.size == 0) {
throw new MissingCharacterError("no valid characters found");
} else if (characters.size > 1) {
throw new MissingCharacterError("we don't yet support multiple characters");
}

const [[key, val]] = characters.entries();
if (val.isLeft()) {
throw val.error;
}

return [key, val.value];
}
25 changes: 22 additions & 3 deletions src/characters/action-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type Datasworn } from "@datasworn/core";
import { App } from "obsidian";
import { ConditionMeterDefinition } from "rules/ruleset";
import { vaultProcess } from "utils/obsidian";
import { CharacterContext } from "../character-tracker";
import { CharacterContext, activeCharacter } from "../character-tracker";
import {
CharReader,
MOMENTUM_METER_DEFINITION,
Expand Down Expand Up @@ -142,13 +142,31 @@ function renderError(e: Error, el: HTMLElement): void {
}
}

export async function requireActiveCharacterContext(
plugin: IronVaultPlugin,
): Promise<CharacterActionContext> {
const context = await determineCharacterActionContext(plugin);
if (!context || !(context instanceof CharacterActionContext)) {
await InfoModal.show(
plugin.app,
"Command requires an active character, but none was found.",
);
throw new Error(
"Command requires an active character, but none was found.",
);
}

return context;
}

export async function determineCharacterActionContext(
plugin: IronVaultPlugin,
): Promise<ActionContext | undefined> {
if (plugin.settings.useCharacterSystem) {
try {
const [characterPath, characterContext] =
plugin.characters.activeCharacter();
const [characterPath, characterContext] = activeCharacter(
plugin.characters,
);
return new CharacterActionContext(
plugin.datastore,
characterPath,
Expand All @@ -168,6 +186,7 @@ export async function determineCharacterActionContext(
}

await InfoModal.show(plugin.app, div);
// TODO: maybe this should just raise an exception because the alternative is boring.
return undefined;
}
} else {
Expand Down
13 changes: 12 additions & 1 deletion src/characters/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { vaultProcess } from "utils/obsidian";
import { firstUppercase } from "utils/strings";
import { CustomSuggestModal } from "utils/suggest";
import { PromptModal } from "utils/ui/prompt";
import {
NoCharacterActionConext as NoCharacterActionContext,
determineCharacterActionContext,
} from "./action-context";
import {
addAsset,
defaultMarkedAbilitiesForAsset,
Expand All @@ -18,7 +22,14 @@ export async function addAssetToCharacter(
_editor: Editor,
_view: MarkdownView,
): Promise<void> {
const [path, context] = plugin.characters.activeCharacter();
const actionContext = await determineCharacterActionContext(plugin);
// TODO: maybe we could make this part of the checkCallback? (i.e., if we are in no character
// mode, don't even bother to list this command?)
if (!actionContext || actionContext instanceof NoCharacterActionContext) {
return;
}
const path = actionContext.characterPath;
const context = actionContext.characterContext;
const { character, lens } = context;
const characterAssets = lens.assets.get(character);

Expand Down
Loading

0 comments on commit 6468e23

Please sign in to comment.