diff --git a/CHANGELOG.md b/CHANGELOG.md index e627575a..803528dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # mauss changelog +## 0.5.0 - Unreleased + +- ([#223](https://github.com/alchemauss/mauss/pull/223)) remove `create` from `ntv` namespace +- ([#222](https://github.com/alchemauss/mauss/pull/222)) remove `/utils` module + +### Breaking Changes + +- [#223](https://github.com/alchemauss/mauss/pull/223) | Removed `create` from `ntv` namespace, use array `.reduce` instead +- [#222](https://github.com/alchemauss/mauss/pull/222) | Removed `/utils` namespace + - `dt` has been moved to core module + - `random` has been moved to core module + - `tryNumber` has been removed, use `Number.isNaN(Number(s)) ? s : Number(s)` + +## 0.4.12 - 2023/05/02 + +- ([#224](https://github.com/alchemauss/mauss/pull/224)) add string guards +- ([#221](https://github.com/alchemauss/mauss/pull/221)) add `tsf` function to `/std` module + ## 0.4.11 - 2023/03/30 - ([#216](https://github.com/alchemauss/mauss/pull/216)) remove deprecated option for TS 5.0 diff --git a/package.json b/package.json index 27f299b8..8cad66a1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mauss", "author": "Ignatius Bagus", "description": "fast and efficient type-safe SDK", - "version": "0.4.11", + "version": "0.4.12", "license": "MIT", "type": "module", "types": "./core/index.d.ts", @@ -10,7 +10,7 @@ "clean": "git add * && git clean -dfx -e node_modules", "watch": "pnpm pub:build --watch", "check": "pnpm check:code && pnpm check:style", - "check:code": "tsc --noEmit --target ES2020", + "check:code": "pnpm pub:build --noEmit", "check:style": "prettier -c \"src/**/*.ts\"", "test": "pnpm test:unit", "test:unit": "uvu -r tsm src \"(spec\\.ts)\"", @@ -52,10 +52,10 @@ "settings" ], "devDependencies": { - "@types/node": "^18.15.11", - "prettier": "^2.8.7", + "@types/node": "^18.16.0", + "prettier": "^2.8.8", "tsm": "^2.3.0", - "typescript": "^5.0.2", + "typescript": "^5.0.4", "uvu": "^0.5.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d6c1268..1c3e4b54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,17 +2,17 @@ lockfileVersion: '6.0' devDependencies: '@types/node': - specifier: ^18.15.11 - version: 18.15.11 + specifier: ^18.16.0 + version: 18.16.0 prettier: - specifier: ^2.8.7 - version: 2.8.7 + specifier: ^2.8.8 + version: 2.8.8 tsm: specifier: ^2.3.0 version: 2.3.0 typescript: - specifier: ^5.0.2 - version: 5.0.2 + specifier: ^5.0.4 + version: 5.0.4 uvu: specifier: ^0.5.6 version: 0.5.6 @@ -37,8 +37,8 @@ packages: dev: true optional: true - /@types/node@18.15.11: - resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==} + /@types/node@18.16.0: + resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} dev: true /dequal@2.0.3: @@ -271,8 +271,8 @@ packages: engines: {node: '>=4'} dev: true - /prettier@2.8.7: - resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true dev: true @@ -292,8 +292,8 @@ packages: esbuild: 0.15.18 dev: true - /typescript@5.0.2: - resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} + /typescript@5.0.4: + resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} hasBin: true dev: true diff --git a/src/core/README.md b/src/core/README.md index 0ae77dbb..7522f106 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -90,6 +90,37 @@ dSearch('mauss'); // execute after 500ms tSearch('mauss'); // execute every 500ms ``` +## `dt` + +Simple `date/time` (`dt`) utility namespace. + +```ts +type DateValue = string | number | Date; + +interface BuildOptions { + base?: 'UTC'; +} + +interface TravelOptions { + /** relative point of reference to travel */ + from?: DateValue; + /** relative days to travel in number */ + to: number; +} + +export const dt: { + current(d?: DateValue): Date; + build(options: BuildOptions): (date?: DateValue) => (mask?: string) => string; + format: ReturnType; + travel({ from, to }: TravelOptions): Date; +} +``` + +- `dt.current` is a function `(d?: DateValue) => Date` that optionally takes in a `DateValue` to be converted into a `Date` object, `new Date()` will be used if nothing is passed +- `dt.build` is a function that accepts `BuildOptions` and builds a formatter, a convenience export is included with all the default options as `dt.format` +- `dt.format` is a function that takes in a `DateValue` and returns a renderer that accepts a string mask to format the date in, defaults to `'DDDD, DD MMMM YYYY'` +- `dt.travel` is a function `({ from, to }) => Date` that takes in a `{ from, to }` object with `from` property being optional + ## `execute` ```ts diff --git a/src/core/compare/index.spec.ts b/src/core/compare/index.spec.ts index aa2dbb14..51eaa01c 100644 --- a/src/core/compare/index.spec.ts +++ b/src/core/compare/index.spec.ts @@ -2,31 +2,26 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as compare from './index.js'; -const basics = { - inspect: suite('compare:inspect'), - wildcard: suite('compare:wildcard'), +const suites = { + 'inspect/': suite('compare/inspect'), + 'wildcard/': suite('compare/wildcard'), - undefined: suite('compare:undefined'), - boolean: suite('compare:boolean'), - number: suite('compare:number'), - bigint: suite('compare:bigint'), - symbol: suite('compare:symbol'), - string: suite('compare:string'), - object: suite('compare:object'), + 'undefined/': suite('compare/undefined'), + 'boolean/': suite('compare/boolean'), + 'number/': suite('compare/number'), + 'bigint/': suite('compare/bigint'), + 'symbol/': suite('compare/symbol'), + 'string/': suite('compare/string'), + 'object/': suite('compare/object'), - date: suite('compare:date'), - time: suite('compare:time'), + 'date/': suite('compare/date'), + 'time/': suite('compare/time'), - order: suite('compare:order'), -}; + 'order/': suite('compare/order'), + 'order/key': suite('compare/order:key'), +} as const; -const composite = { - order: suite('compare:key+order'), -}; - -// ---- standard ---- - -basics.inspect('inspect', () => { +suites['inspect/']('inspect', () => { assert.type(compare.inspect, 'function'); const data = [{ id: 0, name: 'B' }, { name: 'A' }, { id: 1, name: 'C' }]; @@ -37,25 +32,28 @@ basics.inspect('inspect', () => { ]); }); -basics.undefined('sort undefined values with null values above', () => { +suites['undefined/']('sort undefined values with null values above', () => { assert.equal( [undefined, 3, 0, null, 1, -1, undefined, -2, undefined, null].sort(compare.undefined), [3, 0, 1, -1, -2, null, null, undefined, undefined, undefined] ); }); -basics.boolean('sort boolean values with true above', () => { + +suites['boolean/']('sort boolean values with true above', () => { assert.equal( [true, false, true, false, true, false, true, false, true, false].sort(compare.boolean), [true, true, true, true, true, false, false, false, false, false] ); }); -basics.number('sort number in descending order', () => { + +suites['number/']('sort number in descending order', () => { assert.equal( [5, 3, 9, 6, 0, 2, 1, -1, 4, -2].sort(compare.number), [9, 6, 5, 4, 3, 2, 1, 0, -1, -2] ); }); -basics.string('sort string in alphabetical order', () => { + +suites['string/']('sort string in alphabetical order', () => { assert.equal( ['k', 'h', 'g', 'f', 'e', 'l', 'd', 'm', 'c', 'b', 'j', 'i', 'a'].sort(compare.string), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'] @@ -66,18 +64,14 @@ basics.string('sort string in alphabetical order', () => { ); }); -basics.order('customized compare with order', () => { +suites['order/']('customized compare with order', () => { const months = ['January', 'February', 'March', 'April', 'May', 'June']; const list = ['March', 'June', 'May', 'April', 'January', 'June', 'February']; const result = ['January', 'February', 'March', 'April', 'May', 'June', 'June']; assert.equal(list.sort(compare.order(months)), result); }); -Object.values(basics).forEach((v) => v.run()); - -// ---- composite ---- - -composite.order('nested keyed compare with order', () => { +suites['order/key']('nested keyed compare with order', () => { const months = ['January', 'February', 'March', 'April', 'May', 'June']; const posts = [ { date: { pub: { month: 'March' } } }, @@ -99,4 +93,4 @@ composite.order('nested keyed compare with order', () => { ]); }); -Object.values(composite).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/core/index.ts b/src/core/index.ts index 3972f8be..6c144da8 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,4 +3,5 @@ export * from './processor/index.js'; export * from './standard/index.js'; export * as compare from './compare/index.js'; -export { default as unique } from './standard/unique.js'; +export * as random from './random/index.js'; +export * as dt from './temporal/index.js'; diff --git a/src/core/lambda/index.spec.ts b/src/core/lambda/index.spec.ts index 50c7ef77..29af0528 100644 --- a/src/core/lambda/index.spec.ts +++ b/src/core/lambda/index.spec.ts @@ -2,16 +2,13 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as lambda from './index.js'; -const basics = { - curry: suite('lambda:curry'), - pipe: suite('lambda:pipe'), +const suites = { + 'curry/': suite('lambda/curry'), + 'pipe/': suite('lambda/pipe'), + 'masked/reveal': suite('lambda/masked:reveal'), }; -const composite = { - masked: suite('lambda:mask+reveal'), -}; - -basics.curry('properly curry a function', () => { +suites['curry/']('properly curry a function', () => { const sum = (a: number, b: number, c: number) => a + b + c; const curried = lambda.curry(sum); @@ -22,7 +19,7 @@ basics.curry('properly curry a function', () => { assert.equal(curried(1)(1)(1), 3); }); -basics.pipe('properly apply functions in ltr order', () => { +suites['pipe/']('properly apply functions in ltr order', () => { const cap = (v: string) => v.toUpperCase(); const name = (v: T) => v.name; const split = (v: string) => v.split(''); @@ -31,11 +28,7 @@ basics.pipe('properly apply functions in ltr order', () => { assert.equal(pipeline({ name: 'mom' }), ['M', 'O', 'M']); }); -Object.values(basics).forEach((v) => v.run()); - -// ---- composite ---- - -composite.masked('properly mask and reveal a value', () => { +suites['masked/reveal']('properly mask and reveal a value', () => { const { mask, reveal } = lambda; const answer = mask.of(() => 42); @@ -49,4 +42,4 @@ composite.masked('properly mask and reveal a value', () => { assert.equal(reveal(wrapped).expect('unreachable'), '2023-04-06'); }); -Object.values(composite).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/utils/random/index.spec.ts b/src/core/random/index.spec.ts similarity index 58% rename from src/utils/random/index.spec.ts rename to src/core/random/index.spec.ts index 7fb01ffe..2002e1a2 100644 --- a/src/utils/random/index.spec.ts +++ b/src/core/random/index.spec.ts @@ -3,55 +3,53 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as random from './index.js'; -const basics = { - float: suite('random:float'), - int: suite('random:int'), - bool: suite('random:bool'), - array: suite('random:array'), - key: suite('random:key'), - val: suite('random:val'), - // hex: suite('random:hex'), - // ipv4: suite('random:ipv4'), - uuid: suite('random:uuid'), +const suites = { + 'float/': suite('random/float'), + 'int/': suite('random/int'), + 'bool/': suite('random/bool'), + 'array/': suite('random/array'), + 'key/': suite('random/key'), + 'val/': suite('random/val'), + // 'hex/': suite('random/hex'), + // 'ipv4/': suite('random/ipv4'), + 'uuid/': suite('random/uuid'), }; -// ---- standard ---- - -basics.float('generate random float', () => { +suites['float/']('generate random float', () => { const number = random.float(); assert.type(number, 'number'); assert.ok(number >= 0 && number <= 1); }); -basics.int('generate random integer', () => { +suites['int/']('generate random integer', () => { const number = random.int(); assert.type(number, 'number'); assert.ok(number === 0 || number === 1); }); -basics.bool('generate random bool', () => { +suites['bool/']('generate random bool', () => { assert.type(random.bool(), 'boolean'); }); -basics.array('generate array with random values', () => { +suites['array/']('generate array with random values', () => { const array = random.array(5, 3); assert.type(array, 'object'); assert.equal(array.length, 5); }); -basics.key('get random key from object', () => { +suites['key/']('get random key from object', () => { const key = random.key({ foo: 0, bar: 1 }); assert.type(key, 'string'); assert.ok(key === 'foo' || key === 'bar'); }); -basics.val('get random value from object', () => { +suites['val/']('get random value from object', () => { const val = random.val({ foo: 0, bar: 1 }); assert.type(val, 'number'); assert.ok(val === 0 || val === 1); }); -basics.uuid('generate random uuid', () => { +suites['uuid/']('generate random uuid', () => { const floating = random.uuid(); assert.equal(floating.length, 36); assert.equal(floating.split('-').length, 5); @@ -61,4 +59,4 @@ basics.uuid('generate random uuid', () => { assert.equal(secure.split('-').length, 5); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/utils/random/index.ts b/src/core/random/index.ts similarity index 100% rename from src/utils/random/index.ts rename to src/core/random/index.ts diff --git a/src/core/standard/index.spec.ts b/src/core/standard/index.spec.ts index 5bfe1696..e2ed5eb7 100644 --- a/src/core/standard/index.spec.ts +++ b/src/core/standard/index.spec.ts @@ -2,11 +2,19 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as std from './index.js'; -const basics = { - identical: suite('std:identical'), +const suites = { + 'capitalize/': suite('std/capitalize'), + 'identical/': suite('std/identical'), }; -basics.identical('identical primitive checks', () => { +suites['capitalize/']('change one letter for one word', () => { + assert.equal(std.capitalize('hello'), 'Hello'); +}); +suites['capitalize/']('change two letter for two words', () => { + assert.equal(std.capitalize('hello world'), 'Hello World'); +}); + +suites['identical/']('identical primitive checks', () => { // boolean assert.ok(std.identical(true, true)); assert.ok(!std.identical(true, false)); @@ -47,22 +55,22 @@ basics.identical('identical primitive checks', () => { ) ); }); -basics.identical('identical array checks', () => { +suites['identical/']('identical array checks', () => { assert.ok(std.identical([], [])); assert.ok(std.identical(['', 1, !0], ['', 1, !0])); assert.ok(std.identical([{ x: [] }], [{ x: [] }])); assert.ok(!std.identical(['', 0, !0], ['', 1, !1])); assert.ok(!std.identical([{ x: [] }], [{ y: [] }])); }); -basics.identical('identical object checks', () => { +suites['identical/']('identical object checks', () => { assert.ok(std.identical({}, {})); assert.ok(std.identical({ a: '', b: 1, c: !0 }, { a: '', b: 1, c: !0 })); assert.ok(std.identical({ x: [{}], y: { a: 0 } }, { x: [{}], y: { a: 0 } })); }); -basics.identical('identical clone', async () => { +suites['identical/']('identical clone', async () => { const { clone } = await import('../../std/ntv/object.js'); const data = { a: [1, '', {}], o: { now: new Date() } }; assert.ok(std.identical(data, clone(data))); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/core/standard/index.ts b/src/core/standard/index.ts index b9274490..60ac1dd7 100644 --- a/src/core/standard/index.ts +++ b/src/core/standard/index.ts @@ -1,6 +1,18 @@ import type { AlsoPromise } from '../../typings/extenders.js'; import type { AnyFunction, Reverse } from '../../typings/helpers.js'; +interface CapitalizeOptions { + /** only capitalize the very first letter */ + cap?: boolean; + /** convert the remaining word to lowercase */ + normalize?: boolean; +} +export function capitalize(text: string, { cap, normalize }: CapitalizeOptions = {}): string { + if (normalize) text = text.toLowerCase(); + if (cap) return `${text[0].toUpperCase()}${text.slice(1)}`; + return text.replace(/(?:^|\s)\S/g, (a) => a.toUpperCase()); +} + export function execute( condition: boolean, correct: () => AlsoPromise | AnyFunction<[]>, diff --git a/src/core/standard/unique.spec.ts b/src/core/standard/unique.spec.ts index bc5fb713..77561bbd 100644 --- a/src/core/standard/unique.spec.ts +++ b/src/core/standard/unique.spec.ts @@ -3,12 +3,12 @@ import * as assert from 'uvu/assert'; import unique from './unique.js'; -const basics = { - simple: suite('unique:simple'), - object: suite('unique:object'), +const suites = { + 'simple/': suite('unique/simple'), + 'object/': suite('unique/object'), }; -basics.simple('make array items unique', () => { +suites['simple/']('make array items unique', () => { assert.equal(unique([true, false, !0, !1]), [true, false]); assert.equal(unique([1, 1, 2, 3, 2, 4, 5]), [1, 2, 3, 4, 5]); assert.equal(unique(['a', 'a', 'b', 'c', 'b']), ['a', 'b', 'c']); @@ -17,7 +17,7 @@ basics.simple('make array items unique', () => { assert.equal(unique(months), ['jan', 'feb', 'mar']); }); -basics.object('make array of object unique', () => { +suites['object/']('make array of object unique', () => { assert.equal( unique( [ @@ -53,4 +53,4 @@ basics.object('make array of object unique', () => { ); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/utils/temporal/index.spec.ts b/src/core/temporal/index.spec.ts similarity index 76% rename from src/utils/temporal/index.spec.ts rename to src/core/temporal/index.spec.ts index b71b566b..789edd0e 100644 --- a/src/utils/temporal/index.spec.ts +++ b/src/core/temporal/index.spec.ts @@ -2,17 +2,15 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as dt from './index.js'; -const basics = { - build: suite('temporal:build'), - format: suite('temporal:format'), - travel: suite('temporal:travel'), +const suites = { + 'build/': suite('temporal/build'), + 'format/': suite('temporal/format'), + 'travel/': suite('temporal/travel'), }; const fixed = new Date('2017/09/08, 13:02:03'); -// ---- build ---- - -basics.build('basic formatter builder', () => { +suites['build/']('basic formatter builder', () => { const format = dt.build({ base: 'UTC' }); assert.type(format, 'function'); @@ -22,9 +20,7 @@ basics.build('basic formatter builder', () => { assert.equal(renderer('DD/MM/YYYY (Z)'), '08/09/2017 (+0)'); }); -// ---- format ---- - -basics.format('basic rendering', () => { +suites['format/']('basic rendering', () => { const renderer = dt.format(fixed); assert.equal(renderer('foo'), 'foo'); @@ -55,12 +51,8 @@ basics.format('basic rendering', () => { 'Valid from: [2017-09-08 ~ 13:02:03]' ); }); -basics.format('throw on invalid date', () => { +suites['format/']('throw on invalid date', () => { assert.throws(() => dt.format('invalid')); }); -// ---- travel ---- - -// basics.travel('basic travelling', () => {}); - -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/utils/temporal/index.ts b/src/core/temporal/index.ts similarity index 100% rename from src/utils/temporal/index.ts rename to src/core/temporal/index.ts diff --git a/src/guards/README.md b/src/guards/README.md index 5b4c058f..4aaab558 100644 --- a/src/guards/README.md +++ b/src/guards/README.md @@ -49,3 +49,11 @@ A utility guard that takes in any guards above and negates the result. For examp - `not(exists)` will return `true` if the input is nullish or an empty string - `not(natural)` will return `true` if the input exists or is a number less than or equal to 0. + +## `lowercase` + +A string guard that returns `true` if the input is a string with all lowercase characters. + +## `uppercase` + +A string guard that returns `true` if the input is a string with all uppercase characters. diff --git a/src/guards/index.spec.ts b/src/guards/index.spec.ts index a581af6a..775b4af6 100644 --- a/src/guards/index.spec.ts +++ b/src/guards/index.spec.ts @@ -5,62 +5,71 @@ import * as guards from './index.js'; // checked based on https://developer.mozilla.org/en-US/docs/Glossary/Falsy const data = [true, false, 'a', 'b', 0, 1, 2, '', null, undefined, NaN]; const numbers = [-2, -1, 0, 1, 2, 3]; +const strings = ['a', 'A', 'b', 'B', 'c', 'C']; -const basics = { - guards: suite('guards'), - inverse: suite('not()'), +const suites = { + 'guards/': suite('guards/core'), + 'inverse/': suite('guards/not'), }; -basics.guards('filters values that exists', () => { +suites['guards/']('filters values that exists', () => { const filtered = data.filter(guards.exists); assert.equal(filtered, [true, false, 'a', 'b', 0, 1, 2, NaN]); }); -basics.guards('filters values that are nullish', () => { +suites['guards/']('filters values that are nullish', () => { const filtered = data.filter(guards.nullish); assert.equal(filtered, [null, undefined]); }); -basics.guards('filters values that are truthy', () => { +suites['guards/']('filters values that are truthy', () => { const filtered = data.filter(guards.truthy); assert.equal(filtered, [true, 'a', 'b', 1, 2]); }); -basics.guards('filters numbers that are natural', () => { +suites['guards/']('filters numbers that are natural', () => { const filtered = numbers.filter(guards.natural); assert.equal(filtered, [1, 2, 3]); }); -basics.guards('filters numbers that are whole', () => { +suites['guards/']('filters numbers that are whole', () => { const filtered = numbers.filter(guards.whole); assert.equal(filtered, [0, 1, 2, 3]); }); -// ---- not() suite ---- +suites['guards/']('filters strings that are lowercase', () => { + const filtered = strings.filter(guards.lowercase); + assert.equal(filtered, ['a', 'b', 'c']); +}); + +suites['guards/']('filters strings that are uppercase', () => { + const filtered = strings.filter(guards.uppercase); + assert.equal(filtered, ['A', 'B', 'C']); +}); -basics.inverse('filters values that does not exists', () => { +suites['inverse/']('filters values that does not exists', () => { const filtered = data.filter(guards.not(guards.exists)); assert.equal(filtered, ['', null, undefined]); }); -basics.inverse('filters values that are not nullish', () => { +suites['inverse/']('filters values that are not nullish', () => { const filtered = data.filter(guards.not(guards.nullish)); assert.equal(filtered, [true, false, 'a', 'b', 0, 1, 2, '', NaN]); }); -basics.inverse('filters values that are falsy', () => { +suites['inverse/']('filters values that are falsy', () => { const filtered = data.filter(guards.not(guards.truthy)); assert.equal(filtered, [false, 0, '', null, undefined, NaN]); }); -basics.inverse('filters numbers that are not natural', () => { +suites['inverse/']('filters numbers that are not natural', () => { const filtered = numbers.filter(guards.not(guards.natural)); assert.equal(filtered, [-2, -1, 0]); }); -basics.inverse('filters numbers that are not whole', () => { +suites['inverse/']('filters numbers that are not whole', () => { const filtered = numbers.filter(guards.not(guards.whole)); assert.equal(filtered, [-2, -1]); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/guards/index.ts b/src/guards/index.ts index 4f21be4c..d8e6d738 100644 --- a/src/guards/index.ts +++ b/src/guards/index.ts @@ -36,3 +36,13 @@ export function not(fn: F) { type D = F extends typeof exists ? Primitives : F extends typeof nullish ? Nullish : Empty; return (i: T | D): i is T => !fn(i); } + +// string guards +/** @returns true if string input is all lowercase letters */ +export function lowercase(s: string): boolean { + return s === s.toLowerCase(); +} +/** @returns true if string input is all uppercase letters */ +export function uppercase(s: string): boolean { + return s === s.toUpperCase(); +} diff --git a/src/math/set/index.spec.ts b/src/math/set/index.spec.ts index e11b5124..9dc7d671 100644 --- a/src/math/set/index.spec.ts +++ b/src/math/set/index.spec.ts @@ -2,23 +2,24 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as set from './index.js'; -const basics = { - permutation: suite('permutation'), +const suites = { + 'permutation/': suite('set/permutation'), + 'permutation/+': suite('set/permutation:+'), }; -basics.permutation('returns array with empty array for empty array', () => { +suites['permutation/']('returns array with empty array for empty array', () => { assert.equal(set.permutation([]), [[]]); }); -basics.permutation('returns immediate wrapped input for one length array', () => { +suites['permutation/']('returns immediate wrapped input for one length array', () => { assert.equal(set.permutation(['a']), [['a']]); }); -basics.permutation('correctly permute 2 words and returns array of results', () => { +suites['permutation/']('correctly permute 2 words and returns array of results', () => { assert.equal(set.permutation(['a', 'b']), [ ['a', 'b'], ['b', 'a'], ]); }); -basics.permutation('correctly permute 3 words and returns array of results', () => { +suites['permutation/']('correctly permute 3 words and returns array of results', () => { assert.equal(set.permutation(['a', 'b', 'c']), [ ['a', 'b', 'c'], ['a', 'c', 'b'], @@ -28,7 +29,7 @@ basics.permutation('correctly permute 3 words and returns array of results', () ['c', 'b', 'a'], ]); }); -basics.permutation('correctly permute 4 words and returns array of results', () => { +suites['permutation/']('correctly permute 4 words and returns array of results', () => { assert.equal(set.permutation(['a', 'b', 'c', 'd']), [ ['a', 'b', 'c', 'd'], ['a', 'b', 'd', 'c'], @@ -57,23 +58,17 @@ basics.permutation('correctly permute 4 words and returns array of results', () ]); }); -// ---- mutated suite ---- - -const advanced = { - permutation: suite('permutation+'), -}; - const dashed = (i: string[]) => i.join('-'); -advanced.permutation('returns array with empty string for empty array', () => { +suites['permutation/+']('returns array with empty string for empty array', () => { assert.equal(set.permutation([], dashed), ['']); }); -advanced.permutation('returns immediate input for one length array', () => { +suites['permutation/+']('returns immediate input for one length array', () => { assert.equal(set.permutation(['a'], dashed), ['a']); }); -advanced.permutation('correctly permute and mutate 2 words', () => { +suites['permutation/+']('correctly permute and mutate 2 words', () => { assert.equal(set.permutation(['a', 'b'], dashed), ['a-b', 'b-a']); }); -advanced.permutation('correctly permute and mutate 3 words', () => { +suites['permutation/+']('correctly permute and mutate 3 words', () => { assert.equal(set.permutation(['a', 'b', 'c'], dashed), [ 'a-b-c', 'a-c-b', @@ -83,7 +78,7 @@ advanced.permutation('correctly permute and mutate 3 words', () => { 'c-b-a', ]); }); -advanced.permutation('correctly permute and mutate 4 words', () => { +suites['permutation/+']('correctly permute and mutate 4 words', () => { assert.equal(set.permutation(['a', 'b', 'c', 'd'], dashed), [ 'a-b-c-d', 'a-b-d-c', @@ -112,5 +107,4 @@ advanced.permutation('correctly permute and mutate 4 words', () => { ]); }); -Object.values(basics).forEach((v) => v.run()); -Object.values(advanced).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/std/README.md b/src/std/README.md index 30ff3e78..a27de2fb 100644 --- a/src/std/README.md +++ b/src/std/README.md @@ -35,14 +35,6 @@ export function clone(i: T): T; Creating a copy of a data type, especially an object, is useful for removing the reference to the original object, keeping it clean from unexpected changes and side effects. This is possible because we are creating a new instance, making sure that any mutation or changes that are applied won't affect one or the other. -### `ntv.create` - -```typescript -export function create(array: T[], i: any): { [K in T]: typeof i }; -``` - -Create an object with keys from an array of strings with the option to specify the initial value, defaulting to `null`. - ### `ntv.freeze` Augmented `Object.freeze()`, deep freezes and strongly-typed. @@ -90,3 +82,35 @@ Original function, aggregates elements from each of the arrays and returns a sin ```typescript export function zip>(...arrays: T[]): Record[]; ``` + +## `tsf` + +A template string function. This takes a template string and returns a function that takes an object of functions, which is used to manipulate the name of the braces in the template string. + +This assumes the braces inside the template string are balanced and not nested. The function will not throw an error if the braces are not balanced, but the result will be unexpected. If you're using TypeScript and are passing a [string literal](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types), it will point out any unbalanced braces by throwing an error from the compiler. + +```typescript +export function tsf( + template: string +): (table: { + [key: string]: string | false | Nullish | ((key: string) => string | false | Nullish); +}) => string; +``` + +The template string is parsed into an array of strings, which are then executed with the provided table of functions, which is an object with the key being the name of the braces from the template string, and the value being the function to manipulate the name of the braces. + +```javascript +import { tsf } from 'mauss/std'; + +const render = tsf('https://api.example.com/v1/{category}/{id}'); + +function publish({ category, id }) { + const prefix = // ... + const url = render({ + category: () => category !== 'new' && category, + id: (v) => prefix + uuid(`${v}-${id}`), + }); + + return fetch(url); +} +``` diff --git a/src/std/index.ts b/src/std/index.ts index faa5fe32..75b7c80f 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,2 +1,3 @@ export * as csv from './csv/index.js'; export * as ntv from './ntv/index.js'; +export { tsf } from './tsf/index.js'; diff --git a/src/std/ntv/array.spec.ts b/src/std/ntv/array.spec.ts index 372271ea..8ca8a010 100644 --- a/src/std/ntv/array.spec.ts +++ b/src/std/ntv/array.spec.ts @@ -3,11 +3,11 @@ import * as assert from 'uvu/assert'; import * as ntv from './array.js'; -const basics = { - zip: suite('obj:zip'), +const suites = { + 'arr/zip': suite('arr/zip'), }; -basics.zip('zip multiple arrays of objects', () => { +suites['arr/zip']('zip multiple arrays of objects', () => { const zipped = ntv.zip( [{ a: 0 }, { x: 0 }], [{ b: 0 }, { y: 0 }], @@ -20,7 +20,7 @@ basics.zip('zip multiple arrays of objects', () => { { x: 1, y: 0, z: 0 }, ]); }); -basics.zip('zip multiple uneven arrays', () => { +suites['arr/zip']('zip multiple uneven arrays', () => { const zipped = ntv.zip( [{ a: 0 }], [{ a: 1 }, { x: 0 }], @@ -39,7 +39,7 @@ basics.zip('zip multiple uneven arrays', () => { { w: 0, x: 0, y: 0 }, ]); }); -basics.zip('zip remove all nullish index', () => { +suites['arr/zip']('zip remove all nullish index', () => { const zipped = ntv.zip( [{ a: 0 }, null, { x: 0 }, null, { a: 0 }, undefined], [{ b: 0 }, null, { y: 0 }, undefined, { b: 0 }, null], @@ -54,4 +54,4 @@ basics.zip('zip remove all nullish index', () => { ]); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/std/ntv/object.spec.ts b/src/std/ntv/object.spec.ts index ea865e89..cb9ae108 100644 --- a/src/std/ntv/object.spec.ts +++ b/src/std/ntv/object.spec.ts @@ -3,18 +3,17 @@ import * as assert from 'uvu/assert'; import * as ntv from './object.js'; -const basics = { - clone: suite('obj:clone'), - create: suite('obj:create'), - entries: suite('obj:entries'), - freeze: suite('obj:freeze'), - iterate: suite('obj:iterate'), - keys: suite('obj:keys'), - pick: suite('obj:pick'), - size: suite('obj:size'), +const suites = { + 'obj/clone': suite('obj/clone'), + 'obj/entries': suite('obj/entries'), + 'obj/freeze': suite('obj/freeze'), + 'obj/iterate': suite('obj/iterate'), + 'obj/keys': suite('obj/keys'), + 'obj/pick': suite('obj/pick'), + 'obj/size': suite('obj/size'), }; -basics.clone('clone any possible data type', () => { +suites['obj/clone']('clone any possible data type', () => { const base = { arr: [0, 'hi', /wut/], obj: { now: new Date() } }; const cloned = ntv.clone(base); @@ -28,14 +27,7 @@ basics.clone('clone any possible data type', () => { assert.equal(base.obj.now, cloned.obj.now); }); -basics.create('create object from an array', () => { - const numbers = ntv.create([1, 2, 0]); - assert.equal(numbers, { 1: null, 2: null, 0: null }); - const vowels = ntv.create(['a', 'i', 'u', 'e', 'o'], ''); - assert.equal(vowels, { a: '', i: '', u: '', e: '', o: '' }); -}); - -basics.entries('return object entries', () => { +suites['obj/entries']('return object entries', () => { assert.equal(ntv.entries({ hello: 'world', foo: 0, bar: { baz: 1 } }), [ ['hello', 'world'], ['foo', 0], @@ -43,7 +35,7 @@ basics.entries('return object entries', () => { ]); }); -basics.freeze('deep freezes nested objects', () => { +suites['obj/freeze']('deep freezes nested objects', () => { const nested = ntv.freeze({ foo: { a: 0 }, bar: { b: 1 }, @@ -53,7 +45,7 @@ basics.freeze('deep freezes nested objects', () => { assert.ok(Object.isFrozen(nested.foo)); assert.ok(Object.isFrozen(nested.bar)); }); -basics.freeze('deep freeze ignore function', () => { +suites['obj/freeze']('deep freeze ignore function', () => { const nested = ntv.freeze({ identity: (v: any) => v, namespace: { a() {} }, @@ -64,7 +56,7 @@ basics.freeze('deep freeze ignore function', () => { assert.ok(!Object.isFrozen(nested.namespace.a)); }); -basics.iterate('iterate over nested objects', () => { +suites['obj/iterate']('iterate over nested objects', () => { const months = 'jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec'.split(','); const currencies = 'usd,eur,sgd,gbp,aud,jpy'.split(','); @@ -98,7 +90,7 @@ basics.iterate('iterate over nested objects', () => { }, {}) ); }); -basics.iterate('iterate with empty/falsy return', () => { +suites['obj/iterate']('iterate with empty/falsy return', () => { assert.equal( ntv.iterate({}, ([]) => {}), {} @@ -122,25 +114,25 @@ basics.iterate('iterate with empty/falsy return', () => { }); }); }); -basics.iterate('iterate creates deep copy', () => { +suites['obj/iterate']('iterate creates deep copy', () => { const original = { x: 1, y: { z: 'foo' } }; const copy = ntv.iterate(original); assert.ok(original !== copy); assert.ok(original.y !== copy.y); }); -basics.keys('return object keys', () => { +suites['obj/keys']('return object keys', () => { assert.equal(ntv.keys({ a: 0, b: 1, c: 2 }), ['a', 'b', 'c']); }); -basics.pick('pick properties from an object', () => { +suites['obj/pick']('pick properties from an object', () => { const unwrap = ntv.pick(['a', 'b', 'c', 'z']); const picked = unwrap({ a: 0, c: 'b', y: undefined, z: null }); assert.equal(picked, { a: 0, c: 'b', z: null }); }); -basics.size('return size of an object', () => { +suites['obj/size']('return size of an object', () => { assert.equal(ntv.size({ a: 0, b: 1, c: 2 }), 3); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/std/ntv/object.ts b/src/std/ntv/object.ts index eeca7158..5406d542 100644 --- a/src/std/ntv/object.ts +++ b/src/std/ntv/object.ts @@ -10,12 +10,6 @@ export function clone(i: T): T { return iterate(i) as T; } -export function create(array: T[], i: any = null) { - const object = {} as { [K in T]: typeof i }; - for (const key of array) object[key] = i; - return object; -} - export function entries(o: T) { return Object.entries(o) as Entries; } diff --git a/src/std/tsf/index.spec.ts b/src/std/tsf/index.spec.ts new file mode 100644 index 00000000..a3544243 --- /dev/null +++ b/src/std/tsf/index.spec.ts @@ -0,0 +1,50 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { tsf } from './index.js'; + +test.skip('throws on nested braces', () => { + assert.throws(() => tsf('/{foo/{bar}}' as string)); + assert.throws(() => tsf('/{nested-{}-braces}' as string)); +}); + +test('parses template without braces', () => { + assert.equal(tsf('')({}), ''); + assert.equal(tsf('/')({}), '/'); + assert.equal(tsf('/foo')({}), '/foo'); +}); + +test('parses template correctly', () => { + const r1 = tsf('/{foo}/{bar}'); + + assert.equal( + r1({ + foo: 'hello', + bar: 'world', + }), + '/hello/world' + ); + assert.equal( + r1({ + foo: (v) => v, + bar: (v) => v, + }), + '/foo/bar' + ); + assert.equal( + r1({ + foo: (v) => [...v].reverse().join(''), + bar: (v) => [...v].reverse().join(''), + }), + '/oof/rab' + ); +}); + +test.skip('parses template with nested braces', () => { + const r1 = tsf('/{foo/{bar}}' as string); + assert.equal(r1({ 'foo/{bar}': (v) => v }), '/foo/{bar}'); + + const r2 = tsf('/{nested-{}-braces}' as string); + assert.equal(r2({ 'nested-{}-braces': (v) => v }), '/nested-{}-braces'); +}); + +test.run(); diff --git a/src/std/tsf/index.test.ts b/src/std/tsf/index.test.ts new file mode 100644 index 00000000..72a5ad10 --- /dev/null +++ b/src/std/tsf/index.test.ts @@ -0,0 +1,51 @@ +import { tsf } from './index.js'; + +tsf(''); +tsf('/'); +tsf('/')({}); +tsf('/{foo}')({ foo: (v) => v }); +tsf('/{foo}/{bar}')({ foo: 'hello', bar: () => 'world' }); +tsf('/{foo}/{bar}')({ foo: (v) => v, bar: (v) => v }); +tsf('/{foo}')({ foo: (v) => v.length > 1 && v.replace('o', 'u') }); +tsf('' as string)({ boo: (v) => v }); +tsf('' as `${string}/v1/posts/{id}/comments`)({ id: (v) => v }); + +// ---- errors ---- + +// @ts-expect-error +tsf('{}'); +// @ts-expect-error +tsf('/{}'); +// @ts-expect-error +tsf('{}/'); +// @ts-expect-error +tsf('/{{}}'); +// @ts-expect-error +tsf('/{}}'); +// @ts-expect-error +tsf('/{{}'); +// @ts-expect-error +tsf('/{{}{'); +// @ts-expect-error +tsf('/{{foo}}'); +// @ts-expect-error +tsf('/{{foo}{'); +// @ts-expect-error +tsf('/{}/{bar}'); +// @ts-expect-error +tsf('/{foo}/{{}}'); + +// @ts-expect-error +tsf('/')(); +// @ts-expect-error +tsf('/{foo}')(); +// @ts-expect-error +tsf('/{foo}/{bar}')({}); +// @ts-expect-error +tsf('/{foo}/{bar}')({ foo: (v) => v }); +// @ts-expect-error +tsf('/{foo}')({ foo: (v) => v, bar: (v) => v }); +// @ts-expect-error +tsf('' as `${string}/v1/posts/{id}/comments`)({}); + +tsf('{hello-world}')({ 'hello-world': (v) => v }); diff --git a/src/std/tsf/index.ts b/src/std/tsf/index.ts new file mode 100644 index 00000000..a34f0b66 --- /dev/null +++ b/src/std/tsf/index.ts @@ -0,0 +1,47 @@ +import { Nullish } from '../../typings/aliases.js'; +import { UnaryFunction } from '../../typings/helpers.js'; + +type Parse = T extends `${string}{${infer P}}${infer R}` ? P | Parse : never; +export function tsf( + template: Input extends + | `${string}{}${string}` + | `${string}{{${string}}}${string}` + | `${string}{{${string}}${string}` + | `${string}{${string}}}${string}` + ? never + : Input +) { + const parts: string[] = []; + for (let i = 0, start = 0; i < template.length; i += 1) { + if (template[i] === '{') { + if (i > start) parts.push(template.slice(start, i)); + + const end = template.indexOf('}', i); + if (end === -1 /** missing closing */) { + parts.push(template.slice(i)); + break; + } + + parts.push(template.slice(i + 1, end)); + start = (i = end) + 1; + } else if (i === template.length - 1) { + parts.push(template.slice(start)); + } + } + + type ConditionalString = string | false | Nullish; + type Replacer = ConditionalString | UnaryFunction; + type ExpectedProperties = string extends Input ? string : Parse; + return function render(table: { [K in ExpectedProperties]: Replacer }) { + let transformed = ''; + for (let i = 0; i < parts.length; i += 1) { + const replace = table[parts[i] as ExpectedProperties]; + if (typeof replace === 'function') { + transformed += replace(parts[i]) || ''; + } else { + transformed += replace || parts[i]; + } + } + return transformed; + }; +} diff --git a/src/utils/README.md b/src/utils/README.md deleted file mode 100644 index 6d58ee3b..00000000 --- a/src/utils/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# mauss/utils - -```js -import { :util } from 'mauss/utils'; -``` - -## `capitalize` - -```ts -interface CapitalizeOptions { - /** only capitalize the very first letter */ - cap?: boolean; - /** convert the remaining word to lowercase */ - normalize?: boolean; -} - -export function capitalize(text: string, options?: CapitalizeOptions): string; -``` - -```js -capitalize('hi there'); // 'Hi There' -capitalize('hI thErE'); // 'HI ThErE' -capitalize('hI thErE', { cap: true }); // 'HI thErE' -capitalize('hI thErE', { normalize: true }); // 'Hi There' -capitalize('hI thErE', { cap: true, normalize: true }); // 'Hi there' -``` - -## `dt` - -Simple `date/time` (`dt`) utility namespace. - -```ts -type DateValue = string | number | Date; - -interface BuildOptions { - base?: 'UTC'; -} - -interface TravelOptions { - /** relative point of reference to travel */ - from?: DateValue; - /** relative days to travel in number */ - to: number; -} - -export const dt: { - current(d?: DateValue): Date; - build(options: BuildOptions): (date?: DateValue) => (mask?: string) => string; - format: ReturnType; - travel({ from, to }: TravelOptions): Date; -} -``` - -- `dt.current` is a function `(d?: DateValue) => Date` that optionally takes in a `DateValue` to be converted into a `Date` object, `new Date()` will be used if nothing is passed -- `dt.build` is a function that accepts `BuildOptions` and builds a formatter, a convenience export is included with all the default options as `dt.format` -- `dt.format` is a function that takes in a `DateValue` and returns a renderer that accepts a string mask to format the date in, defaults to `'DDDD, DD MMMM YYYY'` -- `dt.travel` is a function `({ from, to }) => Date` that takes in a `{ from, to }` object with `from` property being optional - -## `tryNumber` - -will check an input and convert to number when applicable, otherwise it will return the input as is. - -```ts -type Possibilities = string | number | null | undefined; - -export function tryNumber( - input: Input, - fallback?: Fallback -): Input is number ? number : Fallback | Input; -``` - -Example inputs and outputs - -```js -tryNumber('0'); // 0 -tryNumber(0); // 0 -tryNumber('1H'); // '1H' -``` - -## `random` - -```js -/** random number from [min, max) */ -random.int(2); // 0 - 1 -random.int(1000); // 0 - 999 -random.int(9, 1); // 1 - 8 - -/** random key from any object */ -const data = { a: {}, b: 1, c: [3] }; -// returns a random value from an object -random.key(data); // a || 1 || [3] -``` diff --git a/src/utils/index.spec.ts b/src/utils/index.spec.ts deleted file mode 100644 index 97d69140..00000000 --- a/src/utils/index.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { suite } from 'uvu'; -import * as assert from 'uvu/assert'; -import * as utils from './index.js'; - -const basics = { - capitalize: suite('capitalize'), - tryNumber: suite('tryNumber'), -}; - -// ---- capitalize ---- - -basics.capitalize('change one letter for one word', () => { - assert.equal(utils.capitalize('hello'), 'Hello'); -}); -basics.capitalize('change two letter for two words', () => { - assert.equal(utils.capitalize('hello world'), 'Hello World'); -}); - -// ---- tryNumber ---- - -basics.tryNumber('convert to numbers', () => { - assert.equal(utils.tryNumber(null), 0); - assert.equal(utils.tryNumber('0'), 0); - assert.equal(utils.tryNumber('1'), 1); - assert.equal(utils.tryNumber('-1'), -1); - assert.equal(utils.tryNumber('1e3'), 1e3); -}); - -basics.tryNumber('fallback to original value as-is', () => { - assert.equal(utils.tryNumber('a'), 'a'); - assert.equal(utils.tryNumber('a1a'), 'a1a'); - assert.equal(utils.tryNumber('1a1'), '1a1'); - assert.equal(utils.tryNumber('-1a'), '-1a'); - assert.equal(utils.tryNumber('-1e'), '-1e'); - - const dyn: string = 'dynamic'; - assert.equal(utils.tryNumber(dyn), 'dynamic'); - assert.equal(utils.tryNumber(undefined), undefined); -}); - -basics.tryNumber('fallback to provided as expected', () => { - assert.equal(utils.tryNumber('a', 0), 0); - assert.equal(utils.tryNumber('a', 1), 1); -}); - -Object.values(basics).forEach((v) => v.run()); diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts deleted file mode 100644 index 020874b5..00000000 --- a/src/utils/index.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { tryNumber } from './index.js'; - -declare function expect(v: T): void; - -expect<''>(tryNumber('')); -expect(tryNumber(null)); -expect(tryNumber(0)); -expect(tryNumber(1)); -expect(tryNumber('0')); -expect(tryNumber('1')); -expect(tryNumber('12')); -expect(tryNumber('123')); -expect(tryNumber('123456')); -expect<'asd'>(tryNumber('asd')); -expect<'123asd'>(tryNumber('123asd')); -expect<'asd123'>(tryNumber('asd123')); -expect<'123asd123'>(tryNumber('123asd123')); -expect<'asd123asd'>(tryNumber('asd123asd')); -expect<'12as'>(tryNumber('12as')); - -// @ts-expect-error -expect(tryNumber('nan')); -// @ts-expect-error -expect(tryNumber('000nope')); -// @ts-expect-error -expect(tryNumber('123')); diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 91b03ee7..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -export * as random from './random/index.js'; -export * as dt from './temporal/index.js'; - -interface CapitalizeOptions { - /** only capitalize the very first letter */ - cap?: boolean; - /** convert the remaining word to lowercase */ - normalize?: boolean; -} -export function capitalize(text: string, { cap, normalize }: CapitalizeOptions = {}): string { - if (normalize) text = text.toLowerCase(); - if (cap) return `${text[0].toUpperCase()}${text.slice(1)}`; - return text.replace(/(?:^|\s)\S/g, (a) => a.toUpperCase()); -} - -type Numeric = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; -type TryValidator = Text extends `${infer Character}${infer Rest}` - ? [Character, TryValidator] extends [Numeric, infer Validated] - ? [Validated] extends [''] - ? number - : Validated extends string - ? Text - : number - : Text - : Text; - -type Possibilities = string | number | null | undefined; -export function tryNumber( - input: Input, - fallback: Fallback = input as unknown as Fallback -) { - type Narrow = Other extends number | null ? number : Fallback; - type TryReturned = Input extends string ? TryValidator : Narrow; - const converted = Number(input); - return (Number.isNaN(converted) ? fallback : converted) as TryReturned; -} diff --git a/src/web/cookies/index.spec.ts b/src/web/cookies/index.spec.ts index 394812ff..78d162f0 100644 --- a/src/web/cookies/index.spec.ts +++ b/src/web/cookies/index.spec.ts @@ -2,23 +2,21 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as cookies from './index.js'; -const basics = { - parse: suite('cookie:parse'), - create: suite('cookie:create'), - remove: suite('cookie:remove'), - bulk: suite('cookie:bulk'), - raw: suite('cookie:raw'), +const suites = { + 'parse/': suite('cookie/parse'), + 'create/': suite('cookie/create'), + 'remove/': suite('cookie/remove'), + 'bulk/': suite('cookie/bulk'), + 'raw/': suite('cookie/raw'), }; -// ---- parse ---- - -basics.parse('parse basic input', () => { +suites['parse/']('parse basic input', () => { const jar = cookies.parse('foo=bar;hi=mom;hello=world'); assert.equal(jar.get('foo'), 'bar'); assert.equal(jar.get('hi'), 'mom'); assert.equal(jar.get('hello'), 'world'); }); -basics.parse('parse nullish header', () => { +suites['parse/']('parse nullish header', () => { let header: null | undefined = null; const jar = cookies.parse(header); assert.ok(!jar.has('foo')); @@ -28,92 +26,84 @@ basics.parse('parse nullish header', () => { assert.equal(jar.get('hi'), undefined); assert.equal(jar.get('hello'), undefined); }); -basics.parse('parse ignore spaces', () => { +suites['parse/']('parse ignore spaces', () => { const jar = cookies.parse('foo = bar; hi= mom'); assert.equal(jar.get('foo'), 'bar'); assert.equal(jar.get('hi'), 'mom'); }); -basics.parse('parse handle quoted', () => { +suites['parse/']('parse handle quoted', () => { const jar = cookies.parse('foo="bar=123&hi=mom"'); assert.equal(jar.get('foo'), 'bar=123&hi=mom'); }); -basics.parse('parse escaped values', () => { +suites['parse/']('parse escaped values', () => { const jar = cookies.parse('foo=%20%22%2c%2f%3b'); assert.equal(jar.get('foo'), ' ",/;'); }); -basics.parse('parse ignore errors', () => { +suites['parse/']('parse ignore errors', () => { const jar = cookies.parse('foo=%1;bar=baz;huh;'); assert.equal(jar.get('foo'), '%1'); assert.equal(jar.get('bar'), 'baz'); assert.ok(!jar.has('huh')); }); -basics.parse('parse ignore missing values', () => { +suites['parse/']('parse ignore missing values', () => { const jar = cookies.parse('foo=;bar= ;huh'); assert.ok(!jar.has('foo')); assert.ok(!jar.has('bar')); assert.ok(!jar.has('huh')); }); -// ---- create ---- - -basics.create('generate Set-Cookie value to set cookie', () => { +suites['create/']('generate Set-Cookie value to set cookie', () => { const value = cookies.create()('foo', 'bar'); assert.match(value, /foo=bar; Expires=(.*); Path=\/; SameSite=Lax; HttpOnly/); }); -basics.create('set Secure attribute for SameSite=None', () => { +suites['create/']('set Secure attribute for SameSite=None', () => { const printer = cookies.create({ sameSite: 'None' }); const value = printer('foo', 'bar'); assert.match(value, /foo=bar; Expires=(.*); Path=\/; SameSite=None; HttpOnly; Secure/); }); -// ---- remove ---- - -basics.remove('generate Set-Cookie value to remove cookie', () => { +suites['remove/']('generate Set-Cookie value to remove cookie', () => { const value = cookies.remove('foo'); assert.match(value, /foo=; Path=\/; Expires=Thu, 01 Jan 1970 00:00:01 GMT/); }); -// ---- bulk ---- - -basics.bulk('bulk generate Set-Cookie values', () => { +suites['bulk/']('bulk generate Set-Cookie values', () => { const data = { foo: 'bar' }; for (const value of cookies.bulk(data)) { assert.match(value, /(.*)=(.*); Expires=(.*); Path=\/; SameSite=Lax; HttpOnly/); } }); -// ---- raw ---- - -basics.raw('raw basic input', () => { +suites['raw/']('raw basic input', () => { const header = 'foo=bar;hi=mom;hello=world'; assert.equal(cookies.raw(header, 'foo'), 'bar'); assert.equal(cookies.raw(header, 'hi'), 'mom'); assert.equal(cookies.raw(header, 'hello'), 'world'); }); -basics.raw('raw nullish header', () => { +suites['raw/']('raw nullish header', () => { let header: null | undefined = null; assert.ok(!cookies.raw(header, 'foo')); assert.ok(!cookies.raw(header, 'hi')); assert.ok(!cookies.raw(header, 'hello')); }); -basics.raw('raw ignore spaces', () => { +suites['raw/']('raw ignore spaces', () => { const header = 'foo = bar; hi= mom'; assert.equal(cookies.raw(header, 'foo'), ' bar'); assert.equal(cookies.raw(header, 'hi'), ' mom'); }); -basics.raw('raw handle quoted', () => { +suites['raw/']('raw handle quoted', () => { const header = 'foo="bar=123&hi=mom"'; assert.equal(cookies.raw(header, 'foo'), '"bar=123&hi=mom"'); }); -basics.raw('raw escaped values', () => { +suites['raw/']('raw escaped values', () => { const header = 'foo=%20%22%2c%2f%3b'; assert.equal(cookies.raw(header, 'foo'), '%20%22%2c%2f%3b'); }); -basics.raw('raw return empty values', () => { +suites['raw/']('raw return empty values', () => { const header = 'foo=;bar= ;huh'; assert.ok(!cookies.raw(header, 'foo')); assert.ok(cookies.raw(header, 'bar')); assert.ok(!cookies.raw(header, 'huh')); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run()); diff --git a/src/web/query/decoder.ts b/src/web/query/decoder.ts index 4873c02e..33313a92 100644 --- a/src/web/query/decoder.ts +++ b/src/web/query/decoder.ts @@ -2,7 +2,6 @@ import type { IndexSignature, Primitives } from '../../typings/aliases.js'; import type { AlsoArray } from '../../typings/extenders.js'; import type { Intersection } from '../../typings/helpers.js'; import type { Flatten } from '../../typings/prototypes.js'; -import { tryNumber } from '../../utils/index.js'; type CombineExisting< A extends Record, @@ -37,7 +36,8 @@ export default function qsd(qs: Q) { const dec = (s: string) => { if (!s.trim()) return ''; s = decodeURIComponent(s); - return ['true', 'false'].includes(s) ? s[0] === 't' : tryNumber(s); + if (['true', 'false'].includes(s)) return s[0] === 't'; + return Number.isNaN(Number(s)) ? s : Number(s); }; const dqs: Record> = {}; diff --git a/src/web/query/index.spec.ts b/src/web/query/index.spec.ts index b9c348c0..d8de9033 100644 --- a/src/web/query/index.spec.ts +++ b/src/web/query/index.spec.ts @@ -4,14 +4,12 @@ import * as assert from 'uvu/assert'; import qsd from './decoder.js'; import qse from './encoder.js'; -const basics = { - decoder: suite('query:decoder'), - encoder: suite('query:encoder'), +const suites = { + 'decoder/': suite('query/decoder'), + 'encoder/': suite('query/encoder'), }; -// ---- decoder ---- - -basics.decoder('decode query string to object', () => { +suites['decoder/']('decode query string to object', () => { const pairs = [ ['?hi=mom&hello=world', { hi: 'mom', hello: 'world' }], ['fam=mom&fam=dad&fam=sis', { fam: ['mom', 'dad', 'sis'] }], @@ -23,9 +21,7 @@ basics.decoder('decode query string to object', () => { } }); -// ---- encoder ---- - -basics.encoder('encode object to query string', () => { +suites['encoder/']('encode object to query string', () => { let payload: string = 'dynamic'; const pairs = [ [{ hi: 'mom', hello: 'world' }, '?hi=mom&hello=world'], @@ -40,7 +36,7 @@ basics.encoder('encode object to query string', () => { assert.equal(qse(input), output); } }); -basics.encoder('transform final string if it exists', () => { +suites['encoder/']('transform final string if it exists', () => { const bound = { q: '' }; assert.equal(qse(bound), ''); @@ -49,4 +45,4 @@ basics.encoder('transform final string if it exists', () => { assert.equal(qse(bound), '?q=hi'); }); -Object.values(basics).forEach((v) => v.run()); +Object.values(suites).forEach((v) => v.run());