Skip to content

Commit

Permalink
fix the function changing behavior of underlying functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Oskar-V committed Dec 6, 2024
1 parent 64b9f84 commit daf3812
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 20 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Create Release and Publish

# Trigger the create release workflow
# Trigger the release workflow
on:
push:
branches:
- main
tags:
- "v*"

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ivl",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "Oskar Voorel",
"email": "oskar@voorel.com"
Expand Down
37 changes: 25 additions & 12 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import type { RULES, SCHEMA, CHECKABLE_OBJECT, CHECKED_SCHEMA, RULES_SYNC, SCHEMA_SYNC, CHECKED_SCHEMA_SYNC, SCHEMA_OPTIONS, RULE } from '@types';
import type {
RULES,
SCHEMA,
CHECKABLE_OBJECT,
CHECKED_SCHEMA,
RULES_SYNC, SCHEMA_SYNC,
CHECKED_SCHEMA_SYNC,
SCHEMA_OPTIONS
} from '@types';

const DEFAULT_SCHEMA_OPTIONS: SCHEMA_OPTIONS = {
strict: false,
// break_early: false // To be implemented
};

/**
* Detect if a function is async or not
*
* @param {Function} fn a rules object
* @returns {boolean} true if any of the rules is an async function otherwise false
*/
export const isAsyncFunction = (fn: Function) =>
typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'

