Skip to content

Commit

Permalink
feat: add extras.maxLength options to configure the maximum extras …
Browse files Browse the repository at this point in the history
…lengths

To achieve this nicely in the code, intorduce a separate `Extras` formatter. This formatter is solely focussed on formatting the extras and is used by `Formatter`.
  • Loading branch information
jdbruijn committed Mar 25, 2022
1 parent 468dfdd commit 8450b08
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 60 deletions.
119 changes: 119 additions & 0 deletions src/formatter/extras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { ParsedOptions } from '../options';

class Extras {
readonly options = {
keyValueSeparator: '=',
separator: ', ',
start: '(',
end: ')',
} as const;

private readonly _maxLength: ParsedOptions['extras']['maxLength'];
private _extras: string[];
private _length: number;

constructor(maxLength: ParsedOptions['extras']['maxLength']) {
this._maxLength = maxLength;
this._length = 0;
this._extras = [];
}

get length() {
return this._length;
}

get extras() {
return this._extras;
}

/**
* Parse a key-value pair and add to the extras if it is a valid extra.
*
* @param key
* @param value
* @returns `true` if the key-value was valid and is added, false otherwise.
*/
parseAndAdd(key: string, value: unknown): boolean {
if (typeof value === 'object' || typeof value === 'function') {
return false;
}

const extra = this.formatExtra(key, value);
if (
extra.key.length > this._maxLength.key ||
extra.value.length > this._maxLength.value ||
this.lengthAfterAdding(extra.formatted) > this._maxLength.total
) {
return false;
}

this.add(extra.formatted);
return true;
}

format(): string {
if (this._extras.length === 0) {
return '';
}

return [
this.options.start,
this._extras.join(this.options.separator),
this.options.end,
].join('');
}

formatExtra(
key: string,
value: unknown,
): { formatted: string; key: string; value: string } {
const stringifiedKey = this.stringify(key);
const stringifiedValue = this.stringify(value);
const formatted = [
stringifiedKey,
this.options.keyValueSeparator,
stringifiedValue,
].join('');

return {
formatted,
key: stringifiedKey,
value: stringifiedValue,
};
}

private lengthAfterAdding(formattedExtra: string): number {
let length = this._length + formattedExtra.length;
if (this._length === 0) {
length += this.options.start.length + this.options.end.length;
} else if (this._extras.length >= 1) {
length += this.options.separator.length;
}

return length;
}

private add(formattedExtra: string): void {
this._extras.push(formattedExtra);
this._length = this.lengthAfterAdding(formattedExtra);
}

private stringify(value: unknown): string {
if (
typeof value === 'string' &&
value.length > 0 &&
!this.containsWhitespace(value) &&
!value.includes(this.options.keyValueSeparator)
) {
return value;
}

return JSON.stringify(value);
}

private containsWhitespace(value: string): boolean {
return /\s/.test(value);
}
}

export { Extras };
106 changes: 106 additions & 0 deletions src/formatter/extras.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { beforeEach, describe, expect, it } from '@jest/globals';
import { Extras } from './extras';

describe('Extras', () => {
let extras: Extras;
beforeEach(() => {
const options = { key: 10, value: 20, total: 50 } as const;
extras = new Extras(options);
});

it('formats an extra key with spaces in quotations', () => {
const extra = extras.formatExtra('some key', 'value');
expect(extra.key).toBe('"some key"');
});

it('formats an extra value with spaces in quotations', () => {
const extra = extras.formatExtra('key', 'some value');
expect(extra.value).toBe('"some value"');
});

it('formats an extra key and value with spaces in quotations', () => {
const extra = extras.formatExtra('some key', 'some value');
expect(extra.key).toBe('"some key"');
expect(extra.value).toBe('"some value"');
});

it('formats an extra with a key-value separator', () => {
const extra = extras.formatExtra('key', 'value');
expect(extra.formatted).toMatch(/^key.+value$/);
});

describe('length', () => {
it('is "0" for no extras', () => {
expect(extras).toHaveLength(0);
});

it('is the correct length for a single extra', () => {
const key = 'myKey';
const value = 'myValue';

extras.parseAndAdd(key, value);
expect(extras).toHaveLength(
extras.options.start.length +
key.length +
extras.options.keyValueSeparator.length +
value.length +
extras.options.end.length,
);
});

it('is the correct length for a multiple extras', () => {
const key = 'myKey';
const value = 'myValue';
extras.parseAndAdd(`${key}1`, value);
extras.parseAndAdd(`${key}2`, value);
expect(extras).toHaveLength(
extras.options.start.length +
(key.length +
1 +
extras.options.keyValueSeparator.length +
value.length) *
2 +
extras.options.separator.length +
extras.options.end.length,
);
});

it('is the correct length for non-included extras', () => {
const key = 'myKey';
const value = 'myValue';
extras.parseAndAdd(key, value);
extras.parseAndAdd('my very long key that cannot be added', value);
expect(extras).toHaveLength(
extras.options.start.length +
key.length +
extras.options.keyValueSeparator.length +
value.length +
extras.options.end.length,
);
});
});

describe('parseAndAdd', () => {
it('returns "true" if the extra was added', () => {
expect(extras.parseAndAdd('myKey', 'myValue')).toBe(true);
});

it('returns "false" if the extra was not added', () => {
expect(
extras.parseAndAdd('my very long key that cannot be added', 'myValue'),
).toBe(false);
});

it('adds the extra if it can', () => {
expect(extras.extras).toHaveLength(0);
extras.parseAndAdd('myKey', 'myValue');
expect(extras.extras).toHaveLength(1);
});

it('does not add the extra if it is invalid', () => {
expect(extras.extras).toHaveLength(0);
extras.parseAndAdd('my very long key that cannot be added', 'myValue');
expect(extras.extras).toHaveLength(0);
});
});
});
74 changes: 18 additions & 56 deletions src/formatter.ts → src/formatter/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BunyanRecord, coreFields } from './bunyan-record';
import { Options, ParsedOptions, schema } from './options';
import { BunyanRecord, coreFields } from '../bunyan-record';
import { Options, ParsedOptions, schema } from '../options';
import { Extras } from './extras';
import bunyan from 'bunyan';
import chalk from 'chalk';
import is from '@sindresorhus/is';
Expand All @@ -26,11 +27,8 @@ interface ParsedRecord
time: moment.Moment;
message: BunyanRecord['msg'];
source: BunyanRecord['src'];

