counterfact 0.1.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  module.exports = {
4
+ ignorePatterns: ["/node_modules/", "/coverage/", "/reports/"],
5
+
4
6
  extends: ["hardcore", "hardcore/ts", "hardcore/node"],
5
7
 
6
8
  parserOptions: {
@@ -77,5 +79,43 @@ module.exports = {
77
79
  "node/no-unpublished-import": "off",
78
80
  },
79
81
  },
82
+
83
+ {
84
+ files: ["*.cjs"],
85
+ extends: ["hardcore", "hardcore/node"],
86
+
87
+ rules: {
88
+ "import/no-commonjs": "off",
89
+ },
90
+
91
+ parserOptions: {
92
+ sourceType: "script",
93
+ },
94
+ },
95
+
96
+ {
97
+ files: ["demo-ts/**/*.ts"],
98
+ extends: ["hardcore", "hardcore/node", "hardcore/ts"],
99
+
100
+ parserOptions: {
101
+ sourceType: "module",
102
+ project: "./demo-ts/tsconfig.json",
103
+ },
104
+
105
+ rules: {
106
+ "import/prefer-default-export": "off",
107
+ "import/no-unused-modules": "off",
108
+ "func-style": "off",
109
+ camelcase: "off",
110
+ "@typescript-eslint/naming-convention": "off",
111
+ "no-magic-numbers": "off",
112
+ "no-param-reassign": "off",
113
+ "import/group-exports": "off",
114
+ "max-len": "off",
115
+ "etc/prefer-interface": "off",
116
+ "@typescript-eslint/prefer-readonly-parameter-types": "off",
117
+ "eslint-comments/no-unused-disable": "off",
118
+ },
119
+ },
80
120
  ],
81
121
  };
@@ -0,0 +1,28 @@
1
+ on: ["pull_request"]
2
+
3
+ name: Coveralls
4
+
5
+ jobs:
6
+
7
+ build:
8
+ name: Coveralls
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+
12
+ - uses: actions/checkout@v3
13
+
14
+ - name: Setup Node.js 16.x
15
+ uses: actions/setup-node@v3
16
+ with:
17
+ node-version: 16.x
18
+ cache: yarn
19
+
20
+ - name: install, run tests
21
+ run: |
22
+ yarn
23
+ yarn test
24
+
25
+ - name: Coveralls
26
+ uses: coverallsapp/github-action@1.1.3
27
+ with:
28
+ github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npm run lint
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # counterfact
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab38cb2: add a tools.randomFromSchema(schema: JSONSchema) function
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 406b907: Add a tools object that will be provided to the request
14
+
15
+ ## 0.2.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 741e4fe: change path parameters from [this] to {this} for consistency with OpenAPI
20
+ - f237c38: proof of concept specifying routes with TypeScript and ts-node
21
+
22
+ ### Patch Changes
23
+
24
+ - 420cd52: return a 404 with a helpful error message when a handler for a route does not exist
25
+ - c11a475: allow the intial context (nee "store") to be passed as the second argument to `counterfact()`
26
+
3
27
  ## 0.1.2
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -1,26 +1,7 @@
1
1
  # Counterfact
2
2
 
3
- <!--
4
- To make this work we had to trick Stryker into thinking it was running on Travis CI
5
- As of this writing it's not getting updated automatically.
6
-
7
- TRAVIS=true STRYKER_DASHBOARD_API_KEY=XXXXXXX yarn stryker run
8
-
9
- https://github.com/stryker-mutator/stryker-js/issues/744
10
- -->
11
-
12
3
  [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpmcelhaney%2Fcounterfact%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/pmcelhaney/counterfact/main)
13
4
 
14
- ## This is a work in progress.
15
-
16
- If you're nosy and don't mind a whole lot of incompleteness:
17
-
18
- ```sh
19
- yarn
20
- yarn test
21
- node demo/index.js
22
- ```
23
-
24
5
  ## Why?
25
6
 
