Skip to content

Commit

Permalink
Merge pull request #739 from pmcelhaney/rework-context
Browse files Browse the repository at this point in the history
Rework context
  • Loading branch information
pmcelhaney committed Jan 26, 2024
2 parents 9768ff8 + f267cb7 commit 615b9d5
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 538 deletions.
7 changes: 7 additions & 0 deletions .changeset/sharp-comics-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"counterfact": minor
---

minor breaking change: `$.context.ts` is now `_.context.ts` and exports a class named Context

See https://github.com/pmcelhaney/counterfact/blob/main/docs/context-change.md
42 changes: 42 additions & 0 deletions docs/context-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Context Switch

Version 0.36 has a minor breaking change in the way context objects are defined.

## The file is now named `_.context.ts` instead of `$.context.ts`.

This change was made because putting a `$` in a file name can be confusing in Mac/Linux/Unix systems.

## Instead of exporting a default _value_, it the file should now export a class named `Context`

### Old

```ts
class Context {
// ...
}

export default new Context();
```

### New

```ts
export class Context {
// ...
}
```

In the future, the context class will be able to have a constructor that takes one argument, an instance of the _parent_ context.

```ts
// paths/accounts/_context.ts
import { CounterfactContext } from "../../types.ts";

export class Context extends CounterfactContext {
constructor(registry) {
this.mainContext = registry.find("/");
this.productsContext = registry.find("/products");
}
// ...
}
```
11 changes: 5 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ The `$.path` parameters are identified by dynamic sections of the path, i.e. `/g

<a id="context-object"></a>

### Working with state: the `$.context` object and `$.context.ts`
### Working with state: the `$.context` object and `_.context.ts`

There's one more parameter we need to explore, the `$.context` object. It stands in for microservices, databases, and other entities with which the real API interacts. It looks something like this:

Expand All @@ -140,10 +140,10 @@ export const POST: HTTP_POST = ($) => {
};
```

The `context` object is defined in `$.context.ts` in the same directory as the file that uses it. It's up to you to define the API for a context object. For example, your `$.context.ts` file might look like this.
The `context` object is defined in `_.context.ts` in the same directory as the file that uses it. It's up to you to define the API for a context object. For example, your `_.context.ts` file might look like this.

```ts
class PetStore {
export class Context {
pets: Pet[] = [];
addPet(pet: Pet) {
Expand All @@ -157,10 +157,9 @@ class PetStore {
}
}
export default new PetStore();
```

By default, each `$.context.ts` delegates to its parent directory, so you can define one context object in the root `$.context.ts` and use it everywhere.
By default, each `_.context.ts` delegates to its parent directory, so you can define one context object in the root `_.context.ts` and use it everywhere.

> You can make the context objects do whatever you want, including things like writing to databases. But remember that Counterfact is meant for testing, so holding on to data between sessions is an anti-pattern. Keeping everything in memory also makes it fast.

