Skip to content
This repository has been archived by the owner on Mar 19, 2024. It is now read-only.

Commit

Permalink
feat: new @scalar/mock-server package
Browse files Browse the repository at this point in the history
  • Loading branch information
hanspagel committed Feb 14, 2024
1 parent f651657 commit a0fc84f
Show file tree
Hide file tree
Showing 27 changed files with 5,528 additions and 183 deletions.
6 changes: 6 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"swagger",
"cli"
],
"repository": {
"type": "git",
"url": "https://github.com/scalar/cli.git",
"directory": "packages/cli"
},
"engines": {
"node": ">=18"
},
Expand All @@ -32,6 +37,7 @@
"@hono/node-server": "^1.7.0",
"@parcel/watcher": "^2.4.0",
"@scalar/api-reference": "^1.16.1",
"@scalar/mock-server": "workspace:*",
"@seriousme/openapi-schema-validator": "^2.1.6",
"commander": "^12.0.0",
"hono": "^3.12.12",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/petstore.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"security": [{ "petstore_auth": ["write:pets", "read:pets"] }]
}
},
"/pet/findByStatus": {
"/pet/findByStatusa": {
"get": {
"tags": ["pet"],
"summary": "Finds Peats by status",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/format/temp.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "foo": "bar" }
{"foo": "bar"}
190 changes: 102 additions & 88 deletions packages/cli/src/commands/mock/MockCommand.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { serve } from '@hono/node-server'
import { getExampleFromSchema } from '@scalar/api-reference'
import { createMockServer } from '@scalar/mock-server'
import { Command } from 'commander'
import { Hono } from 'hono'
import type { Context } from 'hono'
import kleur from 'kleur'
import type { OpenAPI } from 'openapi-types'

import {
getMethodColor,
getOperationByMethodAndPath,
loadOpenApiFile,
useGivenFileOrConfiguration,
watchFile,
Expand All @@ -25,110 +24,125 @@ export function MockCommand() {
fileArgument: string,
{ watch, port }: { watch?: boolean; port?: number },
) => {
// Server instance
let server: ReturnType<typeof serve> = null

// Configuration
const file = useGivenFileOrConfiguration(fileArgument)

let schema = (await loadOpenApiFile(file))
// Load OpenAPI file
let specification = (await loadOpenApiFile(file))
.specification as OpenAPI.Document

// watch file for changes
// Watch OpenAPI file for changes
if (watch) {
await watchFile(file, async () => {
console.log(
kleur.bold().white('[INFO]'),
kleur.grey('Mock Server was updated.'),
)
schema = (await loadOpenApiFile(file))
specification = (await loadOpenApiFile(file))
.specification as OpenAPI.Document
})
}

console.log(kleur.bold().white('Available Paths'))
console.log()

if (
schema?.paths === undefined ||
Object.keys(schema?.paths).length === 0
) {
console.log(
kleur.bold().yellow('[WARN]'),
kleur.grey('Couldn’t find any paths in the OpenAPI file.'),
)
}

// loop through all paths
for (const path in schema?.paths ?? []) {
// loop through all methods
for (const method in schema.paths?.[path]) {
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
)
}
server.close()
server = await bootServer({
openapi: specification,
port,
})
})
}

console.log()
// Show all paths from the specification
printAvailablePaths(specification)

const app = new Hono()

app.all('/*', (c) => {
const { method, path } = c.req

const operation = getOperationByMethodAndPath(schema, method, path)

console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
`${kleur.grey('→')} ${
operation?.operationId
? kleur.white(operation.operationId)
: kleur.red('[ERROR] 404 Not Found')
}`,
)

if (!operation) {
return c.text('Not found', 404)
}

// if (!operation) {
// return c.text('Method not allowed', 405)
// }

const jsonResponseConfiguration =
operation.responses['200'].content['application/json']

const response = jsonResponseConfiguration.example
? jsonResponseConfiguration.example
: jsonResponseConfiguration.schema
? getExampleFromSchema(jsonResponseConfiguration.schema, {
emptyString: '…',
})
: null

return c.json(response)
// Listen for requests
server = await bootServer({
openapi: specification,
port,
})
},
)

