Skip to content

Commit

Permalink
Merge pull request #919 from pmcelhaney/types-directory
Browse files Browse the repository at this point in the history
directory structure change
  • Loading branch information
pmcelhaney authored May 30, 2024
2 parents ae7614a + a03a4ed commit eb951d0
Show file tree
Hide file tree
Showing 27 changed files with 227 additions and 139 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-camels-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": minor
---

**New file structure** - In preparation for the 1.0 release, `paths` has moved to `routes`; `path-types` and `components` are now under `types`. Counterfact will automatically migrate your code for you. We don't like making disruptive changes like this; this will be the last one for the foreseeable future.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ out
templates/typescript/counterfact/components
templates/typescript/counterfact/internal
templates/typescript/counterfact/openapi
templates/typescript/counterfact/path-types
templates/typescript/counterfact/types/paths
templates/typescript/counterfact/paths
templates/typescript/package-lock.json
templates/typescript/yarn.lock
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@

### Patch Changes

- 7f70c2c: added a comment to the top of files under path-types which links to an [FAQ on generated code](https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md) -- thanks @ingovals for the [nudge](https://github.com/pmcelhaney/counterfact/issues/787)
- 7b4bdc2: fixed [#788](https://github.com/pmcelhaney/counterfact/issues/788): On Windows, the import field from path-types to context gets wrong slashes while others are fine
- 7f70c2c: added a comment to the top of files under types/paths which links to an [FAQ on generated code](https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md) -- thanks @ingovals for the [nudge](https://github.com/pmcelhaney/counterfact/issues/787)
- 7b4bdc2: fixed [#788](https://github.com/pmcelhaney/counterfact/issues/788): On Windows, the import field from types/paths to context gets wrong slashes while others are fine
- 9601b20: Allows Counterfact to handle requests that contain the OpenApi basePath

## 0.37.1
Expand Down
37 changes: 36 additions & 1 deletion bin/counterfact.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/* eslint-disable complexity */

import fs from "node:fs";
import { readFile } from "node:fs/promises";
import nodePath from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -97,7 +98,7 @@ function createWatchMessage(config) {
return watchMessage;
}

// eslint-disable-next-line max-statements
// eslint-disable-next-line max-statements, sonarjs/cognitive-complexity
async function main(source, destination) {
debug("executing the main function");

Expand Down Expand Up @@ -165,6 +166,23 @@ async function main(source, destination) {

debug("loading counterfact (%o)", config);

let didMigrate = false;

// eslint-disable-next-line n/no-sync
if (fs.existsSync(nodePath.join(config.basePath, "paths"))) {
await fs.promises.rmdir(nodePath.join(config.basePath, "paths"), {
recursive: true,
});
await fs.promises.rmdir(nodePath.join(config.basePath, "path-types"), {
recursive: true,
});
await fs.promises.rmdir(nodePath.join(config.basePath, "components"), {
recursive: true,
});

didMigrate = true;
}

const { start } = await counterfact(config);

debug("loaded counterfact", config);
Expand Down Expand Up @@ -205,6 +223,23 @@ async function main(source, destination) {
await open(guiUrl);
debug("opened browser");
}

if (didMigrate) {
process.stdout.write("\n\n\n*******************************\n");
process.stdout.write("MIGRATING TO NEW FILE STRUCTURE\n\n");
process.stdout.write(
"In preparation for version 1.0, Counterfact has migrated to a new file structure.\n",
);
process.stdout.write("- The paths directory has been renamed to routes.\n");
process.stdout.write(
"- The path-types and components directories are now stored under types.\n",
);
process.stdout.write("Your files have automatically been migrated.\n");
process.stdout.write(
"Please report any issues to https://github.com/pmcelhaney/counterfact/issues\n",
);
process.stdout.write("*******************************\n\n\n");
}
}

program
Expand Down
10 changes: 5 additions & 5 deletions docs/faq-generated-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

## Am I meant to edit the generated code?

Yes, you're meant to edit the _imperative_ code under the `paths` directory in order to customize your mock server's behavior. Don't touch the _types_ under `path-types` and `components`. If you do, Counterfact will overwrite your changes anyway.
Yes, you're meant to edit the _imperative_ code under the `routes` directory in order to customize your mock server's behavior. Don't touch the _types_ under `types`. If you do, Counterfact will overwrite your changes anyway.

## Should I commit generated code to source control?

Short answer: yes, commit everything.

Use your Git client to add and commit the directory containing Counterfact's output. It's best not get bogged down in the details. But if you really want to know, here are the details.

You absolutely _should_ put the files under the `paths` directory under version control. That's _your_ code. Counterfact generates starter files and after that doesn't touch the code again.
You absolutely _should_ put the files under the `routes` directory under version control. That's _your_ code. Counterfact generates starter files and after that doesn't touch the code again.

In theory, code under `path-types` and `components` can be reproduced exactly given the same input (OpenAPI file), so it might make sense to put those directories in your `.gitignore`. However, that depends on whether the OpenAPI file is in the same repository. That's not always the case, especially if the API is owned by a different team -- or a different organization.
In theory, code under `types` can be reproduced exactly given the same input (OpenAPI file), so it might make sense to put those directories in your `.gitignore`. However, that depends on whether the OpenAPI file is in the same repository. That's not always the case, especially if the API is owned by a different team -- or a different organization.

Note that Counterfact creates `.gitignore` file that excludes the `.cache` directory. Do commit `.gitignore`, don't commit `.cache`.

## What's with these `never` types?

If you peek into the type definitions under `path-types` you may notice a bunch of references to `never`. For example, you might see `query: never`.
If you peek into the type definitions under `types/paths` you may notice a bunch of references to `never`. For example, you might see `query: never`.

The upshot is you won't see `query` offered as an autocomplete when you type `$.` when you're writing code for that operation. And if you have code like `$.query.id`, TypeScript will yell at you.

Expand All @@ -32,7 +32,7 @@ If you point to a URL like https://petstore3.swagger.io/api/v3/openapi.yaml, Cou

If you have control over the OpenAPI (i.e. you're not getting it from a third party), we recommend working with a local file for a great developer experience.

## Do I need to restart after changing a file in paths?
## Do I need to restart after changing a file in `routes`?

No. The fact that you can change the code while the server is running is what makes mocking in Counterfact so pleasant. It watches for file changes and updates automatically. And the state of your context objects is preserved. You can even change the definition of a context object (`_.context.js`) and Counterfact will preserve the values of any properties you didn't explicitly change.

Expand Down
2 changes: 1 addition & 1 deletion docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api --op
4. starts a server which implements the API
5. opens your browser to [Swagger UI](https://swagger.io/tools/swagger-ui/) (`--open`)

You can use Swagger to try out the auto-generated API. Out of the box, it returns random responses using metadata from the OpenAPI document. Edit the files under `./api/paths` to add more realistic behavior. There's no need to restart the server.
You can use Swagger to try out the auto-generated API. Out of the box, it returns random responses using metadata from the OpenAPI document. Edit the files under `./api/routes` to add more realistic behavior. There's no need to restart the server.

To learn more, see the [Usage Guide](./usage.md).
2 changes: 1 addition & 1 deletion docs/usage-without-openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ npx counterfact _ api

## Creating routes

In the file where the code is generated, you should find a directory -- initially empty -- called `paths`. Here you can start adding mock APIs by adding JS or TS files. For example, to mock an API that responds to `GET /hello/world`, with "World says hello!" you would create `paths/hello/world.js` that looks like this.
In the file where the code is generated, you should find a directory -- initially empty -- called `routes`. Here you can start adding mock APIs by adding JS or TS files. For example, to mock an API that responds to `GET /hello/world`, with "World says hello!" you would create `routes/hello/world.js` that looks like this.

```js
// hello/world.js
Expand Down
12 changes: 6 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,20 @@ This will let your team use the same version of Counterfact across all environme

After generating code you should have three directories:

- 📂 **components** contains the TypeScript types for the objects used in your REST API. These components correspond to the `/schema/components` section of the [OpenAPI spec](https://swagger.io/specification/).
- 📂 **paths** contains the implementation of each endpoint, a.k.a. routes. Counterfact uses the word "paths" because it corresponds to the `/paths` section of the spec.
- 📂 **path-types** contains the type information for paths.
- 📂 **routes** contains the implementation of each endpoint / path.
- 📂 **types/paths** contains the type information for paths.
- 📂 **types/components** contains the TypeScript types for the objects used in your REST API. These components correspond to the `/components/schema` section of the [OpenAPI spec](https://swagger.io/specification/).

When you launch Counterfact with no command line options, the code under `components` and `path-types` is regenerated every time you run Counterfact, so that the types can stay in sync with any OpenAPI changes. The code under paths is minimal boilerplate that you're meant to edit by hand. Counterfact will not overwrite your changes in the `paths` directory, but it will add new files when necessary. If you use any of the command line options then it will only regenerate the code when you tell it to via the `--watch` or `--generate` options.
The code under `types` is regenerated every time you run Counterfact, so that the types can stay in sync with any OpenAPI changes. The code under `routes` is minimal boilerplate that you're meant to edit by hand. Counterfact will not overwrite your changes in the `routes` directory, but it will add new files when necessary. If you use any of the command line options then it will only regenerate the code when you tell it to via the `--watch` or `--generate` options.

See also [Generated Code FAQ](./faq-generated-code.md)

> [!TIP]
> You don't have to use the code generator. It wasn't even part of Counterfact originally. You can also create the files under `paths` by hand. The main benefit of generating code is all the type information that's managed for you and kept in sync with OpenAPI. See [What if I don't have an OpenAPI document?](./usage-without-openapi.md)
> You don't have to use the code generator. It wasn't even part of Counterfact originally. You can also create the files under `routes` by hand. The main benefit of generating code is all the type information that's managed for you and kept in sync with OpenAPI. See [What if I don't have an OpenAPI document?](./usage-without-openapi.md)
## Routing is where it's at 🔀

In the `paths` directory, you should find TypeScript files with code like the following.
In the `routes` directory, you should find TypeScript files with code like the following.

```ts
export const GET: HTTP_GET = ($) => {
Expand Down
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function counterfact(config: Config) {
);

const transpiler = new Transpiler(
nodePath.join(modulesPath, "paths").replaceAll("\\", "/"),
nodePath.join(modulesPath, "routes").replaceAll("\\", "/"),
compiledPathsDirectory,
"commonjs",
);
Expand Down
36 changes: 36 additions & 0 deletions src/migrate/paths-to-routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-console */
import { promises as fs } from "node:fs";
import path from "node:path";

// eslint-disable-next-line max-statements
async function copyAndModifyFiles(sourceDirectory, destinationDirectory) {
try {
const entries = await fs.readdir(sourceDirectory, { withFileTypes: true });

await fs.mkdir(destinationDirectory, { recursive: true });

for (const entry of entries) {
const sourcePath = path.join(sourceDirectory, entry.name);
const destinationPath = path.join(destinationDirectory, entry.name);

if (entry.isDirectory()) {
await copyAndModifyFiles(sourcePath, destinationPath);
} else {
let fileContent = await fs.readFile(sourcePath, "utf8");

fileContent = fileContent.replaceAll("path-types", "types/paths");
await fs.writeFile(destinationPath, fileContent);
}
}
} catch (error) {
console.error("Error copying and modifying files:", error);
}
}

export async function pathsToRoutes(rootDirectory) {
await copyAndModifyFiles(
path.join(rootDirectory, "paths"),
path.join(rootDirectory, "routes"),
);
}
8 changes: 4 additions & 4 deletions src/typescript-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ paths:
type: string
```
Our goal is to produce a file at `/paths/hello.ts` that exports a function named `GET` (1). Our implementation will depend on a type for the `GET` function which lives in `/paths/types-hello.ts` (2). That type, in turn, will depend on a type named `Message` which lives in `/components/message.ts` (3).
Our goal is to produce a file at `/routes/hello.ts` that exports a function named `GET` (1). Our implementation will depend on a type for the `GET` function which lives in `/routes/types-hello.ts` (2). That type, in turn, will depend on a type named `Message` which lives in `/components/message.ts` (3).

(1) We kick off the process by iterating over the paths. (In our simple example there's only one path, at `/hello`.) In our model, each path is represented as a `Requirement`. We give each path / `Requirement` to an `OperationCoder` who will write the code. We ask the repository for a `Script` and then hand the `Script` our `OperationCoder` instance and ask it to create an export.

(2a) The `OperationCoder` knows that the function has to have a type, but it doesn't know to create that type. So it recruits an `OperationTypeCoder` and hands it the `Requirement`. It then asks the `Script` to _import_ a type, passing along the `OperationTypeCoder`. The `Script` returns the name of the variable to which the imported type will be assigned. That name is all the `OperationCoder` needs to know to continue writing its portion of the code. The `OperationCoder` continues on, writing the `GET` function. Depending on what it finds in the `Requirement`, it will break off parts and delegate some of the work to other `Coder`s.

(2b) The `Script` has promised that it will import the type from `/path-types/hello.type.ts` so it needs to make sure that file exists and has a matching export. It goes to the repository to get another `Script` and asks it to _export_ the type, passing along the `OperationTypeCoder` in the process. It's a little tricky here because `OperationTypeCoder`'s `Requirement` may not have the information it needs to proceed. It might have a `$ref` pointer to some _other_ requirement that might be in a different file. So before asking for the export, it asks the `OperationTypeCoder` to give it _another_ `OperationTypeCoder` that definitely has the requirement. Because the other requirement may be in another file that the `Repository` hasn't loaded yet, this part happens asynchronously.
(2b) The `Script` has promised that it will import the type from `/types/paths/hello.type.ts` so it needs to make sure that file exists and has a matching export. It goes to the repository to get another `Script` and asks it to _export_ the type, passing along the `OperationTypeCoder` in the process. It's a little tricky here because `OperationTypeCoder`'s `Requirement` may not have the information it needs to proceed. It might have a `$ref` pointer to some _other_ requirement that might be in a different file. So before asking for the export, it asks the `OperationTypeCoder` to give it _another_ `OperationTypeCoder` that definitely has the requirement. Because the other requirement may be in another file that the `Repository` hasn't loaded yet, this part happens asynchronously.

(2c) When it's ready, the `Script` asks the `OperationTypeCoder` which definitely has an immediately usable requirement to write the export for the `Script` at `/path-types/hello.types.ts`.
(2c) When it's ready, the `Script` asks the `OperationTypeCoder` which definitely has an immediately usable requirement to write the export for the `Script` at `/types/paths/hello.types.ts`.

(3) The `OperationTypeCoder` needs the help of a `SchemaCoder` so it asks the `Script` at `/path-types/hello.types.ts` for an export...
(3) The `OperationTypeCoder` needs the help of a `SchemaCoder` so it asks the `Script` at `/types/paths/hello.types.ts` for an export...
2 changes: 1 addition & 1 deletion src/typescript-generator/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export async function generate(

pathDefinition.forEach((operation, requestMethod) => {
repository
.get(`paths${key}.ts`)
.get(`routes${key}.ts`)
.export(new OperationCoder(operation, requestMethod, securitySchemes));
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/typescript-generator/operation-coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class OperationCoder extends Coder {
.replaceAll("~1", "/");

return `${nodePath
.join("path", pathString)
.join("routes", pathString)
.replaceAll("\\", "/")}.types.ts`;
}
}
2 changes: 1 addition & 1 deletion src/typescript-generator/operation-type-coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class OperationTypeCoder extends TypeCoder {
.replaceAll("~1", "/");

return `${nodePath
.join("path-types", pathString)
.join("types/paths", pathString)
.replaceAll("\\", "/")}.types.ts`;
}

Expand Down
18 changes: 13 additions & 5 deletions src/typescript-generator/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,16 @@ export class Repository {

const writeFiles = Array.from(
this.scripts.entries(),

async ([path, script]) => {
const contents = await script.contents();

const fullPath = nodePath.join(destination, path).replaceAll("\\", "/");

await ensureDirectoryExists(fullPath);

const shouldWriteRoutes = routes && path.startsWith("paths");
const shouldWriteTypes = types && !path.startsWith("paths");
const shouldWriteRoutes = routes && path.startsWith("routes");
const shouldWriteTypes = types && !path.startsWith("routes");

if (
shouldWriteRoutes &&
Expand Down Expand Up @@ -121,7 +122,11 @@ export class Repository {
}

async createDefaultContextFile(destination) {
const contextFilePath = nodePath.join(destination, "paths", "_.context.ts");
const contextFilePath = nodePath.join(
destination,
"routes",
"_.context.ts",
);

if (existsSync(contextFilePath)) {
return;
Expand Down Expand Up @@ -158,13 +163,16 @@ export class Context {
}

nearestContextFile(destination, path) {
const directory = nodePath.dirname(path).replace("path-types", "paths");
const directory = nodePath
.dirname(path)
.replaceAll("\\", "/")
.replace("types/paths", "routes");

const candidate = nodePath.join(destination, directory, "_.context.ts");

if (directory.length <= 1) {
// No _context.ts was found so import the one that should be in the root
return nodePath.join(destination, "paths", "_.context.ts");
return nodePath.join(destination, "routes", "_.context.ts");
}

if (existsSync(candidate)) {
Expand Down
2 changes: 1 addition & 1 deletion src/typescript-generator/response-type-coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class ResponseTypeCoder extends TypeCoder {
}

modulePath() {
return `components/${this.requirement.data.$ref.split("/").at(-1)}.ts`;
return `types/${this.requirement.data.$ref}.ts`;
}

writeCode(script) {
Expand Down
2 changes: 1 addition & 1 deletion src/typescript-generator/schema-coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class SchemaCoder extends Coder {
}

modulePath() {
return `components/${this.requirement.data.$ref.split("/").at(-1)}.ts`;
return `types/${this.requirement.data.$ref}.ts`;
}

writeCode(script) {
Expand Down
Loading

0 comments on commit eb951d0

Please sign in to comment.