Skip to content

Commit

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

- replace Actors with Domains which is more comprehensive solution to organize the participants of stories, and can be used for DDD.
  • Loading branch information
boan-anbo committed Nov 18, 2023
1 parent b8b9147 commit eb9161b
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 262 deletions.
2 changes: 1 addition & 1 deletion packages/cantos/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cantos",
"author": "Bo An",
"version": "0.0.5",
"version": "0.0.6",
"keywords": [
"User Story",
"TDD",
Expand Down
48 changes: 25 additions & 23 deletions packages/cantos/src/entrance.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Utility function to map descriptions to act records
// Utility function to map descriptions to act records
import {NameList} from "@src/util-types.ts";
import {Story, UserNameList, UserStories, UserStory} from "@src/stories/stories.ts";
import {Story, UserNameList, UserStories, UserStory} from "@src/stories/stories.ts";

import {CastProfiles, IStory, IStoryScript} from "@src/stories/interfaces.ts";
import {Domain, IStory, IStoryScript, UserDomain} from "@src/stories/interfaces.ts";
import {IStoryScripts} from "@src/stories/story-types.ts";
import {StoryOptions} from "@src/stories/story-options.ts";

type StoryField = { story: string }
type StoryField = {
story: string
}

function mapNameList<T extends NameList>(tell: T): Record<keyof T, StoryField> {
return Object.keys(tell).reduce((acc, key) => {
Expand All @@ -16,9 +18,13 @@ function mapNameList<T extends NameList>(tell: T): Record<keyof T, StoryField> {
}, {} as Record<keyof T, StoryField>);
}

export function loadList<T extends NameList, CAST extends CastProfiles>(descriptions: T): UserNameList<T, CAST> {
export function loadList<T extends NameList, DOMAINS extends Domain>(descriptions: T): UserNameList<T, DOMAINS> {
const result = mapNameList(descriptions);
return loadScriptRecord(result) as UserNameList<T, CAST>;
return loadScriptRecord(result) as unknown as UserNameList<T, DOMAINS>;
}

export function loadDomain<T extends Domain>(domain: T): UserDomain<T> {
return domain
}

/**
Expand All @@ -27,38 +33,34 @@ export function loadList<T extends NameList, CAST extends CastProfiles>(descript
* @param opt - Optional parameters.
* @returns A ReadonlyDeep Act.
*/
export function loadScript<SCRIPT extends IStoryScript<CAST>, CAST extends CastProfiles>(partialEntity: SCRIPT, opt?: StoryOptions): UserStory<SCRIPT, CAST> {
const newAct = new Story(partialEntity, opt) as Story<CAST>
for (const actKey in newAct.scenes) {
newAct.scenes[actKey] = instantiateStoriesRecursively(newAct.scenes[actKey]);
export function loadScript<SCRIPT extends IStoryScript<DOMAINS>, DOMAINS extends Domain>(partialEntity: SCRIPT, opt?: StoryOptions): UserStory<SCRIPT, DOMAINS> {
const newStory = new Story(partialEntity, opt) as Story<DOMAINS>
for (const actKey in newStory.scenes) {
newStory.scenes[actKey] = instantiateStoriesRecursively(newStory.scenes[actKey]);
}
return newAct as UserStory<SCRIPT, CAST>;
}

export function loadCast<CAST extends CastProfiles>(cast: CAST): CAST {
return cast
return newStory as UserStory<SCRIPT, DOMAINS>;
}

export function loadScriptRecord<SCRIPTS extends IStoryScripts<CAST>, CAST extends CastProfiles>(actRecords: SCRIPTS): UserStories<SCRIPTS, CAST> {
const acts = {} as any;
export function loadScriptRecord<SCRIPTS extends IStoryScripts<DOMAINS>, DOMAINS extends Domain>(actRecords: SCRIPTS): UserStories<SCRIPTS, DOMAINS> {
const stories = {} as any;

Object.keys(actRecords).forEach((actKey) => {
const story: IStoryScript<CAST> = actRecords[actKey as keyof SCRIPTS] as IStoryScript<CAST>;
acts[actKey as keyof SCRIPTS] = loadScript<typeof story, CAST>(story);
const story: IStoryScript<DOMAINS> = actRecords[actKey as keyof SCRIPTS] as IStoryScript<DOMAINS>;
stories[actKey as keyof SCRIPTS] = loadScript<typeof story, DOMAINS>(story);
});
return acts as UserStories<SCRIPTS, CAST>;
return stories as UserStories<SCRIPTS, DOMAINS>;
}

function instantiateStoriesRecursively<T extends Story<CAST>, CAST extends CastProfiles>(partialAct: IStory<CAST>): T {
const act = new Story(partialAct) as Story<CAST>;
function instantiateStoriesRecursively<T extends Story<DOMAIN>, DOMAIN extends Domain>(partialAct: IStory<DOMAIN>): T {
const act = new Story(partialAct) as Story<DOMAIN>;
for (const actKey in act.scenes) {
act.scenes[actKey] = instantiateStoriesRecursively(act.scenes[actKey] as unknown as Story<CAST>);
act.scenes[actKey] = instantiateStoriesRecursively(act.scenes[actKey] as unknown as Story<DOMAIN>);
}
return act as T;
}

/**
* Types for Records.
*/
export type Scenes<CAST extends CastProfiles> = Record<string, IStory<CAST>>;
export type Scenes<DOMAIN extends Domain> = Record<string, IStory<DOMAIN>>;
// Export entrance methods
5 changes: 3 additions & 2 deletions packages/cantos/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {UserStory} from "@src/stories/stories.ts";
import {StoryScript, CastProfiles} from "@src/stories/story-types.ts";
import {StoryScript} from "@src/stories/story-types.ts";
import {UserDomain} from "@src/stories/interfaces.ts";


// Re-export entrance methods
Expand All @@ -14,7 +15,7 @@ export {entrance as Cantos, entrance as C};


// Export types for story
export type { StoryScript, UserStory, CastProfiles}
export type { StoryScript, UserStory, UserDomain}

// export Common Test helpers
export {CommonScences as CS, CommonTest as CT, CommonScences, CommonTest, customizeCommonTests}
Expand Down
186 changes: 107 additions & 79 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<CAST extends CastProfiles = typeof EmptyCast> {
export interface Test<DOMAIN extends Domain = typeof EmptyDomain> {
/**
* The type of tests.
*/
Expand All @@ -23,47 +23,116 @@ export interface Test<CAST extends CastProfiles = typeof EmptyCast> {
* 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>;
receivedAs?: (rootStory: Story<DOMAIN>, currentStory: Story<DOMAIN>) => 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;
expectedAs?: (rootStory: Story<DOMAIN>, currentStory: Story<DOMAIN>) => any;

}

export interface StoryActor {
role?: string;
roleBio?: string;
/**
* The name of the role.
*/
actor?: string;
actorBio?: string;

export interface DomainEvent {
name: string;
}

export interface DomainCommand {
name: string;
}

export interface Aggregate<T extends DomainObjects> {
// root: keyof TObject['entities'];
root: T extends DomainObjects ? (T['entities'] extends Record<string, DomainEntity> ? keyof T['entities'] : string) : string;

[key: string]: any; // Allows arbitrary additional properties
}

export interface DomainEntity {
// Unique identity of an entity
id: string
// Description of the entity
bio?: string
}

export const DefaultEntities: Record<string, DomainEntity> = {};

export interface DomainObjects<
TEntities extends Record<string, DomainEntity> = typeof DefaultEntities,
TValues extends Record<string, any> = typeof EmptyRecord
> {
entities?: TEntities;
values?: Record<string, TValues>;
}

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

export interface Domain<
TOBjects extends DomainObjects = typeof EmptyRecord,
TEvents extends DomainEvent = DomainEvent,
TCommands extends DomainCommand = DomainCommand,
> {
subdomains?: Record<string, Domain<any>>;
objects?: TOBjects;
aggregates?: Record<string, Aggregate<TOBjects>>;
events?: Record<string, TEvents>;
commands?: Record<string, TCommands>;
}

export type DomainObjectDef<T extends DomainObjects = DomainObjects> = T


export type UserDomain<T extends Domain> = T & Domain;


export interface RuleExamples {
rule: string;
examples: StringRecord
}

export const EmptyRecord = {} as const;
export const EmptyDomain: Domain<any> = {} as const;
// Dumb, or smart?, way to debug type inference: change never to a definite type, such as number, and see if `who` field inferre to that type, if it is, debug there, if not, move up the chain.
// Simplified type to extract entity keys from a domain
export type DomainEntityKeys<T extends Domain> = T['objects'] extends DomainObjects ? keyof T['objects']['entities'] : string;

// Revised Who<T> type using the simplified utility type
export type Who<T extends Domain> = {
[K in keyof T]: T[K] extends Domain<any>
? DomainEntityKeys<T[K]>
: string;
}[keyof T];

export type StringRecord = Record<string, string>;

/**
* Bare Act is the base structure for any entity or behavior without methods or nested entities.
*/
export interface IStoryScript<Cast extends CastProfiles = typeof EmptyCast> {
export interface IStoryScript<DOMAIN extends Domain = typeof EmptyRecord> {
/**
* The actors of the story.
*
* @remark
* It defaults to inherited who, and if it has not inherited any "who", it will default to "it".
*
* Use it to override when the story does not make sense with the current parent story.
*
* You usually can avoid overriding who by using active voice and assigning important roles as the subject.
*/
who?: Who<DOMAIN>[];
/**
* Text of the story
*
* @remarks
* What "who" do(es).
*/
story: string;
scenes?: IStoryScripts<Cast>;
scenes?: IStoryScripts<DOMAIN>;
/**
* The order of the story among its sibling stories.
*/
order?: number;
cast?: Cast;
domains?: DOMAIN;
/**
* Story-by-Story options that can override the global story options.
*/
Expand All @@ -86,89 +155,48 @@ export interface IStoryScript<Cast extends CastProfiles = typeof EmptyCast> {

/**
* A requirement, acceptance criterion, or a rule that the story, and its examples, must satisfy.
*
* @remarks
* For documenting workshop activity, therefore only contains string values.
* To formalize the rule, use {@link Story} under `scenes` instead.
*/
rules?: IStoryScripts<Cast>;
examples?: IStoryScripts<Cast>;
// Remaining questions
questions?: IStoryScripts<Cast>;
rules?: Record<string, RuleExamples>;


/**
* The actors of the story.
*
* @remark
* 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"
* }
* }
* }
* }
*
* ```
* Any tricky issues that are current unsolved.
*/
who?: Who<Cast>[];
questions?: StringRecord;


/**
* The context of the story.
*/
context?: IStoryScripts<Cast>;
context?: IStoryScripts<DOMAIN>;
/**
* The action of the story.
*/
action?: IStoryScripts<Cast>;
action?: IStoryScripts<DOMAIN>;
/**
* The outcome of the story.
*/
outcome?: IStoryScripts<Cast>;
outcome?: IStoryScripts<DOMAIN>;
/**
* The consequence of the story.
*/
so?: IStoryScripts<Cast>;
so?: IStoryScripts<DOMAIN>;

tellAs?: (fn: (entity: Story<DOMAIN>) => string) => string;

hasLongStory?: () => boolean;

tellAs?: (fn: (entity: Story<Cast>) => string) => string;
}

export interface IStory<CAST extends CastProfiles> extends IStoryScript<CAST> {
scenes: Scenes<CAST>;
readonly testId: () => string;
export interface IStory<DOMAIN extends Domain = typeof EmptyDomain> extends IStoryScript<DOMAIN> {
scenes: Scenes<DOMAIN>;
testId: () => string;
path: () => string;
nextActToDo: () => Story<CAST> | undefined;
nextActToDo: () => Story<DOMAIN> | undefined;

/**
* Simply tell the story, using teller preference {@link StoryTellingOptions} to decide whether to tell the short or long version.
Expand Down
Loading

0 comments on commit eb9161b

Please sign in to comment.