-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(formatter): Add YScope formatter for structured logs and remove Logback-style formatter. #123
Changes from 18 commits
c29025d
e44d47f
71ee56c
d406e51
b55ebab
19c31d5
9ea0006
47bd40e
ebfa463
9d5ae88
c74eb59
c9a48e0
61b4769
087548d
01a3889
2acd20a
acf7a2c
197125a
cd23262
267b1f9
2385919
de03857
c6c65a1
2bba5ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,8 +33,11 @@ import ThemeSwitchToggle from "./ThemeSwitchToggle"; | |
|
||
const CONFIG_FORM_FIELDS = [ | ||
{ | ||
helperText: "[JSON] Log messages conversion pattern. The current syntax is similar to" + | ||
" Logback conversion patterns but will change in a future release.", | ||
helperText: `[JSON] Log messages conversion pattern. Add field-placeholders to insert | ||
fields from JSON log events. A field-placeholder uses the following syntax: | ||
\`{<field-name>[:<formatter-name>[:<formatter-options>]]}\`. \`field-name\` is required, | ||
while \`formatter-name\` and \`formatter-options\` are optional. See the default pattern | ||
for an example.`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got rid about the bit about the default pattern since the default pattern will be empty soon. I added a small example instead There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
initialValue: getConfig(CONFIG_KEY.DECODER_OPTIONS).formatString, | ||
label: "Decoder: Format string", | ||
name: LOCAL_STORAGE_KEY.DECODER_OPTIONS_FORMAT_STRING, | ||
|
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {Nullable} from "../../../../typings/common"; | ||
import {YScopeFieldFormatter} from "../../../../typings/formatters"; | ||
import {JsonValue} from "../../../../typings/js"; | ||
import {jsonValueToString} from "../utils"; | ||
|
||
|
||
/** | ||
* A field formatter that rounds numerical values to the nearest integer. | ||
* If the field value is not a number, it is returned as-is after being | ||
* converted to a string. Does not currently support any options. | ||
*/ | ||
davemarco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class RoundFormatter implements YScopeFieldFormatter { | ||
constructor (options: Nullable<string>) { | ||
if (null !== options) { | ||
throw Error(`Round formatter does not support option ${options}`); | ||
davemarco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
// eslint-disable-next-line class-methods-use-this | ||
formatField (field: JsonValue): string { | ||
if ("number" === typeof field) { | ||
field = Math.round(field); | ||
} | ||
junhaoliao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return jsonValueToString(field); | ||
} | ||
} | ||
|
||
export default RoundFormatter; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {Dayjs} from "dayjs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {Nullable} from "../../../../typings/common"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {YScopeFieldFormatter} from "../../../../typings/formatters"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {JsonValue} from "../../../../typings/js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {convertToDayjsTimestamp} from "../../../decoders/JsonlDecoder/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* A formatter that formats timestamp values using a specified date-time pattern. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* If no pattern is provided as an option, the timestamp is formatted in the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* default ISO 8601 format. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davemarco marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class TimestampFormatter implements YScopeFieldFormatter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#dateFormat: Nullable<string> = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
constructor (options: Nullable<string>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.#dateFormat = options; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
formatField (field: JsonValue): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// eslint-disable-next-line no-warning-comments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// TODO: We already parsed the timestamp during deserialization so this is perhaps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// inefficient. However, this field formatter can be used for multiple keys, so using | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// the single parsed timestamp by itself would not work. Perhaps in future we can check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// if the key is the same as timestamp key and avoid parsing again. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timestamp: Dayjs = convertToDayjsTimestamp(field); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (null === this.#dateFormat) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return timestamp.format(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return timestamp.format(this.#dateFormat); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for invalid inputs. The method should handle cases where the input cannot be converted to a timestamp. Consider adding try-catch: formatField (field: JsonValue): string {
+ try {
const timestamp: Dayjs = convertToDayjsTimestamp(field);
if (null === this.#dateFormat) {
return timestamp.format();
}
return timestamp.format(this.#dateFormat);
+ } catch (error) {
+ throw new Error(`Failed to format timestamp: ${error.message}`);
+ }
} 📝 Committable suggestion
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. convert to dayjs already has error handling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @davemarco, thank you for the clarification. Since (_/) ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export default TimestampFormatter; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {Nullable} from "../../../typings/common"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
FIELD_PLACEHOLDER_REGEX, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Formatter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
FormatterOptionsType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
REPLACEMENT_CHARACTER_REGEX, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
YScopeFieldFormatter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
YScopeFieldPlaceholder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} from "../../../typings/formatters"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {LogEvent} from "../../../typings/logs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
getFormattedField, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
removeEscapeCharacters, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
replaceDoubleBacklash, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
splitFieldPlaceholder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
YSCOPE_FORMATTERS_MAP, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} from "./utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* A formatter that uses a YScope format string to format log events into a string. See | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* `YScopeFormatterOptionsType` for details about the format string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class YScopeFormatter implements Formatter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit picking - any better names than YscopeFormatter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JsonFormatter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed offline - it seems we can't find a better name than |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
readonly #processedFormatString: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#fieldPlaceholders: YScopeFieldPlaceholder[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
constructor (options: FormatterOptionsType) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (REPLACEMENT_CHARACTER_REGEX.test(options.formatString)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log("Replacement character is an invalid character in format string." + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Replacement character will appear as \\."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.#processedFormatString = replaceDoubleBacklash(options.formatString); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.#parseFieldPlaceholder(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
junhaoliao marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation and improve warning message Two improvements needed:
Apply this diff: constructor (options: FormatterOptionsType) {
+ if (!options?.formatString?.trim()) {
+ throw new Error("Format string must not be empty");
+ }
+
if (REPLACEMENT_CHARACTER_REGEX.test(options.formatString)) {
- console.log("Replacement character is an invalid character in format string." +
- "Replacement character will appear as \\.");
+ console.warn(
+ "Found Unicode replacement character (U+FFFD) in format string. " +
+ "This character will be displayed as '\\' in the output."
+ );
}
this.#processedFormatString = replaceDoubleBacklash(options.formatString);
this.#parseFieldPlaceholder();
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
formatLogEvent (logEvent: LogEvent): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let formattedLog = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Keeps track of the last position in format string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let lastIndex = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const fieldPlaceholder of this.#fieldPlaceholders) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const formatStringFragment = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.#processedFormatString.slice(lastIndex, fieldPlaceholder.range.start); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const cleanedFragment = removeEscapeCharacters(formatStringFragment); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
formattedLog += cleanedFragment; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
formattedLog += getFormattedField(logEvent, fieldPlaceholder); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lastIndex = fieldPlaceholder.range.end; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const remainder = this.#processedFormatString.slice(lastIndex); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
formattedLog += removeEscapeCharacters(remainder); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return `${formattedLog}\n`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize string concatenation and add error handling Consider these improvements:
Apply this diff: formatLogEvent (logEvent: LogEvent): string {
- let formattedLog = "";
+ const fragments: string[] = [];
let lastIndex = 0;
for (const fieldPlaceholder of this.#fieldPlaceholders) {
+ try {
const formatStringFragment =
this.#processedFormatString.slice(lastIndex, fieldPlaceholder.range.start);
- const cleanedFragment = removeEscapeCharacters(formatStringFragment);
- formattedLog += cleanedFragment;
+ fragments.push(removeEscapeCharacters(formatStringFragment));
+ fragments.push(getFormattedField(logEvent, fieldPlaceholder));
lastIndex = fieldPlaceholder.range.end;
+ } catch (error) {
+ console.warn(
+ `Failed to format field "${fieldPlaceholder.fieldNameKeys.join(".")}":`,
+ error
+ );
+ fragments.push("[ERROR]");
+ }
}
const remainder = this.#processedFormatString.slice(lastIndex);
- formattedLog += removeEscapeCharacters(remainder);
+ fragments.push(removeEscapeCharacters(remainder));
- return `${formattedLog}\n`;
+ return `${fragments.join("")}\n`;
} 📝 Committable suggestion
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the Rabbit on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @junhaoliao, understood. Here's the updated formatLogEvent (logEvent: LogEvent): string {
const fragments: string[] = [];
let lastIndex = 0;
for (const fieldPlaceholder of this.#fieldPlaceholders) {
const formatStringFragment =
this.#processedFormatString.slice(lastIndex, fieldPlaceholder.range.start);
fragments.push(removeEscapeCharacters(formatStringFragment));
fragments.push(getFormattedField(logEvent, fieldPlaceholder));
lastIndex = fieldPlaceholder.range.end;
}
const remainder = this.#processedFormatString.slice(lastIndex);
fragments.push(removeEscapeCharacters(remainder));
return `${fragments.join("")}\n`;
} This optimizes string concatenation using an array and |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Parses field name, formatter name, and formatter options from placeholders in the format | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* string. For each placeholder, it creates a corresponding `YScopeFieldFormatter` and adds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* it to the class-level array. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @throws Error if `FIELD_PLACEHOLDER_REGEX` does not contain a capture group. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @throws Error if a specified formatter is not supported. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#parseFieldPlaceholder () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const placeholderPattern = new RegExp(FIELD_PLACEHOLDER_REGEX, "g"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const it = this.#processedFormatString.matchAll(placeholderPattern); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const match of it) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// `fullMatch` includes braces and `groupMatch` excludes them. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [fullMatch, groupMatch]: (string | undefined) [] = match; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ("undefined" === typeof groupMatch) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw Error("Field placeholder regex is invalid and does not have a capture group"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const {fieldNameKeys, formatterName, formatterOptions} = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
splitFieldPlaceholder(groupMatch); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let fieldFormatter: Nullable<YScopeFieldFormatter> = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (null !== formatterName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const FieldFormatterConstructor = YSCOPE_FORMATTERS_MAP[formatterName]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ("undefined" === typeof FieldFormatterConstructor) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw Error(`Formatter ${formatterName} is not currently supported`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fieldFormatter = new FieldFormatterConstructor(formatterOptions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.#fieldPlaceholders.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fieldNameKeys: fieldNameKeys, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fieldFormatter: fieldFormatter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
range: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
start: match.index, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end: match.index + fullMatch.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize regex usage and add validation Consider these improvements:
Apply this diff: class YScopeFormatter implements Formatter {
+ static readonly #PLACEHOLDER_PATTERN = new RegExp(FIELD_PLACEHOLDER_REGEX, "g");
readonly #processedFormatString: string;
#fieldPlaceholders: YScopeFieldPlaceholder[] = [];
#parseFieldPlaceholder () {
+ if (!this.#processedFormatString.includes("{")) {
+ throw new Error("Format string contains no placeholders");
+ }
- const placeholderPattern = new RegExp(FIELD_PLACEHOLDER_REGEX, "g");
- const it = this.#processedFormatString.matchAll(placeholderPattern);
+ const it = this.#processedFormatString.matchAll(YScopeFormatter.#PLACEHOLDER_PATTERN);
+ let hasPlaceholders = false;
for (const match of it) {
+ hasPlaceholders = true;
// ... rest of the code ...
}
+ if (!hasPlaceholders) {
+ throw new Error("No valid placeholders found in format string");
+ }
}
}
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export default YScopeFormatter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: add this on the docs site once the site infra is ready
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai let's open a GitHub issue for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@junhaoliao, I've opened a GitHub issue as requested: #128
🔗 Analysis chain
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
Length of output: 490