Skip to content

Instantly share code, notes, and snippets.

@rclayton-the-terrible
Last active November 29, 2021 19:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rclayton-the-terrible/493cd0811542ff9693ac02746517ba71 to your computer and use it in GitHub Desktop.
Save rclayton-the-terrible/493cd0811542ff9693ac02746517ba71 to your computer and use it in GitHub Desktop.
TypeScript WebSequenceDiagram Client
module.exports = {
globals: {
'ts-jest': {
tsConfig: 'tsconfig.json'
}
},
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
testMatch: ['**/*.integ.(ts|js)'],
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/', '.*.d.ts'],
modulePathIgnorePatterns: [
// Ignore Interface files
'.*Interface.ts',
// Ignore Type files
'.*.type.ts',
// Ignore entrypoints
'run-*',
// Ignore prototype entrypoints
'prototype-*',
// Ignore built javascript files
'dist/*'
]
}

Copyright (c) 2019 Richard Clayton

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# In my package.json, test:integ script is:
# ./node_modules/.bin/jest --config jest.integ.config.js
> npm run test:integ
import Axios from 'axios'
import WebSequenceDiagramClient, {
Options,
Styles,
ImageTypes,
InvalidSequenceDiagramError,
YouNeedToPayWSDSome$$$Error,
} from './WebSequenceDiagramClient'
const diagram = `
title This is a test
A->B: text
`
const badDiagram = `
title Ugh I wrote this wrong
A->
`
describe('WebSequenceDiagrams Client', () => {
const API_KEY = process.env.WEBSEQUENCEDIAGRAMS_API_KEY
// SVG and PDF are Pro Only
const imageTypes = !!API_KEY ? Object.values(ImageTypes) : ['png']
const axios = Axios.create({ baseURL: 'http://www.websequencediagrams.com' })
describe('when creating images with valid diagram text', () => {
Object.values(Styles).forEach((style) => {
imageTypes.forEach((imageType) => {
it(`should allow style [${style}] and image type [${imageType}]`, async () => {
const options: Options = {
style,
imageType,
}
if (API_KEY) {
options.apiKey = API_KEY
}
const client = new WebSequenceDiagramClient(axios, options)
// I'm using buffer because I'm watch for changes in
// files and reading them from disk
const image = await client.generateDiagram(Buffer.from(diagram))
expect(image).toBeInstanceOf(Buffer)
})
})
})
})
describe('when creating an image with invalid diagram text', () => {
it('should throw an InvalidSequenceDiagramError', async () => {
const client = new WebSequenceDiagramClient(axios)
await expect(client.generateDiagram(Buffer.from(badDiagram)))
.rejects.toThrow(InvalidSequenceDiagramError)
})
})
describe('when trying to access a paid feature without an API key', () => {
it('should throw a YouNeedToPayWSDSome$$$Error', async () => {
const client = new WebSequenceDiagramClient(axios, {
imageType: ImageTypes.svg,
})
await expect(client.generateDiagram(Buffer.from(diagram)))
.rejects.toThrow(YouNeedToPayWSDSome$$$Error)
})
})
})
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const url_1 = require("url");
var Styles;
(function (Styles) {
Styles["default"] = "default";
Styles["earth"] = "earth";
Styles["modernblue"] = "modern-blue";
Styles["mscgen"] = "mscgen";
Styles["omegapple"] = "omegapple";
Styles["qsd"] = "qsd";
Styles["rose"] = "rose";
Styles["roundgreen"] = "roundgreen";
Styles["napkin"] = "napkin";
Styles["magazine"] = "magazine";
Styles["vs2010"] = "vs2010";
Styles["patent"] = "patent";
})(Styles = exports.Styles || (exports.Styles = {}));
var ImageTypes;
(function (ImageTypes) {
ImageTypes["png"] = "png";
ImageTypes["pdf"] = "pdf";
ImageTypes["svg"] = "svg";
})(ImageTypes = exports.ImageTypes || (exports.ImageTypes = {}));
const DefaultOptions = {
style: Styles.default,
imageType: ImageTypes.png,
};
class InvalidSequenceDiagramError extends Error {
constructor(errors) {
super(`Errors found in diagram syntax: ${errors.join(',')}`);
Error.captureStackTrace(this, InvalidSequenceDiagramError);
}
}
exports.InvalidSequenceDiagramError = InvalidSequenceDiagramError;
class YouNeedToPayWSDSome$$$Error extends Error {
constructor() {
super('WebSequenceDiagrams rate limited you because you need a pro account.');
Error.captureStackTrace(this, YouNeedToPayWSDSome$$$Error);
}
}
exports.YouNeedToPayWSDSome$$$Error = YouNeedToPayWSDSome$$$Error;
class ClientError extends Error {
constructor(nestedError) {
super('Client failed to execute call for some reason.');
this.nestedError = nestedError;
Error.captureStackTrace(this, ClientError);
}
}
exports.ClientError = ClientError;
class RequestError extends Error {
constructor(statusCode, body) {
super('Client failed due to unknown HTTP Request Error.');
this.statusCode = statusCode;
this.body = body;
Error.captureStackTrace(this, RequestError);
}
}
exports.RequestError = RequestError;
class InvalidResponseError extends Error {
constructor() {
super('WebSequenceDiagrams returned an invalid response.');
Error.captureStackTrace(this, InvalidResponseError);
}
}
exports.InvalidResponseError = InvalidResponseError;
class WebSequenceDiagramClient {
constructor(client, options = {}) {
this.client = client;
this.options = Object.assign({}, DefaultOptions, options);
}
getContentType() {
switch (this.options.imageType) {
case ImageTypes.pdf: return 'application/pdf';
case ImageTypes.svg: return 'image/svg+xml';
default: return 'image/png';
}
}
// This is just a wrapper around an Axios request to allow reuse of the error handling logic.
static tryRequest(promise) {
return __awaiter(this, void 0, void 0, function* () {
try {
const { data } = yield promise;
return data;
}
catch (error) {
if (error.response) {
// TODO: enumerate other HTTP errors when discovered in the future.
switch (error.response.status) {
case 402: throw new YouNeedToPayWSDSome$$$Error();
default: throw new RequestError(error.response.status, error.response.data);
}
}
throw new ClientError(error);
}
});
}
generateDiagram(contents) {
return __awaiter(this, void 0, void 0, function* () {
const params = new url_1.URLSearchParams({
apiVersion: '1',
message: contents.toString('utf-8'),
style: this.options.style,
format: this.options.imageType,
apikey: this.options.apiKey || undefined,
});
const { errors, img } = yield WebSequenceDiagramClient.tryRequest(this.client.post('/index.php', params));
if (errors) {
if (errors.length === 0) {
return yield WebSequenceDiagramClient.tryRequest(this.client.get(`/${img}`, {
responseType: 'arraybuffer',
transformResponse: data => Buffer.from(data),
}));
}
else {
throw new InvalidSequenceDiagramError(errors);
}
}
throw new InvalidResponseError();
});
}
}
exports.default = WebSequenceDiagramClient;
//# sourceMappingURL=WebSequenceDiagramClient.js.map
import { AxiosInstance, AxiosPromise } from 'axios'
import { URLSearchParams } from 'url'
export enum Styles {
default = 'default',
earth = 'earth',
modernblue = 'modern-blue',
mscgen = 'mscgen',
omegapple = 'omegapple',
qsd = 'qsd',
rose = 'rose',
roundgreen = 'roundgreen',
napkin = 'napkin',
magazine = 'magazine',
vs2010 = 'vs2010',
patent = 'patent',
}
export enum ImageTypes {
png = 'png',
pdf = 'pdf',
svg = 'svg',
}
export type Options = {
style: Styles,
imageType: ImageTypes,
apiKey?: string,
}
const DefaultOptions: Options = {
style: Styles.default,
imageType: ImageTypes.png,
}
type GenerateDiagramResponse = {
errors: string[],
img: string,
}
export class InvalidSequenceDiagramError extends Error {
constructor(errors: string[]) {
super(`Errors found in diagram syntax: ${errors.join(',')}`)
Error.captureStackTrace(this, InvalidSequenceDiagramError)
}
}
export class YouNeedToPayWSDSome$$$Error extends Error {
constructor() {
super('WebSequenceDiagrams rate limited you because you need a pro account.')
Error.captureStackTrace(this, YouNeedToPayWSDSome$$$Error)
}
}
export class ClientError extends Error {
constructor(protected nestedError: any) {
super('Client failed to execute call for some reason.')
Error.captureStackTrace(this, ClientError)
}
}
export class RequestError extends Error {
constructor(protected statusCode: number, protected body: any) {
super('Client failed due to unknown HTTP Request Error.')
Error.captureStackTrace(this, RequestError)
}
}
export class InvalidResponseError extends Error {
constructor() {
super('WebSequenceDiagrams returned an invalid response.')
Error.captureStackTrace(this, InvalidResponseError)
}
}
export default class WebSequenceDiagramClient {
protected options: Options
constructor(protected client: AxiosInstance, options: Partial<Options> = {}) {
this.options = Object.assign({}, DefaultOptions, options)
}
getContentType(): string {
switch (this.options.imageType) {
case ImageTypes.pdf: return 'application/pdf'
case ImageTypes.svg: return 'image/svg+xml'
default: return 'image/png'
}
}
// This is just a wrapper around an Axios request to allow reuse of the error handling logic.
protected static async tryRequest<T>(promise: AxiosPromise<T>): Promise<T> {
try {
const { data } = await promise
return data
} catch (error) {
if (error.response) {
// TODO: enumerate other HTTP errors when discovered in the future.
switch (error.response.status) {
case 402: throw new YouNeedToPayWSDSome$$$Error()
default: throw new RequestError(error.response.status, error.response.data)
}
}
throw new ClientError(error)
}
}
async generateDiagram(contents: Buffer): Promise<Buffer> {
const params = new URLSearchParams({
apiVersion: '1',
message: contents.toString('utf-8'),
style: this.options.style as any as string,
format: this.options.imageType as any as string,
apikey: this.options.apiKey || undefined,
})
const { errors, img } = await WebSequenceDiagramClient.tryRequest(
this.client.post<GenerateDiagramResponse>(
'/index.php',
params
)
)
if (errors) {
if (errors.length === 0) {
return await WebSequenceDiagramClient.tryRequest(
this.client.get<Buffer>(`/${img}`, {
responseType: 'arraybuffer',
transformResponse: data => Buffer.from(data),
})
)
} else {
throw new InvalidSequenceDiagramError(errors)
}
}
throw new InvalidResponseError()
}
}
@MurylloEx
Copy link

AMAZING! Thank you!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment