Skip to content

Commit

Permalink
chore: extract filter functions into ExprFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
ahochsteger committed Dec 26, 2024
1 parent 16b497f commit 7a58487
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 237 deletions.
123 changes: 13 additions & 110 deletions src/lib/expr/ExprEvaluator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import parseDuration from "parse-duration"
import { Context, MetaInfo, ProcessingContext } from "./../Context"
import { Context, MetaInfo } from "./../Context"
/* eslint-disable @typescript-eslint/no-extraneous-class */
import {
ATNSimulator,
Expand All @@ -13,9 +12,9 @@ import {
Recognizer,
Token,
} from "antlr4ng"
import { addMilliseconds, format, parse } from "date-fns"
import { format } from "date-fns"
import { AttachmentContext, MessageContext, ThreadContext } from "../Context"
import { defaultDateFormat, getDateFnInfo } from "../utils/DateExpression"
import { defaultDateFormat, executeFilter } from "./ExprFilter"
import { ExprLexer } from "./generated/ExprLexer"
import {
DataRefNameContext,
Expand Down Expand Up @@ -44,7 +43,7 @@ export interface ExprState {
filterArgs: ValueType[]
}

interface Contexts {
export interface Contexts {
data: MetaInfo
gmailProcessor: Context
parseRoot?: TemplateContext
Expand Down Expand Up @@ -111,8 +110,7 @@ export class ExprListener extends ExprParserListener {
}
exitFilterExpr = (ctx: FilterExprContext) => {
this.ctxs.parseCurrent = ctx
this.value = ExpressionFilter.execute(
this.ctxs,
this.value = executeFilter(
ctx.filterName().getText(),
this.value,
...this.filterArgs,
Expand All @@ -130,34 +128,14 @@ export class ExprListener extends ExprParserListener {
ctx.LegacyPlaceHolderModifierOffsetFormat()
) {
const [offset, format] = (args ?? "").split(":")
this.value = ExpressionFilter.execute(
this.ctxs,
"offsetDate",
this.value,
offset,
)
this.value = ExpressionFilter.execute(
this.ctxs,
"formatDate",
this.value,
format,
)
this.value = executeFilter("offsetDate", this.value, offset)
this.value = executeFilter("formatDate", this.value, format)
}
if (ctx.LegacyPlaceholderModifierFormat()) {
this.value = ExpressionFilter.execute(
this.ctxs,
"formatDate",
this.value,
args,
)
this.value = executeFilter("formatDate", this.value, args)
}
if (ctx.LegacyPlaceholderModifierJoin()) {
this.value = ExpressionFilter.execute(
this.ctxs,
"join",
this.value,
args ?? ",",
)
this.value = executeFilter("join", this.value, args ?? ",")
}
}
exitLegacyDataRefName = (ctx: LegacyDataRefNameContext) => {
Expand All @@ -180,63 +158,6 @@ export class ExprListener extends ExprParserListener {
}
}

class ExpressionFilter {
public static asType<T>(v: ValueType): T {
return v as T
}
public static execute(
ctxs: Contexts,
name: string,
value: ValueType,
...args: ValueType[]
): ValueType {
//console.log(name, value, args)
switch (name) {
case "formatDate":
{
// TODO: Assert DateType
const fmt: string =
(args[0] ?? "") != "" ? (args[0] as string) : defaultDateFormat
value = format(value as Date, fmt)
}
break
case "join":
ExprEvaluator.evaluateJoinExpression(
ctxs,
value as ValueBaseType[],
args[0] as string,
)
break
case "offsetDate":
{
// TODO: Assert DateType
const fmt = args[0] as string
if (fmt.trim() != "") {
const durationValue = parseDuration(fmt)
if (!durationValue) {
throw new Error(`ERROR: Cannot parse date offset: ${fmt}`)
}
value = addMilliseconds(value as DateType, durationValue)
}
}
break
case "parseDate":
// TODO: Assert string type
value = parse(value as string, args[0] as string, new Date())
break
default:
// TODO: Assert DateType
const fnInfo = getDateFnInfo(name)
if (!fnInfo) {
throw new Error(`Unknown function '${name}'`)
}
value = fnInfo.fn(value as Date)
break
}
return value
}
}

export class ExprErrorListener extends BaseErrorListener {
syntaxError<S extends Token, T extends ATNSimulator>(
_recognizer: Recognizer<T>,
Expand All @@ -262,7 +183,7 @@ export class ExprEvaluator {
switch (typeof value) {
// TODO: Add support for boolean, number, bigint, symbol, function
case "object":
stringValue = this.objectValueToString(ctxs, value, defaultValue)
stringValue = this.objectValueToString(value, defaultValue)
break
case "string":
stringValue = value
Expand All @@ -278,36 +199,18 @@ export class ExprEvaluator {
}
return stringValue
}
public static evaluateJoinExpression(
ctxs: Contexts,
value: ValueBaseType[],
separator?: string,
): string | undefined {
if (Array.isArray(value)) {
separator =
separator ??
(ctxs.gmailProcessor as ProcessingContext).proc.config.settings
.defaultArrayJoinSeparator
return value.join(separator) // TODO: Maybe recursively evaluate each value before joining
} else {
ctxs.gmailProcessor.log.warn(
`Non-array type cannot be converted to string during ${ctxs.parseCurrent?.getText()} with value: ${JSON.stringify(
value,
)}`,
)
}
}
public static objectValueToString(
ctxs: Contexts,
value: object | null,
defaultValue: string,
): string {
let stringValue = defaultValue
switch (value?.constructor.name) {
case "Array":
stringValue =
this.evaluateJoinExpression(ctxs, value as ValueBaseType[], ",") ??
(executeFilter("join", value as ValueType) as string | undefined) ??
defaultValue
// this.evaluateJoinExpression(ctxs, value as ValueBaseType[], ",") ??
// defaultValue
break
case "Date": {
stringValue = format(value as Date, defaultDateFormat)
Expand Down
44 changes: 44 additions & 0 deletions src/lib/expr/ExprFilter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { fakedSystemDateTime } from "../../test/mocks/MockFactory"
import { executeFilter } from "./ExprFilter"

jest.useFakeTimers({ now: fakedSystemDateTime })

describe("executeFilter()", () => {
it("should handle invalid filter functions", () => {
expect(() => executeFilter("invalid", "value")).toThrow()
})

describe("custom filter", () => {
it("should handle formatDate", () => {
const actual = executeFilter(
"formatDate",
new Date("2024-12-01"),
"dd.MM.yyyy",
)
expect(actual).toEqual("01.12.2024")
})
it("should handle join", () => {
const actual = executeFilter("join", ["value1", "value2"])
expect(actual).toEqual("value1,value2")
})
it("should handle offsetDate", () => {
const actual = executeFilter("offsetDate", new Date("2024-12-01"), "-2d")
expect(actual).toEqual(new Date("2024-11-29"))
})
it("should handle parseDate", () => {
const actual = executeFilter("parseDate", "01.12.2024", "dd.MM.yyyy")
expect(actual).toEqual(new Date("2024-12-01"))
})
})

describe("date-fns filter", () => {
it("should handle startOfISOWeek", () => {
const actual = executeFilter("startOfISOWeek", new Date("2024-12-01"))
expect(actual).toEqual(new Date("2024-11-25"))
})
it("should handle startOfTomorrow", () => {
const actual = executeFilter("startOfTomorrow", "")
expect(actual).toEqual(new Date("2023-06-27"))
})
})
})
Loading

0 comments on commit 7a58487

Please sign in to comment.