Skip to content

Commit ce170c2

Browse files
committed
refactor!: Remove fp-ts (#729)
BREAKING CHANGE: Usage of `fp-ts` has been removed across all packaged. Functions that used to return a `TaskEither` now return a simple `Promise`.
1 parent d362bb4 commit ce170c2

51 files changed

Lines changed: 444 additions & 1685 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
AQICN_TOKEN=
1+
AQICN_TOKEN=6bb4237574756ba29f05cea553bd22576596c11e

packages/dataproviders/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
"@shootismoke/convert": "^0.9.1",
2222
"axios": "^0.27.2",
2323
"date-fns": "^2.29.3",
24-
"date-fns-tz": "^1.3.7",
25-
"fp-ts": "^2.12.3",
26-
"io-ts": "^2.2.18"
24+
"date-fns-tz": "^1.3.7"
2725
},
2826
"devDependencies": {
2927
"dotenv": "^16.0.3"

packages/dataproviders/src/fp/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
export * from './providers/types';
12
export { AqicnOptions } from './providers/aqicn/fetchBy';
23
export { OpenAQOptions } from './providers/openaq/fetchBy';
3-
export * from './promise';
44
export * from './types';
55
export {
6-
AllProviders,
76
ACCURATE_RADIUS,
87
getDominantPol,
98
getCountryFromCode,
109
stationName,
1110
} from './util';
12-
export * from './util/fp';
1311
export * from './util/openaq';
12+
13+
export * from './providers/aqicn/aqicn';
14+
export * from './providers/openaq/openaq';
15+
export * from './providers/waqi/waqi';

packages/dataproviders/src/promise.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import * as E from 'fp-ts/lib/Either';
2-
31
import { aqicn } from './aqicn';
42

53
describe('aqicn', () => {
64
it('should throw without token', async () => {
7-
expect(await aqicn.fetchByStation('foo')()).toEqual(
8-
E.left(new Error('AqiCN requires a token'))
5+
await expect(aqicn.fetchByStation('foo')).rejects.toThrowError(
6+
new Error('AqiCN requires a token')
97
);
108
});
119
});
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { ProviderFP } from '../types';
1+
import { Provider } from '../types';
22
import { AqicnOptions, fetchByGps, fetchByStation } from './fetchBy';
33
import { normalize } from './normalize';
4-
import { AqicnStaton } from './validation';
4+
import { AqicnData } from './validation';
55

66
/**
77
* @see https://aqicn.org
88
*/
9-
export const aqicn: ProviderFP<AqicnStaton, AqicnStaton, AqicnOptions> = {
9+
export const aqicn: Provider<AqicnData, AqicnOptions> = {
1010
fetchByGps,
1111
fetchByStation,
1212
id: 'aqicn',
1313
name: 'AQI CN',
14-
normalizeByGps: normalize,
15-
normalizeByStation: normalize,
14+
normalize,
1615
};
Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
import { pipe } from 'fp-ts/lib/pipeable';
2-
import * as TE from 'fp-ts/lib/TaskEither';
3-
41
import { LatLng } from '../../types';
5-
import { fetchAndDecode } from '../../util';
6-
import { AqicnStaton, ByStation, ByStationCodec } from './validation';
2+
import { fetchAndDecode } from '../../util/fetch';
3+
import { AqicnData, AqicnResponse } from './validation';
74

85
/**
96
* Check if the response we get from aqicn is `{"status": "error", "msg": "..."}`,
107
* if yes, return an error.
118
*/
12-
function checkError({
13-
status,
14-
data,
15-
msg,
16-
}: ByStation): TE.TaskEither<Error, AqicnStaton> {
17-
return status === 'ok'
18-
? TE.right(data)
19-
: TE.left(new Error(msg || (data as string)));
9+
function checkError({ status, data, msg }: AqicnResponse): AqicnData {
10+
if (status === 'ok' && typeof data === 'object') {
11+
return data;
12+
} else {
13+
throw new Error(msg || (data as string));
14+
}
2015
}
2116

2217
export interface AqicnOptions {
@@ -27,18 +22,6 @@ export interface AqicnOptions {
2722
token: string;
2823
}
2924

30-
/**
31-
* Check that a token has been correctly passed
32-
*
33-
* @param options - Options to pass to aqicn
34-
*/
35-
function checkToken(options?: AqicnOptions): TE.TaskEither<Error, undefined> {
36-
if (!options || !options.token) {
37-
return TE.left(new Error('AqiCN requires a token'));
38-
}
39-
return TE.right(undefined);
40-
}
41-
4225
/**
4326
* Fetch the closest station to the user's current position
4427
*
@@ -47,38 +30,32 @@ function checkToken(options?: AqicnOptions): TE.TaskEither<Error, undefined> {
4730
export function fetchByGps(
4831
gps: LatLng,
4932
options: AqicnOptions
50-
): TE.TaskEither<Error, AqicnStaton> {
33+
): Promise<AqicnData> {
34+
if (!options || !options.token) {
35+
throw new Error('AqiCN requires a token');
36+
}
37+
5138
const { latitude, longitude } = gps;
5239

53-
return pipe(
54-
checkToken(options),
55-
TE.chain(() =>
56-
fetchAndDecode(
57-
`https://api.waqi.info/feed/geo:${latitude};${longitude}/?token=${options.token}`,
58-
ByStationCodec
59-
)
60-
),
61-
TE.chain(checkError)
62-
);
40+
return fetchAndDecode<AqicnResponse>(
41+
`https://api.waqi.info/feed/geo:${latitude};${longitude}/?token=${options.token}`
42+
).then(checkError);
6343
}
6444

6545
/**
6646
* Fetch data by station
6747
*
6848
* @param stationId - The station ID to search
6949
*/
70-
export function fetchByStation(
50+
export async function fetchByStation(
7151
stationId: string,
7252
options: AqicnOptions
73-
): TE.TaskEither<Error, AqicnStaton> {
74-
return pipe(
75-
checkToken(options),
76-
TE.chain(() =>
77-
fetchAndDecode(
78-
`https://api.waqi.info/feed/@${stationId}/?token=${options.token}`,
79-
ByStationCodec
80-
)
81-
),
82-
TE.chain(checkError)
83-
);
53+
): Promise<AqicnData> {
54+
if (!options || !options.token) {
55+
throw new Error('AqiCN requires a token');
56+
}
57+
58+
return fetchAndDecode<AqicnResponse>(
59+
`https://api.waqi.info/feed/@${stationId}/?token=${options.token}`
60+
).then(checkError);
8461
}

packages/dataproviders/src/providers/aqicn/normalize.ts

Lines changed: 53 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ import {
66
usaEpa,
77
} from '@shootismoke/convert';
88
import { format, utcToZonedTime } from 'date-fns-tz';
9-
import * as E from 'fp-ts/lib/Either';
10-
import { pipe } from 'fp-ts/lib/pipeable';
119

1210
import { OpenAQResults } from '../../types';
1311
import { getCountryCode, providerError } from '../../util';
1412
import sanitized from './sanitized.json';
15-
import type { AqicnStaton } from './validation';
13+
import type { AqicnData } from './validation';
1614

1715
/**
1816
* Sanitize the country we get here from aqicn. For example, for China, the
@@ -32,28 +30,20 @@ export function sanitizeCountry(input: string): string {
3230
}
3331

3432
/**
35-
* Normalize aqicn byGps data
33+
* Normalize aqicn byGps data. Throws an error if the data cannot be normalized.
3634
*
3735
* @param data - The data to normalize
3836
*/
39-
export function normalize(data: AqicnStaton): E.Either<Error, OpenAQResults> {
40-
if (!data || typeof data === 'string') {
41-
return E.left(
42-
providerError('aqicn', `Cannot normalized ${data || 'undefined'}`)
43-
);
44-
}
45-
37+
export function normalize(data: AqicnData): OpenAQResults {
4638
const stationId = `aqicn|${data.idx}`;
4739

4840
// Sometimes we don't get geo
4941
if (!data.city.geo) {
50-
return E.left(
51-
providerError(
52-
'aqicn',
53-
`Cannot normalize station ${stationId}, no city: ${JSON.stringify(
54-
data
55-
)}`
56-
)
42+
throw providerError(
43+
'aqicn',
44+
`Cannot normalize station ${stationId}, no city: ${JSON.stringify(
45+
data
46+
)}`
5747
);
5848
}
5949

@@ -63,27 +53,21 @@ export function normalize(data: AqicnStaton): E.Either<Error, OpenAQResults> {
6353
usaEpa.pollutants.includes(pol as Pollutant)
6454
);
6555
if (!pollutants.length) {
66-
return E.left(
67-
providerError(
68-
'aqicn',
69-
`Cannot normalize station ${stationId}, no pollutants currently tracked: ${JSON.stringify(
70-
data
71-
)}`
72-
)
56+
throw providerError(
57+
'aqicn',
58+
`Cannot normalize station ${stationId}, no pollutants currently tracked: ${JSON.stringify(
59+
data
60+
)}`
7361
);
7462
}
7563
// We now need to get the country from AQICN response. The only place I found
7664
// is city.url...
7765
// Example: https://aqicn.org/city/france/lorraine/thionville-nord/garche/
7866
const AQICN_DOMAIN = 'aqicn.org/city/';
7967
if (!data.city.url || !data.city.url.includes(AQICN_DOMAIN)) {
80-
return E.left(
81-
providerError(
82-
'aqicn',
83-
`Cannot extract country, got city.url: ${
84-
data.city.url as string
85-
}`
86-
)
68+
throw providerError(
69+
'aqicn',
70+
`Cannot extract country, got city.url: ${data.city.url as string}`
8771
);
8872
}
8973
const countryRaw = sanitizeCountry(
@@ -99,44 +83,43 @@ export function normalize(data: AqicnStaton): E.Either<Error, OpenAQResults> {
9983
"yyyy-MM-dd'T'HH:mm:ss.SSSxxx"
10084
);
10185

102-
return pipe(
103-
getCountryCode(countryRaw),
104-
E.fromOption(() =>
105-
providerError('aqicn', `Cannot get code from country ${countryRaw}`)
106-
),
107-
E.map(
108-
(country) =>
109-
pollutants.map(([pol, { v }]) => {
110-
const pollutant = pol as Pollutant;
86+
const countryCode = getCountryCode(countryRaw);
87+
if (!countryCode) {
88+
throw providerError(
89+
'aqicn',
90+
`Cannot get code from country ${countryRaw}`
91+
);
92+
}
11193

112-
if (!data.city.geo) {
113-
throw new Error(
114-
'We returned TE.left if data.city.geo was not defined. qed.'
115-
);
116-
}
94+
return pollutants.map(([pol, { v }]) => {
95+
const pollutant = pol as Pollutant;
11796

118-
return {
119-
attribution: data.attributions,
120-
averagingPeriod: {
121-
unit: 'day',
122-
value: 1,
123-
},
124-
city: data.city.name,
125-
coordinates: {
126-
latitude: +data.city.geo[0],
127-
longitude: +data.city.geo[1],
128-
},
129-
country,
130-
date: { local, utc },
131-
location: stationId,
132-
isMobile: false,
133-
parameter: pollutant,
134-
sourceName: 'aqicn',
135-
entity: 'other',
136-
value: convert(pollutant, 'usaEpa', ugm3, v),
137-
unit: getPollutantMeta(pollutant).preferredUnit,
138-
};
139-
}) as OpenAQResults
140-
)
141-
);
97+
if (!data.city.geo) {
98+
throw new Error(
99+
'We returned TE.left if data.city.geo was not defined. qed.'
100+
);
101+
}
102+
103+
return {
104+
attribution: data.attributions,
105+
averagingPeriod: {
106+
unit: 'day',
107+
value: 1,
108+
},
109+
city: data.city.name,
110+
coordinates: {
111+
latitude: +data.city.geo[0],
112+
longitude: +data.city.geo[1],
113+
},
114+
country: countryCode,
115+
date: { local, utc },
116+
location: stationId,
117+
isMobile: false,
118+
parameter: pollutant,
119+
sourceName: 'aqicn',
120+
entity: 'other',
121+
value: convert(pollutant, 'usaEpa', ugm3, v),
122+
unit: getPollutantMeta(pollutant).preferredUnit,
123+
};
124+
}) as OpenAQResults;
142125
}

0 commit comments

Comments
 (0)