Skip to content

Commit

Permalink
Merge pull request #4 from Geordi7/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Geordi7 authored Feb 6, 2023
2 parents d7cd4c7 + 018ac97 commit f73a5ae
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 28 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@geordi7/fint",
"version": "0.1.2",
"version": "0.1.3",
"description": "Strongly typed tools for typescript",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
28 changes: 28 additions & 0 deletions src/config-merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import { rec, set } from "./transforms";
import { raise } from "./misc";
import { flow } from "./pipes";
import { clone, is, PlainOldData } from "./types";

// merge semantics:
// deep copy
// replace keys in host
// append items in arrays
// replace scalars with whatever comes in
export const configMerge = (previous: PlainOldData, next: PlainOldData): PlainOldData =>
configMergeRecursive(previous, next, '');

const configMergeRecursive = (previous: PlainOldData, next: PlainOldData, path: string): PlainOldData => {
return is.array(previous) ? is.array(next) ?
[...clone(previous), ...clone(next)] :
raise(`${path}: cannot merge non-array into array`) :
is.record(previous) ? is.record(next) ?
flow(set.union(rec.k(previous), rec.k(next)),
rec.fromKeys(k => configMergeRecursive(previous[k], next[k], `${path}.${k}`))) :
raise(`${path}: cannot merge non-record into record`) :
// previous is scalar
is.nullish(next) ?
previous :
clone(next)
;
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

export * from './config-merge';
export * from './misc';
export * from './pipes';
export * from './proxy';
Expand Down
6 changes: 3 additions & 3 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// r.isErr() -- to check if its Err
// r.err() -- to get the err value or throw an exception

import { isPromise } from "./types";
import { is, MaybePromise } from "./types";

export type Result<R,E> = Ok<R> | Err<E>;
export type PResult<R,E> = Promise<Result<R,E>>;
Expand All @@ -31,10 +31,10 @@ export function Err<E>(error: E): Result<never, E> {return new ResultErr(error);
export const Result = {
// wrap a function that can raise an exception
// create a function which captures success in Ok and exceptions in Err
lift: (<V extends unknown[], R>(fn: (...v: V) => R) => (...v: V): Result<R,Error> | PResult<R,Error> => {
lift: (<V extends unknown[], R>(fn: (...v: V) => R) => (...v: V): MaybePromise<Result<R,Error>> => {
try {
const r = fn(...v);
if (isPromise(r)) {
if (is.promise(r)) {
return r.then(v => Ok(v))
.catch(e => e instanceof Error ? Err(e) :
Err(new Error('unexpected error type', {cause: e})));
Expand Down
39 changes: 39 additions & 0 deletions src/tests/test-config-merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

import {describe, it} from 'mocha';
import {expect} from 'chai';

import {
configMerge
} from '../config-merge';

export function test() {
describe('config-merge', () => {
describe('configMerge', () => {
it('appends arrays', () => {
const a = [1,2,3];
const b = [4,5,6];
const c = configMerge(a,b);

expect(c).deep.equals([1,2,3,4,5,6]);
});

it('merges objects', () => {
const a = {a: 1, b: 1};
const b = {b: 2, c: 2};
const c = configMerge(a,b);

expect(c).deep.equals({a: 1, b: 2, c: 2});
});

it('merges at depth', () => {
const a = {a: {aa: {aaa: 1, aab: 1}, ab: [1,2]}};
const b = {a: {aa: {aab: 2, aac: 2}, ab: [3,4]}};
const c = configMerge(a,b);

expect(c).deep.equals({a: {aa: {aaa: 1, aab: 2, aac: 2}, ab: [1,2,3,4]}})
})
});
});
}


3 changes: 3 additions & 0 deletions src/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ transforms.test();

import * as result from './test-result';
result.test();

import * as configMerge from './test-config-merge';
configMerge.test();
63 changes: 41 additions & 22 deletions src/transforms.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@

import { assert } from './misc';
import {flow} from './pipes';
import { is } from './types';

export const identity = <T>(t: T): T => t;

export const inspect = <V>(fn: (v: V) => unknown) => <U extends V>(v: U): U => (fn(v), v);

export const rec = ({
k: Object.keys as <K extends PropertyKey>(record: Record<K,unknown>) => K[],
v: Object.values as <V>(r: Record<PropertyKey, V>) => V[],
e: Object.entries as <K extends PropertyKey, V>(r: Record<K, V>) => [K, V][],
f: Object.fromEntries as <K extends PropertyKey, V>(a: [K, V][]) => Record<K,V>,
f: Object.fromEntries as <K extends PropertyKey, V>(a: Iterable<[K, V]>) => Record<K,V>,

// point-free tools

map: <V, U, K extends PropertyKey>(fn: (v: V, k: K) => U) =>
(record: Record<K,V>): Record<K,U> =>
rec.e(record).reduce((l, [k,v]) => (l[k] = fn(v,k), l), {} as Record<K,U>),

filter: <V, K extends PropertyKey>(fn: (v: V, k: K) => boolean) =>
filter: (<V, K extends PropertyKey>(fn: (v: V, k: K) => boolean) =>
(record: Record<K,V>): Record<K,V> =>
rec.e(record).reduce((l, [k,v]) => (fn(v,k) ? l[k] = v : null, l), {} as Record<K,V>),
rec.e(record).reduce((l, [k,v]) => (fn(v,k) ? l[k] = v : null, l), {} as Record<K,V>)) as
& (<V, U extends V, K extends PropertyKey>(fn: (v: V, k: K) => v is U) => (record: Record<K,V>) => Record<K,U>)
& (<V, K extends PropertyKey>(fn: (v: V, k: K) => boolean) => (record: Record<K,V>) => Record<K,V>),

reduce: <V, U, K extends PropertyKey>(u: U, fn: (u: U, v: V, k: K) => U) =>
(record: Record<K, V>): U => rec.e(record).reduce((u, [k,v]) => fn(u,v,k), u),
Expand All @@ -40,13 +45,20 @@ export const rec = ({
}
return false;
},

// take a set of keys and a function and create a record with them
fromKeys: <K extends PropertyKey,V>(fn: (k: K) => V) => (keys: Iterable<K>): Record<K,V> =>
rec.f(flow(keys,
iter.map(k => [k, fn(k)] as [K,V]),
iter.filter(([_,v]) => !is.undefined(v)))),
});

export const arr = {
map: <V,U>(fn: (v: V, i: number) => U) =>
(array: V[]): U[] => array.map(fn),
filter: <V>(fn: (v: V, i: number) => boolean) =>
(array: V[]): V[] => array.filter(fn),
filter: ((fn: (v: any, i: number) => unknown) => (a: any[]) => a.filter(fn)) as
& (<V,U extends V>(fn: (v: V, i: number) => v is U) => (array: V[]) => U[])
& (<V>(fn: (v: V, i: number) => boolean) => (array: V[]) => V[]),
reduce: <V,U>(u: U, fn: (u: U, v: V, i: number) => U) =>
(array: V[]): U => array.reduce(fn, u),
inspect: <V>(fn: (v: V, i: number) => void) =>
Expand Down Expand Up @@ -163,9 +175,21 @@ export const str = {

export const iter = {
ate: <V>(i: Iterable<V>): Iterator<V> => i[Symbol.iterator](),
map:<V,U>(fn: (v: V) => U) => function*(i: IterableIterator<V>): IterableIterator<U> {
for(const item of i) yield fn(item);
map:<V,U>(fn: (v: V) => U) => function*(i: Iterable<V>): IterableIterator<U> {
for (const item of i) yield fn(item);
},
filter: (<V>(fn: (v: V) => boolean) => function*<U extends V>(i: Iterable<U>): IterableIterator<U> {
for (const item of i)
if (fn(item))
yield item;
}) as
& (<V>(fn: (v: V) => boolean) => <U extends V>(i: Iterable<U>) => IterableIterator<U>)
& (<V,U extends V>(fn: (v: V) => v is U) => (i: Iterable<V>) => IterableIterator<U>),
reduce: (<V,U>(u: U, fn: (u: U, v: V) => U) => (i: Iterable<V>): U => {
for (const item of i)
u = fn(u,item);
return u;
}),
zip: function*<Q extends Iterable<unknown>[]>(...iters: Q): IterableIterator<IterZipType<Q>> {
const ptrs = iters.map(i => iter.ate(i));
while(true) {
Expand All @@ -189,17 +213,10 @@ export const iter = {
},
step: <T>(step: (t: T) => void) => (g: IterableIterator<T>): boolean =>
flow(g.next(), n => n.done ? false : (step(n.value), true)),
collect: <T>(i: IterableIterator<T>): T[] => {
const r = [] as T[];
while(true) {
const n = i.next();
if (n.done)
return r;
else
r.push(n.value);
}
collect: <T>(i: Iterable<T>): T[] => {
return [...i] as T[];
},
enumerate: function*<T>(i: IterableIterator<T>): IterableIterator<[number,T]> {
enumerate: function*<T>(i: Iterable<T>): IterableIterator<[number,T]> {
yield * iter.zip(iter.count(), i);
},
count: function*(): Generator<number, never, void> {
Expand Down Expand Up @@ -257,7 +274,7 @@ export const iter = {
while(true) yield t;
},

aperture: <V>(n: number) => function*(i: IterableIterator<V>): Generator<V[], void, void> {
aperture: <V>(n: number) => function*(i: Iterable<V>): Generator<V[], void, void> {
const ap = [] as V[];
for(const item of i) {
ap.push(item);
Expand All @@ -284,17 +301,19 @@ export const maybe = {
thing == undefined ? orElse() : map(thing),
mapOr: <V,U>(map: (v: V) => U, or: U) => (thing: V | undefined | null) : U =>
thing == undefined ? or : map(thing),
or: <V,U>(u: U) => (thing: V | undefined | null) : V | U =>
or: <U>(u: U) => <V>(thing: V | undefined | null) : V | U =>
thing ?? u,
orElse: <V,U>(fn: () => U) => (thing: V | undefined | null) : V | U =>
orElse: <U>(fn: () => U) => <V>(thing: V | undefined | null) : V | U =>
thing ?? fn(),
}

export const set = {
union: <T>(...sets: Set<T>[]): Set<T> => {
union: <T>(...sets: Iterable<T>[]): Set<T> => {
return new Set(iter.chain(...sets));
},
intersect: <T>(...sets: Set<T>[]): Set<T> => {
intersect: <T>(...collections: Iterable<T>[]): Set<T> => {
const sets = collections.map(c =>
c instanceof Set ? c : new Set(c));
const [head, ...tail] = sets;

const result = new Set(head);
Expand Down
43 changes: 41 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
import { raise } from "./misc";
import { rec } from "./transforms";

export type PlainOldData =
| null
| string
| number
| boolean
| integer
| unsigned
| bigint
| PlainOldData[]
| {[K in string]: PlainOldData}
;

export type PrettyIntersection<V> = Extract<{ [K in keyof V]: V[K] }, unknown>;

export type MaybePromise<T> = T | Promise<T>;

export const isPromise = <T>(thing: MaybePromise<T>): thing is Promise<T> =>
('then' in (thing as object) && typeof (thing as {then: unknown}).then === "function");
export const is = {
undefined: (thing: unknown): thing is undefined => thing === undefined,
null: (thing: unknown): thing is null => thing === null,

string: (thing: unknown): thing is string => (typeof thing) === 'string',
number: (thing: unknown): thing is number => (typeof thing) === 'number',
object: (thing: unknown): thing is object => (typeof thing) === 'object',
function: (thing: unknown): thing is ((...a: any[]) => unknown) => (typeof thing) === 'function',

nullish: (t: unknown): t is null | undefined => (t == null),

array: <T>(thing: unknown | T[]): thing is T[] => Array.isArray(thing),

record: <K extends PropertyKey, V>(thing: unknown | Record<K,V>): thing is Record<K,V> =>
!is.array(thing) && is.object(thing),

promise: <T>(thing: MaybePromise<T>): thing is Promise<T> =>
('then' in (thing as object) && is.function((thing as {then: unknown}).then)),
}

// nominal types
declare const brand: unique symbol;
Expand All @@ -26,3 +56,12 @@ export const unsigned = {
raise(`${u} is not an integer`);
}
}

export const clone = <V extends PlainOldData>(v: V): V => {
if (is.array(v))
return v.map(clone) as V;
else if (is.record(v))
return rec.map(clone)(v) as V;
else
return v;
}

0 comments on commit f73a5ae

Please sign in to comment.