diff options
Diffstat (limited to 'u')
-rw-r--r-- | u/deno.json | 2 | ||||
-rw-r--r-- | u/fn/either.ts | 4 | ||||
-rw-r--r-- | u/mod.ts | 1 | ||||
-rw-r--r-- | u/process/env.ts | 2 | ||||
-rw-r--r-- | u/process/run.ts | 97 | ||||
-rw-r--r-- | u/process/validate_identifier.ts | 19 | ||||
-rw-r--r-- | u/server/activity/health.ts | 39 | ||||
-rw-r--r-- | u/server/activity/mod.ts | 8 | ||||
-rw-r--r-- | u/server/filter/json.ts | 32 | ||||
-rw-r--r-- | u/server/filter/method.ts | 37 | ||||
-rw-r--r-- | u/server/filter/mod.ts | 13 | ||||
-rw-r--r-- | u/server/mod.ts | 1 | ||||
-rw-r--r-- | u/trace/itrace.ts | 68 | ||||
-rw-r--r-- | u/trace/logger.ts | 67 | ||||
-rw-r--r-- | u/trace/trace.ts | 40 |
15 files changed, 320 insertions, 110 deletions
diff --git a/u/deno.json b/u/deno.json index 46e74d6..26b08bf 100644 --- a/u/deno.json +++ b/u/deno.json @@ -1,5 +1,5 @@ { - "name": "@emprespresso/utils", + "name": "@emprespresso/pengueno", "version": "0.1.0", "exports": "./mod.ts", "workspace": ["./*"] diff --git a/u/fn/either.ts b/u/fn/either.ts index eaf77fd..12240d0 100644 --- a/u/fn/either.ts +++ b/u/fn/either.ts @@ -1,4 +1,4 @@ -import type { Mapper, Supplier } from "./mod.ts"; +import type { Mapper, Supplier } from "@emprespresso/pengueno"; export interface IEither<E, T> { mapBoth: <Ee, Tt>( @@ -21,7 +21,7 @@ export class Either<E, T> implements IEither<E, T> { return Either.right(okBranch(this.ok!)); } - public flatMap<Tt>(mapper: Mapper<T, Either<E, Tt>>) { + public flatMap<Tt>(mapper: Mapper<T, Either<E, Tt>>): Either<E, Tt> { if (this.ok) return mapper(this.ok); return Either.left<E, Tt>(this.err!); } @@ -2,3 +2,4 @@ export * from "./fn/mod.ts"; export * from "./leftpadesque/mod.ts"; export * from "./process/mod.ts"; export * from "./trace/mod.ts"; +export * from "./server/mod.ts"; diff --git a/u/process/env.ts b/u/process/env.ts index c3ae800..26e1158 100644 --- a/u/process/env.ts +++ b/u/process/env.ts @@ -1,4 +1,4 @@ -import { Either, type IEither } from "@emprespresso/utils"; +import { Either, type IEither } from "@emprespresso/pengueno"; export const getRequiredEnv = (name: string): IEither<Error, string> => Either diff --git a/u/process/run.ts b/u/process/run.ts index 6dc37d0..670f567 100644 --- a/u/process/run.ts +++ b/u/process/run.ts @@ -1,40 +1,63 @@ -import { Either, type Traceable } from "@emprespresso/utils"; +import { + Either, + type IEither, + type ITraceable, + LogLevel, + TraceUtil, +} from "@emprespresso/pengueno"; + +type Command = string[] | string; +type CommandOutputDecoded = { + code: number; + stdoutText: string; + stderrText: string; +}; export class ProcessError extends Error {} -export const getStdout = async ( - { item: cmd, logger: _logger }: Traceable<string[] | string>, +export const getStdout = ( + c: ITraceable<Command>, options: Deno.CommandOptions = {}, -): Promise<Either<ProcessError, string>> => { - const logger = _logger.addTracer(() => "[getStdout]"); - - logger.log(`:> im gonna run this command!`, cmd); - const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd; - const command = new Deno.Command(exec, { - args, - stdout: "piped", - stderr: "piped", - ...options, - }); - - try { - const { code, stdout, stderr } = await command.output(); - const stdoutText = new TextDecoder().decode(stdout); - const stderrText = new TextDecoder().decode(stderr); - - if (code !== 0) { - logger.error(`i weceived an exit code of ${code} i wanna zeroooo :<`); - return Either.left<ProcessError, string>( - new ProcessError(`command failed\n${stderrText}`), - ); - } - - logger.log("yay! i got code 0 :3", cmd); - return Either.right<ProcessError, string>(stdoutText); - } catch (e) { - logger.error(`o.o wat`, e); - if (e instanceof Error) { - return Either.left<ProcessError, string>(e); - } - throw new Error("unknown error " + e); - } -}; +): Promise<IEither<ProcessError, string>> => + c.bimap(TraceUtil.withFunctionTrace(getStdout)) + .map(({ item: cmd, trace }) => { + trace.trace(`:> im gonna run this command! ${cmd}`); + const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd; + return new Deno.Command(exec, { + args, + stdout: "piped", + stderr: "piped", + ...options, + }).output(); + }) + .map(({ item: p }) => + Either.fromFailableAsync<Error, Deno.CommandOutput>(p) + ) + .map( + TraceUtil.promiseify(({ item: eitherOutput, trace }) => + eitherOutput.flatMap(({ code, stderr, stdout }) => + Either + .fromFailable<Error, CommandOutputDecoded>(() => { + const stdoutText = new TextDecoder().decode(stdout); + const stderrText = new TextDecoder().decode(stderr); + return { code, stdoutText, stderrText }; + }) + .mapLeft((e) => { + trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`); + return new ProcessError(`${e}`); + }) + .flatMap((decodedOutput): Either<ProcessError, string> => { + const { code, stdoutText, stderrText } = decodedOutput; + trace.addTrace(LogLevel.DEBUG).trace( + `stderr hehehe ${stderrText}`, + ); + if (code !== 0) { + const msg = + `i weceived an exit code of ${code} i wanna zewoooo :<`; + trace.addTrace(LogLevel.ERROR).trace(msg); + return Either.left(new ProcessError(msg)); + } + return Either.right(stdoutText); + }) + ) + ), + ).item; diff --git a/u/process/validate_identifier.ts b/u/process/validate_identifier.ts index ec8b77b..32952a6 100644 --- a/u/process/validate_identifier.ts +++ b/u/process/validate_identifier.ts @@ -1,4 +1,4 @@ -import { Either } from "./mod.ts"; +import { Either, type IEither } from "@emprespresso/pengueno"; export const validateIdentifier = (token: string) => { return (/^[a-zA-Z0-9_\-:. \/]+$/).test(token) && !token.includes(".."); @@ -6,11 +6,18 @@ export const validateIdentifier = (token: string) => { // ensure {@param obj} is a Record<string, string> with stuff that won't // have the potential for shell injection, just to be super safe. -export const validateExecutionEntries = ( - obj: Record<string, unknown>, -): Either<Array<[string, unknown]>, Record<string, string>> => { - const invalidEntries = Object.entries(obj).filter((e) => - !e.every((x) => typeof x === "string" && validateIdentifier(x)) +type InvalidEntry<K, T> = [K, T]; +export const validateExecutionEntries = < + T, + K extends symbol | number | string = symbol | number | string, +>( + obj: Record<K, T>, +): IEither< + Array<InvalidEntry<K, T>>, + Record<string, string> +> => { + const invalidEntries = <Array<InvalidEntry<K, T>>> Object.entries(obj).filter( + (e) => !e.every((x) => typeof x === "string" && validateIdentifier(x)), ); if (invalidEntries.length > 0) return Either.left(invalidEntries); return Either.right(<Record<string, string>> obj); diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts new file mode 100644 index 0000000..bf1f52c --- /dev/null +++ b/u/server/activity/health.ts @@ -0,0 +1,39 @@ +import { + Either, + getRequiredEnv, + getStdout, + type ITraceable, + LogLevel, + type Mapper, + TraceUtil, +} from "@emprespresso/pengueno"; + +type HealthCheckInput = "healthy?"; +type HealthCheckOutput = "healthy!"; + +const HealthCheckActivity = <Trace>( + check: Mapper< + ITraceable<HealthCheckInput, Trace>, + Promise<Either<Error, HealthCheckOutput>> + >, +) => +(req: ITraceable<Request, Trace>) => + req.bimap(TraceUtil.withFunctionTrace(HealthCheckActivity)) + .flatMap((r) => r.move(<HealthCheckInput> "healthy?")) + .map(check) + .map(TraceUtil.promiseify(({ item: health, trace }) => { + health.mapBoth((e) => { + trace.addTrace(LogLevel.ERROR).trace(`${e}`); + return new Response( + "oh no, i need to eat more vegetables (。•́︿•̀。)...\n", + { status: 500 }, + ); + }, (_healthy) => { + const msg = `think im healthy!! (✿˘◡˘) ready to do work~`; + trace.trace(msg); + return new Response( + msg + "\n", + { status: 200 }, + ); + }); + })); diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts new file mode 100644 index 0000000..6908c26 --- /dev/null +++ b/u/server/activity/mod.ts @@ -0,0 +1,8 @@ +import type { RequestFilter } from "@emprespresso/pengueno"; + +export class r200 extends Response { + public override readonly status = 200; +} + +export interface IActivity<Trace> extends RequestFilter<r200, Trace> { +} diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts new file mode 100644 index 0000000..3f11915 --- /dev/null +++ b/u/server/filter/json.ts @@ -0,0 +1,32 @@ +import { + Either, + type IEither, + type ITraceable, + LogLevel, + type RequestFilter, + TraceUtil, +} from "@emprespresso/pengueno"; + +type JsonTransformer<JsonT, R> = ( + json: ITraceable<JsonT>, +) => IEither<Error, R>; +export const json = <BodyT, Trace, JsonT = unknown>( + jsonTransformer: JsonTransformer<JsonT, BodyT>, +): RequestFilter<BodyT, Trace> => +(r: ITraceable<Request, Trace>) => + r.bimap(TraceUtil.withFunctionTrace(json)) + .map(({ item: request }) => Either.fromFailableAsync(request.json())) + .map( + TraceUtil.promiseify(({ item: eitherJson, trace }) => + eitherJson.mapLeft((errReason) => { + trace.addTrace(LogLevel.WARN).trace(`${errReason}`); + const err = "seems to be invalid JSON (>//<) can you fix?"; + return new Error(err); + }) + .flatMap(jsonTransformer) + .mapLeft((err) => { + trace.addTrace(LogLevel.WARN).trace(`${err}`); + return new Response(err.message, { status: 400 }); + }) + ), + ).item; diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts new file mode 100644 index 0000000..2bf45a0 --- /dev/null +++ b/u/server/filter/method.ts @@ -0,0 +1,37 @@ +import { + Either, + type ITraceable, + LogLevel, + type RequestFilter, + TraceUtil, +} from "@emprespresso/pengueno"; + +type HttpMethod = + | "POST" + | "GET" + | "HEAD" + | "PUT" + | "DELETE" + | "CONNECT" + | "OPTIONS" + | "TRACE" + | "PATCH"; + +export const requireMethod = + <Trace>(methods: Array<HttpMethod>): RequestFilter<HttpMethod, Trace> => + (req: ITraceable<Request, Trace>) => + req.bimap(TraceUtil.withFunctionTrace(requireMethod)) + .map(({ item }) => Promise.resolve(item)) + .map(TraceUtil.promiseify(({ item: request, trace }) => { + const { method: _method } = request; + const method = <HttpMethod> _method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + trace.addTrace(LogLevel.WARN).trace(msg); + return Either.left<Response, HttpMethod>( + new Response(msg + "\n", { status: 405 }), + ); + } + + return Either.right<Response, HttpMethod>(method); + })).item; diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts new file mode 100644 index 0000000..3256d35 --- /dev/null +++ b/u/server/filter/mod.ts @@ -0,0 +1,13 @@ +import type { IEither, ITraceable } from "@emprespresso/pengueno"; + +export interface RequestFilter< + T, + Trace, + RIn = ITraceable<Request, Trace>, + Err = Response, +> { + (req: RIn): Promise<IEither<Err, T>>; +} + +export * from "./method.ts"; +export * from "./json.ts"; diff --git a/u/server/mod.ts b/u/server/mod.ts index e69de29..556771d 100644 --- a/u/server/mod.ts +++ b/u/server/mod.ts @@ -0,0 +1 @@ +export * from "./filter/mod.ts"; diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts index b483067..b9b750d 100644 --- a/u/trace/itrace.ts +++ b/u/trace/itrace.ts @@ -1,71 +1,95 @@ -import { Mapper, SideEffect } from "../fn/mod.ts"; +import type { Mapper, SideEffect } from "@emprespresso/pengueno"; -export interface ITrace<TracingW> { - addTrace: Mapper<TracingW, ITrace<TracingW>>; - trace: SideEffect<TracingW>; +// the "thing" every Trace writer must "trace()" +type BaseTraceWith = string; +export type ITraceWith<T> = BaseTraceWith | T; +export interface ITrace<TraceWith> { + addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>; + trace: SideEffect<ITraceWith<TraceWith>>; } -export type ITraceableTuple<T, Trace> = [T, Trace]; -export type ITraceableMapper<T, Trace, U, W = ITraceable<T, Trace>> = ( +export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith]; +export type ITraceableMapper< + T, + U, + TraceWith, + W = ITraceable<T, TraceWith>, +> = ( w: W, ) => U; -export interface ITraceable<T, Trace> { +export interface ITraceable<T, Trace = BaseTraceWith> { readonly item: T; readonly trace: ITrace<Trace>; move<U>(u: U): ITraceable<U, Trace>; map: <U>( - mapper: ITraceableMapper<T, Trace, U>, + mapper: ITraceableMapper<T, U, Trace>, ) => ITraceable<U, Trace>; bimap: <U>( - mapper: ITraceableMapper<T, Trace, ITraceableTuple<U, Trace>>, + mapper: ITraceableMapper<T, ITraceableTuple<U, Trace>, Trace>, ) => ITraceable<U, Trace>; - peek: (peek: ITraceableMapper<T, Trace, void>) => ITraceable<T, Trace>; + peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; flatMap: <U>( - mapper: ITraceableMapper<T, Trace, ITraceable<U, Trace>>, + mapper: ITraceableMapper<T, ITraceable<U, Trace>, Trace>, ) => ITraceable<U, Trace>; flatMapAsync<U>( - mapper: ITraceableMapper<T, Trace, Promise<ITraceable<U, Trace>>>, + mapper: ITraceableMapper<T, Promise<ITraceable<U, Trace>>, Trace>, ): ITraceable<Promise<U>, Trace>; } -export class TraceableImpl<T, L> implements ITraceable<T, L> { +export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { protected constructor( readonly item: T, - readonly trace: ITrace<L>, + readonly trace: ITrace<TraceWith>, ) {} - public map<U>(mapper: ITraceableMapper<T, L, U>) { + public map<U>( + mapper: ITraceableMapper<T, U, TraceWith>, + ) { const result = mapper(this); return new TraceableImpl(result, this.trace); } public flatMap<U>( - mapper: ITraceableMapper<T, L, ITraceable<U, L>>, - ): ITraceable<U, L> { + mapper: ITraceableMapper< + T, + ITraceable<U, TraceWith>, + TraceWith + >, + ): ITraceable<U, TraceWith> { return mapper(this); } public flatMapAsync<U>( - mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, - ): ITraceable<Promise<U>, L> { + mapper: ITraceableMapper< + T, + Promise<ITraceable<U, TraceWith>>, + TraceWith + >, + ): ITraceable<Promise<U>, TraceWith> { return new TraceableImpl( mapper(this).then(({ item }) => item), this.trace, ); } - public peek(peek: ITraceableMapper<T, L, void>) { + public peek(peek: ITraceableMapper<T, void, TraceWith>) { peek(this); return this; } - public move<Tt>(t: Tt): ITraceable<Tt, L> { + public move<Tt>(t: Tt): ITraceable<Tt, TraceWith> { return this.map(() => t); } - public bimap<U>(mapper: ITraceableMapper<T, L, ITraceableTuple<U, L>>) { + public bimap<U>( + mapper: ITraceableMapper< + T, + ITraceableTuple<U, TraceWith>, + TraceWith + >, + ) { const [item, trace] = mapper(this); return new TraceableImpl(item, this.trace.addTrace(trace)); } diff --git a/u/trace/logger.ts b/u/trace/logger.ts index 79da367..4f3c856 100644 --- a/u/trace/logger.ts +++ b/u/trace/logger.ts @@ -1,10 +1,9 @@ import { isDebug, - isObject, type ITrace, type SideEffect, type Supplier, -} from "@emprespresso/utils"; +} from "@emprespresso/pengueno"; export interface ILogger { log: (...args: unknown[]) => void; @@ -12,74 +11,88 @@ export interface ILogger { warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; } -export type ILoggerLevel = "UNKNOWN" | "INFO" | "WARN" | "DEBUG" | "ERROR"; -const logLevelOrder: Array<ILoggerLevel> = ["DEBUG", "INFO", "WARN", "ERROR"]; +export enum LogLevel { + UNKNOWN = "UNKNOWN", + INFO = "INFO", + WARN = "WARN", + DEBUG = "DEBUG", + ERROR = "ERROR", +} +const logLevelOrder: Array<LogLevel> = [ + LogLevel.DEBUG, + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, +]; +export const isLogLevel = (l: string): l is LogLevel => + logLevelOrder.some((level) => <string> level === l); + const defaultAllowedLevels = () => [ - "UNKNOWN", - ...(isDebug() ? ["DEBUG"] : []), - "INFO", - "WARN", - "ERROR", - ] as Array<ILoggerLevel>; + LogLevel.UNKNOWN, + ...(isDebug() ? [LogLevel.DEBUG] : []), + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + ] as Array<LogLevel>; export const logWithLevel = ( logger: ILogger, - level: ILoggerLevel, + level: LogLevel, ): SideEffect<unknown> => { switch (level) { - case "UNKNOWN": - case "INFO": + case LogLevel.UNKNOWN: + case LogLevel.INFO: return logger.log; - case "DEBUG": + case LogLevel.DEBUG: return logger.debug; - case "WARN": + case LogLevel.WARN: return logger.warn; - case "ERROR": + case LogLevel.ERROR: return logger.error; } }; export const LoggerImpl = console; -export type LogTraceSupplier = string | Supplier<string> | { - level: ILoggerLevel; -}; +export type LogTraceSupplier = string | Supplier<string>; const foldTraces = (traces: Array<LogTraceSupplier>) => { const { line, level } = traces.reduce( (acc: { line: string; level: number }, t) => { - if (isObject(t) && "level" in t) { + const val = typeof t === "function" ? t() : t; + if (isLogLevel(val)) { return { ...acc, - level: Math.max(logLevelOrder.indexOf(t.level), acc.level), + level: Math.max(logLevelOrder.indexOf(val), acc.level), }; } const prefix = [ acc.line, - typeof t === "function" ? t() : t, + val, ].join(" "); return { ...acc, prefix }; }, { line: "", level: -1 }, ); - return { line, level: logLevelOrder[level] ?? "UNKNOWN" }; + return { line, level: logLevelOrder[level] ?? LogLevel.UNKNOWN }; }; const defaultTrace = () => `[${new Date().toISOString()}]`; export const LogTrace = ( logger: ILogger, traces: Array<LogTraceSupplier> = [defaultTrace], - allowedLevels: Supplier<Array<ILoggerLevel>> = defaultAllowedLevels, - defaultLevel: ILoggerLevel = "INFO", + allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels, + defaultLevel: LogLevel = LogLevel.INFO, ): ITrace<LogTraceSupplier> => { return { addTrace: (trace: LogTraceSupplier) => - LogTrace(logger, traces.concat(trace)), + LogTrace(logger, traces.concat(trace), allowedLevels, defaultLevel), trace: (trace: LogTraceSupplier) => { const { line, level: _level } = foldTraces(traces.concat(trace)); if (!allowedLevels().includes(_level)) return; - const level = _level === "UNKNOWN" ? defaultLevel : _level; + + const level = _level === LogLevel.UNKNOWN ? defaultLevel : _level; logWithLevel(logger, level)(`[${level}]${line}`); }, }; diff --git a/u/trace/trace.ts b/u/trace/trace.ts index 5d5c59b..1d3d2d8 100644 --- a/u/trace/trace.ts +++ b/u/trace/trace.ts @@ -1,31 +1,43 @@ -import type { Callable } from "@emprespresso/utils"; import { + type Callable, type ITraceableMapper, type ITraceableTuple, + LoggerImpl, + LogTrace, + type LogTraceSupplier, TraceableImpl, - TraceableLogger, -} from "./mod.ts"; +} from "@emprespresso/pengueno"; -export class Traceable<T> extends TraceableImpl<T, TraceableLogger> { +export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> { static from<T>(t: T) { - return new Traceable(t, new TraceableLogger()); + return new LogTraceable(t, LogTrace(LoggerImpl)); } +} - static withFunctionTrace<F extends Callable, T>( +export class TraceUtil { + static withFunctionTrace<F extends Callable, T, Trace>( f: F, - ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { - return (t) => [t.item, f.name]; + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace>, + Trace + > { + return (t) => [t.item, `[${f.name}]`]; } - static withClassTrace<C extends object, T>( + static withClassTrace<C extends object, T, Trace>( c: C, - ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { - return (t) => [t.item, c.constructor.name]; + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace>, + Trace + > { + return (t) => [t.item, `[${c.constructor.name}]`]; } - static promiseify<T, U>( - mapper: ITraceableMapper<T, TraceableLogger, U>, - ): ITraceableMapper<Promise<T>, TraceableLogger, Promise<U>> { + static promiseify<T, U, Trace>( + mapper: ITraceableMapper<T, U, Trace>, + ): ITraceableMapper<Promise<T>, Promise<U>, Trace> { return (traceablePromise) => traceablePromise.flatMapAsync(async (t) => t.move(await t.item).map(mapper) |