serve(
{
fetch: app.fetch,
port: port ?? 3000,
},
(info) => {
console.log(
`${kleur.bold().green('➜ Mock Server')} ${kleur.white(
'listening on',
)} ${kleur.cyan(`http://localhost:${info.port}`)}`,
)
console.log()
},
return cmd
}

async function bootServer({
openapi,
port,
}: {
openapi: OpenAPI.Document
port?: number
}) {
const app = await createMockServer({
openapi,
onRequest,
})

return serve(
{
fetch: app.fetch,
port: port ?? 3000,
},
(info) => {
console.log(
`${kleur.bold().green('➜ Mock Server')} ${kleur.white(
'listening on',
)} ${kleur.cyan(`http://localhost:${info.port}`)}`,
)
console.log()
},
)
}

return cmd
function printAvailablePaths(specification: OpenAPI.Document) {
console.log(kleur.bold().white('Available Paths'))
console.log()

if (
specification?.paths === undefined ||
Object.keys(specification?.paths).length === 0
) {
console.log(
kleur.bold().yellow('[WARN]'),
kleur.grey('Couldn’t find any paths in the OpenAPI file.'),
)
}

// loop through all paths
for (const path in specification?.paths ?? []) {
// loop through all methods
for (const method in specification.paths?.[path]) {
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`,
)
}
}

console.log()
}

function onRequest({
context,
operation,
}: {
context: Context
operation: OpenAPI.Operation
}) {
const { method } = context.req
console.log(
`${kleur
.bold()
[
getMethodColor(method)
](method.toUpperCase().padEnd(6))} ${kleur.grey(`${context.req.path}`)}`,
`${kleur.grey('→')} ${
operation?.operationId
? kleur.white(operation.operationId)
: kleur.red('[ERROR] 404 Not Found')
}`,
)
}
4 changes: 4 additions & 0 deletions packages/cli/src/utils/readFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import fs from 'node:fs'

export function readFile(file: string) {
try {
if (fs.existsSync(file) === false) {
return undefined
}

return fs.readFileSync(file, 'utf8')
} catch (err) {
console.error(err)
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/utils/useGivenFileOrConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function useGivenFileOrConfiguration(file?: string) {

// Try to load the configuration
try {
const configuration = JSON.parse(readFile(CONFIG_FILE))
// check if file exists

const content = readFile(CONFIG_FILE)
const configuration = JSON.parse(content)

if (configuration?.reference?.file) {
return configuration.reference.file
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"module": "esnext",
"allowSyntheticDefaultImports": true,
"outDir": "dist",
"sourceMap": false
"sourceMap": false,
"paths": {
"@scalar/mock-server": ["../packages/mock-server/*"]
}
},
"include": ["src/**/*", "./package.json", "tests/**/*"]
"include": ["src/**/*", "./package.json", "tests/**/*", "vite.config.ts"]
}
13 changes: 13 additions & 0 deletions packages/cli/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from 'node:path'
import { defineConfig } from 'vitest/config'

export default defineConfig({
resolve: {
alias: [
{
find: '@scalar/mock-server',
replacement: path.resolve(__dirname, '../mock-server/src/index.ts'),
},
],
},
})
59 changes: 59 additions & 0 deletions packages/mock-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Scalar Mock Server

A server returning fake data based on an OpenAPI specification

## Installation

```bash
npm install @scalar/mock-server
```

## Usage

```ts
import { serve } from '@hono/node-server'
import { createMockServer } from '@scalar/mock-server'

// Your OpenAPI specification
const openapi = {
openapi: '3.1.0',
info: {
title: 'Hello World',
version: '1.0.0',
},
paths: {
'/foobar': {
get: {
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
example: {
foo: 'bar',
},
},
},
},
},
},
},
},
}

// Create the mocked routes
const app = await createMockServer({
openapi,
onRequest({ context, operation }) {
console.log(context.req.method, context.req.path)
},
})

// Start the server
serve({
fetch: app.fetch,
port: 3000,
}, (info) => {
console.log(`Listening on http://localhost:${info.port}`)
})
```
Loading

0 comments on commit a0fc84f

Please sign in to comment.