Skip to content

Commit

Permalink
feat: support DDD/BDD workshop activities
Browse files Browse the repository at this point in the history
## Added

- new types, `rules`, and `examples` fields to support design activities such example mapping, event storming etc.
  • Loading branch information
boan-anbo committed Nov 17, 2023
1 parent e32f78c commit b8b9147
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 82 deletions.
4 changes: 2 additions & 2 deletions packages/cantos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "Bo An",
"version": "0.0.5",
"keywords": [
"User Stories",
"User Story",
"TDD",
"BDD",
"Design documents",
Expand All @@ -12,7 +12,7 @@
"Vitest",
"Playwright",
"testing-library",
"Cucumber",
"cucumber.js",
"Gherkin",
"Agile",
"Scrum",
Expand Down
87 changes: 52 additions & 35 deletions packages/cantos/src/stories/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {Story, TestKind} from "@src/stories/stories.ts";
import {Genres} from "@src/stories/story-kinds.ts";
import type {StoryType} from "@src/stories/story-kinds.ts";
import {Scenes} from "@src/entrance.ts";
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<CAST extends CastProfiles=typeof EmptyCast> {
export interface Test<CAST extends CastProfiles = typeof EmptyCast> {
/**
* The type of tests.
*/
Expand Down Expand Up @@ -52,7 +52,49 @@ export type CastProfiles = Record<string, StoryActor>;
*/
export interface IStoryScript<Cast extends CastProfiles = typeof EmptyCast> {
/**
* The optional protagonists of the story.
* Text of the story
*
* @remarks
* What "who" do(es).
*/
story: string;
scenes?: IStoryScripts<Cast>;
/**
* The order of the story among its sibling stories.
*/
order?: number;
cast?: Cast;
/**
* Story-by-Story options that can override the global story options.
*/
options?: StoryOptions;
type?: StoryType;
tests?: Record<string, Test>
parentPath?: string;
explain?: string;
/**
* The condition when the story is considered done.
*/
done?: string;
/**
* The current status of the story
*/
status?: StoryStatus | string;
lastUpdate?: string;

priority?: number;

/**
* A requirement, acceptance criterion, or a rule that the story, and its examples, must satisfy.
*/
rules?: IStoryScripts<Cast>;
examples?: IStoryScripts<Cast>;
// Remaining questions
questions?: IStoryScripts<Cast>;


/**
* The actors of the story.
*
* @remark
* It defaults to inherited who, and if it has not inherited any "who", it will default to "it".
Expand Down Expand Up @@ -103,45 +145,20 @@ export interface IStoryScript<Cast extends CastProfiles = typeof EmptyCast> {
*/
who?: Who<Cast>[];
/**
* Text of the story
*
* @remarks
* What "who" do(es).
* The context of the story.
*/
story: string;
scenes?: IStoryScripts<Cast>;
context?: IStoryScripts<Cast>;
/**
* The order of the story among its sibling stories.
* The action of the story.
*/
order?: number;
cast?: Cast;
action?: IStoryScripts<Cast>;
/**
* Story-by-Story options that can override the global story options.
* The outcome of the story.
*/
options?: StoryOptions;
genre?: Genres;
tests?: Record<string, Test>
parentPath?: string;
explain?: string;
/**
* The condition when the story is considered done.
*/
done?: string;
outcome?: IStoryScripts<Cast>;
/**
* The current status of the story
* The consequence of the story.
*/
status?: StoryStatus | string;
lastUpdate?: string;

priority?: number;


context?: IStoryScripts<Cast>;

when?: IStoryScripts<Cast>;

then?: IStoryScripts<Cast>;

so?: IStoryScripts<Cast>;

tellAs?: (fn: (entity: Story<Cast>) => string) => string;
Expand Down
39 changes: 21 additions & 18 deletions packages/cantos/src/stories/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,55 @@ import {STORY_DEFAULTS} from "@src/consts.ts";
import {Scenes} from "@src/entrance.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";
import {printTag, printTestTags, printWho, tellStory} from "@src/stories/storyteller.ts";
import {IStoryScripts} from "@src/stories/story-types.ts";
import {NameList} from "@src/util-types.ts";
import {StoryOptions, StoryVersion} from "@src/stories/story-options.ts";
import {StoryStatus} from "@src/stories/status.ts";
import {StoryType, StoryTypes} from "@src/stories/story-kinds.ts";


class StoryScript implements IStoryScript {
story: string = STORY_DEFAULTS.DEFAULT_NARRATIVE;
parentPath?: string | undefined;
genre?: Genres = Genres.ACT;
type?: StoryType = StoryTypes.STORY;
implemented?: boolean = false;
protagonist?: string = "it";
explain?: string;
options?: StoryOptions;


constructor(
entity: Partial<IStoryScript>,
opt?: StoryOptions,
) {
if (opt?.defaultKind) {
this.genre = opt.defaultKind;
this.type = opt.defaultKind;
}
Object.assign(this, entity);


}
}

export interface Rule {
rule: string;
examples: string[];
}

export class Story<CAST extends CastProfiles = typeof EmptyCast> extends StoryScript implements IStory<CAST> {
who?: Who<CAST>[];
options?: StoryOptions;
scenes: Scenes<CAST> = {};
context?: Scenes<CAST>
when?: Scenes<CAST>;
then?: Scenes<CAST>;

type?: StoryType = StoryTypes.STORY;
action?: Scenes<CAST>;
outcome?: Scenes<CAST>;
status?: StoryStatus | string;
priority?: number;


cast?: CAST;
path = () => getPath(this.story, this.parentPath)
// get a getter to get test id
Expand Down Expand Up @@ -136,25 +142,22 @@ export class Story<CAST extends CastProfiles = typeof EmptyCast> extends StorySc
*
* @remarks
* Represents an enhanced version of an `IActRecord` with additional methods from the `Act` class.
* This type recursively applies itself to each nested act within the `acts` property, ensuring that
* This type recursively applies itself to each nested act within the `stories` property, ensuring that
* each nested act also receives the benefit of intellisense.
*
* The `acts` property is a mapped type that iterates over each key in the original `acts` property
* of the provided `IActRecord`. For each key, it checks if the corresponding value extends `IActRecord`.
* If it does, the type is recursively applied to this value, enhancing it with `Act` methods.
* If it does not extend `IActRecord`, the type for that key is set to `never`, indicating an invalid type.
*
* This approach allows the `UserAct` type to maintain the original structure of the `IActRecord`,
* including any nested acts, while also adding the methods and properties defined in the `Act` class.
* As a result, instances of `ActWithMethods` have both the data structure defined by their specific `IActRecord`
* implementation and the functionality provided by `Act`.
*
* @template T - A type extending `IActRecord` that represents the structure of the act record.
* This generic type allows `ActWithMethods` to be applied to any specific implementation
* of `IActRecord`, preserving its unique structure.
*/
export type UserStory<T extends IStoryScript<CAST>, CAST extends CastProfiles> = ReadonlyDeep<T> & Story<CAST> & {
scenes: { [K in keyof T['scenes']]: T['scenes'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['scenes'][K], CAST>> : never };
examples: { [K in keyof T['examples']]: T['examples'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['examples'][K], CAST>> : never };
rules: { [K in keyof T['rules']]: T['rules'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['rules'][K], CAST>> : never };
questions: { [K in keyof T['questions']]: T['questions'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['questions'][K], CAST>> : never };
context: { [K in keyof T['context']]: T['context'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['context'][K], CAST>> : never };
action: { [K in keyof T['action']]: T['action'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['action'][K], CAST>> : never };
outcome: { [K in keyof T['outcome']]: T['outcome'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['outcome'][K], CAST>> : never };
so: { [K in keyof T['so']]: T['so'][K] extends IStoryScript<CAST> ? ReadonlyDeep<UserStory<T['so'][K], CAST>> : never };
};

export type UserCast<CAST extends CastProfiles> = ReadonlyDeep<CAST>;
Expand Down
28 changes: 24 additions & 4 deletions packages/cantos/src/stories/story-kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* Types for Act kinds.
*/
export enum GenreEntity {
ACT = "ACT",
ENTITY = "ENTITY",
BEHAVIOR = "BEHAVIOR",
DOMAIN = "DOMAIN",
Expand All @@ -20,6 +19,25 @@ export enum GenreGherkin {
THEN = "THEN",
}

/**
*
*/
export enum GenreSpec {
RULE = "RULE",
ACCEPTANCE_CRITERIA = "ACCEPTANCE_CRITERIA",
REQUIREMENT = "REQUIREMENT",
}

export enum GenreActivities {
/**
* A mapping workshop is a workshop where the participants map out the domain with `rules` and `examples`
*/
EXAMPLE_MAPPING = "EXAMPLE_MAPPING",
EVENT_STORMING = "EVENT_STORMING",
DOMAIN_STORYTELLING = "DOMAIN_STORYTELLING",
DOMAIN_MODELING = "DOMAIN_MODELING",
}

export enum GenreUserStory {
EPIC = "EPIC",
STORY = "STORY",
Expand All @@ -30,12 +48,14 @@ export enum GenreUserStory {
SO_THAT = "SO THAT",
}

export type GenreBDD = GenreGherkin | GenreUserStory;
export type GenreBDD = GenreGherkin | GenreUserStory | GenreSpec | GenreActivities;

export const Genres = {
export const StoryTypes = {
...GenreEntity,
...GenreGherkin,
...GenreUserStory,
...GenreSpec,
...GenreActivities,
}

export type Genres = GenreEntity | GenreBDD;
export type StoryType = GenreEntity | GenreBDD;
4 changes: 2 additions & 2 deletions packages/cantos/src/stories/story-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Genres} from "@src/stories/story-kinds.ts";
import {StoryType} from "@src/stories/story-kinds.ts";

export enum StoryVersion {
NO_PREFERENCE = "NO_PREFERENCE",
Expand All @@ -21,7 +21,7 @@ export interface StoryTellingOptions {
}

export interface StoryOptions {
defaultKind?: Genres
defaultKind?: StoryType
capitalizeKeywords?: boolean
teller?: StoryTellingOptions
}
6 changes: 3 additions & 3 deletions packages/cantos/src/stories/storyteller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function gatherTags(story: Story, tags: StoryTag[]): string {
let tagText = '';
switch (tag) {
case StoryTag.Genre:
tagText = story.genre ? story.genre : '';
tagText = story.type ? story.type : '';
break;
case StoryTag.Status:
tagText = story.status ? story.status : '';
Expand Down Expand Up @@ -134,8 +134,8 @@ export function tellLongStory(entity: Story): string {
const actName = entity.protagonist ?? STORY_DEFAULTS.DEFAULT_WHO;
const statements = [
tellGWT(entity.context, STATEMENT_TYPE.GIVEN),
tellGWT(entity.when, STATEMENT_TYPE.WHEN),
tellGWT(entity.then, STATEMENT_TYPE.THEN, actName),
tellGWT(entity.action, STATEMENT_TYPE.WHEN),
tellGWT(entity.outcome, STATEMENT_TYPE.THEN, actName),
]

const collectedStatements = statements.filter(statement => statement).join(", ").trim();
Expand Down
44 changes: 44 additions & 0 deletions packages/cantos/tests/fixtures/example-mapping-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {CastProfiles} from "@src/stories/interfaces.ts";
import {loadCast, loadScript} from "@src/entrance.ts";
import {StoryScript} from "@src/stories/story-types.ts";
import {StoryTypes} from "@src/stories/story-kinds.ts";

const exampleMappingProfiles = {
AI: {
role: "AI"
},
User: {
role: "User"
},
} satisfies CastProfiles;

const exampleMappingCast = loadCast(exampleMappingProfiles);


const exampleMappingScript = {
type: StoryTypes.EXAMPLE_MAPPING,
story: "Chat App",
cast: exampleMappingCast,
rules: {
userMessageCannotBeTooLong: {
story: "User message cannot exceed 100 characters",
examples: {
userSendsMessageTooLong: {
story: "User sends message with 200 characters",
},
userSendsMessageWithinLimit: {
story: "User sends message with 100 characters",
rules: {
somethingElse: {
story : "Something else",
}
}
},
}
}
},
} satisfies StoryScript<typeof exampleMappingCast>;

export const exampleMappingStory = loadScript(exampleMappingScript);


5 changes: 3 additions & 2 deletions packages/cantos/tests/fixtures/story-test-fixture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {loadScript} from "@src/entrance.ts";


export const StoryTestFixture = loadScript({
story: "Story Test Fixture",
scenes: {
Expand All @@ -21,12 +22,12 @@ export const StoryTestFixture = loadScript({
story: "a given is provided"
}
},
when: {
action: {
askedToDescribeItselfFully: {
story: "asked to describe itself fully"
}
},
then: {
outcome: {
shouldDescribeItself: {
story: "should describe itself fully"
}
Expand Down
Loading

0 comments on commit b8b9147

Please sign in to comment.