From dc48cbc44b32f2faa13debf4e9d38e731fc98293 Mon Sep 17 00:00:00 2001 From: robonen Date: Sat, 4 May 2024 06:59:35 +0700 Subject: [PATCH] feat(packages/stdlib): start getByPath and template tools --- packages/stdlib/src/arrays/getByPath/index.ts | 84 ++++++++++++++ packages/stdlib/src/arrays/index.ts | 1 + packages/stdlib/src/text/index.ts | 3 +- .../stdlib/src/text/template/index.test-d.ts | 105 ++++++++++++++++++ .../stdlib/src/text/template/index.test.ts | 42 +++++++ packages/stdlib/src/text/template/index.ts | 52 +++++++++ 6 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 packages/stdlib/src/arrays/getByPath/index.ts create mode 100644 packages/stdlib/src/arrays/index.ts create mode 100644 packages/stdlib/src/text/template/index.test-d.ts create mode 100644 packages/stdlib/src/text/template/index.test.ts create mode 100644 packages/stdlib/src/text/template/index.ts diff --git a/packages/stdlib/src/arrays/getByPath/index.ts b/packages/stdlib/src/arrays/getByPath/index.ts new file mode 100644 index 0000000..1a1ad7e --- /dev/null +++ b/packages/stdlib/src/arrays/getByPath/index.ts @@ -0,0 +1,84 @@ +type Exist = T extends undefined | null ? never : T; + +type ExtractFromObject, K> = + K extends keyof O + ? O[K] + : K extends keyof Exist + ? Exist[K] + : never; + +type ExtractFromArray = any[] extends A + ? A extends readonly (infer T)[] + ? T | undefined + : undefined + : K extends keyof A + ? A[K] + : undefined; + +type GetWithArray = K extends [] + ? O + : K extends [infer Key, ...infer Rest] + ? O extends Record + ? GetWithArray, Rest> + : O extends readonly any[] + ? GetWithArray, Rest> + : never + : never; + +type Path = T extends `${infer Key}.${infer Rest}` + ? [Key, ...Path] + : T extends `${infer Key}` + ? [Key] + : []; + +// Type that generate a type of a value by a path; +// e.g. ['a', 'b', 'c'] => { a: { b: { c: PropertyKey } } } +// e.g. ['a', 'b', 'c', 'd'] => { a: { b: { c: { d: PropertyKey } } } } +// e.g. ['a'] => { a: PropertyKey } +// e.g. ['a', '0'], => { a: [PropertyKey] } +// e.g. ['a', '0', 'b'] => { a: [{ b: PropertyKey }] } +// e.g. ['a', '0', 'b', '0'] => { a: [{ b: [PropertyKey] }] } +// e/g/ ['0', 'a'] => [{ a: PropertyKey }] +// +// Input: ['a', 'b', 'c'], constrain: PropertyKey +// Output: { a: { b: { c: PropertyKey } } } + +export type UnionToIntersection = ( + Union extends unknown + ? (distributedUnion: Union) => void + : never +) extends ((mergedIntersection: infer Intersection) => void) + ? Intersection & Union + : never; + + +type PathToType = T extends [infer Head, ...infer Rest] + ? Head extends string + ? Head extends `${number}` + ? Rest extends string[] + ? PathToType[] + : never + : Rest extends string[] + ? { [K in Head & string]: PathToType } + : never + : never + : unknown; + +export type Generate = UnionToIntersection>>; +type Get = GetWithArray>; + +export function getByPath(obj: O, path: K): Get; +export function getByPath(obj: Record, path: string): unknown { + const keys = path.split('.'); + let currentObj = obj; + + for (const key of keys) { + const value = currentObj[key]; + + if (value === undefined || value === null) return undefined; + + currentObj = value as Record; + } + + return currentObj; +} diff --git a/packages/stdlib/src/arrays/index.ts b/packages/stdlib/src/arrays/index.ts new file mode 100644 index 0000000..64eab40 --- /dev/null +++ b/packages/stdlib/src/arrays/index.ts @@ -0,0 +1 @@ +export * from './getByPath'; \ No newline at end of file diff --git a/packages/stdlib/src/text/index.ts b/packages/stdlib/src/text/index.ts index c740f6d..c4fea60 100644 --- a/packages/stdlib/src/text/index.ts +++ b/packages/stdlib/src/text/index.ts @@ -1,2 +1,3 @@ export * from './levenshtein-distance'; -export * from './trigram-distance'; \ No newline at end of file +export * from './trigram-distance'; +export * from './template'; \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.test-d.ts b/packages/stdlib/src/text/template/index.test-d.ts new file mode 100644 index 0000000..7842c4c --- /dev/null +++ b/packages/stdlib/src/text/template/index.test-d.ts @@ -0,0 +1,105 @@ +import { describe, expectTypeOf, it } from "vitest"; +import type { ClearPlaceholder, ExtractPlaceholders } from "./index"; + +describe('template', () => { + describe('ClearPlaceholder', () => { + it('ignores strings without braces', () => { + type actual = ClearPlaceholder<'name'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('removes all balanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}'>; + type actual2 = ClearPlaceholder<'{{name}}'>; + type actual3 = ClearPlaceholder<'{{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('removes all unbalanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}}'>; + type actual2 = ClearPlaceholder<'{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + }); + + describe('ExtractPlaceholders', () => { + it('string without placeholders', () => { + type actual = ExtractPlaceholders<'Hello name, how are?'>; + type expected = never; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one idexed placeholder', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you?'>; + type expected = '0'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two indexed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>; + type expected = '0' | '1'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one key placeholder', () => { + type actual = ExtractPlaceholders<'Hello {name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two key placeholders', () => { + type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>; + type expected = 'name' | 'managers.0.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with mixed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>; + type expected = '0' | '1.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.test.ts b/packages/stdlib/src/text/template/index.test.ts new file mode 100644 index 0000000..63f9797 --- /dev/null +++ b/packages/stdlib/src/text/template/index.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { templateObject } from './index'; + +describe('templateObject', () => { + // it('replace template placeholders with corresponding values from args', () => { + // const template = 'Hello, {names.0}!'; + // const args = { names: ['John'] }; + // const result = templateObject(template, args); + // expect(result).toBe('Hello, John!'); + // }); + + // it('replace template placeholders with corresponding values from args', () => { + // const template = 'Hello, {name}!'; + // const args = { name: 'John' }; + // const result = templateObject(template, args); + // expect(result).toBe('Hello, John!'); + // }); + + // it('replace template placeholders with fallback value if corresponding value is undefined', () => { + // const template = 'Hello, {name}!'; + // const args = { age: 25 }; + // const fallback = 'Guest'; + // const result = templateObject(template, args, fallback); + // expect(result).toBe('Hello, Guest!'); + // }); + + // it(' replace template placeholders with fallback value returned by fallback function if corresponding value is undefined', () => { + // const template = 'Hello, {name}!'; + // const args = { age: 25 }; + // const fallback = (key: string) => `Unknown ${key}`; + // const result = templateObject(template, args, fallback); + // expect(result).toBe('Hello, Unknown name!'); + // }); + + it('replace template placeholders with nested values from args', () => { + const template = 'Hello, {user.name}!'; + const args = { user: { name: 'John' } }; + const result = templateObject(template, args); + + expect(result).toBe('Hello, John!'); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.ts b/packages/stdlib/src/text/template/index.ts new file mode 100644 index 0000000..7c6cbee --- /dev/null +++ b/packages/stdlib/src/text/template/index.ts @@ -0,0 +1,52 @@ +import { getByPath, type Generate } from '../../arrays'; + +/** + * Type of a value that will be used to replace a placeholder in a template. + */ +type StringPrimitive = string | number | bigint | null | undefined; + +/** + * Type of a fallback value when a template key is not found. + */ +type TemplateFallback = string | ((key: string) => string); + +/** + * Type of an object that will be used to replace placeholders in a template. + */ +type TemplateArgsObject = StringPrimitive[] | { [key: string]: TemplateArgsObject | StringPrimitive }; + +/** + * Type of a template string with placeholders. + */ +const TEMPLATE_PLACEHOLDER = /{([^{}]+)}/gm; + +export type ClearPlaceholder = + T extends `${string}{${infer Template}` + ? ClearPlaceholder