From a7baca034af13624c314351eb89fe38dadf9c950 Mon Sep 17 00:00:00 2001 From: Guillaume Rochat Date: Mon, 27 Jul 2020 04:37:06 -0400 Subject: [PATCH] feat: Extending AWS Preset wildcards and checks. (#80) * Ensure at least one day has a ? for AWS preset In the AWS documentation, they say that only one of the field can be specified, the other must be `?`. To be considered specified, you need either a value or *. So it basically means that either dayOfWeek or dayOfMonth must be a ? so we always expect one ?. This checks ensure that with AWS there will always be one and only one ? * Add last day of month/week support * Adjust the L checks according to quartz behavior and add test matrix * Add nearest weekday flag for day of month * Add Nth occurrence of weekday during month (# symbol) * Updating readme feature list --- README.md | 25 ++- package.json | 2 +- src/fieldCheckers/dayOfMonthChecker.ts | 34 +++- src/fieldCheckers/dayOfWeekChecker.ts | 34 +++- src/fieldCheckers/hourChecker.ts | 2 +- src/fieldCheckers/minuteChecker.ts | 2 +- src/fieldCheckers/monthChecker.ts | 2 +- src/fieldCheckers/secondChecker.ts | 2 +- src/fieldCheckers/yearChecker.ts | 2 +- src/helper.ts | 126 ++++++++++----- src/index.test.ts | 53 +++++++ src/index.ts | 3 +- src/matrix.test.ts | 211 +++++++++++++++++++++++++ src/option.ts | 137 +++++----------- src/presets.ts | 10 ++ src/types.ts | 54 +++++++ 16 files changed, 551 insertions(+), 148 deletions(-) create mode 100644 src/matrix.test.ts create mode 100644 src/types.ts diff --git a/README.md b/README.md index 89b22df..21cbc3b 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ registerOptionPreset('YOUR-PRESET-ID', { useYears: false, useBlankDay: false, allowOnlyOneBlankDayField: false, + mustHaveBlankDayField: false, // optional, default to false + useLastDayOfMonth: false, // optional, default to false + useLastDayOfWeek: false, // optional, default to false seconds: { minValue: 0, maxValue: 59, @@ -169,7 +172,23 @@ The preset properties explained: - `useBlankDay: boolean` - enables blank day notation '?' in daysOfMonth and daysOfWeek field - `allowOnlyOneBlankDayField: boolean` - - required at least day field to not be blank (so not both day fields can be blank) + - requires a day field to not be blank (so not both day fields can be blank) +- `mustHaveBlankDayField: boolean` + - requires a day field to be blank (so not both day fields are specified) + - when mixed with `allowOnlyOneBlankDayField`, it means that there will always be either day or day of week as `?` +- `useLastDayOfMonth: boolean` + - enables the 'L' character to specify the last day of the month. + - accept negative offset after the 'L' for nth last day of the month. + - e.g.: `L-2` would me the 2nd to last day of the month. +- `useLastDayOfWeek: boolean` + - enables the 'L' character to specify the last occurrence of a weekday in a month. + - e.g.: `5L` would mean the last friday of the month. +- `useNearestWeekday: boolean` + - enables the 'W' character to specify the use of the closest weekday. + - e.g.: `15W` would mean the weekday (mon-fri) closest to the 15th when the 15th is on sat-sun. +- `useNthWeekdayOfMonth: boolean` + - enables the '#' character to specify the Nth weekday of the month. + - e.g.: `6#3` would mean the 3rd friday of the month (assuming 6 = friday). * in cron fields (like seconds, minutes etc.): - `minValue: number` @@ -276,4 +295,8 @@ console.log( - [x] Years field support. - [x] Option presets (classic cron, node-cron, etc.) - [x] Blank '?' daysOfMonth/daysOfWeek support +- [x] Last day of month. +- [x] Last specific weekday of month. (e.g. last Tuesday) +- [x] Closest weekday to a specific day of the month. +- [x] Nth specific weekday of month. (e.g. 2nd Tuesday) - [ ] Cron alias support. diff --git a/package.json b/package.json index ff48b1f..c4bf400 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "release-next": "npm run build && env-cmd npx semantic-release --branches next --no-ci", "release-next-major": "npm run build && env-cmd npx semantic-release --branches next-major --no-ci", "test": "jest --coverage", - "test:watch": "jest --coverage --watchAll" + "test:watch": "jest --watchAll --verbose" }, "repository": { "type": "git", diff --git a/src/fieldCheckers/dayOfMonthChecker.ts b/src/fieldCheckers/dayOfMonthChecker.ts index 72163a5..ba6542e 100644 --- a/src/fieldCheckers/dayOfMonthChecker.ts +++ b/src/fieldCheckers/dayOfMonthChecker.ts @@ -1,7 +1,7 @@ import type { CronData } from '../index' import { err, Result } from '../result' import checkField from '../helper' -import type { Options } from '../option' +import type { Options } from '../types' const checkDaysOfMonth = (cronData: CronData, options: Options): Result => { if (!cronData.daysOfMonth) { @@ -21,6 +21,38 @@ const checkDaysOfMonth = (cronData: CronData, options: Options): Result => { if (!cronData.daysOfWeek) { @@ -20,6 +20,38 @@ const checkDaysOfWeek = (cronData: CronData, options: Options): Result => { if (!cronData.hours) { diff --git a/src/fieldCheckers/minuteChecker.ts b/src/fieldCheckers/minuteChecker.ts index 76fd7fd..b40a95a 100644 --- a/src/fieldCheckers/minuteChecker.ts +++ b/src/fieldCheckers/minuteChecker.ts @@ -1,7 +1,7 @@ import type { CronData } from '../index' import { err, Result } from '../result' import checkField from '../helper' -import type { Options } from '../option' +import type { Options } from '../types' const checkMinutes = (cronData: CronData, options: Options): Result => { if (!cronData.minutes) { diff --git a/src/fieldCheckers/monthChecker.ts b/src/fieldCheckers/monthChecker.ts index 1d662f2..8050a89 100644 --- a/src/fieldCheckers/monthChecker.ts +++ b/src/fieldCheckers/monthChecker.ts @@ -1,7 +1,7 @@ import type { CronData } from '../index' import { err, Result } from '../result' import checkField from '../helper' -import type { Options } from '../option' +import type { Options } from '../types' const checkMonths = (cronData: CronData, options: Options): Result => { if (!cronData.months) { diff --git a/src/fieldCheckers/secondChecker.ts b/src/fieldCheckers/secondChecker.ts index f0b3e62..0eb88bb 100644 --- a/src/fieldCheckers/secondChecker.ts +++ b/src/fieldCheckers/secondChecker.ts @@ -1,7 +1,7 @@ import type { CronData } from '../index' import { err, Result } from '../result' import checkField from '../helper' -import type { Options } from '../option' +import type { Options } from '../types' const checkSeconds = (cronData: CronData, options: Options): Result => { if (!cronData.seconds) { diff --git a/src/fieldCheckers/yearChecker.ts b/src/fieldCheckers/yearChecker.ts index 296fdbf..c22ecff 100644 --- a/src/fieldCheckers/yearChecker.ts +++ b/src/fieldCheckers/yearChecker.ts @@ -1,7 +1,7 @@ import type { CronData } from '../index' import { err, Result } from '../result' import checkField from '../helper' -import type { Options } from '../option' +import type { Options } from '../types' const checkYears = (cronData: CronData, options: Options): Result => { if (!cronData.years) { diff --git a/src/helper.ts b/src/helper.ts index cd4c36f..c1fa017 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,6 +1,6 @@ import type { CronFieldType } from './index' import { Err, err, Result, Valid, valid } from './result' -import type { Options } from './option' +import type { Options } from './types' const checkWildcardLimit = (cronFieldType: CronFieldType, options: Options) => { return ( @@ -11,28 +11,11 @@ const checkWildcardLimit = (cronFieldType: CronFieldType, options: Options) => { ) } -const checkSingleElement = ( +const checkSingleElementWithinLimits = ( element: string, cronFieldType: CronFieldType, options: Options ): Result => { - if (element === '*') { - if (!checkWildcardLimit(cronFieldType, options)) { - // console.log( - // `Field ${cronFieldType} uses wildcard '*', but is limited to ${options[cronFieldType].lowerLimit}-${options[cronFieldType].upperLimit}` - // ) - return err( - `Field ${cronFieldType} uses wildcard '*', but is limited to ${options[cronFieldType].lowerLimit}-${options[cronFieldType].upperLimit}` - ) - } - - return valid(true) - } - - if (element === '') { - return err(`One of the elements is empty in ${cronFieldType} field.`) - } - const number = Number(element) if (isNaN(number)) { return err(`Element '${element} of ${cronFieldType} field is invalid.`) @@ -55,39 +38,101 @@ const checkSingleElement = ( return valid(true) } -const checkRangeElement = ( +const checkSingleElement = ( element: string, cronFieldType: CronFieldType, options: Options ): Result => { if (element === '*') { - return err(`'*' can't be part of a range in ${cronFieldType} field.`) + if (!checkWildcardLimit(cronFieldType, options)) { + return err( + `Field ${cronFieldType} uses wildcard '*', but is limited to ${options[cronFieldType].lowerLimit}-${options[cronFieldType].upperLimit}` + ) + } + + return valid(true) } if (element === '') { - return err(`One of the range elements is empty in ${cronFieldType} field.`) + return err(`One of the elements is empty in ${cronFieldType} field.`) } - const number = Number(element) - if (isNaN(number)) { - return err(`Element '${element} of ${cronFieldType} field is invalid.`) + if (cronFieldType === 'daysOfMonth' && options.useLastDayOfMonth && element === 'L') { + return valid(true) } - const { lowerLimit } = options[cronFieldType] - const { upperLimit } = options[cronFieldType] - if (lowerLimit && number < lowerLimit) { - return err( - `Number ${number} of ${cronFieldType} field is smaller than lower limit '${lowerLimit}'` - ) + // We must do that check here because L is used with a number to specify the day of the week for which + // we look for the last occurrence in the month. + // We use `endsWith` here because anywhere else is not valid so it will be caught later on. + if (cronFieldType === 'daysOfWeek' && options.useLastDayOfWeek && element.endsWith('L')) { + const day = element.slice(0, -1) + if (day === '') { + // This means that element is only `L` which is the equivalent of saturdayL + return valid(true) + } + + return checkSingleElementWithinLimits(day, cronFieldType, options) } - if (upperLimit && number > upperLimit) { - return err( - `Number ${number} of ${cronFieldType} field is bigger than upper limit '${upperLimit}'` - ) + // We must do that check here because W is used with a number to specify the day of the month for which + // we must run over a weekday instead. + // We use `endsWith` here because anywhere else is not valid so it will be caught later on. + if (cronFieldType === 'daysOfMonth' && options.useNearestWeekday && element.endsWith('W')) { + const day = element.slice(0, -1) + if (day === '') { + return err(`The 'W' must be preceded by a day`) + } + + // Edge case where the L can be used with W to form last weekday of month + if (options.useLastDayOfMonth && day === 'L') { + return valid(true) + } + + return checkSingleElementWithinLimits(day, cronFieldType, options) } - return valid(true) + if (cronFieldType === 'daysOfWeek' && options.useNthWeekdayOfMonth && element.indexOf('#') !== -1) { + const [day, occurrence, ...leftOvers] = element.split('#') + if (leftOvers.length !== 0) { + return err(`Unexpected number of '#' in ${element}, can only be used once.`) + } + + const occurrenceNum = Number(occurrence) + if (!occurrence || isNaN(occurrenceNum)) { + return err(`Unexpected value following the '#' symbol, a positive number was expected but found ${occurrence}.`) + } + + return checkSingleElementWithinLimits(day, cronFieldType, options) + } + + return checkSingleElementWithinLimits(element, cronFieldType, options) +} + +const checkRangeElement = ( + element: string, + cronFieldType: CronFieldType, + options: Options, + position: 0 | 1 +): Result => { + if (element === '*') { + return err(`'*' can't be part of a range in ${cronFieldType} field.`) + } + + if (element === '') { + return err(`One of the range elements is empty in ${cronFieldType} field.`) + } + + // We can have `L` as the first element of a range to specify an offset. + if ( + options.useLastDayOfMonth && + cronFieldType === 'daysOfMonth' && + element === 'L' && + position === 0 + ) { + return valid(true) + } + + return checkSingleElementWithinLimits(element, cronFieldType, options) } const checkFirstStepElement = ( @@ -105,16 +150,19 @@ const checkFirstStepElement = ( if (rangeArray.length === 1) { return checkSingleElement(rangeArray[0], cronFieldType, options) } + if (rangeArray.length === 2) { const firstRangeElementResult = checkRangeElement( rangeArray[0], cronFieldType, - options + options, + 0 ) const secondRangeElementResult = checkRangeElement( rangeArray[1], cronFieldType, - options + options, + 1 ) if (firstRangeElementResult.isError()) { @@ -179,7 +227,7 @@ const checkListElement = ( if (Number(secondStepElement) === 0) { return err( - `Second step element '${secondStepElement}' of '${listElement}' is cannot be zero.` + `Second step element '${secondStepElement}' of '${listElement}' cannot be zero.` ) } } diff --git a/src/index.test.ts b/src/index.test.ts index fbf0e93..93686c7 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -536,5 +536,58 @@ describe('Test cron validation', () => { }, }).isValid() ).toBeFalsy() + + // minimum one blank required + expect( + cron('* * * * *', { + override: { useBlankDay: true, allowOnlyOneBlankDayField: true, mustHaveBlankDayField: true }, + }).isValid() + ).toBeFalsy() + + expect( + cron('* * * * ?', { + override: { useBlankDay: true, allowOnlyOneBlankDayField: true, mustHaveBlankDayField: true }, + }).isValid() + ).toBeTruthy() + + expect( + cron('* * ? * *', { + override: { useBlankDay: true, allowOnlyOneBlankDayField: true, mustHaveBlankDayField: true }, + }).isValid() + ).toBeTruthy() + + expect( + cron('* * * * * *', { + override: { + useSeconds: true, + useBlankDay: true, + allowOnlyOneBlankDayField: true, + mustHaveBlankDayField: true, + }, + }).isValid() + ).toBeFalsy() + + expect( + cron('* * * * * *', { + override: { + useYears: true, + useBlankDay: true, + allowOnlyOneBlankDayField: true, + mustHaveBlankDayField: true, + }, + }).isValid() + ).toBeFalsy() + + expect( + cron('* * * * * * *', { + override: { + useSeconds: true, + useYears: true, + useBlankDay: true, + allowOnlyOneBlankDayField: true, + mustHaveBlankDayField: true, + }, + }).isValid() + ).toBeFalsy() }) }) diff --git a/src/index.ts b/src/index.ts index 9dc3e2d..cf112a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,8 @@ import checkDaysOfMonth from './fieldCheckers/dayOfMonthChecker' import checkMonths from './fieldCheckers/monthChecker' import checkDaysOfWeek from './fieldCheckers/dayOfWeekChecker' import checkYears from './fieldCheckers/yearChecker' -import { InputOptions, Options, validateOptions } from './option' +import { validateOptions } from './option' +import type { InputOptions, Options } from './types' export interface CronData { seconds?: string diff --git a/src/matrix.test.ts b/src/matrix.test.ts new file mode 100644 index 0000000..fc8ce49 --- /dev/null +++ b/src/matrix.test.ts @@ -0,0 +1,211 @@ +import cron from './index' +import type { InputOptions } from './types' + +type TestCase = { + value: string + description?: string +} + +type Matrix = { + describe: string + options: InputOptions + + validIndexes?: number[] + valids: TestCase[] + invalids: TestCase[] + unuseds: TestCase[] +} + +describe('test', () => { + const itSucceeds = (testCase: TestCase, expression: string, options: InputOptions = {}) => { + it(`${expression.padEnd(15, ' ')} should be valid ${testCase.description ? `(${testCase.description})` : ''}`, () => { + expect(cron(expression, options).isValid()).toBeTruthy() + }) + } + + const itFails = (testCase: TestCase, expression: string, options: InputOptions = {}) => { + it(`${expression.padEnd(15, ' ')} should be invalid ${testCase.description ? `(${testCase.description})` : ''}`, () => { + expect(cron(expression, options).isValid()).toBeFalsy() + }) + } + + const forEachIndex = (testCase: TestCase): [number, string][] => { + return [0, 1, 2, 3, 4].map((index: number): [number, string] => { + const fragments = Array(5).fill('*') + fragments[index] = testCase.value + return [index, fragments.join(' ')] + }) + } + + const withOptions = (matrix: Matrix) => { + describe('With Options', () => { + for (const valid of matrix.valids) { + for (const [index, expression] of forEachIndex(valid)) { + if (matrix.validIndexes?.indexOf(index) !== -1) { + itSucceeds(valid, expression, matrix.options) + } else { + itFails(valid, expression, matrix.options) + } + } + } + + for (const invalid of matrix.invalids) { + for (const [_index, expression] of forEachIndex(invalid)) { + itFails(invalid, expression, matrix.options) + } + } + }) + } + + const withoutOptions = (matrix: Matrix) => { + describe('Without Options', () => { + for (const testCase of [...matrix.valids, ...matrix.invalids]) { + for (const [_index, expression] of forEachIndex(testCase)) { + itFails(testCase, expression) + } + } + }) + } + + const flagValueUnused = (matrix: Matrix) => { + describe('Flag value unused', () => { + for (const unused of matrix.unuseds) { + for (const index of (matrix.validIndexes ?? [])) { + const fragments = Array(5).fill('*') + fragments[index] = unused.value + const expression = fragments.join(' ') + itSucceeds(unused, expression, matrix.options) + } + } + }) + } + + const matrixes: Matrix[] = [{ + describe: 'useLastDayOfMonth', + options: { + override: { + useLastDayOfMonth: true, + daysOfMonth: { lowerLimit: 1, upperLimit: 31 }, + }, + }, + validIndexes: [2], + valids: [ + { value: 'L', description: 'alone' }, + { value: 'L-2', description: 'with offset' }, + ], + invalids: [ + { value: '15,L', description: 'cannot be in a list' }, + { value: '2-L', description: 'cannot be the end of a range' }, + { value: '2/L', description: 'cannot be in a step' }, + { value: 'L/2', description: 'cannot be in a step' }, + { value: 'L-32', description: 'cannot have offset out of limit range' }, + { value: 'LL', description: 'cannot have multiple occurrence' }, + ], + unuseds: [ + { value: '1-15,20-22', description: 'no impact when option is on but no L specified' }, + ], + }, { + describe: 'useLastDayOfWeek', + options: { + override: { + useLastDayOfWeek: true, + daysOfWeek: { lowerLimit: 1, upperLimit: 7 }, + }, + }, + validIndexes: [4], + valids: [ + { value: 'L', description: 'alone implies last saturday' }, + { value: '5L', description: 'with a day implies last 5th weekday of the month' }, + ], + invalids: [ + { value: '15,5L', description: 'cannot be in a list' }, + { value: '1-5L', description: 'cannot be in a range' }, + { value: '5/L', description: 'cannot be in a step' }, + { value: 'L/5', description: 'cannot be in a step' }, + { value: '8L', description: 'cannot have a weekday value out of limit' }, + { value: 'LL', description: 'cannot have multiple occurrence' }, + ], + unuseds: [ + { value: '1-3,5-7', description: 'no impact when option is on but no L specified' }, + ], + }, { + describe: 'useNearestWeekday', + options: { + override: { + useNearestWeekday: true, + daysOfMonth: { lowerLimit: 1, upperLimit: 31 }, + }, + }, + validIndexes: [2], + valids: [ + { value: '15W', description: 'nearest weekday to the 15th' }, + ], + invalids: [ + { value: 'W', description: 'means nothing alone' }, + { value: '1,15W', description: 'cannot be in a list' }, + { value: '1-15W', description: 'cannot be in a range' }, + { value: '15/W', description: 'cannot be in a step' }, + { value: 'W/15', description: 'cannot be in a step' }, + { value: '1W6W', description: 'cannot have multiple occurrence' }, + ], + unuseds: [ + { value: '1-15,20-25', description: 'no impact when option is on but no W specified' }, + ], + }, { + describe: 'useNearestWeekday with useLastDayOfMonth', + options: { + override: { + useLastDayOfMonth: true, + useNearestWeekday: true, + daysOfMonth: { lowerLimit: 1, upperLimit: 31 }, + }, + }, + validIndexes: [2], + valids: [ + { value: 'LW', description: 'last weekday of month' }, + ], + invalids: [ + { value: '15,LW', description: 'cannot be in a list' }, + { value: 'WL', description: 'cannot be reversed' }, + { value: '1-15LW', description: 'cannot be in a range' }, + { value: '15/LW', description: 'cannot be in a step' }, + { value: 'LW/15', description: 'cannot be in a step' }, + ], + unuseds: [ + { value: '1-15,20-25', description: 'no impact when option is on but no W or L specified' }, + ], + }, { + describe: 'useNthWeekdayOfMonth', + options: { + override: { + useNthWeekdayOfMonth: true, + daysOfWeek: { lowerLimit: 1, upperLimit: 7 }, + }, + }, + validIndexes: [4], + valids: [ + { value: '6#3', description: '3rd friday of the month' }, + ], + invalids: [ + { value: '6#', description: 'must have a number after' }, + { value: '#3', description: 'must have a number before' }, + { value: '2,6#3', description: 'cannot be in a list' }, + { value: '2-6#3', description: 'cannot be in a range' }, + { value: '2/6#3', description: 'cannot be in a step' }, + { value: '6#3/2', description: 'cannot be in a step' }, + { value: '8#3', description: 'must respect limits' }, + { value: '6##3', description: 'cannot have multiple occurrence' }, + ], + unuseds: [ + { value: '1-3,5-7', description: 'no impact when option is on but no # specified' }, + ], + }] + + for (const matrix of matrixes) { + describe(matrix.describe, () => { + withOptions(matrix) + withoutOptions(matrix) + flagValueUnused(matrix) + }) + } +}) diff --git a/src/option.ts b/src/option.ts index b6eff82..651e297 100644 --- a/src/option.ts +++ b/src/option.ts @@ -2,32 +2,7 @@ import * as yup from 'yup' import type { ValidationError } from 'yup' import { err, valid, Result } from './result' import presets from './presets' - -interface OptionPreset { - presetId: string - - useSeconds: boolean - useYears: boolean - useBlankDay: boolean - allowOnlyOneBlankDayField: boolean - // useAliases: boolean - // useNonStandardCharacters: boolean - - seconds: OptionPresetFieldOptions - minutes: OptionPresetFieldOptions - hours: OptionPresetFieldOptions - daysOfMonth: OptionPresetFieldOptions - months: OptionPresetFieldOptions - daysOfWeek: OptionPresetFieldOptions - years: OptionPresetFieldOptions -} - -interface OptionPresetFieldOptions { - minValue: number - maxValue: number - lowerLimit?: number - upperLimit?: number -} +import type { Options, OptionPreset, InputOptions } from './types' const optionPresets: { [presetId: string]: OptionPreset } = { // http://crontab.org/ @@ -37,6 +12,11 @@ const optionPresets: { [presetId: string]: OptionPreset } = { useYears: false, useBlankDay: false, allowOnlyOneBlankDayField: false, + mustHaveBlankDayField: false, + useLastDayOfMonth: false, + useLastDayOfWeek: false, + useNearestWeekday: false, + useNthWeekdayOfMonth: false, seconds: { minValue: 0, maxValue: 59, @@ -68,30 +48,6 @@ const optionPresets: { [presetId: string]: OptionPreset } = { }, } -export const getOptionPreset = (presetId: string): Result => { - if (optionPresets[presetId]) { - return valid(optionPresets[presetId]) - } - - return err(`Option preset '${presetId}' not found.`) -} - -export const getOptionPresets = (): typeof optionPresets => { - return optionPresets -} - -export const registerOptionPreset = ( - presetName: string, - preset: OptionPreset -): void => { - optionPresets[presetName] = optionPresetSchema.validateSync(preset, { - strict: false, - abortEarly: false, - stripUnknown: true, - recursive: true, - }) -} - const optionPresetSchema = yup .object({ presetId: yup.string().required(), @@ -99,6 +55,11 @@ const optionPresetSchema = yup useYears: yup.boolean().required(), useBlankDay: yup.boolean().required(), allowOnlyOneBlankDayField: yup.boolean().required(), + mustHaveBlankDayField: yup.boolean(), + useLastDayOfMonth: yup.boolean(), + useLastDayOfWeek: yup.boolean(), + useNearestWeekday: yup.boolean(), + useNthWeekdayOfMonth: yup.boolean(), seconds: yup .object({ minValue: yup.number().min(0).required(), @@ -158,60 +119,28 @@ const optionPresetSchema = yup }) .required() -export interface Options { - presetId: string - preset: OptionPreset - - useSeconds: boolean - useYears: boolean - useBlankDay: boolean - allowOnlyOneBlankDayField: boolean - // useAliases: boolean - // useNonStandardCharacters: boolean +export const getOptionPreset = (presetId: string): Result => { + if (optionPresets[presetId]) { + return valid(optionPresets[presetId]) + } - seconds: FieldOption - minutes: FieldOption - hours: FieldOption - daysOfMonth: FieldOption - months: FieldOption - daysOfWeek: FieldOption - years: FieldOption + return err(`Option preset '${presetId}' not found.`) } -interface FieldOption { - lowerLimit?: number - upperLimit?: number +export const getOptionPresets = (): typeof optionPresets => { + return optionPresets } -export interface InputOptions { - preset?: string | OptionPreset - override?: { - useSeconds?: boolean - useYears?: boolean - useBlankDay?: boolean - allowOnlyOneBlankDayField?: boolean - // useAliases?: boolean - // useNonStandardCharacters?: boolean - - seconds?: FieldOption - minutes?: FieldOption - hours?: FieldOption - daysOfMonth?: FieldOption - months?: FieldOption - daysOfWeek?: FieldOption - years?: FieldOption - } - useSeconds?: boolean - useYears?: boolean - seconds?: FieldOption - minutes?: FieldOption - hours?: FieldOption - daysOfMonth?: FieldOption - months?: FieldOption - daysOfWeek?: FieldOption - years?: FieldOption - // useAliases: boolean - // useNonStandardCharacters: boolean +export const registerOptionPreset = ( + presetName: string, + preset: OptionPreset +): void => { + optionPresets[presetName] = optionPresetSchema.validateSync(preset, { + strict: false, + abortEarly: false, + stripUnknown: true, + recursive: true, + }) } export const validateOptions = (inputOptions: InputOptions): Result => { @@ -242,6 +171,11 @@ export const validateOptions = (inputOptions: InputOptions): Result { useYears: false, useBlankDay: false, allowOnlyOneBlankDayField: false, + mustHaveBlankDayField: false, + useLastDayOfMonth: false, + useLastDayOfWeek: false, + useNearestWeekday: false, + useNthWeekdayOfMonth: false, seconds: { minValue: 0, maxValue: 59, @@ -45,6 +50,11 @@ export default (): void => { useYears: true, useBlankDay: true, allowOnlyOneBlankDayField: true, + mustHaveBlankDayField: true, + useLastDayOfMonth: true, + useLastDayOfWeek: true, + useNearestWeekday: true, + useNthWeekdayOfMonth: true, seconds: { minValue: 0, maxValue: 59, diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f49d3fd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,54 @@ +type OptionPresetFieldOptions = { + minValue: number + maxValue: number + lowerLimit?: number + upperLimit?: number +} + +type FieldOption = { + lowerLimit?: number + upperLimit?: number +} + +type Fields = { + seconds: T + minutes: T + hours: T + daysOfMonth: T + months: T + daysOfWeek: T + years: T +} + +type ExtendFields = { + useSeconds: boolean + useYears: boolean +} + +type ExtendWildcards = { + useBlankDay: boolean + allowOnlyOneBlankDayField: boolean + + // Optional for backward compatibility. Undefined implies false. + mustHaveBlankDayField?: boolean + useLastDayOfMonth?: boolean + useLastDayOfWeek?: boolean + useNearestWeekday?: boolean + useNthWeekdayOfMonth?: boolean + // useAliases: boolean + // useNonStandardCharacters: boolean +} + +export type OptionPreset = { + presetId: string +} & Fields & ExtendFields & ExtendWildcards + +export type Options = { + presetId: string + preset: OptionPreset +} & Fields & ExtendFields & ExtendWildcards + +export type InputOptions = { + preset?: string | OptionPreset + override?: Partial> & Partial & Partial +} & Partial> & Partial