-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #325 from mittwald/chore/error-messages
Add specialized error formats when IDs for unexpected resource types are supplied
- Loading branch information
Showing
16 changed files
with
411 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { articleForWord } from "../language.js"; | ||
|
||
export default class UnexpectedShortIDPassedError extends Error { | ||
public readonly resourceName: string; | ||
public readonly format: string; | ||
|
||
public constructor(name: string, format: string) { | ||
super( | ||
`This command expects ${articleForWord(name)} ${name}, which is typically formatted as ${format}. It looks like you passed a short ID for another type of resource, instead.`, | ||
); | ||
|
||
this.resourceName = name; | ||
this.format = format; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* Helper function to determine the grammatically correct article for a word. | ||
* | ||
* This is an approximation and ignores common exceptions, like unsounded "h"s. | ||
* However, for our purposes, it should be sufficient. | ||
* | ||
* @param word | ||
* @returns Returns "an" if the word starts with a vowel, "a" otherwise | ||
*/ | ||
export function articleForWord(word: string): "an" | "a" { | ||
return /^[aeiou]/i.test(word) ? "an" : "a"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { | ||
ApiClientError, | ||
AxiosResponseHeaders, | ||
} from "@mittwald/api-client-commons"; | ||
import { Box, Text } from "ink"; | ||
import { RawAxiosResponseHeaders } from "axios"; | ||
import ErrorStack from "./ErrorStack.js"; | ||
import ErrorText from "./ErrorText.js"; | ||
import ErrorBox from "./ErrorBox.js"; | ||
|
||
function RequestHeaders({ headers }: { headers: string }) { | ||
const lines = headers.trim().split("\r\n"); | ||
const requestLine = lines.shift(); | ||
const values = lines.map((line) => line.split(": ", 2)) as [string, string][]; | ||
const maxKeyLength = Math.max(...values.map(([key]) => key.length)); | ||
|
||
return ( | ||
<Box flexDirection="column"> | ||
<Text bold underline> | ||
{requestLine} | ||
</Text> | ||
{values.map(([key, value]) => ( | ||
<Box flexDirection="row" key={key}> | ||
<Text dimColor>{key.toLowerCase().padEnd(maxKeyLength, " ")} </Text> | ||
<Text bold>{key === "x-access-token" ? "[redacted]" : value}</Text> | ||
</Box> | ||
))} | ||
</Box> | ||
); | ||
} | ||
|
||
function Response({ | ||
status, | ||
statusText, | ||
body, | ||
headers, | ||
}: { | ||
status: number; | ||
statusText: string; | ||
body: unknown; | ||
headers: RawAxiosResponseHeaders | AxiosResponseHeaders; | ||
}) { | ||
const keys = Object.keys(headers); | ||
const maxKeyLength = Math.max(...keys.map((key) => key.length)); | ||
return ( | ||
<Box flexDirection="column"> | ||
<Text bold underline key="status"> | ||
{status} {statusText} | ||
</Text> | ||
{keys.map((key) => ( | ||
<Box flexDirection="row" key={key}> | ||
<Text dimColor>{key.toLowerCase().padEnd(maxKeyLength, " ")} </Text> | ||
<Text bold> | ||
{key === "x-access-token" ? "[redacted]" : headers[key]} | ||
</Text> | ||
</Box> | ||
))} | ||
<Box marginTop={1} key="body"> | ||
<Text>{JSON.stringify(body, undefined, 2)}</Text> | ||
</Box> | ||
</Box> | ||
); | ||
} | ||
|
||
function HttpMessages({ err }: { err: ApiClientError }) { | ||
const response = err.response ? ( | ||
<Response | ||
status={err.response.status!} | ||
statusText={err.response.statusText} | ||
body={err.response.data} | ||
headers={err.response.headers} | ||
/> | ||
) : ( | ||
<Text>no response received</Text> | ||
); | ||
|
||
return ( | ||
<Box marginX={2} marginY={1} flexDirection="column" rowGap={1}> | ||
<RequestHeaders headers={err.request._header} /> | ||
{response} | ||
</Box> | ||
); | ||
} | ||
|
||
interface APIErrorProps { | ||
err: ApiClientError; | ||
withStack: boolean; | ||
withHTTPMessages: "no" | "body" | "full"; | ||
} | ||
|
||
/** | ||
* Render an API client error to the terminal. In the case of an API client | ||
* error, the error message will be displayed, as well as (when enabled) the | ||
* request and response headers and body. | ||
*/ | ||
export default function APIError({ | ||
err, | ||
withStack, | ||
withHTTPMessages, | ||
}: APIErrorProps) { | ||
return ( | ||
<> | ||
<ErrorBox> | ||
<ErrorText bold underline> | ||
API CLIENT ERROR | ||
</ErrorText> | ||
<ErrorText> | ||
An error occurred while communicating with the API: {err.message} | ||
</ErrorText> | ||
|
||
<Text>{JSON.stringify(err.response?.data, undefined, 2)}</Text> | ||
</ErrorBox> | ||
|
||
{withHTTPMessages === "full" ? <HttpMessages err={err} /> : undefined} | ||
{withStack && "stack" in err ? <ErrorStack err={err} /> : undefined} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Box, BoxProps } from "ink"; | ||
import { PropsWithChildren } from "react"; | ||
|
||
const defaultErrorBoxProps: BoxProps = { | ||
width: 80, | ||
flexDirection: "column", | ||
borderColor: "red", | ||
borderStyle: "round", | ||
paddingX: 1, | ||
rowGap: 1, | ||
}; | ||
|
||
/** A pre-styled box for displaying errors. */ | ||
export default function ErrorBox(props: PropsWithChildren<BoxProps>) { | ||
return ( | ||
<Box {...defaultErrorBoxProps} {...props}> | ||
{props.children} | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Box } from "ink"; | ||
import ErrorText from "./ErrorText.js"; | ||
|
||
/** Render the stack trace of an error. */ | ||
export default function ErrorStack({ err }: { err: Error }) { | ||
return ( | ||
<Box marginX={2} marginY={1} flexDirection="column" rowGap={1}> | ||
<ErrorText dimColor bold> | ||
ERROR STACK TRACE | ||
</ErrorText> | ||
<ErrorText dimColor> | ||
Please provide this when opening a bug report. | ||
</ErrorText> | ||
<ErrorText dimColor>{err.stack}</ErrorText> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Text, TextProps } from "ink"; | ||
|
||
/** A pre-styled text for displaying errors. */ | ||
export default function ErrorText(props: TextProps) { | ||
return ( | ||
<Text color="red" {...props}> | ||
{props.children} | ||
</Text> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Box } from "ink"; | ||
import ErrorStack from "./ErrorStack.js"; | ||
import ErrorText from "./ErrorText.js"; | ||
import ErrorBox from "./ErrorBox.js"; | ||
|
||
const issueURL = "https://github.com/mittwald/cli/issues/new"; | ||
|
||
interface GenericErrorProps { | ||
err: Error; | ||
withStack: boolean; | ||
withIssue?: boolean; | ||
title?: string; | ||
} | ||
|
||
/** | ||
* Render a generic error to the terminal. This is used for errors that don't | ||
* have a specific rendering function. | ||
*/ | ||
export default function GenericError({ | ||
err, | ||
withStack, | ||
withIssue = true, | ||
title = "Error", | ||
}: GenericErrorProps) { | ||
return ( | ||
<> | ||
<ErrorBox> | ||
<ErrorText bold underline> | ||
{title.toUpperCase()} | ||
</ErrorText> | ||
<ErrorText>An error occurred while executing this command:</ErrorText> | ||
<Box marginX={2}> | ||
<ErrorText>{err.toString()}</ErrorText> | ||
</Box> | ||
{withIssue ? ( | ||
<ErrorText> | ||
If you believe this to be a bug, please open an issue at {issueURL}. | ||
</ErrorText> | ||
) : undefined} | ||
</ErrorBox> | ||
|
||
{withStack && "stack" in err ? <ErrorStack err={err} /> : undefined} | ||
</> | ||
); | ||
} |
Oops, something went wrong.