/**
* Detect if a RULES object has any async rules in it
*
* @param {Record<string, RULE>|Record<string, RULE>[]} rules a rules object
* @param {RULES|RULES[]} rules a rules object
* @returns {boolean} true if any of the rules is an async function otherwise false
*/
export const hasAsyncFunction = (rules: Record<string, RULE> | Record<string, RULE>[]): boolean => {
export const hasAsyncFunction = (rules: RULES | RULES[]): boolean => {
if (Array.isArray(rules)) {
return rules.some((e) =>
Object.values(e).some((rule =>
typeof rule === 'function' && rule.constructor.name === 'AsyncFunction'
))
Object.values(e).some(isAsyncFunction)
)
}
return Object.values(rules).some(rule =>
typeof rule === 'function' && rule.constructor.name === 'AsyncFunction'
)
return Object.values(rules).some(isAsyncFunction)
};

/**
Expand All @@ -37,7 +50,7 @@ export const getValueErrorsAsync = async (
rules: RULES,
...overload: unknown[]
): Promise<string[]> => {
const errors: { [index: string]: Promise<boolean> | boolean } = {};
const errors: { [key: string]: Promise<boolean> | boolean } = {};
Object.entries(rules).forEach(([key, rule]) => {
// Wrap everything into a promise
errors[key] = Promise.resolve(false)
Expand Down Expand Up @@ -70,8 +83,8 @@ export const getSchemaErrorsAsync = async <T extends keyof CHECKABLE_OBJECT>(
schema: { [K in T]: RULES | RULES[] },
options: SCHEMA_OPTIONS = DEFAULT_SCHEMA_OPTIONS,
...overload: unknown[]
) => {
const errors: Partial<CHECKED_SCHEMA_SYNC<T>> | Promise<T[]>[] = {};
): CHECKED_SCHEMA<T> => {
const errors: Partial<CHECKED_SCHEMA_SYNC<T>> = {};
const validationPromises: Promise<void>[] = [];
Object.entries<RULES | RULES[]>(schema).forEach(([key, rules]) => {
let promise;
Expand Down
10 changes: 7 additions & 3 deletions src/helpers/schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Functions which affect the a whole rule set - to be used inside schema objects

import type { RULES } from '@types';
import { isAsyncFunction } from 'core';

export const allowUndefined = (rules: RULES): RULES =>
Object.entries(rules).reduce((acc, [key, rule]) => ({
...acc, [key]: (i: unknown, ...overload: unknown[]) => typeof i === 'undefined' ? true : rule(i, ...overload)
}), {});
Object.entries(rules).reduce((acc, [key, rule]) => {
if (isAsyncFunction(rule)) {
return { ...acc, [key]: async (i: unknown, ...overload: unknown[]) => typeof i === 'undefined' ? true : rule(i, ...overload) }
}
return { ...acc, [key]: (i: unknown, ...overload: unknown[]) => typeof i === 'undefined' ? true : rule(i, ...overload) }
}, {});

export const preprocess = (fn: Function, rules: RULES): RULES =>
Object.entries(rules).reduce((acc, [key, rule]) => ({
Expand Down
6 changes: 3 additions & 3 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import type { SCHEMA } from '../src/types';

describe('Successfully detect failing rules', () => {
const passing_input = 'string';
const failing_input = undefined;
const failing_input = 123;
const conditional_rules = { "Is string": (i: unknown) => typeof i === 'string' }
const passing_rules = { "Passes": () => true }
const failing_rules = { "Fails": () => false }

test('Smart rules running as async', async () => {
const func = getValueErrors(passing_input, { "Async": async (i) => true });
const func = getValueErrors(passing_input, { "Async": async () => await Promise.resolve(true) });
expect(func.constructor.name).toBe('Promise');
expect(await func).toBeArray();
})

test('Smart rules running as sync', () => {
const func = getValueErrors(passing_input, { "Sync": (i) => true });
const func = getValueErrors(passing_input, { "Sync": () => true });
expect(func).toBeArray();
});

Expand Down
83 changes: 83 additions & 0 deletions test/schema-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, test, expect } from 'bun:test'

import { getSchemaErrors } from '../src';
import { allowUndefined } from '../src/helpers'

describe('Test allowUndefined schema helper', () => {
const passing_key_object = { incoming_key: "string" }
const failing_key_object = { incoming_key: 123 }
const undefined_key_object = { undefined_key: 'string' };


test('Helper doesn\'t alter the underlying function type', async () => {
const schema = {
Sync: () => true,
Async: async () => await Promise.resolve(true),
};

const result = allowUndefined(schema);

// Check that function behavior remains unchanged
expect(await result.Async()).toBe(await schema.Async());
expect(result.Sync()).toBe(schema.Sync());

// Check that async and sync functions retain their types
expect(result.Async.constructor.name).toBe(schema.Async.constructor.name);
expect(result.Sync.constructor.name).toBe(schema.Sync.constructor.name);
});

test('Undefined value on synchronous rules', () => {
const schema = {
incoming_key: allowUndefined({
"Passes": () => true,
"Fails": () => false,
"Is string": (i: unknown) => typeof i === 'string',
})
}
const i = getSchemaErrors(passing_key_object, schema);
const j = getSchemaErrors(failing_key_object, schema);
const k = getSchemaErrors(undefined_key_object, schema);

// Make sure all functions ran as sync
for (const func of [i, j, k]) {
expect(func.constructor.name).not.toBe('Promise')
}

expect(i).toEqual({ incoming_key: ['Fails'] })
expect(j).toEqual({ incoming_key: ['Fails', 'Is string'] })
expect(k).toEqual({ incoming_key: [] })
})

test('Undefined value on asynchronous rules', async () => {
const schema = {
incoming_key: allowUndefined({
"Passes": async () => await Promise.resolve(true),
"Fails": async () => await Promise.resolve(false),
"Rejects": async () => await Promise.reject(),
"Is string": async (i: unknown) => await Promise.resolve(typeof i === 'string'),
})
};

const i = getSchemaErrors(passing_key_object, schema);
const j = getSchemaErrors(failing_key_object, schema);
const k = getSchemaErrors(undefined_key_object, schema);


// Make sure all functions ran as async
for (const func of [i, j, k]) {
expect(func.constructor.name).toBe('Promise');
}

const t = (await Promise.allSettled([i, j, k]))
const answers = [
['Fails', 'Rejects'],
['Fails', 'Rejects', 'Is string'],
[]
]
for (let idx = 0; idx < t.length; idx++) {
expect(t[idx].status).toEqual("fulfilled");
// @ts-ignore
expect(t[idx].value).toEqual({ 'incoming_key': answers[idx] })
}
})
});

0 comments on commit daf3812

Please sign in to comment.