From 2e7f4833a7a5c852607f1fe2fcd61ad89861fcbc Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Mon, 1 Apr 2024 18:01:17 -0500 Subject: [PATCH] documentation, some basic testing --- .npmignore | 1 + README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- pkg/async.ts | 2 +- pkg/csv.ts | 18 ++++++++++--- pkg/dom.ts | 22 +++++++++++++++ pkg/logger.ts | 0 pkg/validators.ts | 17 ++++++++---- tests/csv.test.ts | 3 +++ tests/obj.test.ts | 62 ++++++++++++++++++++++++++++++++++++++++++ types/index.ts | 1 - 11 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 .npmignore create mode 100644 README.md create mode 100644 pkg/logger.ts create mode 100644 tests/csv.test.ts create mode 100644 tests/obj.test.ts delete mode 100644 types/index.ts diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..2b29f27 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2df60d --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Utility Closet + +I've collected a few personal favorite problem-solvers that I reuse often. They tend to solve common problems that I +encounter across domains, and often gravitate towards object- and type-wrangling. + +## Installation + +```bash +npm install utility-closet +pnpm install utility-closet +bun install utility-closet +``` + +## Examples + +Quick and easy object manipulation: + +```ts +import { excludeFromObject, pickFromObject } from "utility-closet/obj"; + +const starting = { + one: "one", + two: "two" +} + +const result = excludeFromObject(starting, 'one') satisfies Exclude; +const otherResult = pickFromObject(starting, 'one') satisfies Pick; +``` + +Moving fast in React: + +```tsx +import type { PickElementProps, ElementProps } from "utility-closet/dom" + +type ButtonProps = PickElementProps<'button', 'onClick' | 'className'>; + +function Button({ onClick, className }: ButtonProps) { + return ( + + ) +} + +// or if you want to skip some more steps: +type LinkProps = PickElementProps<'a', 'className' | 'href' | 'target' | 'rel'>; +function Link(props: LinkProps) { + return Link +} + +// this also pairs well with your own generics +type SVGProps< + TProps extends ElementProps<'svg'> = ElementProps<'svg'> +> = PickElementProps<'svg', TProps>; + +function FirstSVG(props: SVGProps<'onClick'>) { + return +} + +function SecondSVG(props: SVGProps<'className'>) { + return +} + +// assigning a default value to the type parameter allows us to optionally reference an element's full subset of props +function UnboundedSVG(props: SVGProps) { + return +} + +``` diff --git a/package.json b/package.json index af87a72..f8ec480 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest" }, "keywords": [], "author": "", diff --git a/pkg/async.ts b/pkg/async.ts index a85156b..bfd1de8 100644 --- a/pkg/async.ts +++ b/pkg/async.ts @@ -1,4 +1,4 @@ -export async function promiseAllSafe(tasks: Promise[]) { +export async function promiseAllOptimistic(tasks: Promise[]) { return await Promise.allSettled(tasks) .then(res => { const fulfilled: NonNullable[] = []; diff --git a/pkg/csv.ts b/pkg/csv.ts index 3ffb14a..f739422 100644 --- a/pkg/csv.ts +++ b/pkg/csv.ts @@ -1,6 +1,15 @@ import { Options, parse, Parser } from "csv-parse"; import { z } from "zod"; +/** + * Converts raw CSV data into a validated array of entries of a given type, + * specified by a Zod validator provided in the function parameters. + * + * @param text a raw CSV text entry containing the data you wish to read + * @param validator a Zod schema representing the type of the target object + * @param options optional configuration options for the CSV parser + * @returns an array of validated + */ export async function readCSVToType< TData extends Record >( @@ -18,10 +27,13 @@ export async function readCSVToType< const parser = parse(text, options); const records: TData[] = []; + // the type of the iterable is irrelevant, as it will be asserted by the Zod schema for await (const record of parser as (Parser & AsyncIterable)) { - records.push( - validator.parse(record) - ) + try { + records.push(validator.parse(record)) + } catch (e) { + console.error(e); + } } return records; diff --git a/pkg/dom.ts b/pkg/dom.ts index dd92b35..3e6e284 100644 --- a/pkg/dom.ts +++ b/pkg/dom.ts @@ -1,10 +1,32 @@ import React from 'react'; +/** + * Given an JSX element type `TElement`, returns a subset of its props + * specified in the union `TProps`. + */ export type PickElementProps< TElement extends keyof React.JSX.IntrinsicElements, TProps extends keyof React.ComponentProps > = Pick, TProps>; +/** + * Given an JSX element type `TElement`, returns its full set of props, + * excluding members of the union `TProps`. + */ +export type ExcludeElementProps< + TElement extends keyof React.JSX.IntrinsicElements, + TProps extends keyof React.ComponentProps +> = Exclude, TProps>; + +export type ElementProps = React.ComponentProps; + +/** + * Mounts a virtual tag and virtually clicks it, intiating a download + * on the client's device. + * + * @param url the URL location of the desired resource + * @param filename the name to assign to the download once completed + */ export function clickVirtualDownloadLink( url: string, filename: string, diff --git a/pkg/logger.ts b/pkg/logger.ts new file mode 100644 index 0000000..e69de29 diff --git a/pkg/validators.ts b/pkg/validators.ts index 55dfe15..7984bb6 100644 --- a/pkg/validators.ts +++ b/pkg/validators.ts @@ -1,7 +1,14 @@ -export function must( - evaluation: T, - errorMessage = "Failed to fulfill requirements for function" -): NonNullable | never { - if (!evaluation) throw new Error(errorMessage); +/** + * Assert that a given value, @param evaluation, is truthy. @returns the evaluation, asserted as non-nullable. + */ + +import { Callable } from "./types"; + +export function must(evaluation: T, callback?: Callable): NonNullable | never { + if (!evaluation) { + if (!callback) throw new Error("Assertion failed: value is falsy"); + return callback(); + } + return evaluation; } diff --git a/tests/csv.test.ts b/tests/csv.test.ts new file mode 100644 index 0000000..b2e032b --- /dev/null +++ b/tests/csv.test.ts @@ -0,0 +1,3 @@ +import { describe, assert, it } from "vitest"; + + diff --git a/tests/obj.test.ts b/tests/obj.test.ts new file mode 100644 index 0000000..5f5ebdf --- /dev/null +++ b/tests/obj.test.ts @@ -0,0 +1,62 @@ +import { describe, it, assert } from 'vitest'; +import Obj from '../pkg/obj'; + +describe("obj", () => { + describe("exclude", () => { + it("should exclude properties from an object", () => { + const obj = { + a: 1, + b: 2, + c: 3, + }; + + const result = Obj.exclude(obj, 'a', 'c'); + + assert(result.a === undefined); + assert(result.b === 2); + assert(result.c === undefined); + }); + }) + + describe('pick', () => { + it('should pick properties from an object', () => { + const obj = { + a: 1, + b: 2, + c: 3, + }; + + const result = Obj.pick(obj, 'a', 'c'); + + assert(result.a === 1); + assert(result.b === undefined); + assert(result.c === 3); + }); + }) + + describe('hasAllKeys', () => { + it('should return true if all keys are present', () => { + const obj = { + a: 1, + b: 2, + c: 3, + }; + + const result = Obj.hasAllKeys(obj, 'a', 'b', 'c'); + + assert(result === true); + }); + + it('should return false if any key is missing', () => { + const obj = { + a: 1, + b: 2, + }; + + // @ts-expect-error + const result = Obj.hasAllKeys(obj, 'a', 'b', 'c'); + + assert(result === false); + }); + }) +}) diff --git a/types/index.ts b/types/index.ts deleted file mode 100644 index 8b13789..0000000 --- a/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -