Skip to content

Commit

Permalink
Good progress
Browse files Browse the repository at this point in the history
  • Loading branch information
BBB committed Nov 29, 2023
1 parent d92be2f commit 49beca9
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 98 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ollierelph/result4t": "^0.6.0"
"@ollierelph/result4t": "^0.7.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
Expand Down
92 changes: 25 additions & 67 deletions packages/http-client/src/FilterResult.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,51 @@
import { ImmutableResponse } from "~/src/ImmutableResponse";
import { ImmutableRequest } from "~/src/ImmutableRequest";
import { Result, Task } from "@ollierelph/result4t";

type FilterApply = <SuccessIn, FailureIn, SuccessOut, FailureOut>(
export type FilterApply<SuccessIn, FailureIn, SuccessOut, FailureOut> = (
next: HttpHandler<SuccessIn, FailureIn>,
) => HttpHandler<SuccessOut, FailureOut>;

export type HttpHandler<Success, Failure> = (
request: ImmutableRequest,
) => Promise<Result<Success, Failure>>;

export class Filter<SuccessOut, FailureOut> {
#task: Task<[input: ImmutableRequest], Result<SuccessOut, FailureOut>>;
export class Filter<SuccessIn, FailureIn, SuccessOut, FailureOut> {
#task: Task<
[input: HttpHandler<SuccessIn, FailureIn>],
HttpHandler<SuccessOut, FailureOut>
>;
protected constructor(
fn: (input: ImmutableRequest) => Promise<Result<SuccessOut, FailureOut>>,
fn: FilterApply<SuccessIn, FailureIn, SuccessOut, FailureOut>,
) {
this.#task = Task.of(fn);
}
static of<
Fn extends HttpHandler<any, any>,
Output extends Result<any, any> = Fn extends (
...args: any[]
) => Promise<infer R>
? R
: never,
SuccessOut = Output extends Result<infer R, any> ? R : never,
FailureOut = Output extends Result<any, infer R> ? R : never,
static from<
Fn extends FilterApply<any, any, any, any>,
SuccessIn = Fn extends FilterApply<infer R, any, any, any> ? R : never,
FailureIn = Fn extends FilterApply<any, infer R, any, any> ? R : never,
SuccessOut = Fn extends FilterApply<any, any, infer R, any> ? R : never,
FailureOut = Fn extends FilterApply<any, any, any, infer R> ? R : never,
>(fn: Fn) {
return new Filter<SuccessOut, FailureOut>(fn);
return new Filter<SuccessIn, FailureIn, SuccessOut, FailureOut>(fn);
}

static fromTask<
Task2 extends Task<[request: ImmutableRequest], Result<any, any>>,
SuccessOut = Task2 extends Task<
[request: ImmutableRequest],
Result<infer R, any>
>
then<
Other extends Filter<any, any, SuccessIn, FailureIn>,
SuccessIn2 = Other extends Filter<infer R, any, SuccessIn, FailureIn>
? R
: never,
FailureOut = Task2 extends Task<
[request: ImmutableRequest],
Result<any, infer R>
>
FailureIn2 = Other extends Filter<any, infer R, SuccessIn, FailureIn>
? R
: never,
>(task2: Task2) {
return new Filter<SuccessOut, FailureOut>(task2.call.bind(task2));
}

static alwaysRespondWith(response: ImmutableResponse) {
return Filter.of(async (request: ImmutableRequest) =>
Result.success<ImmutableResponse, Error>(response),
>(filter: Other) {
return new Filter<SuccessIn2, FailureIn2, SuccessOut, FailureOut>((other) =>
this.#task.call(filter.apply(other)),
);
}

map<Output extends Result<any, any>>(
predicate: (
value: (
input: ImmutableRequest,
) => Promise<Result<SuccessOut, FailureOut>>,
) => (input: ImmutableRequest) => Promise<Output>,
) {
return Filter.of(predicate(this.#task.call.bind(this.#task)));
}

mapRequest(
predicate: (input: ImmutableRequest) => ImmutableRequest,
): Filter<SuccessOut, FailureOut> {
return Filter.fromTask(
this.#task.map((next) => (req: ImmutableRequest) => next(predicate(req))),
);
}

mapResponse<
Output extends Result<any, any>,
Success2 = Output extends Result<infer R, any> ? R : never,
Failure2 = Output extends Result<any, infer R> ? R : never,
>(
predicate: (result: Result<SuccessOut, FailureOut>) => Promise<Output>,
): Filter<Success2, Failure2> {
const task2 = this.#task.map((next) => {
const newVar: (req: ImmutableRequest) => Promise<Output> = (
req: ImmutableRequest,
) => next(req).then(predicate);
return newVar;
});
return Filter.fromTask(task2);
}
call(request: ImmutableRequest): Promise<Result<SuccessOut, FailureOut>> {
return this.#task.call(request);
apply(
filter: HttpHandler<SuccessIn, FailureIn>,
): HttpHandler<SuccessOut, FailureOut> {
return this.#task.call(filter);
}
}
76 changes: 50 additions & 26 deletions packages/http-client/test/FilterResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,62 @@ import { StatusCode } from "~/src/StatusCode";
import { ImmutableResponse } from "~/src/ImmutableResponse";
import { Result } from "@ollierelph/result4t";
import { expect, it } from "vitest";
import { Filter } from "~/src/FilterResult";
import { Filter, HttpHandler, FilterApply } from "~/src/FilterResult";
import { ImmutableURL } from "~/src/ImmutableURL";

it("can build a filter chain", async () => {
const myClient = Filter.of(async (request: ImmutableRequest) =>
Result.success(ImmutableResponse.of(null, { status: StatusCode.OK })),
const setHostnameForEnvironment = (env: string) =>
Filter.from(
<SuccessIn, FailureIn>(
next: HttpHandler<SuccessIn, FailureIn>,
): HttpHandler<SuccessIn, FailureIn> =>
async (request: ImmutableRequest) => {
const finalUrl = request.url.copy({ hostname: `${env}.example.com` });
return next(request.copy({ url: finalUrl }));
},
);

const response = await myClient.call(
ImmutableRequest.get(ImmutableURL.fromPathname("/woo")),
const addAuth = () =>
Filter.from(
<SuccessIn, FailureIn>(
next: HttpHandler<SuccessIn, FailureIn>,
): HttpHandler<SuccessIn, FailureIn> =>
(request: ImmutableRequest) => {
const finalHeaders = request.headers.append(
"Authorization",
"Basic 123",
);
return next(request.copy({ headers: finalHeaders }));
},
);
expect(response.isSuccess()).toEqual(true);
});

it("is something like this", async () => {
const f = await Filter.alwaysRespondWith(
ImmutableResponse.of(null, {
status: StatusCode.OK,
}),
)
.mapRequest((req) =>
req.copy({ headers: req.headers.append("woo", "hoo") }),
)
.mapResponse(async (res): Promise<Result<boolean, Error>> => {
return res.map((res) => false);
})
.map((next) => (request: ImmutableRequest) => next(request))
.call(ImmutableRequest.get(ImmutableURL.fromPathname(`/}`)))
.then((result) =>
result.getOrElse((err) => {
throw err;
const alwaysStatusAndReflectRequest =
(status: StatusCode): HttpHandler<ImmutableResponse, Error> =>
async (req: ImmutableRequest) =>
Result.success(
ImmutableResponse.of(null, {
status,
url: req.url,
headers: req.headers,
}),
);
expect(f).toEqual(true);

it("can build a filter chain", async () => {
const chain: Filter<ImmutableResponse, Error, StatusCode, Error> =
setHostnameForEnvironment("stg")
.then(addAuth())
.then(
Filter.from(
(
next: HttpHandler<ImmutableResponse, Error>,
): HttpHandler<StatusCode, Error> =>
(request: ImmutableRequest) =>
next(request).then((res) => res.map((it) => it.status)),
),
);
const client = chain.apply(alwaysStatusAndReflectRequest(StatusCode.OK));
const response = await client(
ImmutableRequest.get(ImmutableURL.fromPathname("/")),
);
expect(response.isSuccess()).toEqual(true);
expect(response.get()).toHaveProperty("status", StatusCode.OK);
});
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 49beca9

Please sign in to comment.