summaryrefslogtreecommitdiff
path: root/u
diff options
context:
space:
mode:
authorElizabeth Hunt <lizhunt@amazon.com>2025-05-13 18:58:45 -0700
committerElizabeth Hunt <lizhunt@amazon.com>2025-05-13 18:58:54 -0700
commit1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11 (patch)
tree07073c060b61688e4635fd4658315cc683589d3d /u
parent2543ac8b11af11f034836591046cdb52911f9403 (diff)
downloadci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.tar.gz
ci-1d66a0f58e4ebcdf4f42c9d78f82a1ab49a2cf11.zip
snapshot!
Diffstat (limited to 'u')
-rw-r--r--u/deno.json6
-rw-r--r--u/fn/callable.ts15
-rw-r--r--u/fn/either.ts62
-rw-r--r--u/fn/mod.ts2
-rw-r--r--u/leftpadesque/debug.ts11
-rw-r--r--u/leftpadesque/mod.ts3
-rw-r--r--u/leftpadesque/object.ts2
-rw-r--r--u/leftpadesque/prepend.ts4
-rw-r--r--u/mod.ts4
-rw-r--r--u/process/env.ts10
-rw-r--r--u/process/mod.ts3
-rw-r--r--u/process/run.ts40
-rw-r--r--u/process/validate_identifier.ts17
-rw-r--r--u/server/mod.ts0
-rw-r--r--u/trace/itrace.ts72
-rw-r--r--u/trace/logger.ts86
-rw-r--r--u/trace/mod.ts3
-rw-r--r--u/trace/trace.ts34
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;
+ }
+}