Skip to content

Commit

Permalink
feat: add "who" and refactor "tell()"
Browse files Browse the repository at this point in the history
Closes: #9
Refs: #6
  • Loading branch information
boan-anbo committed Nov 14, 2023
1 parent 4fbe424 commit 5ea3984
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 43 deletions.
4 changes: 2 additions & 2 deletions packages/cantos/src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {TestKinds} from "@src/stories/test-kinds.ts";

export const ACT_DEFAULT_DESCRIPTIONS = {
export const STORY_DEFAULTS = {
DEFAULT_NARRATIVE: "The story goes",
DEFAULT_ACT_NAME: "it",
DEFAULT_WHO: "it",
}

export const STORY_TELLER = {
Expand Down
66 changes: 61 additions & 5 deletions packages/cantos/src/stories/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IStoryScripts} from "@src/stories/story-types.ts";
import {StoryStatus} from "@src/stories/status.ts";
import {StoryOptions} from "@src/stories/story-options.ts";

export interface Test {
export interface Test<CAST extends CastProfiles=typeof EmptyCast> {
/**
* The type of tests.
*/
Expand All @@ -19,6 +19,18 @@ export interface Test {
*/
closeIssues?: string[];

/**
* FIXME: Not extending the actual type. Perhaps need a factory function, something like createTest which takes in a createdStory for type inference.
* The a function with passed in root story and the current story that has the test, which should return an any to be used as the parameter for expect.
*/
receivedAs?: (rootStory: Story<CAST>, currentStory: Story<CAST>) => any | Promise<any>;

/**
* FIXME: Not extending the actual type.
* The a function with passed in root story and the current story that has the test, which should return an any to be used as the parameter for expect.
*/
expectedAs?: (rootStory: Story<CAST>, currentStory: Story<CAST>) => any;

}

export interface StoryActor {
Expand All @@ -32,20 +44,64 @@ export interface StoryActor {
}

export const EmptyCast = {} as const;
type KeysOfConst<T> = T extends Record<string, any> ? keyof T : never;
export type Who<T> = T extends Record<string, any> ? keyof T : never;
export type CastProfiles = Record<string, StoryActor>;

/**
* Bare Act is the base structure for any entity or behavior without methods or nested entities.
*/
export interface IStoryScript<Cast extends CastProfiles = typeof EmptyCast> {
/**
* The protagonists of the story.
* The optional protagonists of the story.
*
* @remark
* It defaults to "it".
* It defaults to inherited who, and if it has not inherited any "who", it will default to "it".
*
* @example Override who for current story
* Use it to override when the story does not make sense with the current parent story.
* ```ts
* {
* story: "MyApp",
* cast: myAppCast,
* who: ["MyApp"],
* scenes: {
* tellsWeather: {
* story: "tells the weather", // this makes sense without overriding the parent story's "who",
* context: {
* // Suppose tellsWeather features has a requirement that the location service must be enabled.
* locationServiceEnabled: {
* // This story does NOT make sense with the parents who--"MyApp" as in "MyApp is enabled" which will be wrong.
* story: "isEnabled",
* // So we override it with "LocationService". Now it reads "LocationService is enabled".
* who: ["LocationService"],
* }
* }
* }
* }
* }
*```
*
* @example Write stories in a way that needs no overriding `who`
* You usually can avoid overriding who by using active voice and assigning important roles as the subject.
* ```ts
* const myAppStory = {
* story: "MyApp",
* cast: myAppCast,
* who: ["MyApp"],
* scenes: {
* tellsWeather: {
* story: "tells the weather",
* context: {
* hasLocationServicePrivilege: {
* story: "has location service privilege"
* }
* }
* }
* }
*
* ```
*/
who?: KeysOfConst<Cast>[];
who?: Who<Cast>[];
/**
* Text of the story
*
Expand Down
9 changes: 5 additions & 4 deletions packages/cantos/src/stories/stories.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {PartialDeep, ReadonlyDeep} from "type-fest";
import {ACT_DEFAULT_DESCRIPTIONS} from "@src/consts.ts";
import {STORY_DEFAULTS} from "@src/consts.ts";
import {Scenes} from "@src/entrance.ts";

import {CastProfiles, EmptyCast, IStory, IStoryScript, Test} from "@src/stories/interfaces.ts";
import {CastProfiles, EmptyCast, IStory, IStoryScript, Test, Who} from "@src/stories/interfaces.ts";
import {Genres} from "@src/stories/story-kinds.ts";
import {getPath, populateActPath} from "@src/stories/utils.ts";
import {TestKind} from "@src/stories/test-kinds.ts";
Expand All @@ -15,7 +15,7 @@ import {StoryStatus} from "@src/stories/status.ts";


class StoryScript implements IStoryScript {
story: string = ACT_DEFAULT_DESCRIPTIONS.DEFAULT_NARRATIVE;
story: string = STORY_DEFAULTS.DEFAULT_NARRATIVE;
parentPath?: string | undefined;
genre?: Genres = Genres.ACT;
implemented?: boolean = false;
Expand All @@ -38,6 +38,7 @@ class StoryScript implements IStoryScript {


export class Story<CAST extends CastProfiles = typeof EmptyCast> extends StoryScript implements IStory<CAST> {
who?: Who<CAST>[];
options?: StoryOptions;
scenes: Scenes<CAST> = {};
context?: Scenes<CAST>
Expand Down Expand Up @@ -76,7 +77,7 @@ export class Story<CAST extends CastProfiles = typeof EmptyCast> extends StorySc
* Print the story as a tag, e.g. `[STORY]`
*
*/
printAsTag = () => printTag(this.tell());
printStoryAsTag = () => printTag(this.story);

// Get the next act according to the priority and the implementation status of the act.

Expand Down
45 changes: 30 additions & 15 deletions packages/cantos/src/stories/storyteller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Scenes} from "@src/index.ts";
import {ACT_DEFAULT_DESCRIPTIONS, GWT_DESCRIPTIONS, STORY_TELLER} from "@src/consts.ts";
import {GWT_DESCRIPTIONS, STORY_DEFAULTS, STORY_TELLER} from "@src/consts.ts";
import {Story} from "@src/stories/stories.ts";
import {StoryTag, StoryVersion} from "@src/stories/story-options.ts";
import {EmptyCast, Test} from "@src/stories/interfaces.ts";
import {StoryOptions, StoryTag, StoryVersion} from "@src/stories/story-options.ts";
import {EmptyCast, Test, Who} from "@src/stories/interfaces.ts";

enum STATEMENT_TYPE {
GIVEN,
Expand All @@ -19,6 +19,12 @@ export const printTestTags = (tests: Test[]) => {
return tags.length > 0 ? printTags(tags) : '';
}

export const printStory = (who: string, story: string) => joinByStoryTellerSpace([who, story]);

export const joinByStoryTellerSpace = (texts: string[]) => texts.join(STORY_TELLER.SPACE);

export const printWho = (who: Who<any>[] | undefined) => who ? who.join(STORY_TELLER.SPACE) : STORY_DEFAULTS.DEFAULT_WHO

function gatherTags(story: Story, tags: StoryTag[]): string {
const allTags: string [] = [];

Expand Down Expand Up @@ -48,31 +54,40 @@ function gatherTags(story: Story, tags: StoryTag[]): string {
return allTags.length > 0 ? printTags(allTags) : '';
}

const tellShortStory = (story: Story<any>): string => {
const who = printWho(story.who);
const storyText = story.story;
return printStory(who, storyText);
}
const attachTags = (storyText: string, story: Story<any>, opts?: StoryOptions): string => {
if (opts?.teller?.tags) {
return gatherTags(story, opts?.teller?.tags) + STORY_TELLER.SPACE + storyText;
}
return storyText;
}
export function tellStory(story: Story<any>, version: StoryVersion): string {
const opt = story.options;
let storyText = '';
switch (version) {
case StoryVersion.SHORT:
storyText = story.story;
storyText = tellShortStory(story);
break;
case StoryVersion.LONG:
storyText = gatherAllActsStatements(story);
storyText = tellLongStory(story);
break;
// if no preference specified, use the option preference or default to short
case StoryVersion.NO_PREFERENCE:
default:
storyText = opt?.teller?.prefer === StoryVersion.LONG ? gatherAllActsStatements(story) : story.story;
storyText = opt?.teller?.prefer === StoryVersion.LONG ? tellLongStory(story) : tellShortStory(story);
break;
}
if (opt?.teller?.tags) {
storyText = gatherTags(story, opt?.teller?.tags) + STORY_TELLER.SPACE + storyText;
}
storyText = attachTags(storyText, story, opt);
return storyText;


}

export function getActStatements(acts: Scenes<typeof EmptyCast> | undefined, prefixType: STATEMENT_TYPE, actName?: string): string | undefined {
export function tellGWT(acts: Scenes<typeof EmptyCast> | undefined, prefixType: STATEMENT_TYPE, actName?: string): string | undefined {

const actsLength = acts ? Object.keys(acts).length : 0;
if (!acts) {
Expand Down Expand Up @@ -115,12 +130,12 @@ export function getActStatements(acts: Scenes<typeof EmptyCast> | undefined, pre
return `${prefix.toLowerCase()} ${actName ? actName + ' ' : ''}${statement}`;
}

export function gatherAllActsStatements(entity: Story): string {
const actName = entity.protagonist ?? ACT_DEFAULT_DESCRIPTIONS.DEFAULT_ACT_NAME;
export function tellLongStory(entity: Story): string {
const actName = entity.protagonist ?? STORY_DEFAULTS.DEFAULT_WHO;
const statements = [
getActStatements(entity.context, STATEMENT_TYPE.GIVEN),
getActStatements(entity.when, STATEMENT_TYPE.WHEN),
getActStatements(entity.then, STATEMENT_TYPE.THEN, actName),
tellGWT(entity.context, STATEMENT_TYPE.GIVEN),
tellGWT(entity.when, STATEMENT_TYPE.WHEN),
tellGWT(entity.then, STATEMENT_TYPE.THEN, actName),
]

const collectedStatements = statements.filter(statement => statement).join(", ").trim();
Expand Down
108 changes: 91 additions & 17 deletions packages/cantos/tests/units/story-teller.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
import {describe, expect, it} from "vitest";
import {loadScript} from "@src/entrance.ts";
import {beforeEach, describe, expect, it} from "vitest";
import {loadCast, loadScript} from "@src/entrance.ts";
import {StoryScript} from "@src/stories/story-types.ts";
import {GenreUserStory} from "@src/stories/story-kinds.ts";
import {StoryTag} from "@src/stories/story-options.ts";
import {TestKinds} from "@src/stories/test-kinds.ts";
import {CommonScences, CommonTest} from "@src/index";
import {CastProfiles, CommonScences, CommonTest} from "@src/index";

const storyTellerScript = {
story: "Story Teller",
const storyTellCastProfiles = {
Tag: {
role: "Tag",
},
Who: {
role: "Who",
},
User: {
role: "User",
},
} satisfies CastProfiles

const storyTellerCast = loadCast(storyTellCastProfiles);

const storyTellerStoryWho = {
who: ["Who"],
cast: storyTellerCast,
story: CommonScences.SHOULD_WORK.story,
scenes: {
tellWhoStory: {
who: ["Who"],
when: {
tellCalled: {
story: "tell() is called",
}
},
story: "is used with story to form a description",
tests: {
unit: {
kind: TestKinds.UNIT,
doneWhen: "The who is printed together with the story with default setting",
},
},
scenes: {
tellMultipleWhoStory: {
who: ["Who", "User"],
story: "can be used with multiple who",
}
// TODO: Add more corner cases for super many whos and more than 3 whos where the last who has "and" in front of it
}
}
}
} satisfies StoryScript<typeof storyTellerCast>

const storyTellerStoryTags = {
story: "Story Tags",
scenes: {
storyTags: {
story: "User can specify tags to print at the beginning of the story",
who: ["User"],
story: "can specify tags to print at the beginning of the story",
explain: "For example, when the user specified \"Genre\", the printed story, no matter long or short, will have [GenreName] in front of it",
genre: GenreUserStory.FEATURE,
options: {
Expand All @@ -21,7 +65,7 @@ const storyTellerScript = {
}
},
storyTestTags: {
story: "User can print any tests associated with a story when they need to on the fly",
story: "can print any tests associated with a story when they need to on the fly",
tests: {
unit: {
kind: TestKinds.UNIT,
Expand All @@ -33,7 +77,16 @@ const storyTellerScript = {
},
}
}
} satisfies StoryScript
} satisfies StoryScript<typeof storyTellerCast>

const storyTellerScript = {
story: "Story Teller",
cast: storyTellerCast,
scenes: {
who: storyTellerStoryWho,
tags: storyTellerStoryTags,
}
} satisfies StoryScript<typeof storyTellerCast>

const storyTellerStory = loadScript(storyTellerScript);

Expand All @@ -43,18 +96,39 @@ describe(storyTellerStory.short(), () => {
expect(CommonScences.SHOULD_WORK.story).toBe(CommonScences.SHOULD_WORK.story);
});

it(storyTellerStory.scenes.storyTags.tell(), () => {
const storyTags = storyTellerStory.scenes.storyTags;
expect(storyTags.tell()).toBe(`[FEAT] ${storyTags.story}`);
});
const tagsStory = storyTellerStory.scenes.tags;

it(storyTellerStory.scenes.storyTestTags.tell(), () => {
const storyTestTags = storyTellerStory.scenes.storyTestTags;
expect(storyTestTags.printTestTags([storyTestTags.tests.unit])).toBe(CommonTest.UNIT.printAsTag());
describe(tagsStory.tell(), () => {
it(tagsStory.scenes.storyTags.tell(), () => {
const storyTags = tagsStory.scenes.storyTags;
expect(storyTags.tell()).toBe(`[FEAT] ${storyTags.who[0]} ${storyTags.story}`);
});

expect(storyTestTags.tellForTest([storyTestTags.tests.unit])).toBe(`${CommonTest.UNIT.printAsTag()} ${storyTestTags.story}`);
});
let storyTestTags: typeof tagsStory.scenes.storyTestTags;
beforeEach(() => {
storyTestTags = tagsStory.scenes.storyTestTags;
})

it(tagsStory.scenes.storyTestTags.tell(), () => {
expect(storyTestTags.printTestTags([storyTestTags.tests.unit])).toBe(CommonTest.UNIT.printStoryAsTag());

expect(storyTestTags.tellForTest([storyTestTags.tests.unit])).toBe(`${CommonTest.UNIT.printStoryAsTag()} ${storyTestTags.tell()}`);
});
})

const whoStory = storyTellerStory.scenes.who;

describe(whoStory.tell(), () => {
it(whoStory.scenes.tellWhoStory.tell(), () => {
const tellWhoStory = whoStory.scenes.tellWhoStory;
expect(tellWhoStory.tell()).toBe(`${tellWhoStory.who[0]} ${tellWhoStory.story}`);
});

it(whoStory.scenes.tellWhoStory.scenes.tellMultipleWhoStory.tell(), () => {
const tellMultipleWhoStory = whoStory.scenes.tellWhoStory.scenes.tellMultipleWhoStory;
expect(tellMultipleWhoStory.tell()).toBe(`${tellMultipleWhoStory.who.join(' ')} ${tellMultipleWhoStory.story}`);
});
});
});


Expand Down

0 comments on commit 5ea3984

Please sign in to comment.