26
7
  I was building an UI against back end code that was so cumbersome I spent almost as much time trying to get the latest version of the back end code running as I did writing the front end code. Also I needed to build UI code against features and bug fixes that would not be available for a long time. Eventually I determined in the long run it would be quicker to hack together a fake implementation of the API so that I could keep going. Over time that fake implementation evolved to the point that it was pretty nice to work with. And I loved that I could easily recreate the exact scenarios I needed to test against.
@@ -43,19 +24,7 @@ yarn add -D counterfact
43
24
 
44
25
  ## Usage
45
26
 
46
- ### TL;DR
47
-
48
- ```js
49
- // ./path/to/some-route.js
50
- export function REQUEST_METHOD({parts, of, the, request}) {
51
- return {
52
- parts,
53
- of,
54
- the,
55
- response
56
- };
57
- }
58
- ```
27
+ See the [demo](./demo/README.md) directory for an example.
59
28
 
60
29
  ### Using the Koa plugin
61
30
 
@@ -71,8 +40,11 @@ const PORT = 3100;
71
40
 
72
41
  const app = new Koa();
73
42
 
43
+ const initialContext = {};
44
+
74
45
  const { koaMiddleware } = await counterfact(
75
- fileURLToPath(new URL("routes/", import.meta.url))
46
+ fileURLToPath(new URL("routes/", import.meta.url)),
47
+ initialContext
76
48
  );
77
49
 
78
50
  app.use(koaMiddleware);
@@ -93,12 +65,12 @@ To mock an API at `/hello/world`, create a file called `./routes/hello/world.js`
93
65
  Inside that file, output a function that handles a GET request.
94
66
 
95
67
  ```js
96
- export function GET() {
97
- return {
98
- status: 200, // optional HTTP status code (200 is the default)
99
- body: "hello world" // HTTP response body
100
- }
101
- }
68
+ export function GET() {
69
+ return {
70
+ status: 200, // optional HTTP status code (200 is the default)
71
+ body: "hello world", // HTTP response body
72
+ };
73
+ }
102
74
  ```
103
75
 
104
76
  Now when you run your server and call "GET /hello/world" you should get a 200 response with "hello world" in the body.
@@ -108,21 +80,21 @@ Now when you run your server and call "GET /hello/world" you should get a 200 re
108
80
  The get function has one parameter, a context object that contains metadata about the request. We can use that to read a the query string from `/hello/world?greeting=Hi`
109
81
 
110
82
  ```js
111
- export function GET(context) {
112
- return {
113
- body: "${context.query.greeting} world"
114
- }
115
- }
83
+ export function GET(context) {
84
+ return {
85
+ body: "${context.query.greeting} world",
86
+ };
87
+ }
116
88
  ```
117
89
 
118
90
  In practice, tend to use destructuring syntax, which is the closest thing we have in JavaScript to named arguments.
119
91
 
120
92
  ```js
121
- export function GET({ query }) {
122
- return {
123
- body: "${query.greeting} world"
124
- }
125
- }
93
+ export function GET({ query }) {
94
+ return {
95
+ body: "${query.greeting} world",
96
+ };
97
+ }
126
98
  ```
127
99
 
128
100
  ### Dynamic routes
@@ -130,11 +102,11 @@ In practice, tend to use destructuring syntax, which is the closest thing we hav
130
102
  Create another file called `./routes/hello/[name].js`. This file will match `/hello/universe`, `/hello/friends`, and `/hello/whatever-you-want-here`.
131
103
 
132
104
  ```js
133
- export function GET({ greeting, path }) {
134
- return {
135
- body: "${query.greeting}, ${path.name}!"
136
- }
137
- }
105
+ export function GET({ greeting, path }) {
106
+ return {
107
+ body: "${query.greeting}, ${path.name}!",
108
+ };
109
+ }
138
110
  ```
139
111
 
140
112
  The `path` object is analogous to the `query` object, holding values in the dynamic parts of the path.
