diff options
author | Elizabeth Hunt <lizhunt@amazon.com> | 2025-05-13 18:58:45 -0700 |
---|---|---|
committer | Elizabeth Hunt <lizhunt@amazon.com> | 2025-05-13 18:58:54 -0700 |
commit | 1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11 (patch) | |
tree | 07073c060b61688e4635fd4658315cc683589d3d /u | |
parent | 2543ac8b11af11f034836591046cdb52911f9403 (diff) | |
download | ci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.tar.gz ci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.zip |
snapshot!
Diffstat (limited to 'u')
-rw-r--r-- | u/deno.json | 6 | ||||
-rw-r--r-- | u/fn/callable.ts | 15 | ||||
-rw-r--r-- | u/fn/either.ts | 62 | ||||
-rw-r--r-- | u/fn/mod.ts | 2 | ||||
-rw-r--r-- | u/leftpadesque/debug.ts | 11 | ||||
-rw-r--r-- | u/leftpadesque/mod.ts | 3 | ||||
-rw-r--r-- | u/leftpadesque/object.ts | 2 | ||||
-rw-r--r-- | u/leftpadesque/prepend.ts | 4 | ||||
-rw-r--r-- | u/mod.ts | 4 | ||||
-rw-r--r-- | u/process/env.ts | 10 | ||||
-rw-r--r-- | u/process/mod.ts | 3 | ||||
-rw-r--r-- | u/process/run.ts | 40 | ||||
-rw-r--r-- | u/process/validate_identifier.ts | 17 | ||||
-rw-r--r-- | u/server/mod.ts | 0 | ||||
-rw-r--r-- | u/trace/itrace.ts | 72 | ||||
-rw-r--r-- | u/trace/logger.ts | 86 | ||||
-rw-r--r-- | u/trace/mod.ts | 3 | ||||
-rw-r--r-- | u/trace/trace.ts | 34 |
18 files changed, 374 insertions, 0 deletions
diff --git a/u/deno.json b/u/deno.json new file mode 100644 index 0000000..46e74d6 --- /dev/null +++ b/u/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@emprespresso/utils", + "version": "0.1.0", + "exports": "./mod.ts", + "workspace": ["./*"] +} diff --git a/u/fn/callable.ts b/u/fn/callable.ts new file mode 100644 index 0000000..2749947 --- /dev/null +++ b/u/fn/callable.ts @@ -0,0 +1,15 @@ +// deno-lint-ignore no-explicit-any +export interface Callable<T = any, ArgT = any> { + (...args: Array<ArgT>): T; +} + +export interface Supplier<T> extends Callable<T, undefined> { + (): T; +} + +export interface SideEffect<T> extends Callable<void, T> { +} + +export interface Mapper<T, U> extends Callable<U, T> { + (t: T): U; +} diff --git a/u/fn/either.ts b/u/fn/either.ts new file mode 100644 index 0000000..eaf77fd --- /dev/null +++ b/u/fn/either.ts @@ -0,0 +1,62 @@ +import type { Mapper, Supplier } from "./mod.ts"; + +export interface IEither<E, T> { + mapBoth: <Ee, Tt>( + errBranch: Mapper<E, Ee>, + okBranch: Mapper<T, Tt>, + ) => IEither<Ee, Tt>; + flatMap: <Tt>(mapper: Mapper<T, IEither<E, Tt>>) => IEither<E, Tt>; + mapRight: <Tt>(mapper: Mapper<T, Tt>) => IEither<E, Tt>; + mapLeft: <Ee>(mapper: Mapper<E, Ee>) => IEither<Ee, T>; +} + +export class Either<E, T> implements IEither<E, T> { + private constructor(private readonly err?: E, private readonly ok?: T) {} + + public mapBoth<Ee, Tt>( + errBranch: Mapper<E, Ee>, + okBranch: Mapper<T, Tt>, + ): Either<Ee, Tt> { + if (this.err) return Either.left(errBranch(this.err)); + return Either.right(okBranch(this.ok!)); + } + + public flatMap<Tt>(mapper: Mapper<T, Either<E, Tt>>) { + if (this.ok) return mapper(this.ok); + return Either.left<E, Tt>(this.err!); + } + + public mapRight<Tt>(mapper: Mapper<T, Tt>): IEither<E, Tt> { + if (this.ok) return Either.right(mapper(this.ok)); + return Either.left<E, Tt>(this.err!); + } + + public mapLeft<Ee>(mapper: Mapper<E, Ee>) { + if (this.err) return Either.left<Ee, T>(mapper(this.err)); + return Either.right<Ee, T>(this.ok!); + } + + static left<E, T>(e: E) { + return new Either<E, T>(e); + } + + static right<E, T>(t: T) { + return new Either<E, T>(undefined, t); + } + + static fromFailable<E, T>(s: Supplier<T>) { + try { + return Either.right<E, T>(s()); + } catch (e) { + return Either.left<E, T>(e as E); + } + } + + static async fromFailableAsync<E, T>(s: Promise<T>) { + try { + return Either.right<E, T>(await s); + } catch (e) { + return Either.left<E, T>(e as E); + } + } +} diff --git a/u/fn/mod.ts b/u/fn/mod.ts new file mode 100644 index 0000000..f0fbe88 --- /dev/null +++ b/u/fn/mod.ts @@ -0,0 +1,2 @@ +export * from "./callable.ts"; +export * from "./either.ts"; diff --git a/u/leftpadesque/debug.ts b/u/leftpadesque/debug.ts new file mode 100644 index 0000000..a9da1f3 --- /dev/null +++ b/u/leftpadesque/debug.ts @@ -0,0 +1,11 @@ +const _hasEnv = !Deno.permissions.querySync({ name: "env" }); + +const _env: "development" | "production" = + _hasEnv && (Deno.env.get("ENVIRONMENT") ?? "").toLowerCase().includes("prod") + ? "production" + : "development"; +export const isProd = () => _env === "production"; + +const _debug = !isProd() || (_hasEnv && + ["y", "t"].some((Deno.env.get("DEBUG") ?? "").toLowerCase().startsWith)); +export const isDebug = () => _debug; diff --git a/u/leftpadesque/mod.ts b/u/leftpadesque/mod.ts new file mode 100644 index 0000000..801846a --- /dev/null +++ b/u/leftpadesque/mod.ts @@ -0,0 +1,3 @@ +export * from "./object.ts"; +export * from "./prepend.ts"; +export * from "./debug.ts"; diff --git a/u/leftpadesque/object.ts b/u/leftpadesque/object.ts new file mode 100644 index 0000000..73f7f80 --- /dev/null +++ b/u/leftpadesque/object.ts @@ -0,0 +1,2 @@ +export const isObject = (o: unknown): o is object => + typeof o === "object" && !Array.isArray(o) && !!o; diff --git a/u/leftpadesque/prepend.ts b/u/leftpadesque/prepend.ts new file mode 100644 index 0000000..9b77aff --- /dev/null +++ b/u/leftpadesque/prepend.ts @@ -0,0 +1,4 @@ +export const prependWith = (arr: string[], prep: string) => + Array(arr.length * 2).fill(0) + .map((_, i) => i % 2 === 0) + .map((isPrep, i) => isPrep ? prep : arr[i]); diff --git a/u/mod.ts b/u/mod.ts new file mode 100644 index 0000000..fab6804 --- /dev/null +++ b/u/mod.ts @@ -0,0 +1,4 @@ +export * from "./fn/mod.ts"; +export * from "./leftpadesque/mod.ts"; +export * from "./process/mod.ts"; +export * from "./trace/mod.ts"; diff --git a/u/process/env.ts b/u/process/env.ts new file mode 100644 index 0000000..e80ec4a --- /dev/null +++ b/u/process/env.ts @@ -0,0 +1,10 @@ +import { Either, type IEither } from "@emprespresso/utils"; + +export const getRequiredEnv = (name: string): IEither<Error, string> => + Either.fromFailable(() => { + const value = Deno.env.get(name); // could throw when no permission. + if (!value) { + throw new Error(`environment variable "${name}" is required D:`); + } + return value; + }); diff --git a/u/process/mod.ts b/u/process/mod.ts new file mode 100644 index 0000000..3f02d46 --- /dev/null +++ b/u/process/mod.ts @@ -0,0 +1,3 @@ +export * from "./env.ts"; +export * from "./run.ts"; +export * from "./validate_identifier.ts"; diff --git a/u/process/run.ts b/u/process/run.ts new file mode 100644 index 0000000..6dc37d0 --- /dev/null +++ b/u/process/run.ts @@ -0,0 +1,40 @@ +import { Either, type Traceable } from "@emprespresso/utils"; + +export class ProcessError extends Error {} +export const getStdout = async ( + { item: cmd, logger: _logger }: Traceable<string[] | string>, + 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); + } +}; diff --git a/u/process/validate_identifier.ts b/u/process/validate_identifier.ts new file mode 100644 index 0000000..ec8b77b --- /dev/null +++ b/u/process/validate_identifier.ts @@ -0,0 +1,17 @@ +import { Either } from "./mod.ts"; + +export const validateIdentifier = (token: string) => { + return (/^[a-zA-Z0-9_\-:. \/]+$/).test(token) && !token.includes(".."); +}; + +// 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)) + ); + if (invalidEntries.length > 0) return Either.left(invalidEntries); + return Either.right(<Record<string, string>> obj); +}; diff --git a/u/server/mod.ts b/u/server/mod.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/u/server/mod.ts diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts new file mode 100644 index 0000000..b483067 --- /dev/null +++ b/u/trace/itrace.ts @@ -0,0 +1,72 @@ +import { Mapper, SideEffect } from "../fn/mod.ts"; + +export interface ITrace<TracingW> { + addTrace: Mapper<TracingW, ITrace<TracingW>>; + trace: SideEffect<TracingW>; +} + +export type ITraceableTuple<T, Trace> = [T, Trace]; +export type ITraceableMapper<T, Trace, U, W = ITraceable<T, Trace>> = ( + w: W, +) => U; + +export interface ITraceable<T, Trace> { + readonly item: T; + readonly trace: ITrace<Trace>; + + move<U>(u: U): ITraceable<U, Trace>; + map: <U>( + mapper: ITraceableMapper<T, Trace, U>, + ) => ITraceable<U, Trace>; + bimap: <U>( + mapper: ITraceableMapper<T, Trace, ITraceableTuple<U, Trace>>, + ) => ITraceable<U, Trace>; + peek: (peek: ITraceableMapper<T, Trace, void>) => ITraceable<T, Trace>; + flatMap: <U>( + mapper: ITraceableMapper<T, Trace, ITraceable<U, Trace>>, + ) => ITraceable<U, Trace>; + flatMapAsync<U>( + mapper: ITraceableMapper<T, Trace, Promise<ITraceable<U, Trace>>>, + ): ITraceable<Promise<U>, Trace>; +} + +export class TraceableImpl<T, L> implements ITraceable<T, L> { + protected constructor( + readonly item: T, + readonly trace: ITrace<L>, + ) {} + + public map<U>(mapper: ITraceableMapper<T, L, U>) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } + + public flatMap<U>( + mapper: ITraceableMapper<T, L, ITraceable<U, L>>, + ): ITraceable<U, L> { + return mapper(this); + } + + public flatMapAsync<U>( + mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, + ): ITraceable<Promise<U>, L> { + return new TraceableImpl( + mapper(this).then(({ item }) => item), + this.trace, + ); + } + + public peek(peek: ITraceableMapper<T, L, void>) { + peek(this); + return this; + } + + public move<Tt>(t: Tt): ITraceable<Tt, L> { + return this.map(() => t); + } + + public bimap<U>(mapper: ITraceableMapper<T, L, ITraceableTuple<U, L>>) { + 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 new file mode 100644 index 0000000..79da367 --- /dev/null +++ b/u/trace/logger.ts @@ -0,0 +1,86 @@ +import { + isDebug, + isObject, + type ITrace, + type SideEffect, + type Supplier, +} from "@emprespresso/utils"; + +export interface ILogger { + log: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} +export type ILoggerLevel = "UNKNOWN" | "INFO" | "WARN" | "DEBUG" | "ERROR"; +const logLevelOrder: Array<ILoggerLevel> = ["DEBUG", "INFO", "WARN", "ERROR"]; +const defaultAllowedLevels = () => + [ + "UNKNOWN", + ...(isDebug() ? ["DEBUG"] : []), + "INFO", + "WARN", + "ERROR", + ] as Array<ILoggerLevel>; + +export const logWithLevel = ( + logger: ILogger, + level: ILoggerLevel, +): SideEffect<unknown> => { + switch (level) { + case "UNKNOWN": + case "INFO": + return logger.log; + case "DEBUG": + return logger.debug; + case "WARN": + return logger.warn; + case "ERROR": + return logger.error; + } +}; + +export const LoggerImpl = console; + +export type LogTraceSupplier = string | Supplier<string> | { + level: ILoggerLevel; +}; + +const foldTraces = (traces: Array<LogTraceSupplier>) => { + const { line, level } = traces.reduce( + (acc: { line: string; level: number }, t) => { + if (isObject(t) && "level" in t) { + return { + ...acc, + level: Math.max(logLevelOrder.indexOf(t.level), acc.level), + }; + } + const prefix = [ + acc.line, + typeof t === "function" ? t() : t, + ].join(" "); + return { ...acc, prefix }; + }, + { line: "", level: -1 }, + ); + return { line, level: logLevelOrder[level] ?? "UNKNOWN" }; +}; + +const defaultTrace = () => `[${new Date().toISOString()}]`; +export const LogTrace = ( + logger: ILogger, + traces: Array<LogTraceSupplier> = [defaultTrace], + allowedLevels: Supplier<Array<ILoggerLevel>> = defaultAllowedLevels, + defaultLevel: ILoggerLevel = "INFO", +): ITrace<LogTraceSupplier> => { + return { + addTrace: (trace: LogTraceSupplier) => + LogTrace(logger, traces.concat(trace)), + trace: (trace: LogTraceSupplier) => { + const { line, level: _level } = foldTraces(traces.concat(trace)); + if (!allowedLevels().includes(_level)) return; + const level = _level === "UNKNOWN" ? defaultLevel : _level; + logWithLevel(logger, level)(`[${level}]${line}`); + }, + }; +}; diff --git a/u/trace/mod.ts b/u/trace/mod.ts new file mode 100644 index 0000000..9c42858 --- /dev/null +++ b/u/trace/mod.ts @@ -0,0 +1,3 @@ +export * from "./itrace.ts"; +export * from "./logger.ts"; +export * from "./trace.ts"; diff --git a/u/trace/trace.ts b/u/trace/trace.ts new file mode 100644 index 0000000..5d5c59b --- /dev/null +++ b/u/trace/trace.ts @@ -0,0 +1,34 @@ +import type { Callable } from "@emprespresso/utils"; +import { + type ITraceableMapper, + type ITraceableTuple, + TraceableImpl, + TraceableLogger, +} from "./mod.ts"; + +export class Traceable<T> extends TraceableImpl<T, TraceableLogger> { + static from<T>(t: T) { + return new Traceable(t, new TraceableLogger()); + } + + static withFunctionTrace<F extends Callable, T>( + f: F, + ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { + return (t) => [t.item, f.name]; + } + + static withClassTrace<C extends object, T>( + c: C, + ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { + return (t) => [t.item, c.constructor.name]; + } + + static promiseify<T, U>( + mapper: ITraceableMapper<T, TraceableLogger, U>, + ): ITraceableMapper<Promise<T>, TraceableLogger, Promise<U>> { + return (traceablePromise) => + traceablePromise.flatMapAsync(async (t) => + t.move(await t.item).map(mapper) + ).item; + } +} |