UNPKG

10.3 kBJavaScriptView Raw
1// @flow
2// API - UTILITIES
3// =============================================================================
4// General purpose utilities required by the generated output files.
5
6// FLUX STANDARD ACTIONS
7// These may be brought in by something like flow-typed in the future. For the
8// time being, this is a relatively faithful conversion of the TypeScript
9// equivalents defined in https://github.com/redux-utilities/flux-standard-action/blob/master/src/index.d.ts
10
11/* eslint-disable no-use-before-define */
12export type ActionType = string;
13export type NormalFSA<Payload: mixed | Error = typeof undefined, Meta: mixed = typeof undefined> = {|
14 type: ActionType,
15 payload: Payload,
16 error: boolean,
17 meta: Meta
18|};
19export type FluxStandardAction<Payload, Meta> = NormalFSA<Payload, Meta>;
20export type FSA<Payload, Meta> = NormalFSA<Payload, Meta>;
21
22// JSON TYPES
23export type PrimitiveType = null | string | boolean | number;
24export type JSONObject = ({ [key: string]: PrimitiveType | JSONObject | JSONArray | void });
25export type JSONArray = Array<PrimitiveType> | Array<JSONObject> | Array<JSONArray>;
26export type JSONType = JSONObject | JSONArray | PrimitiveType;
27
28// SWAGGER GEN JS UTILITY TYPES
29export type ApiEndpointState = {
30 +success: JSONType | null,
31 +failure: JSONType | null,
32 +timeout: JSONType | null,
33 +mistake: JSONType | null,
34 +isFetching: boolean,
35 +lastUpdate: number | null,
36 +lastResult: 'success' | 'failure' | 'timeout' | 'mistake' | null,
37};
38export type KeyedCollectionState =
39 & ApiEndpointState
40 & { +collection: { [string]: JSONObject } };
41
42export type ReducerState<Name: string> =
43 & { +[string]: ApiEndpointState }
44 & { +[Name]: ApiEndpointState };
45export type KeyedReducerState<Name: string> =
46 & { +[string]: KeyedCollectionState }
47 & { +[Name]: KeyedCollectionState };
48
49export type ParamKey = number | string;
50export type ParamValue = number | string | boolean;
51export type ParamsPair = [ParamKey, ParamValue];
52export type ParamsArray = Array<ParamsPair>;
53export type ParamsObject = { [ParamKey]: * };
54/* eslint-enable no-use-before-define */
55
56
57const queryPrefixRegexp = new RegExp(/^\?=/);
58
59const validateParamsPair = ([key, value]) => {
60 if (typeof key !== 'string') {
61 throw new Error(`Expected ParamsObject to have string keys, not "${typeof key}""`);
62 }
63 if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
64 throw new Error(`Expected ParamsObject values to be strings, numbers, or booleans, not "${typeof value}"`);
65 }
66};
67
68export const validateParamsObject = (params: ParamsObject) => {
69 if (params && typeof params === 'object') {
70 Object.entries(params).forEach(validateParamsPair);
71 } else {
72 throw new Error('Could not validate ParamsObject: Not an object');
73 }
74};
75
76export const validateParamsArray = (params: ParamsArray) => {
77 if (params && Array.isArray(params)) {
78 params.forEach(validateParamsPair);
79 } else {
80 throw new Error('Could not validate ParamsArray: Not an array');
81 }
82};
83
84// Outputs param string pairs in the format 'foo=bar'
85const createParamsPairString = (paramsPair: ParamsPair): string => {
86 if (Array.isArray(paramsPair) && paramsPair.length === 2) {
87 if (typeof paramsPair[0] !== 'string') {
88 throw new TypeError('paramsPair[0] must be a string');
89 }
90
91 if (!['number', 'string', 'boolean'].includes(typeof paramsPair[1])) {
92 throw new TypeError('paramsPair[1] must be a number, string, or boolean');
93 }
94
95 return `${encodeURIComponent(String(paramsPair[0]))}=${encodeURIComponent(String(paramsPair[1]))}`;
96 }
97
98 throw new TypeError('paramsPair must be an array with two entries');
99};
100
101// Converts a URI string back into its original type. Deliberately very narrow
102// in scope. Certainly doesn't cover all possible abuses of URI strings.
103const restoreOriginalType = (text: string): * => {
104 if (typeof text === 'string') {
105 const decodedText = decodeURIComponent(text);
106
107 if (!isNaN(decodedText)) {
108 // Text is a valid number
109 // NOTE: This absolutely does not handle hex or octal. Until someone needs
110 // it to do more, we'll continue to naively use parseFloat.
111 return parseFloat(decodedText);
112 }
113
114 if (decodedText === 'true' || decodedText === 'false') {
115 // Text is a valid boolean
116 return decodedText === 'true';
117 }
118
119 // Text is a valid string, and was not a number or boolean
120 return decodedText;
121 }
122
123 throw new Error('Could not restore type: not a number, string, or boolean');
124};
125
126// Convert a 2D array into a query parameter string
127export const arrayToParamString = (paramsArray: ParamsArray = []): string => {
128 if (Array.isArray(paramsArray)) {
129 return paramsArray.map(createParamsPairString).join('&');
130 }
131
132 throw new TypeError('paramsArray must be an Array');
133};
134
135// Convert a query parameter string into a 2D array
136export const paramStringToArray = (paramsString: string = ''): ParamsArray => {
137 if (typeof paramsString === 'string') {
138 const cleanedPairs: Array<string> = paramsString.replace(queryPrefixRegexp, '').split('&');
139
140 return cleanedPairs.map((param: string) => {
141 const [key, value] = param.split('=');
142
143 return [decodeURIComponent(key), restoreOriginalType(value)];
144 });
145 }
146
147 throw new TypeError('paramsString must be a string');
148};
149
150// Convert an object into a query parameter string
151export const objectToParamString = (paramsObject: ParamsObject = {}): string => {
152 if (paramsObject && typeof paramsObject === 'object') {
153 const entries = Object.keys(paramsObject)
154 .map(key => [key, paramsObject[key]]);
155
156 return arrayToParamString(entries);
157 }
158
159 throw new TypeError('paramsObject must be an Object');
160};
161
162// Convert a query parameter string into an object
163export const paramStringToObject = (paramsString: string = '') => {
164 const paramsArray: ParamsArray = paramStringToArray(paramsString);
165
166 if (Array.isArray(paramsArray)) {
167 const data = {};
168
169 paramsArray.forEach(([key, value]: ParamsPair) => {
170 data[key] = value;
171 });
172
173 return data;
174 }
175
176 throw new Error('Could not convert param string to object beacuse the intermediate Array was not created');
177};
178
179export const convertToParamString = (content: ParamsArray | ParamsObject = {}): string => {
180 if (Array.isArray(content)) {
181 return arrayToParamString(content);
182 } else if (content !== null && typeof content === 'object') {
183 return objectToParamString(content);
184 }
185
186 throw new Error('convertToParamString requires an array or an object as its first parameter');
187};
188
189export const convertQueryToString = (content: ParamsArray | ParamsObject = {}): string => {
190 const convertedString = convertToParamString(content);
191
192 if (typeof convertedString === 'string') {
193 if (convertedString.length) {
194 return `?${convertedString}`;
195 }
196
197 // In the event that we successfully 'converted' an empty object or array -
198 // which is completely legal - we should return an empty string.
199 return '';
200 }
201
202 throw new Error('Query conversion failed');
203};
204
205// SAGA HELPERS
206export const getInit = () => {};
207export const getApiPath = () => '';
208
209// REDUCER HELPERS
210export const initialEndpointState: ApiEndpointState = {
211 success: null,
212 failure: null,
213 timeout: null,
214 mistake: null,
215 isFetching: false,
216 lastUpdate: null,
217 lastResult: null,
218};
219export const initialKeyedCollectionState: ApiEndpointState & KeyedCollectionState = {
220 ...initialEndpointState,
221 collection: {},
222};
223
224const rekeyObject = (key: string, obj: JSONObject) => {
225 if (obj && typeof obj[key] === 'string') {
226 return { [obj[key]]: obj };
227 }
228
229 console.trace(`Could not key response obect by key '${key}'`);
230 console.error(JSON.stringify(obj, null, 2));
231
232 return {};
233};
234
235export const handleKeyedCollection = (key: string, collection: { [string]: JSONObject } = {}, response: JSONObject | Array<JSONObject>) => {
236 if (Array.isArray(response)) {
237 return response.map(obj => rekeyObject(key, obj)).reduce((prev, next) => ({ ...prev, ...next }), collection);
238 }
239
240 return { ...collection, ...rekeyObject(key, response) };
241};
242
243export const handleRequest = <Name: string, State: ReducerState<Name>>(
244 name: Name,
245 state: State,
246 ): State => (
247 {
248 ...state,
249 [name]: {
250 ...initialEndpointState,
251 ...state[name],
252 isFetching: true,
253 },
254 }
255);
256
257export const handleSuccess = <Name: string, State: ReducerState<Name>>(
258 name: Name,
259 state: State,
260 action: FSA<*, *>,
261 ): State => (
262 {
263 ...state,
264 [name]: {
265 ...initialEndpointState,
266 ...state[name],
267 success: action.payload || null,
268 isFetching: false,
269 lastUpdate: Date.now(),
270 lastResult: 'success',
271 },
272 }
273);
274
275export const handleSuccessKeyed = <Name: string, State: KeyedReducerState<Name>>(
276 name: Name,
277 state: State,
278 action: FSA<*, *>,
279 key: string,
280 ): State => (
281 {
282 ...state,
283 [name]: {
284 ...initialKeyedCollectionState,
285 ...state[name],
286 success: action.payload || null,
287 isFetching: false,
288 lastUpdate: Date.now(),
289 lastResult: 'success',
290 collection: handleKeyedCollection(key, state[name].collection, action.payload),
291 },
292 }
293);
294
295export const handleFailure = <Name: string, State: ReducerState<Name>>(
296 name: Name,
297 state: State,
298 action: FSA<*, *>,
299 ): State => (
300 {
301 ...state,
302 [name]: {
303 ...initialEndpointState,
304 ...state[name],
305 failure: action.payload || null,
306 isFetching: false,
307 lastUpdate: Date.now(),
308 lastResult: 'failure',
309 },
310 }
311);
312
313export const handleTimeout = <Name: string, State: ReducerState<Name>>(
314 name: Name,
315 state: State,
316 action: FSA<*, *>,
317 ): State => (
318 {
319 ...state,
320 [name]: {
321 ...initialEndpointState,
322 ...state[name],
323 timeout: action.payload || null,
324 isFetching: false,
325 lastUpdate: Date.now(),
326 lastResult: 'timeout',
327 },
328 }
329);
330
331export const handleMistake = <Name: string, State: ReducerState<Name>>(
332 name: Name,
333 state: State,
334 action: FSA<*, *>,
335 ): State => (
336 {
337 ...state,
338 [name]: {
339 ...initialEndpointState,
340 ...state[name],
341 mistake: action.payload || null,
342 isFetching: false,
343 lastUpdate: Date.now(),
344 lastResult: 'mistake',
345 },
346 }
347);