Skip to content

Commit

Permalink
Merge pull request #156 from nobrainr/fix/type-action-selector
Browse files Browse the repository at this point in the history
Fix/type action selector
  • Loading branch information
emyann authored Dec 14, 2019
2 parents 438a168 + 1384bd9 commit 9cc11c4
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 65 deletions.
62 changes: 50 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SCHEMA_OPTIONS_SYMBOL, SchemaOptions } from './morphism';
import { SCHEMA_OPTIONS_SYMBOL, SchemaOptions } from "./morphism";

/**
* A structure-preserving object from a source data towards a target data.
Expand Down Expand Up @@ -35,22 +35,38 @@ export type StrictSchema<Target = any, Source = any> = {
/** `destinationProperty` is the name of the property of the target object you want to produce */
[destinationProperty in keyof Target]:
| ActionString<Source>
| { (iteratee: Source, source: Source[], target: Target[destinationProperty]): Target[destinationProperty] }
| {
(
iteratee: Source,
source: Source[],
target: Target[destinationProperty]
): Target[destinationProperty];
}
| ActionAggregator<Source>
| ActionSelector<Source, Target>
| ActionSelector<Source, Target, destinationProperty>
| StrictSchema<Target[destinationProperty], Source>;
} & { [SCHEMA_OPTIONS_SYMBOL]?: SchemaOptions<Target> };
export type Schema<Target = any, Source = any> = {
/** `destinationProperty` is the name of the property of the target object you want to produce */
[destinationProperty in keyof Target]?:
| ActionString<Source>
| { (iteratee: Source, source: Source[], target: Target[destinationProperty]): Target[destinationProperty] }
| {
(
iteratee: Source,
source: Source[],
target: Target[destinationProperty]
): Target[destinationProperty];
}
| ActionAggregator<Source>
| ActionSelector<Source, Target>
| ActionSelector<Source, Target, destinationProperty>
| Schema<Target[destinationProperty], Source>;
} & { [SCHEMA_OPTIONS_SYMBOL]?: SchemaOptions<Target | any> };

export type Actions<Target, Source> = ActionFunction<Target, Source> | ActionAggregator | ActionString<Target> | ActionSelector<Source>;
export type Actions<Target, Source> =
| ActionFunction<Target, Source>
| ActionAggregator
| ActionString<Target>
| ActionSelector<Source>;

