Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract from Observable in APIClient #48

Closed
raveclassic opened this issue Oct 8, 2019 · 5 comments · Fixed by #60
Closed

Abstract from Observable in APIClient #48

raveclassic opened this issue Oct 8, 2019 · 5 comments · Fixed by #60
Labels
breaking change enhancement New feature or request language: typescript 2.0 Typescript language template for Swagger 2.0 spec language: typescript 3.0 Typescript language template for OpenAPI 3.0 spec
Milestone

Comments

@raveclassic
Copy link
Contributor

  • research possibility of abstracting Observable out to some generic Monad
  • take a look at final tagless
@raveclassic raveclassic added enhancement New feature or request help wanted Extra attention is needed labels Oct 8, 2019
@raveclassic
Copy link
Contributor Author

An abstract APIClient may be a function request which returns some Functor (and is therefore parametrised by an instance of that Functor). This allows controllers to map over the inner value abstracting from the type of container which may be rxjs Observable, most Stream or native Promise/Task.

@raveclassic raveclassic added this to the 2.0 milestone Oct 22, 2019
@raveclassic raveclassic added language: typescript 2.0 Typescript language template for Swagger 2.0 spec language: typescript 3.0 Typescript language template for OpenAPI 3.0 spec labels Oct 24, 2019
@raveclassic
Copy link
Contributor Author

client.ts

import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT';
import { MonadThrow, MonadThrow1, MonadThrow2 } from 'fp-ts/lib/MonadThrow';

export interface Request {
	readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
	readonly url: string;
	readonly query?: object;
	readonly body?: unknown;
}

export interface HTTPClient<F> extends MonadThrow<F> {
	readonly request: (request: Request) => HKT<F, unknown>;
}
export interface HTTPClient1<F extends URIS> extends MonadThrow1<F> {
	readonly request: (request: Request) => Kind<F, unknown>;
}
export interface HTTPClient2<F extends URIS2> extends MonadThrow2<F> {
	readonly request: (request: Request) => Kind2<F, unknown, unknown>;
}

controller.ts

import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT';
import { HTTPClient, HTTPClient1, HTTPClient2 } from './client';
import { pipe } from 'fp-ts/lib/pipeable';
import { number } from 'io-ts';
import { either } from 'fp-ts';
import { task } from 'fp-ts/lib/Task';
import { Observable, of } from 'rxjs';
import { failure, pending, RemoteData, success } from '@devexperts/remote-data-ts';
import { catchError, map, startWith } from 'rxjs/operators';
import { mapRD } from '@devexperts/rx-utils/dist/rd/operators/mapRD';
import { switchMapRD } from '@devexperts/rx-utils/dist/rd/operators/switchMapRD';
import { combineLatestRD } from '@devexperts/rx-utils/dist/rd/operators/combineLatestRD';
import { ajax } from 'rxjs/ajax';

interface Controller<F> {
	readonly getTime: () => HKT<F, number>;
}
interface Controller1<F extends URIS> {
	readonly getTime: () => Kind<F, number>;
}
interface Controller2<F extends URIS2> {
	readonly getTime: () => Kind2<F, Error, number>;
}

interface Context<F> {
	readonly httpClient: HTTPClient<F>;
}
interface Context1<F extends URIS> {
	readonly httpClient: HTTPClient1<F>;
}
interface Context2<F extends URIS2> {
	readonly httpClient: HTTPClient2<F>;
}

export function controller<F extends URIS2>(e: Context2<F>): Controller2<F>;
export function controller<F extends URIS>(e: Context1<F>): Controller1<F>;
export function controller<F>(e: Context<F>): Controller<F> {
	return {
		getTime: () =>
			pipe(
				e.httpClient.request({
					url: '/time',
					method: 'GET',
				}),
				r =>
					e.httpClient.chain(r, a =>
						pipe(
							number.decode(a),
							either.fold(err => e.httpClient.throwError(err), a => e.httpClient.of(a)),
						),
					),
			),
	};
}

const taskClient: HTTPClient1<'Task'> = {
	...task,
	request: request => () => fetch(request.url, { method: request.method }),
	throwError: e => () => Promise.reject(e),
};

declare module 'fp-ts/lib/HKT' {
	interface URItoKind2<E, A> {
		LiveData: Observable<RemoteData<E, A>>;
	}
}
const liveDataClient: HTTPClient2<'LiveData'> = {
	URI: 'LiveData',
	map: (fa, f) =>
		pipe(
			fa,
			mapRD(f),
		),
	of: a => of(success(a)),
	chain: (fa, f) =>
		pipe(
			fa,
			switchMapRD(f),
		),
	ap: (fab, fa) =>
		pipe(
			combineLatestRD(fab, fa),
			mapRD(([ab, a]) => ab(a)),
		),
	request: request =>
		pipe(
			ajax(request),
			map(r => success(r.response)),
			catchError(liveDataClient.throwError),
			startWith(pending),
		),
	throwError: e => of(failure(e)),
};

//
const r1 = controller({ httpClient: taskClient }).getTime(); // Task<number>
r1()
	.then()
	.catch(); // all Promise methods available

const r2 = controller({ httpClient: liveDataClient }).getTime(); // LiveData<Error, number>
r2.pipe().subscribe(); //all LiveData methods available

@raveclassic raveclassic added breaking change and removed help wanted Extra attention is needed labels Oct 29, 2019
@raveclassic
Copy link
Contributor Author

This is working solution (hooray) however this will break a lot.
Also in addition to separate controllers we could generate a single controllers.ts file containing all available controllers already parametrised with a single HTTPClient instance.

@sutarmin
Copy link
Contributor

Wow, that's beautiful. I'm for it :) basically, this will break only the layer that is autogenerated right know, right?

@raveclassic
Copy link
Contributor Author

raveclassic commented Oct 29, 2019

Currently an idiomatic way to work with generated controller is to add it to combineReader directly. This is going to break because it won't play well with generic HTTPClient.
One possible solution is to create a separate module for API in each project where such controllers.ts would be parametrised with HTTPClient instance.

raveclassic added a commit that referenced this issue Oct 29, 2019
BREAKING CHANGE: apiClient dependency was renamed to httpClient
BREAKING CHANGE: APIClient was renamed to HTTPClient<F> and now extends MonadThrow<F>
BREAKING CHANGE: FullAPIRequest/APIRequest were joined and renamed to Request

closes #48
raveclassic added a commit that referenced this issue Oct 29, 2019
BREAKING CHANGE: apiClient dependency was renamed to httpClient
BREAKING CHANGE: APIClient was renamed to HTTPClient<F> and now extends MonadThrow<F>
BREAKING CHANGE: FullAPIRequest/APIRequest were joined and renamed to Request

closes #48
raveclassic added a commit that referenced this issue Oct 31, 2019
BREAKING CHANGE: apiClient dependency was renamed to httpClient
BREAKING CHANGE: APIClient was renamed to HTTPClient<F> and now extends MonadThrow<F>
BREAKING CHANGE: FullAPIRequest/APIRequest were joined and renamed to Request

closes #48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change enhancement New feature or request language: typescript 2.0 Typescript language template for Swagger 2.0 spec language: typescript 3.0 Typescript language template for OpenAPI 3.0 spec
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants