From be7397d3d7b5793696b5bb84359c7871a46a7cbd Mon Sep 17 00:00:00 2001
From: Owen Yamauchi
Date: Fri, 4 Aug 2023 19:14:22 -0400
Subject: [PATCH] Factor out some common stuff from calculator.ts (#12)
I want to copy this file for the RI calculator. To avoid too much
duplication, this factors out some common logic: the "fetch from API
and interpret errors" code, and the static footer.
It also changes the `api-path` attribute of the calculator element to
`api-host`, which should contain only the protocol and hostname, not
the path. Since the RI calculator will be fetching multiple paths, I
want to separate path from host. `api-path` is not publicly
documented, so I think it's fine to remove it.
---
src/api/fetch.ts | 41 +++++++++++++++++++++++++
src/calculator-footer.ts | 21 +++++++++++++
src/calculator.ts | 65 ++++++++--------------------------------
src/working-copy.html | 2 +-
4 files changed, 75 insertions(+), 54 deletions(-)
create mode 100644 src/api/fetch.ts
create mode 100644 src/calculator-footer.ts
diff --git a/src/api/fetch.ts b/src/api/fetch.ts
new file mode 100644
index 0000000..215ba98
--- /dev/null
+++ b/src/api/fetch.ts
@@ -0,0 +1,41 @@
+/**
+ * Fetches a response from the Incentives API. Handles turning an error response
+ * into an exception with a useful message.
+ */
+export async function fetchApi(
+ apiKey: string,
+ apiHost: string,
+ path: string,
+ query: URLSearchParams,
+) {
+ const url = new URL(apiHost);
+ url.pathname = path;
+ url.search = query.toString();
+ const response: Response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+ if (response.status >= 400) {
+ console.error(response);
+ // statusText isn't always set, but it's a reasonable proxy for a human readable error if it is:
+ let message = response.statusText;
+ try {
+ const error = await response.json();
+ console.error(error);
+ if (error.title && error.detail) {
+ // Zuplo's API key errors have this form:
+ message = `${error.title}: ${error.detail}`;
+ } else if (error.message && error.error) {
+ // Rewiring America's API errors have this form:
+ message = `${error.error}: ${error.message}`;
+ }
+ } catch (e) {
+ // if we couldn't get anything off the response, just go with something generic:
+ message = 'Error loading incentives.';
+ }
+ throw new Error(message);
+ }
+ return response.json();
+}
diff --git a/src/calculator-footer.ts b/src/calculator-footer.ts
new file mode 100644
index 0000000..f72d116
--- /dev/null
+++ b/src/calculator-footer.ts
@@ -0,0 +1,21 @@
+import { html } from 'lit';
+
+export const CALCULATOR_FOOTER = html``;
diff --git a/src/calculator.ts b/src/calculator.ts
index 3f8803b..b8c80da 100644
--- a/src/calculator.ts
+++ b/src/calculator.ts
@@ -10,6 +10,8 @@ import {
ICalculatedIncentiveResults,
OwnerStatus,
} from './calculator-types';
+import { CALCULATOR_FOOTER } from './calculator-footer';
+import { fetchApi } from './api/fetch';
const loadedTemplate = (
results: ICalculatedIncentiveResults,
@@ -32,8 +34,7 @@ const errorTemplate = (error: unknown) => html`
`;
-const DEFAULT_CALCULATOR_API_PATH: string =
- 'https://api.rewiringamerica.org/api/v0/calculator';
+const DEFAULT_CALCULATOR_API_HOST: string = 'https://api.rewiringamerica.org';
@customElement('rewiring-america-calculator')
export class RewiringAmericaCalculator extends LitElement {
@@ -62,8 +63,8 @@ export class RewiringAmericaCalculator extends LitElement {
@property({ type: String, attribute: 'api-key' })
apiKey: string = '';
- @property({ type: String, attribute: 'api-path' })
- apiPath: string = DEFAULT_CALCULATOR_API_PATH;
+ @property({ type: String, attribute: 'api-host' })
+ apiHost: string = DEFAULT_CALCULATOR_API_HOST;
/* supported properties to allow pre-filling the form */
@@ -123,34 +124,12 @@ export class RewiringAmericaCalculator extends LitElement {
tax_filing,
household_size,
});
- const url = new URL(`${this.apiPath}?${query}`);
- const response: Response = await fetch(url, {
- method: 'GET',
- headers: {
- Authorization: `Bearer ${this.apiKey}`,
- },
- });
- if (response.status >= 400) {
- console.error(response);
- // statusText isn't always set, but it's a reasonable proxy for a human readable error if it is:
- let message = response.statusText;
- try {
- const error = await response.json();
- console.error(error);
- if (error.title && error.detail) {
- // Zuplo's API key errors have this form:
- message = `${error.title}: ${error.detail}`;
- } else if (error.message && error.error) {
- // Rewiring America's API errors have this form:
- message = `${error.error}: ${error.message}`;
- }
- } catch (e) {
- // if we couldn't get anything off the response, just go with something generic:
- message = 'Error loading incentives.';
- }
- throw new Error(message);
- }
- return response.json();
+ return await fetchApi(
+ this.apiKey,
+ this.apiHost,
+ '/api/v0/calculator',
+ query,
+ );
},
// if the args array changes then the task will run again
args: () => [
@@ -187,27 +166,7 @@ export class RewiringAmericaCalculator extends LitElement {
error: errorTemplate,
})}
`}
-
+ ${CALCULATOR_FOOTER}
`;
}
diff --git a/src/working-copy.html b/src/working-copy.html
index 439a727..6673b7c 100644
--- a/src/working-copy.html
+++ b/src/working-copy.html
@@ -87,7 +87,7 @@ Rewiring America Embeddable IRA Calculator
Example