/**
* @interface ActionFunction
Expand Down Expand Up @@ -129,7 +145,9 @@ export type ActionString<Source> = string; // TODO: ActionString should support
* //=> { fooAndBar: { foo: 'foo', bar: 'bar' } }
* ```
*/
export type ActionAggregator<T extends unknown = unknown> = T extends object ? (keyof T)[] | string[] : string[];
export type ActionAggregator<T extends unknown = unknown> = T extends object
? (keyof T)[] | string[]
: string[];
/**
* @interface ActionSelector
* @typeparam Source Source/Input Type
Expand Down Expand Up @@ -157,21 +175,41 @@ export type ActionAggregator<T extends unknown = unknown> = T extends object ? (
*```
*
*/
export interface ActionSelector<Source = object, R = any> {
export interface ActionSelector<
Source = object,
Target = any,
TargetProperty extends keyof Target = any
> {
path: ActionString<Source> | ActionAggregator<Source>;
fn: (fieldValue: any, object: Source, items: Source, objectToCompute: R) => R;
fn: (
fieldValue: any,
object: Source,
items: Source,
objectToCompute: Target
) => Target[TargetProperty];
}

export interface Constructable<T> {
new (...args: any[]): T;
}

export type SourceFromSchema<T> = T extends StrictSchema<unknown, infer U> | Schema<unknown, infer U> ? U : never;
export type DestinationFromSchema<T> = T extends StrictSchema<infer U> | Schema<infer U> ? U : never;
export type SourceFromSchema<T> = T extends
| StrictSchema<unknown, infer U>
| Schema<unknown, infer U>
? U
: never;
export type DestinationFromSchema<T> = T extends
| StrictSchema<infer U>
| Schema<infer U>
? U
: never;

export type ResultItem<TSchema extends Schema> = DestinationFromSchema<TSchema>;

export interface Mapper<TSchema extends Schema | StrictSchema, TResult = ResultItem<TSchema>> {
export interface Mapper<
TSchema extends Schema | StrictSchema,
TResult = ResultItem<TSchema>
> {
(data?: SourceFromSchema<TSchema>[] | null): TResult[];
(data?: SourceFromSchema<TSchema> | null): TResult;
}
155 changes: 102 additions & 53 deletions src/typescript.spec.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
import Morphism, { morphism, StrictSchema, Schema, createSchema } from './morphism';

describe('Typescript', () => {
describe('Registry Type Checking', () => {
it('Should return a Mapper when using Register', () => {
import Morphism, {
morphism,
StrictSchema,
Schema,
createSchema
} from "./morphism";

describe("Typescript", () => {
describe("Registry Type Checking", () => {
it("Should return a Mapper when using Register", () => {
class Foo {
foo: string;
}
const schema = { foo: 'bar' };
const source = { bar: 'value' };
const schema = { foo: "bar" };
const source = { bar: "value" };
const mapper = Morphism.register(Foo, schema);

expect(mapper(source).foo).toEqual('value');
expect(mapper([source][0]).foo).toEqual('value');
expect(mapper(source).foo).toEqual("value");
expect(mapper([source][0]).foo).toEqual("value");
});
});

describe('Schema Type Checking', () => {
it('Should allow to type the Schema', () => {
describe("Schema Type Checking", () => {
it("Should allow to type the Schema", () => {
interface IFoo {
foo: string;
bar: number;
}
const schema: Schema<IFoo> = { foo: 'qux' };
const source = { qux: 'foo' };
const schema: Schema<IFoo> = { foo: "qux" };
const source = { qux: "foo" };
const target = morphism(schema, source);

expect(target.foo).toEqual(source.qux);
});

it('Should allow to use a strict Schema', () => {
it("Should allow to use a strict Schema", () => {
interface IFoo {
foo: string;
bar: number;
}
const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 1 };
const source = { qux: 'foo' };
const schema: StrictSchema<IFoo> = { foo: "qux", bar: () => 1 };
const source = { qux: "foo" };
const target = morphism(schema, source);

expect(target.foo).toEqual(source.qux);
expect(target.bar).toEqual(1);
});

it('should accept 2 generic parameters on StrictSchema', () => {
it("should accept 2 generic parameters on StrictSchema", () => {
interface Source {
inputA: string;
inputB: string;
Expand All @@ -53,59 +58,61 @@ describe('Typescript', () => {
fooC: string;
}
const schema: StrictSchema<Destination, Source> = {
fooA: 'inputA',
fooA: "inputA",
fooB: ({ inputB }) => inputB,
fooC: 'inputC'
fooC: "inputC"
};

const mapper = morphism(schema);

expect(mapper({ inputA: 'test', inputB: 'test2', inputC: 'test3' })).toEqual({
fooA: 'test',
fooB: 'test2',
fooC: 'test3'
expect(
mapper({ inputA: "test", inputB: "test2", inputC: "test3" })
).toEqual({
fooA: "test",
fooB: "test2",
fooC: "test3"
});
});

it('should accept 2 generic parameters on Schema', () => {
it("should accept 2 generic parameters on Schema", () => {
interface Source2 {
inputA: string;
}
const schema: Schema<{ foo: string }, Source2> = {
foo: 'inputA'
foo: "inputA"
};
morphism(schema, { inputA: 'test' });
morphism(schema, [{ inputA: '' }]);
morphism(schema, { inputA: "test" });
morphism(schema, [{ inputA: "" }]);
});

it('should accept 2 generic parameters on Schema', () => {
it("should accept 2 generic parameters on Schema", () => {
interface S {
s1: string;
}
interface D {
d1: string;
}
const schema: StrictSchema<D, S> = {
d1: 's1'
d1: "s1"
};
const a = morphism(schema)([{ s1: 'test' }]);
const a = morphism(schema)([{ s1: "test" }]);
const itemA = a.shift();
expect(itemA).toBeDefined();
if (itemA) {
itemA.d1;
}
morphism(schema, { s1: 'teest' }).d1.toString();
const b = morphism(schema, [{ s1: 'teest' }]);
morphism(schema, { s1: "teest" }).d1.toString();
const b = morphism(schema, [{ s1: "teest" }]);
const itemB = b.shift();
expect(itemB).toBeDefined();
if (itemB) {
itemB.d1;
}
morphism(schema, [{ s1: 'teest' }]);
morphism(schema, [{ s1: 'test' }]);
morphism(schema, [{ s1: "teest" }]);
morphism(schema, [{ s1: "test" }]);
});

it('should not fail with typescript', () => {
it("should not fail with typescript", () => {
interface S {
s1: string;
}
Expand All @@ -122,29 +129,45 @@ describe('Typescript', () => {
namingIsHard: string;
}

const a = morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' }, [{ boring_api_field: 2 }]);
const a = morphism<StrictSchema<Destination, Source>>(
{ namingIsHard: "boring_api_field" },
[{ boring_api_field: 2 }]
);
const itemA = a.pop();
expect(itemA).toBeDefined();
if (itemA) {
itemA.namingIsHard;
}

const b = morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' }, { boring_api_field: 2 });
const b = morphism<StrictSchema<Destination, Source>>(
{ namingIsHard: "boring_api_field" },
{ boring_api_field: 2 }
);
b.namingIsHard;

const c = morphism<StrictSchema<Destination>>({ namingIsHard: 'boring_api_field' }, [{ boring_api_field: 2 }]);
const c = morphism<StrictSchema<Destination>>(
{ namingIsHard: "boring_api_field" },
[{ boring_api_field: 2 }]
);
const itemC = c.pop();
expect(itemC).toBeDefined();
if (itemC) {
itemC.namingIsHard;
}

const d = morphism<Destination>({ namingIsHard: 'boring_api_field' }, { boring_api_field: 2 });
const d = morphism<Destination>(
{ namingIsHard: "boring_api_field" },
{ boring_api_field: 2 }
);
d.namingIsHard;

morphism({ namingIsHard: 'boring_api_field' });
morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' })({ boring_api_field: 2 });
const e = morphism<StrictSchema<Destination>>({ namingIsHard: 'boring_api_field' })([{ boring_api_field: 2 }]);
morphism({ namingIsHard: "boring_api_field" });
morphism<StrictSchema<Destination, Source>>({
namingIsHard: "boring_api_field"
})({ boring_api_field: 2 });
const e = morphism<StrictSchema<Destination>>({
namingIsHard: "boring_api_field"
})([{ boring_api_field: 2 }]);
const itemE = e.pop();
expect(itemE).toBeDefined();
if (itemE) {
Expand All @@ -162,7 +185,7 @@ describe('Typescript', () => {
morphism<StrictSchema<D1, S1>>({ a: ({ _a }) => _a.toString() });
});

it('shoud infer result type from source when a class is provided', () => {
it("shoud infer result type from source when a class is provided", () => {
class Source {
constructor(public id: number, public ugly_field: string) {}
}
Expand All @@ -171,31 +194,34 @@ describe('Typescript', () => {
constructor(public id: number, public field: string) {}
}

const source = [new Source(1, 'abc'), new Source(1, 'def')];
const source = [new Source(1, "abc"), new Source(1, "def")];

const schema: StrictSchema<Destination, Source> = {
id: 'id',
field: 'ugly_field'
id: "id",
field: "ugly_field"
};
const expected = [new Destination(1, 'abc'), new Destination(1, 'def')];
const expected = [new Destination(1, "abc"), new Destination(1, "def")];

const result = morphism(schema, source, Destination);
result.forEach((item, idx) => {
expect(item).toEqual(expected[idx]);
});
});

it('should accept union types as Target', () => {
const schema = createSchema<{ a: string } | { a: string; b: string }, { c: string }>({
it("should accept union types as Target", () => {
const schema = createSchema<
{ a: string } | { a: string; b: string },
{ c: string }
>({
a: ({ c }) => c
});

expect(morphism(schema, { c: 'result' }).a).toEqual('result');
expect(morphism(schema, { c: "result" }).a).toEqual("result");
});
});

describe('Morphism Function Type Checking', () => {
it('should infer target type from array input', () => {
describe("Morphism Function Type Checking", () => {
it("should infer target type from array input", () => {
interface Source {
ID: number;
}
Expand All @@ -206,9 +232,32 @@ describe('Typescript', () => {

const rows: Array<Source> = [{ ID: 1234 }];

const schema: StrictSchema<Destination, Source> = { id: 'ID' };
const schema: StrictSchema<Destination, Source> = { id: "ID" };
expect(morphism(schema, rows)).toBeDefined();
expect(morphism(schema, rows)[0].id).toEqual(1234);
});
});

describe("Selector Action", () => {
it("should match return type of fn with target property", () => {
interface Source {
foo: string;
}

interface Target {
foo: number;
}

const schema: StrictSchema<Target, Source> = {
foo: {
path: "foo",
fn: val => {
return Number(val);
}
}
};
const source: Source = { foo: "1" };
expect(morphism(schema, source)).toEqual({ foo: 1 });
});
});
});

0 comments on commit 9cc11c4

Please sign in to comment.