-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor!: redesign entire API set (#4)
- Loading branch information
Showing
37 changed files
with
704 additions
and
326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,132 @@ | ||
import { isPlainObject } from "./isPlainObject.ts" | ||
import { normalizePath } from "./normalizePath.ts" | ||
|
||
/** | ||
* A frozen plain object that accepts paths as index reference. It also supports `Symbol.iterator` interface that enumerates nested leaves with the keys being paths. | ||
* A frozen plain object that accepts paths as index reference. It also supports `Symbol.iterator` interface that enumerates nested values. | ||
*/ | ||
export type Distree<T> = { | ||
readonly [key: string]: Distree<T> | T | undefined | ||
[Symbol.iterator](): Iterator<[string, T]> | ||
type Distree<T> = { | ||
readonly "/": Distree<T> | ||
readonly ".": Distree<T> | ||
readonly "..": Distree<T> | ||
readonly [key: string]: Distree<T> | T | ||
readonly [Symbol.iterator]: () => Iterator<[string, T]> | ||
} | ||
|
||
export const symbol: unique symbol = Symbol() | ||
namespace Distree { | ||
export type Initializer<T> = Distree<T> | { readonly [key: string]: Initializer<T> | T } | ||
export type ItemInitializer<T> = Distree<T> | T | { readonly [key: string]: ItemInitializer<T> } | ||
} | ||
|
||
export const isPlainObject = (value: unknown): value is object => { | ||
return ( | ||
typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype | ||
) | ||
type DistreeInternal<T> = { | ||
"/": DistreeInternal<T> | ||
".": DistreeInternal<T> | ||
"..": DistreeInternal<T> | ||
[key: string]: DistreeInternal<T> | T | ||
[Symbol.iterator]: () => Iterator<[string, T]> | ||
[symbol]: Metadata<T> | ||
} | ||
|
||
namespace DistreeInternal { | ||
export type Initializer<T> = DistreeInternal<T> | { [key: string]: Initializer<T> | T } | ||
export type ItemInitializer<T> = DistreeInternal<T> | T | { [key: string]: ItemInitializer<T> } | ||
} | ||
|
||
type Metadata<T> = { root: DistreeInternal<T> } | ||
|
||
const symbol: unique symbol = Symbol() | ||
|
||
/** | ||
* Tests if a value is a distree. | ||
* | ||
* @param value The value to be tested. | ||
*/ | ||
export const isDistree = <T>(value: unknown): value is Distree<T> => { | ||
return isPlainObject(value) && symbol in value | ||
const isDistree = <T>(value: unknown): value is Distree<T> => { | ||
return isPlainObject(value) && Object.hasOwn(value, symbol) | ||
} | ||
|
||
const readonly = { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
} as const satisfies PropertyDescriptor | ||
|
||
const resolve = <T>( | ||
distree: DistreeInternal<T>, | ||
path: string, | ||
): DistreeInternal<T> | T | undefined => { | ||
if (path === "/") return distree[symbol].root | ||
const components = normalizePath(path).split(/\/+/) | ||
let resolved: DistreeInternal<T> | T | undefined = distree | ||
for (const component of components) { | ||
if (isDistree(resolved)) { | ||
if (component === "") { | ||
resolved = resolved[symbol].root | ||
} else if (Object.hasOwn(resolved, component)) { | ||
resolved = resolved[component] | ||
} else return undefined | ||
} else return undefined | ||
} | ||
return resolved | ||
} | ||
|
||
const iterate = function* <T>( | ||
distree: DistreeInternal<T>, | ||
prefix: string | undefined, | ||
): Generator<[string, T]> { | ||
for (const [key, value] of Object.entries(distree)) { | ||
const path = prefix === undefined ? key : prefix + "/" + key | ||
if (isDistree(value)) yield* iterate(value, path) | ||
else yield [path, value] | ||
} | ||
} | ||
|
||
const rec = <T>( | ||
content: DistreeInternal.Initializer<T>, | ||
parent: DistreeInternal<T> | undefined, | ||
): DistreeInternal<T> => { | ||
const distree = new Proxy<DistreeInternal<T>>(Object.create(null) as DistreeInternal<T>, { | ||
get: (target, key, receiver) => { | ||
if (typeof key === "string") return resolve(target, key) | ||
else return Reflect.get(target, key, receiver) | ||
}, | ||
}) | ||
|
||
// Populate as a distree | ||
Object.defineProperties(distree, { | ||
[symbol]: { ...readonly, value: { root: parent?.[symbol].root ?? distree } }, | ||
".": { ...readonly, value: distree }, | ||
"..": { ...readonly, value: parent ?? distree }, | ||
[Symbol.iterator]: { | ||
...readonly, | ||
*value() { | ||
yield* iterate(distree, undefined) | ||
}, | ||
}, | ||
}) | ||
|
||
// Copy contents into the new distree | ||
for (const [key, value] of Object.entries(content)) { | ||
if (key.search(/\/+/) !== -1) throw new Error(`Keys are not allowed to contain slashes: ${key}`) | ||
if (isPlainObject(value)) distree[key] = rec(value, distree) | ||
else distree[key] = value | ||
} | ||
|
||
Object.setPrototypeOf(distree, Object.prototype) | ||
Object.freeze(distree) | ||
|
||
return distree | ||
} | ||
|
||
/** | ||
* Tests if a value should be treaded as empty in a distree. An empty value is automatically removed from a distree when set. A value is empty when: | ||
* Creates a distree from the provided content. It doesn't modify the passed object. | ||
* | ||
* 1. The value is `undefined` | ||
* 2. The value is a plain object or array and it has no enumerable own string keys. | ||
* | ||
* @param value The value to be tested. | ||
* @param content The initial content of the distree. Only enumerable own string keys are respected, and nested plain objects are recursively converted to distrees. | ||
* @returns A new distree. | ||
*/ | ||
export const isEmpty = <T>(value: Distree<T> | T | undefined): boolean => { | ||
return ( | ||
value === undefined || | ||
((isPlainObject(value) || Array.isArray(value)) && Object.keys(value).length === 0) | ||
) | ||
const from = <T>(content: Distree.Initializer<T>): Distree<T> => { | ||
if (isPlainObject(content)) return rec(content as DistreeInternal<T>, undefined) | ||
else throw new Error("Provided content is not a plain object.") | ||
} | ||
|
||
export default Distree | ||
export default Distree | ||
export { isDistree, from } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,3 @@ | ||
import Distree, { symbol, isPlainObject } from "./Distree.ts" | ||
import iterate from "./iterate.ts" | ||
import resolve from "./resolve.ts" | ||
|
||
export const readonly: PropertyDescriptor = { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
} as const | ||
|
||
const rec = <T>( | ||
content: Distree<T> | { [key: string]: typeof content | T | undefined }, | ||
parent: Distree<T> | undefined, | ||
): Distree<T> => { | ||
type Mutable<T> = { | ||
-readonly [P in keyof T]: T[P] | ||
} | ||
|
||
const distree = new Proxy<Mutable<Distree<T>>>( | ||
// @ts-expect-error: populate `Symbol.iterator` afterwards | ||
{}, | ||
{ | ||
get: (target, key, receiver) => { | ||
if (typeof key === "string") return resolve(target)(key) | ||
else return Reflect.get(target, key, receiver) | ||
}, | ||
}, | ||
) | ||
|
||
// Populate as a distree | ||
Object.defineProperties(distree, { | ||
[symbol]: { ...readonly, value: undefined }, | ||
"": { ...readonly, value: (parent && parent[""]) ?? distree }, | ||
".": { ...readonly, value: distree }, | ||
"..": { ...readonly, value: parent ?? distree }, | ||
[Symbol.iterator]: { | ||
...readonly, | ||
*value() { | ||
yield* iterate(distree, undefined) | ||
}, | ||
}, | ||
}) | ||
|
||
// Copy contents into the new distree | ||
if (content) { | ||
for (const [key, value] of Object.entries(content)) { | ||
if (isPlainObject(value)) { | ||
distree[key] = rec(value, distree) | ||
} else { | ||
distree[key] = value | ||
} | ||
} | ||
} | ||
|
||
return Object.freeze(distree) | ||
} | ||
|
||
/** | ||
* Creates a distree from the provided content. It doesn't modify the passed object. | ||
* | ||
* @param content The initial content of the distree. Only enumerable own string keys are respected, and nested plain objects are recursively converted to distrees. | ||
*/ | ||
const from = <T>( | ||
content: Distree<T> | { [key: string]: typeof content | T | undefined }, | ||
): Distree<T> => { | ||
return rec(content, undefined) | ||
} | ||
import { from } from "./Distree.ts" | ||
|
||
export default from |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import Distree from "./Distree.ts" | ||
import from from "./from.ts" | ||
import transform from "./transform.ts" | ||
import transformAsync from "./transformAsync.ts" | ||
|
||
const rec = async (path: string): Promise<typeof content> => { | ||
const content: { [key: string]: typeof content | string } = Object.create(null) | ||
for await (const item of Deno.readDir(path)) { | ||
const pathItem = path + "/" + item.name | ||
if (item.isDirectory) { | ||
const [key, value] = [item.name, await rec(pathItem)] | ||
content[key] = value | ||
} else if (item.isFile) { | ||
const [key, value] = [item.name, pathItem] | ||
content[key] = value | ||
} else { | ||
// TODO: handle symlinks (breaking change) | ||
} | ||
} | ||
|
||
Object.setPrototypeOf(content, Object.prototype) | ||
return content | ||
} | ||
|
||
const fromDirectory: { | ||
(path: string): Promise<Distree<string>> | ||
(path: string, filter: string | RegExp): Promise<Distree<string>> | ||
<T>( | ||
path: string, | ||
transform: (value: string, path: string) => Promise<Distree.ItemInitializer<T>>, | ||
): Promise<Distree<T>> | ||
} = async < | ||
T, | ||
F extends | ||
| string | ||
| RegExp | ||
| undefined | ||
| ((value: string, path: string) => Promise<Distree.ItemInitializer<T>>), | ||
>( | ||
path: string, | ||
filter?: F, | ||
): Promise<Distree<F extends string | RegExp | undefined ? string : T>> => { | ||
const distree = from(await rec(path)) as Distree<string> | ||
return ( | ||
filter === undefined | ||
? distree | ||
: typeof filter === "function" | ||
? await transformAsync(distree, filter) | ||
: transform(distree, value => { | ||
if (value.match(filter)) return value | ||
else throw undefined | ||
}) | ||
) as Distree<F extends string | RegExp | undefined ? string : T> | ||
} | ||
|
||
export default fromDirectory |
Oops, something went wrong.