@@ -0,0 +1,6 @@ | |||||
- We could make a common code generation strategy between | |||||
CLI and Web API since we have unified the code structure. | |||||
- What this means is we just need to declare the connection | |||||
from the core SDK to the CLI or Web API functions, then we | |||||
can run some sort of code generation, so we don't have to | |||||
bother writing the Web API/CLI code manually. |
@@ -67,6 +67,7 @@ | |||||
"dependencies": { | "dependencies": { | ||||
"@clack/prompts": "^0.6.3", | "@clack/prompts": "^0.6.3", | ||||
"@modal-sh/core": "workspace:*", | "@modal-sh/core": "workspace:*", | ||||
"pino": "^8.14.1", | |||||
"yargs": "^17.7.2" | "yargs": "^17.7.2" | ||||
} | } | ||||
} | } |
@@ -34,7 +34,7 @@ export class AdderControllerImpl implements AdderController { | |||||
const { a, b } = params.args; | const { a, b } = params.args; | ||||
try { | try { | ||||
const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) }); | const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) }); | ||||
params.logger.log(response); | |||||
params.logger.info(response); | |||||
} catch (errorRaw) { | } catch (errorRaw) { | ||||
const error = errorRaw as Error; | const error = errorRaw as Error; | ||||
params.logger.error(error.message); | params.logger.error(error.message); | ||||
@@ -65,7 +65,7 @@ export class AdderControllerImpl implements AdderController { | |||||
const { a, b } = params.args; | const { a, b } = params.args; | ||||
try { | try { | ||||
const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) }); | const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) }); | ||||
params.logger.log(response); | |||||
params.logger.info(response); | |||||
} catch (errorRaw) { | } catch (errorRaw) { | ||||
const error = errorRaw as Error; | const error = errorRaw as Error; | ||||
params.logger.error(error.message); | params.logger.error(error.message); | ||||
@@ -1,13 +1,10 @@ | |||||
import tty from 'tty'; | |||||
import yargs, {Argv} from 'yargs'; | |||||
import yargs, { Argv } from 'yargs'; | |||||
import * as clack from '@clack/prompts'; | import * as clack from '@clack/prompts'; | ||||
import { DummyWriteStream } from './write-stream'; | |||||
import pino, {LogFn} from 'pino'; | |||||
import * as util from 'util'; | |||||
export interface Logger { | |||||
log: (message: unknown) => void; | |||||
error: (message: unknown) => void; | |||||
warn: (message: unknown) => void; | |||||
debug: (message: unknown) => void; | |||||
} | |||||
export interface Logger extends pino.BaseLogger {} | |||||
export interface CommandHandlerArgs { | export interface CommandHandlerArgs { | ||||
self: any; | self: any; | ||||
@@ -33,24 +30,6 @@ type PromptValueArgs = [Record<string, string>, ...never[]] | |||||
| [[string, string], ...never[]] | | [[string, string], ...never[]] | ||||
| [[string, string][], ...never[]]; | | [[string, string][], ...never[]]; | ||||
class DummyWriteStream extends tty.WriteStream { | |||||
private bufferInternal = Buffer.from(''); | |||||
constructor() { | |||||
super(0); | |||||
} | |||||
write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean { | |||||
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]); | |||||
// noop | |||||
return true; | |||||
} | |||||
get buffer(): Buffer { | |||||
return this.bufferInternal; | |||||
} | |||||
} | |||||
export interface TestModeResult { | export interface TestModeResult { | ||||
exitCode?: number; | exitCode?: number; | ||||
stdout: DummyWriteStream; | stdout: DummyWriteStream; | ||||
@@ -69,6 +48,7 @@ interface InteractiveModeOptions { | |||||
export interface CliOptions { | export interface CliOptions { | ||||
name: string; | name: string; | ||||
interactiveMode?: Partial<InteractiveModeOptions>; | interactiveMode?: Partial<InteractiveModeOptions>; | ||||
logger?: Logger | boolean; | |||||
} | } | ||||
export class Cli { | export class Cli { | ||||
@@ -171,27 +151,6 @@ export class Cli { | |||||
}; | }; | ||||
} | } | ||||
private generateLogger() { | |||||
return { | |||||
log: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`${message?.toString()}\n`); | |||||
}, | |||||
warn: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`WARN: ${message?.toString()}\n`); | |||||
}, | |||||
debug: (message: unknown) => { | |||||
const stdout = this.testMode ? this.testModeResult.stdout : process.stdout; | |||||
stdout.write(`DEBUG: ${message?.toString()}\n`); | |||||
}, | |||||
error: (message: unknown) => { | |||||
const stderr = this.testMode ? this.testModeResult.stderr : process.stderr; | |||||
stderr.write(`${message?.toString()}\n`); | |||||
}, | |||||
}; | |||||
} | |||||
private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) { | private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) { | ||||
const thisCli = this; | const thisCli = this; | ||||
return async function handler(this: any, commandArgs: Record<string, unknown>) { | return async function handler(this: any, commandArgs: Record<string, unknown>) { | ||||
@@ -207,11 +166,78 @@ export class Cli { | |||||
interactiveModeOptions, | interactiveModeOptions, | ||||
); | ); | ||||
const stdoutLogFn: LogFn = (...args: unknown[]) => { | |||||
const stream = thisCli.testMode ? thisCli.testModeResult.stdout : process.stdout; | |||||
const [arg0, arg1, ...etcArgs] = args; | |||||
if (typeof arg0 === 'string' || typeof arg0 === 'number') { | |||||
if (arg1) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`, arg1)); | |||||
} | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`)); | |||||
} | |||||
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg1}\n`, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg1}\n`)); | |||||
} | |||||
} | |||||
}; | |||||
const stderrLogFn: LogFn = (...args: unknown[]) => { | |||||
const stream = thisCli.testMode ? thisCli.testModeResult.stderr : process.stderr; | |||||
const [arg0, arg1, ...etcArgs] = args; | |||||
if (typeof arg0 === 'string' || typeof arg0 === 'number') { | |||||
if (arg1) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`, arg1)); | |||||
} | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`)); | |||||
} | |||||
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg1}\n`, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg1}\n`)); | |||||
} | |||||
} | |||||
}; | |||||
const loggerFn = thisCli.options.logger; | |||||
const defaultLogger = { | |||||
level: 'info', | |||||
debug: stdoutLogFn, | |||||
info: stdoutLogFn, | |||||
warn: stdoutLogFn, | |||||
error: stderrLogFn, | |||||
fatal: stderrLogFn, | |||||
trace: stdoutLogFn, | |||||
silent: () => {}, | |||||
} as Logger; | |||||
const loggerBooleanFn = typeof loggerFn === 'boolean' && loggerFn ? defaultLogger : { | |||||
level: 'silent', | |||||
debug: () => {}, | |||||
info: () => {}, | |||||
warn: () => {}, | |||||
error: () => {}, | |||||
fatal: () => {}, | |||||
trace: () => {}, | |||||
silent: () => {}, | |||||
} as Logger; | |||||
const logger = typeof loggerFn === 'undefined' ? defaultLogger : (typeof loggerFn === 'function' ? loggerFn : loggerBooleanFn); | |||||
let exited = false; | let exited = false; | ||||
const returnCode = await handlerFn({ | const returnCode = await handlerFn({ | ||||
self, | self, | ||||
interactive, | interactive, | ||||
logger: thisCli.generateLogger(), | |||||
logger, | |||||
send: (code: number) => { | send: (code: number) => { | ||||
exited = true; | exited = true; | ||||
thisCli.exit(code); | thisCli.exit(code); | ||||
@@ -0,0 +1,19 @@ | |||||
import {WriteStream} from 'tty'; | |||||
export class DummyWriteStream extends WriteStream { | |||||
private bufferInternal = Buffer.from(''); | |||||
constructor() { | |||||
super(0); | |||||
} | |||||
write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean { | |||||
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]); | |||||
// noop | |||||
return true; | |||||
} | |||||
get buffer(): Buffer { | |||||
return this.bufferInternal; | |||||
} | |||||
} |
@@ -1,7 +1,8 @@ | |||||
import { describe, it, expect, vi, beforeAll } from 'vitest'; | import { describe, it, expect, vi, beforeAll } from 'vitest'; | ||||
import { Cli, createCli } from '../src/cli'; | |||||
import { createCli } from '../src/cli'; | |||||
import { addCommands } from '../src/commands'; | import { addCommands } from '../src/commands'; | ||||
import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder'; | import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder'; | ||||
import { Cli } from '../src/packages/cli-wrapper'; | |||||
vi.mock('process'); | vi.mock('process'); | ||||
@@ -10,6 +11,7 @@ describe('blah', () => { | |||||
beforeAll(() => { | beforeAll(() => { | ||||
cli = createCli({ | cli = createCli({ | ||||
name: 'cli-test', | name: 'cli-test', | ||||
logger: false, | |||||
}); | }); | ||||
addCommands(cli); | addCommands(cli); | ||||
}); | }); | ||||
@@ -9,6 +9,7 @@ import { constants } from 'http2'; | |||||
export interface AdderController { | export interface AdderController { | ||||
addNumbers: RouteHandlerMethod; | addNumbers: RouteHandlerMethod; | ||||
subtractNumbers: RouteHandlerMethod; | |||||
} | } | ||||
export class AdderControllerImpl implements AdderController { | export class AdderControllerImpl implements AdderController { | ||||
@@ -21,7 +22,27 @@ export class AdderControllerImpl implements AdderController { | |||||
readonly addNumbers: RouteHandlerMethod = async (request, reply) => { | readonly addNumbers: RouteHandlerMethod = async (request, reply) => { | ||||
const { a, b } = request.body as { a: number; b: number }; | const { a, b } = request.body as { a: number; b: number }; | ||||
try { | try { | ||||
const response = this.adderService.addNumbers({a, b}); | |||||
const response = this.adderService.addNumbers({ a, b }); | |||||
reply.send(response); | |||||
} catch (errorRaw) { | |||||
if (errorRaw instanceof InvalidArgumentTypeError) { | |||||
request.log.info(errorRaw); | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
if (errorRaw instanceof ArgumentOutOfRangeError) { | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
const error = errorRaw as Error; | |||||
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message); | |||||
} | |||||
} | |||||
readonly subtractNumbers: RouteHandlerMethod = async (request, reply) => { | |||||
const { a, b } = request.body as { a: number; b: number }; | |||||
try { | |||||
const response = this.adderService.addNumbers({ a, b: -b }); | |||||
reply.send(response); | reply.send(response); | ||||
} catch (errorRaw) { | } catch (errorRaw) { | ||||
if (errorRaw instanceof InvalidArgumentTypeError) { | if (errorRaw instanceof InvalidArgumentTypeError) { | ||||
@@ -7,7 +7,30 @@ export const addRoutes = (server: FastifyInstance) => { | |||||
return server | return server | ||||
.route({ | .route({ | ||||
method: 'POST', | method: 'POST', | ||||
url: '/', | |||||
url: '/add', | |||||
schema: { | |||||
body: { | |||||
type: 'object', | |||||
properties: { | |||||
a: { type: 'number' }, | |||||
b: { type: 'number' }, | |||||
} | |||||
} | |||||
}, | |||||
handler: adderController.addNumbers, | handler: adderController.addNumbers, | ||||
}) | |||||
.route({ | |||||
method: 'POST', | |||||
url: '/subtract', | |||||
schema: { | |||||
body: { | |||||
type: 'object', | |||||
properties: { | |||||
a: { type: 'number' }, | |||||
b: { type: 'number' }, | |||||
} | |||||
} | |||||
}, | |||||
handler: adderController.subtractNumbers, | |||||
}); | }); | ||||
}; | }; |
@@ -14,6 +14,9 @@ importers: | |||||
'@modal-sh/core': | '@modal-sh/core': | ||||
specifier: workspace:* | specifier: workspace:* | ||||
version: link:../core | version: link:../core | ||||
pino: | |||||
specifier: ^8.14.1 | |||||
version: 8.14.1 | |||||
yargs: | yargs: | ||||
specifier: ^17.7.2 | specifier: ^17.7.2 | ||||
version: 17.7.2 | version: 17.7.2 | ||||