/* eslint-disable @typescript-eslint/no-explicit-any */
extras: Record<string, any>;
details: Record<string, any>;
/* eslint-enable @typescript-eslint/no-explicit-any */
extras: Extras;
details: Record<string, unknown>;
}

class Formatter {
Expand All @@ -40,7 +38,6 @@ class Formatter {
whitespace: /\s/,
} as const;
private readonly _internalOptions = {
maxExtrasValueLength: 50,
timeFormat: {
short: 'HH:mm:ss.SSS',
long: 'YYYY-MM-DD[T]HH:mm:ss.SSS',
Expand Down Expand Up @@ -73,7 +70,7 @@ class Formatter {
time: moment(record.time),
message: record.msg,
source: record.src,
extras: {},
extras: new Extras(this._options.extras.maxLength),
details: sanitise(record),
};

Expand All @@ -87,26 +84,19 @@ class Formatter {
return parsed;
}

const extras = is.undefined(this._options.extras.key)
? {}
const leftOvers = is.undefined(this._options.extras.key)
? parsed.details
: parsed.details[this._options.extras.key];
if (this._options.extras.key !== undefined && is.nonEmptyObject(extras)) {
const extrasKey = this._options.extras.key;
Object.entries(parsed.details[extrasKey]).forEach(([key, value]) => {
if (this.isExtra(value)) {
parsed.extras[key] = value;
delete parsed.details[extrasKey][key];
}
});
} else if (this._options.extras.key === undefined) {
Object.entries(parsed.details).forEach(([key, value]) => {
if (this.isExtra(value)) {
parsed.extras[key] = value;
delete parsed.details[key];
}
});
if (!is.nonEmptyObject(leftOvers)) {
return parsed;
}

Object.entries(leftOvers).forEach(([key, value]) => {
if (parsed.extras.parseAndAdd(key, value)) {
delete leftOvers[key];
}
});

return parsed;
}

Expand Down Expand Up @@ -206,24 +196,8 @@ class Formatter {
}

formatExtras(extras: ParsedRecord['extras']): string {
const entries = Object.entries(extras);
if (entries.length === 0) {
return '';
}

const formattedExtras = entries.map(([key, value]) => {
if (
is.string(value) &&
!this.containsWhitespace(value) &&
value.length > 0
) {
return `${key}=${value}`;
}

return `${key}=${JSON.stringify(value)}`;
});

return chalk.red(` (${formattedExtras.join(', ')})`);
const formattedExtras = extras.format();
return formattedExtras.length === 0 ? '' : chalk.red(` ${formattedExtras}`);
}

formatDetails(
Expand Down Expand Up @@ -258,18 +232,6 @@ class Formatter {
return `${formatted.join(separator)}${suffix}`;
}

isExtra(value: unknown): boolean {
let stringifiedValue = JSON.stringify(value, undefined, 2);
if (is.string(value)) {
stringifiedValue = value;
}

return (
this.isSingleLine(stringifiedValue) &&
stringifiedValue.length <= this._internalOptions.maxExtrasValueLength
);
}

isSingleLine(string: string): boolean {
return !this._regex.newLine.test(string);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from '@jest/globals';
import { BunyanRecord } from './bunyan-record';
import { BunyanRecord } from '../bunyan-record';
import { Formatter } from './formatter';
import { ParsedOptions } from './options';
import { ParsedOptions } from '../options';
import stripAnsi from 'strip-ansi';

describe('Formatter', () => {
Expand All @@ -14,7 +14,7 @@ describe('Formatter', () => {
source: false,
extras: false,
},
extras: {},
extras: { maxLength: { key: 20, value: 50, total: 500 } },
indent: {
details: 4,
json: 2,
Expand Down
3 changes: 3 additions & 0 deletions src/formatter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Formatter } from './formatter';

export { Formatter };
8 changes: 8 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const schema = z
.min(1)
.regex(new RegExp(`^((?!(${bunyanCoreFields().join('|')})).)*$`))
.optional(),
maxLength: z
.object({
key: z.number().int().positive().default(20),
value: z.number().int().positive().default(50),
total: z.number().int().positive().default(500),
})
.strict()
.default({}),
})
.strict()
.default({}),
Expand Down
2 changes: 1 addition & 1 deletion src/options.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('schema', () => {
source: false,
extras: false,
},
extras: {},
extras: { maxLength: { key: 20, value: 50, total: 500 } },
indent: {
details: 4,
json: 2,
Expand Down

0 comments on commit 8450b08

Please sign in to comment.