Expand Down Expand Up @@ -243,7 +242,7 @@ Using convention over configuration and automatically generated types, Counterfa
- Each endpoint is represented by a TypeScript file where the path to the file corresponds to the path of the endpoint.
- You can change the implementation at any time by changing these files.
- You can and should commit the generated code to source control. Files you change will not be overwritten when you start the server again. (The _types_ will be updated if the OpenAPI document changes, but you shouldn't need to edit the type definitions by hand.)
- Put behavior in `$.context.ts` files. These are created for you, but you should rewrite them to suit your needs. (At least update the root `$.context.ts` file.)
- Put behavior in `_.context.ts` files. These are created for you, but you should rewrite them to suit your needs. (At least update the root `_.context.ts` file.)
- Use the REPL to manipulate the server's state at runtime

## We're Just Getting Started 🐣
Expand Down
16 changes: 6 additions & 10 deletions src/server/context-registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface Context {
// eslint-disable-next-line max-classes-per-file
export class Context {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function
public constructor() {}

[key: string]: unknown;
}

Expand All @@ -13,15 +17,7 @@ export class ContextRegistry {
this.add("/", {});
}

public add(path: string, context?: Context): void {
if (context === undefined) {
// If $.context.ts exists but only exports a type, then the context object will be undefined here.
// This should be handled upstream, so that add() is not called in the first place.
// But module-loader.ts needs to be refactored a bit using type guards and the is operator
// before that can be done cleanly.
return;
}

public add(path: string, context: Context): void {
this.entries.set(path, context);
}

Expand Down
40 changes: 29 additions & 11 deletions src/server/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import type { Module, Registry } from "./registry.js";
const debug = createDebug("counterfact:typescript-generator:module-loader");

interface ContextModule {
default?: Context;
Context: Context;
}

function isContextModule(
module: ContextModule | Module,
): module is ContextModule {
return "Context" in module && typeof module.Context === "function";
}

function reportLoadError(error: unknown, fileUrl: string) {
Expand Down Expand Up @@ -55,6 +61,13 @@ export class ModuleLoader extends EventTarget {
(eventName: string, pathNameOriginal: string) => {
const pathName = pathNameOriginal.replaceAll("\\", "/");

if (pathName.includes("$.context") && eventName === "add") {
process.stdout.write(
`\n\n!!! The file at ${pathName} needs a minor update.\n See https://github.com/pmcelhaney/counterfact/blob/main/docs/context-change.md\n\n\n`,
);
return;
}

if (!["add", "change", "unlink"].includes(eventName)) {
return;
}
Expand All @@ -81,13 +94,14 @@ export class ModuleLoader extends EventTarget {
.then((endpoint: ContextModule | Module) => {
this.dispatchEvent(new Event(eventName));

if (pathName.includes("$.context")) {
if (pathName.includes("_.context")) {
this.contextRegistry.update(
parts.dir,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(endpoint as ContextModule).default,
);

// @ts-expect-error TS says Context has no constructable signatures but that's not true?
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/consistent-type-assertions
new (endpoint as ContextModule).Context(),
);
return "context";
}

Expand Down Expand Up @@ -147,6 +161,7 @@ export class ModuleLoader extends EventTarget {
await Promise.all(imports);
}

// eslint-disable-next-line max-statements
private async loadEndpoint(
fullPath: string,
directory: string,
Expand All @@ -162,13 +177,16 @@ export class ModuleLoader extends EventTarget {
| ContextModule
| Module;

if (file.name.includes("$.context")) {
this.contextRegistry.add(
`/${directory.replaceAll("\\", "/")}`,
if (file.name.includes("_.context")) {
if (isContextModule(endpoint)) {
this.contextRegistry.add(
`/${directory.replaceAll("\\", "/")}`,

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(endpoint as ContextModule).default,
);
// @ts-expect-error TS says Context has no constructable signatures but that's not true?
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
new endpoint.Context(),
);
}
} else {
const url = `/${nodePath.join(
directory,
Expand Down
14 changes: 9 additions & 5 deletions src/typescript-generator/context-type-coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,24 @@ export class ContextTypeCoder extends Coder {
}

names() {
return super.names("ContextType");
return super.names("Context");
}

write(script) {
if (script.path === "paths/$.context.ts") {
return "Context";
if (script.path === "paths/_.context.ts") {
return {
raw: "export class Context {};",
};
}

return { raw: 'export type { ContextType } from "../$.context"' };
return {
raw: 'export { Context } from "../_.context.js"',
};
}

modulePath() {
return nodePath
.join("paths", nodePath.dirname(this.pathString()), "$.context.ts")
.join("paths", nodePath.dirname(this.pathString()), "_.context.ts")
.replaceAll("\\", "/");
}
}
3 changes: 1 addition & 2 deletions src/typescript-generator/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import nodePath from "node:path";
import createDebug from "debug";

import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
import { ContextCoder } from "./context-coder.js";
import { OperationCoder } from "./operation-coder.js";
import { Repository } from "./repository.js";
import { Specification } from "./specification.js";
Expand Down Expand Up @@ -86,7 +85,7 @@ export async function generate(
});
});

repository.get("paths/$.context.ts").exportDefault(new ContextCoder(paths));
// repository.get("paths/$.context.ts").exportDefault(new ContextCoder(paths));

debug("telling the repository to write the files to %s", destination);

Expand Down
16 changes: 8 additions & 8 deletions test/server/module-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ describe("a module loader", () => {

it("finds a context and adds it to the context registry", async () => {
const files: { [key: string]: string } = {
"$.context.mjs": 'export default "main"',
"hello/$.context.mjs": 'export default "hello"',
"_.context.mjs": 'export class Context { name = "main"};',
"hello/_.context.mjs": 'export class Context { name = "hello"};',
};

await withTemporaryFiles(files, async (basePath: string) => {
Expand All @@ -183,16 +183,16 @@ describe("a module loader", () => {

await loader.load();

expect(contextRegistry.find("/hello")).toBe("hello");
expect(contextRegistry.find("/hello/world")).toBe("hello");
expect(contextRegistry.find("/some/other/path")).toBe("main");
expect(contextRegistry.find("/hello").name).toBe("hello");
expect(contextRegistry.find("/hello/world").name).toBe("hello");
expect(contextRegistry.find("/some/other/path").name).toBe("main");
});
});

it("provides the parent context if the locale $.context.ts doesn't export a default", async () => {
it("provides the parent context if the locale _.context.ts doesn't export a default", async () => {
const files: { [key: string]: string } = {
"$.context.mjs": "export default { value: 0 }",
"hello/$.context.mjs": "export default { value: 100 }",
"_.context.mjs": "export class Context { value = 0 }",
"hello/_.context.mjs": "export class Context { value = 100 }",
};

await withTemporaryFiles(files, async (basePath: string) => {
Expand Down
Loading

0 comments on commit 615b9d5

Please sign in to comment.