@@ -145,21 +117,20 @@ This feature was [inspired by Next.js](https://nextjs.org/docs/routing/dynamic-r
145
117
 
146
118
  ### State
147
119
 
148
- State management is handled through a plain old JavaScript object called `store`. The store is initialized as an empty object (`{}`). You can read and write its keys however you like. Changes will persist from one request to another as long as the server is running.
120
+ State management is handled through a plain old JavaScript object called `context`, which is passed as the second argument to the `counterfact()` function (default value = `{}`). You can modify the context object however you like. Changes will persist from one request to another as long as the server is running.
149
121
 
150
- There are no rules around how you manipulate the store. Yes, you read that right.
122
+ There are no rules around how you manipulate the context. Yes, you read that right.
151
123
 
152
124
  ```js
153
- export function GET({ greeting, path, store }) {
154
-
155
- store.visits ??= {};
156
- store.visits[path.name] ??= 0;
157
- store.visits[path.name] += 1;
125
+ export function GET({ greeting, path, context }) {
126
+ context.visits ??= {};
127
+ context.visits[path.name] ??= 0;
128
+ context.visits[path.name] += 1;
158
129
 
159
- return {
160
- body: "${query.greeting}, ${path.name}!"
161
- }
162
- }
130
+ return {
131
+ body: "${query.greeting}, ${path.name}!",
132
+ };
133
+ }
163
134
  ```
164
135
 
165
136
  ### Request Methods
@@ -167,23 +138,23 @@ There are no rules around how you manipulate the store. Yes, you read that right
167
138
  So far we've only covered `GET` requests. What about `POST`, `PUT`, `PATCH` and `DELETE`? All HTTP request methods are supported. It's a matter exporting functions with the corresponding names.
168
139
 
169
140
  ```js
170
- export function GET({ path, store }) {
141
+ export function GET({ path, context }) {
171
142
 
172
- store.friends ??= {};
173
- store.friends[path.name] ??= {
143
+ context.friends ??= {};
144
+ context.friends[path.name] ??= {
174
145
  appearance: "lovely"
175
146
  };
176
147
 
177
148
  return {
178
- body: "Hello, ${path.name}. You look ${store.friends[name].appearance} today!"
149
+ body: "Hello, ${path.name}. You look ${context.friends[name].appearance} today!"
179
150
  }
180
151
  }
181
152
 
182
- export function POST(path, store, body) {
153
+ export function POST(path, context, body) {
183
154
 
184
- store.friends ??= {};
185
- store.friends[path.name] ??= {};
186
- store.friends[appearance] = body.appearance;
155
+ context.friends ??= {};
156
+ context.friends[path.name] ??= {};
157
+ context.friends[appearance] = body.appearance;
187
158
 
188
159
  return {
189
160
  body: {
@@ -198,18 +169,18 @@ So far we've only covered `GET` requests. What about `POST`, `PUT`, `PATCH` and
198
169
  If you need to do work asynchronously, return a promise or use async / await.
199
170
 
200
171
  ```js
201
- export function PUT({body}) {
202
- return doSomeStuffWith(body).then(() => {
203
- body: "Successfully did an async PUT"
204
- });
205
- }
172
+ export function PUT({ body }) {
173
+ return doSomeStuffWith(body).then(() => {
174
+ body: "Successfully did an async PUT";
175
+ });
176
+ }
206
177
 
207
- export async function DELETE() {
208
- await deleteMe();
209
- return {
210
- body: "Took a while, but now it's deleted."
211
- };
212
- }
178
+ export async function DELETE() {
179
+ await deleteMe();
180
+ return {
181
+ body: "Took a while, but now it's deleted.",
182
+ };
183
+ }
213
184
  ```
214
185
 
215
186
  ### Coming soon
@@ -1,12 +1,12 @@
1
- export function GET({ store }) {
2
- if (!store.visits) {
1
+ export function GET({ context }) {
2
+ if (!context.visits) {
3
3
  return {
4
4
  body: "You have not visited anyone yet.",
5
5
  };
6
6
  }
7
7
 
8
8
  return {
9
- body: Object.entries(store.visits)
9
+ body: Object.entries(context.visits)
10
10
  .map(([page, count]) => `You visited ${page} ${count} times.`)
11
11
  .join("\n"),
12
12
  };
@@ -1,7 +1,7 @@
1
- export function GET({ path, store, query }) {
2
- store.visits ??= {};
3
- store.visits[path.name] ??= 0;
4
- store.visits[path.name] += 1;
1
+ export function GET({ path, context, query }) {
2
+ context.visits ??= {};
3
+ context.visits[path.name] ??= 0;
4
+ context.visits[path.name] += 1;
5
5
 
6
6
  if (!path) {
7
7
  return { body: "Hello, stranger!" };
@@ -0,0 +1,19 @@
1
+ # Counterfact Demo - TypeScript
2
+
3
+ This demo illustrates some of the basic features of Counterfact, with TypeScript.
4
+
5
+ It requires ts-node (`npm i -g ts-node`).
6
+
7
+ Start it by running `ts-node-esm index.ts`.
8
+
9
+ `index.ts` starts a Koa server and loads Counterfact's middleware pointed to route definitions at `./routes`.
10
+
11
+ Under `./routes` you will find a few endpoints definitions:
12
+
13
+ `hello.ts` defines `/hello/:name` and says hello to your friends.
14
+
15
+ `hello/kitty.ts` defines `/hello/kitty`, overriding the behavior defined in `hello.ts`.
16
+
17
+ `count.ts` defines `/count` and reports how many times you've visited the other URLs.
18
+
19
+ Try adding more routes. You should be able to see the updates without restarting the server.
@@ -0,0 +1,3 @@
1
+ # Context
2
+
3
+ This folder will contain the context object (currently named "store"), which will define the server state. The `Store.ts` file exports a class which defines the context's type and an instance of that class which holds the state in memory. (It also defines the initial state.)
@@ -0,0 +1,19 @@
1
+ // This file will be maintained by hand.
2
+
3
+ interface Visits {
4
+ [name: string]: number;
5
+ }
6
+
7
+ export class Context {
8
+ public visits: Visits = {};
9
+
10
+ public lastVisited = "";
11
+
12
+ public visit(page: string) {
13
+ this.lastVisited = page;
14
+ this.visits[page] = (this.visits[page] || 0) + 1;
15
+ }
16
+ }
17
+
18
+ // Counterfact will load this object and use it as the initial state.
19
+ export const context = new Context();
@@ -0,0 +1,34 @@
1
+ /* eslint-disable node/no-missing-import */
2
+ /* eslint-disable node/file-extension-in-import */
3
+ /* eslint-disable import/no-unresolved */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
6
+ /* eslint-disable node/no-unsupported-features/node-builtins */
7
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
8
+ /* eslint-disable no-console */
9
+
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ import Koa from "koa";
13
+ import { counterfact } from "counterfact";
14
+
15
+ import { context } from "./context/context.js";
16
+
17
+ const PORT = 3100;
18
+
19
+ const app = new Koa();
20
+
21
+ const { koaMiddleware } = await counterfact(
22
+ fileURLToPath(new URL("paths/", import.meta.url)),
23
+ context
24
+ );
25
+
26
+ app.use(koaMiddleware);
27
+
28
+ app.listen(PORT);
29
+ console.log("Try these URLs:");
30
+ console.log(`http://localhost:${PORT}/hello/world`);
31
+ console.log(`http://localhost:${PORT}/hello/friends`);
32
+ console.log(`http://localhost:${PORT}/hello/kitty`);
33
+ console.log(`http://localhost:${PORT}/hello/world?greeting=Hi`);
34
+ console.log(`http://localhost:${PORT}/count`);
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "demo-ts",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "private": true,
6
+ "description": "This demo illustrates some of the basic features of Counterfact, with TypeScript.",
7
+ "main": "index.js",
8
+ "dependencies": {
9
+ "counterfact": "^0.2.0",
10
+ "koa": "^2.13.4"
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^18.0.0",
14
+ "i": "^0.3.7",
15
+ "npm": "^8.13.1"
16
+ },
17
+ "scripts": {
18
+ "start": "ts-node index.ts",
19
+ "test": "echo \"Error: no test specified\" && exit 1"
20
+ },
21
+ "author": "",
22
+ "license": "ISC"
23
+ }
@@ -0,0 +1,3 @@
1
+ # Routes
2
+
3
+ This directory defines the routes. These will be maintained by hand with the exception of files named `#types.ts`, which will be generated from the OpenAPI spec. We may generate files for routes as well, but only if they don't already exist. The `#types.ts` files will be overwritten. The others won't.
@@ -0,0 +1,18 @@
1
+ import type { HTTP_GET } from "./count.types";
2
+
3
+ export const GET: HTTP_GET = ({ context }) => {
4
+ if (Object.keys(context.visits).length === 0) {
5
+ return {
6
+ body: "You have not visited anyone yet.",
7
+ };
8
+ }
9
+
10
+ return {
11
+ body: Object.entries(context.visits)
12
+ .map(
13
+ ([page, count]: [string, number]) =>
14
+ `You visited ${page} ${count} times.`
15
+ )
16
+ .join("\n"),
17
+ };
18
+ };
@@ -0,0 +1,7 @@
1
+ import type { HttpResponseStatusCode } from "../types/Http";
2
+ import type { Context } from "../context/context";
3
+
4
+ export type HTTP_GET = (request: { context: Context }) => {
5
+ body: string;
6
+ status?: HttpResponseStatusCode;
7
+ };
@@ -0,0 +1,8 @@
1
+ import type { HTTP_GET } from "./kitty.types";
2
+
3
+ export const GET: HTTP_GET = ({ context }) => {
4
+ context.visit("kitty");
5
+ return {
6
+ body: '<img src="https://upload.wikimedia.org/wikipedia/en/0/05/Hello_kitty_character_portrait.png">',
7
+ };
8
+ };
@@ -0,0 +1,9 @@
1
+ import type { Context } from "../../context/context";
2
+ import type { HttpResponseStatusCode } from "../../types/Http";
3
+ import type { HtmlImgTag } from "../../types/HtmlImgTag";
4
+
5
+ export type HTTP_GET = (request: {
6
+ context: Context;
7
+ query: { greeting?: string };
8
+ path: { name: string };
9
+ }) => { body: HtmlImgTag; status?: HttpResponseStatusCode };
@@ -0,0 +1,8 @@
1
+ import type { HTTP_GET } from "./{name}.types";
2
+
3
+ export const GET: HTTP_GET = ({ path, context, query }) => {
4
+ context.visit(path.name);
5
+ return {
6
+ body: `${query.greeting ?? "Hello"}, ${path.name}!`,
7
+ };
8
+ };
@@ -0,0 +1,9 @@
1
+ import type { Context } from "../../context/context";
2
+ import type { HttpResponseStatusCode } from "../../types/Http";
3
+ import type { Greeting } from "../../types/Greeting";
4
+
5
+ export type HTTP_GET = (request: {
6
+ context: Context;
7
+ query: { greeting?: string };
8
+ path: { name: string };
9
+ }) => { body: Greeting; status?: HttpResponseStatusCode };
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "node",
4
+ "module": "es2022",
5
+ "target": "es2022",
6
+ "strictNullChecks": true,
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "ts-node": {
10
+ "esm": true
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export type Greeting = `${string}, ${string}!`;
@@ -0,0 +1 @@
1
+ export type HtmlImgTag = `<img src="${string}">`;
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
2
+ export type HttpResponseStatusCode = 200 | 404 | 500;
@@ -0,0 +1,5 @@
1
+ # Types
2
+
3
+ This directory is currently maintained by hand. The goal is to generate it from an OpenAPI spec.
4
+
5
+ Some of the example types currently use template literals so the examples can be something more interesting than plain old strings. We're not expecting to generate template literal types from OpenAPI.