Skip to content

Commit

Permalink
correctly use defineProperties and defineProperty, add required, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Aug 20, 2024
1 parent 11c4ad9 commit d501174
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 188 deletions.
247 changes: 174 additions & 73 deletions packages/getters/src/utils/create-getter.test.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,211 @@
import { assertType, describe, expect, it } from 'vitest';
/* eslint-disable no-restricted-syntax */

import { describe, expect, expectTypeOf, it, test } from 'vitest';
import { createGetter } from './create-getter.js';
import { Getter } from './getter.js';

type City = 'Seattle' | 'San Francisco' | 'New York City';

interface Address {
city: string;
state: string;
country?: string;
city: City;
state?: string;
}

interface Employee {
interface Person {
firstName: string;
lastName?: string;
address?: Address;
}

const employee: Employee = {
const alice: Person = {
firstName: 'Alice',
lastName: 'Liddell',
address: {
city: 'San Francisco',
state: 'California',
},
};

const getFirstName = createGetter((employee?: Employee) => employee?.firstName);
const getLastName = createGetter((employee?: Employee) => employee?.lastName);
const getAddress = createGetter((employee?: Employee) => employee?.address);
const getCity = createGetter((address?: Address) => address?.city);
const getCountry = createGetter((address?: Address) => address?.country);
const getFirstLetter = createGetter((value?: string) => value?.[0]);
const bob: Person = {
firstName: 'Bob',
address: {
city: 'Seattle',
},
};

const charlie: Person = {
firstName: 'Charlie',
lastName: '',
};

describe('createGetter()', () => {
describe('getter()', () => {
it('gets the value via the function passed into `createGetter()`', () => {
expect(getFirstName(employee)).toBe('Alice');
});
const selectFirstName = (p?: Person) => p?.firstName;
const selectStringIndexOne = (a?: string) => a?.[1];

it('creates a getter', () => {
let getFirstNameFromPerson;
expectTypeOf((getFirstNameFromPerson = createGetter(selectFirstName))).toEqualTypeOf<
Getter<Person, string>
>();
expect(getFirstNameFromPerson).toBeInstanceOf(Function);
expect(getFirstNameFromPerson).toHaveProperty(['optional', 'pipe']);
});

it('creates a getter with optional', () => {
let getFirstNameFromPerson_optional;
expectTypeOf(
(getFirstNameFromPerson_optional = createGetter(selectFirstName).optional),
).toEqualTypeOf<Getter<Person, string | undefined>>();

expect(getFirstNameFromPerson_optional).toBeInstanceOf(Function);
expect(getFirstNameFromPerson_optional).toHaveProperty('optional');
expect(getFirstNameFromPerson_optional).toHaveProperty('pipe');

expect(getFirstNameFromPerson_optional.optional).toBe(getFirstNameFromPerson_optional);
});

it('creates a getter pipe', () => {
const getFirstName = createGetter(selectFirstName);
const getSecondLetter = createGetter(selectStringIndexOne);

let getSecondLetterOfFirstNameFromPerson;
expectTypeOf(
(getSecondLetterOfFirstNameFromPerson = getFirstName.pipe(getSecondLetter)),
).toEqualTypeOf<Getter<Person, string>>();

expect(getSecondLetterOfFirstNameFromPerson).toBeInstanceOf(Function);
expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('optional');
expect(getSecondLetterOfFirstNameFromPerson).toHaveProperty('pipe');
});
});

it('throws when the whole value is undefined', () => {
describe('getting values and optional', () => {
const getFirstName = createGetter((p?: Person) => p?.firstName);
const getLastName = createGetter((p?: Person) => p?.lastName);

it('gets the expected value', () => {
expect(getFirstName(alice)).toBe('Alice');
expect(getFirstName.optional(alice)).toBe('Alice');

expect(getLastName(alice)).toBe('Liddell');
expect(getLastName.optional(alice)).toBe('Liddell');
});

describe('undefined in the getter', () => {
it('handles undefined input', () => {
expect(() => getFirstName(undefined)).toThrow();
expect(getFirstName.optional(undefined)).toBeUndefined();
});

it('throws for an undefined property', () => {
expect(() => getLastName(employee)).toThrow();
it('handles undefined property', () => {
expect(() => getLastName(bob)).toThrow();
expect(getLastName.optional(bob)).toBeUndefined();
});
});

it('does not throw if a value is falsey but not undefined', () => {
const employee: Employee = { firstName: 'Alice', lastName: '' };
expect(() => getLastName(employee)).not.toThrow();
});
test('successfully returns a falsy value', () => {
expect(() => getLastName(charlie)).not.toThrow();
});
});

describe('getter.optional()', () => {
it('returns `undefined` when the whole value is undefined', () => {
expect(getLastName.optional()(undefined)).toBeUndefined();
});
describe('getter pipes', () => {
const selectAddress = (p?: Person) => p?.address;
const getAddressFromPerson = createGetter(selectAddress);
const selectCity = (a?: Address) => a?.city;
const getCityFromAddress = createGetter(selectCity);
const getStateFromAddress = createGetter((a?: Address) => a?.state);

it('returns `undefined` for an undefined property', () => {
expect(getLastName.optional()(employee)).toBeUndefined();
});
it('pipes the getters together and returns the final result', () => {
let getCityFromPerson;

expectTypeOf((getCityFromPerson = getAddressFromPerson.pipe(getCityFromAddress))).toEqualTypeOf<
Getter<Person, City>
>();

expect(getCityFromPerson(alice)).toBe('San Francisco');
expect(getCityFromPerson(bob)).toBe('Seattle');
});

describe('getter.pipe()', () => {
it('pipes the getters together and returns the final result', () => {
expect(getAddress.pipe(getCity)(employee)).toBe('San Francisco');
});
describe('undefined in the pipe', () => {
let getStateFromPerson;

expectTypeOf(
(getStateFromPerson = getAddressFromPerson.pipe(getStateFromAddress)),
).toEqualTypeOf<Getter<Person, string>>();

it('throws when any value in the property chain is undefined', () => {
expect(() => getAddress.pipe(getCity)(undefined)).toThrow();
expect(() => getAddress.pipe(getCity)({ firstName: 'Alice' })).toThrow();
expect(() => getAddress.pipe(getCountry)(employee)).toThrow();
it('throws on undefined', () => {
expect(() => getStateFromPerson(undefined)).toThrow();
expect(getStateFromPerson(alice)).toBe('California');
expect(() => getStateFromPerson(bob)).toThrow();
});

describe('getter.pipe() with .optional())', () => {
const employee: Employee = {
firstName: 'Alice',
address: {
city: '', // `getFirstLetter` will return undefined
state: 'California',
},
};

it('does not throw when the first getter is used with `.optional()` and some value in the chain is undefined', () => {
expect(() =>
getAddress.optional().pipe(getCity).pipe(getFirstLetter)(employee),
).not.toThrow();
});

it('does not throw when a later getter is used with `.optional()` and some value in the chain is undefined', () => {
const baseGetter = getAddress.pipe(getCity).pipe(getFirstLetter);

// Before testing that it _doesn't_ throw with `.optional()`, first make
// sure that it _does_ throw without it, to ensure that this test is
// valid.
expect(() => baseGetter(employee)).toThrow();
expect(() => baseGetter.optional()(employee)).not.toThrow();
});

it('does throw when used without `.optional()` and some value in the chain is undefined', () => {
expect(() => getAddress.pipe(getCity).pipe(getFirstLetter)(employee)).toThrow();
});
it("doesn't throw on undefined when optional", () => {
expect(getStateFromPerson.optional(undefined)).toBeUndefined();
expect(getStateFromPerson.optional(alice)).toBe('California');
expect(getStateFromPerson.optional(bob)).toBeUndefined();
});
});

// Type assertions - these will be run at build time, rather than at test
// time.
assertType<string>(getAddress.pipe(getCity)(employee));
// @ts-expect-error - Assert that `string` on its own is incorrect for an
// optional getter -- it should be `string | undefined`.
assertType<string>(getAddress.pipe(getCity).optional()(employee));
assertType<string | undefined>(getAddress.pipe(getCity).optional()(employee));
describe('longer chains', () => {
const getSecondLetter = createGetter((s?: string) => s?.[1]);
it('applies optional to the chain', () => {
const notOptionalStateLetter = getAddressFromPerson
.pipe(getStateFromAddress)
.pipe(getSecondLetter);
expect(notOptionalStateLetter(alice)).toBe('a');
expect(() => notOptionalStateLetter(bob)).toThrow();
expect(() => notOptionalStateLetter(charlie)).toThrow();

const notOptionalCityLetter = getAddressFromPerson
.pipe(getCityFromAddress)
.pipe(getSecondLetter);
expect(notOptionalCityLetter(alice)).toBe('a');
expect(notOptionalCityLetter(bob)).toBe('e');
expect(() => notOptionalCityLetter(charlie)).toThrow();

const midOptionalStateLetter = getAddressFromPerson // address is required
.pipe(getStateFromAddress) // state is optional, turning the whole chain optional
.optional.pipe(getSecondLetter); // letter is required
expect(midOptionalStateLetter(alice)).toBe('a');
expect(midOptionalStateLetter(bob)).toBeUndefined();
expect(midOptionalStateLetter(charlie)).toBeUndefined();

const midOptionalCityLetter = getAddressFromPerson // address is required
.pipe(getCityFromAddress) // city is optional, turning the whole chain optional
.optional.pipe(getSecondLetter); // letter is required
expect(midOptionalCityLetter(alice)).toBe('a');
expect(midOptionalCityLetter(bob)).toBe('e');
expect(midOptionalCityLetter(charlie)).toBeUndefined();

const firstOptionalStateLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional
.pipe(getStateFromAddress) // state is required
.pipe(getSecondLetter); // letter is required
expect(firstOptionalStateLetter(alice)).toBe('a');
expect(firstOptionalStateLetter(bob)).toBeUndefined();
expect(firstOptionalStateLetter(charlie)).toBeUndefined();

const firstOptionalCityLetter = getAddressFromPerson.optional // address is optional, turning the whole chain optional
.pipe(getCityFromAddress) // city is required
.pipe(getSecondLetter); // letter is required
expect(firstOptionalCityLetter(alice)).toBe('a');
expect(firstOptionalCityLetter(bob)).toBe('e');
expect(firstOptionalCityLetter(charlie)).toBeUndefined();
});

it('applies required to the chain', () => {
const allOptionalStateLetter = getAddressFromPerson.optional
.pipe(getStateFromAddress)
.optional.pipe(getSecondLetter).optional;
expect(allOptionalStateLetter.required(alice)).toBe('a');
expect(() => allOptionalStateLetter.required(bob)).toThrow();
expect(() => allOptionalStateLetter.required(charlie)).toThrow();

const allOptionalCityLetter = getAddressFromPerson.optional
.pipe(getCityFromAddress)
.optional.pipe(getSecondLetter).optional;
expect(allOptionalCityLetter.required(alice)).toBe('a');
expect(allOptionalCityLetter.required(bob)).toBe('e');
expect(() => allOptionalCityLetter.required(charlie)).toThrow();
});
});
});
Loading

0 comments on commit d501174

Please sign in to comment.