Skip to content

Commit

Permalink
[compiler] Add optional validation message to @pattern decorator. (m…
Browse files Browse the repository at this point in the history
…icrosoft#2863)

Closes microsoft#2718

This change adds support for an optional message that emitters may use
to communicate the context of a pattern validation error.

I also added some baseline tests for `@pattern` since there were none in
decorators.spec.ts.

---------

Co-authored-by: Will Temple <[email protected]>
Co-authored-by: Timothee Guerin <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent c4ba287 commit 15f6dbe
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-panthers-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec/compiler": minor
---

Added an optional validation message to the @pattern decorator.
10 changes: 8 additions & 2 deletions docs/standard-library/built-in-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,8 +645,13 @@ Specify the the pattern this string should respect using simple regular expressi
The following syntax is allowed: alternations (`|`), quantifiers (`?`, `*`, `+`, and `{ }`), wildcard (`.`), and grouping parentheses.
Advanced features like look-around, capture groups, and references are not supported.

This decorator may optionally provide a custom validation _message_. Emitters may choose to use the message to provide
context when pattern validation fails. For the sake of consistency, the message should be a phrase that describes in
plain language what sort of content the pattern attempts to validate. For example, a complex regular expression that
validates a GUID string might have a message like "Must be a valid GUID."

```typespec
@pattern(pattern: valueof string)
@pattern(pattern: valueof string, validationMessage?: valueof string)
```

#### Target
Expand All @@ -657,11 +662,12 @@ Advanced features like look-around, capture groups, and references are not suppo
| Name | Type | Description |
|------|------|-------------|
| pattern | `valueof scalar string` | Regular expression. |
| validationMessage | `valueof scalar string` | Optional validation message that may provide context when validation fails. |

#### Examples

```typespec
@pattern("[a-z]+")
@pattern("[a-z]+", "Must be a string consisting of only lower case letters and of at least one character.")
scalar LowerAlpha extends string;
```

Expand Down
14 changes: 12 additions & 2 deletions packages/compiler/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,25 @@ extern dec format(target: string | bytes | ModelProperty, format: valueof string
* The following syntax is allowed: alternations (`|`), quantifiers (`?`, `*`, `+`, and `{ }`), wildcard (`.`), and grouping parentheses.
* Advanced features like look-around, capture groups, and references are not supported.
*
* This decorator may optionally provide a custom validation _message_. Emitters may choose to use the message to provide
* context when pattern validation fails. For the sake of consistency, the message should be a phrase that describes in
* plain language what sort of content the pattern attempts to validate. For example, a complex regular expression that
* validates a GUID string might have a message like "Must be a valid GUID."
*
* @param pattern Regular expression.
* @param validationMessage Optional validation message that may provide context when validation fails.
*
* @example
* ```typespec
* @pattern("[a-z]+")
* @pattern("[a-z]+", "Must be a string consisting of only lower case letters and of at least one character.")
* scalar LowerAlpha extends string;
* ```
*/
extern dec pattern(target: string | bytes | ModelProperty, pattern: valueof string);
extern dec pattern(
target: string | bytes | ModelProperty,
pattern: valueof string,
validationMessage?: valueof string
);

/**
* Specify the minimum length this string type should be.
Expand Down
36 changes: 34 additions & 2 deletions packages/compiler/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,21 +405,53 @@ export function getFormat(program: Program, target: Type): string | undefined {

const patternValuesKey = createStateSymbol("patternValues");

export interface PatternData {
readonly pattern: string;
readonly validationMessage?: string;
}

export function $pattern(
context: DecoratorContext,
target: Scalar | ModelProperty,
pattern: string
pattern: string,
validationMessage?: string
) {
validateDecoratorUniqueOnNode(context, target, $pattern);

if (!validateTargetingAString(context, target, "@pattern")) {
return;
}

context.program.stateMap(patternValuesKey).set(target, pattern);
const patternData: PatternData = {
pattern,
validationMessage,
};

context.program.stateMap(patternValuesKey).set(target, patternData);
}

/**
* Gets the pattern regular expression associated with a given type, if one has been set.
*
* @see getPatternData
*
* @param program - the Program containing the target Type
* @param target - the type to get the pattern for
* @returns the pattern string, if one was set
*/
export function getPattern(program: Program, target: Type): string | undefined {
return getPatternData(program, target)?.pattern;
}

/**
* Gets the associated pattern data, including the pattern regular expression and optional validation message, if any
* has been set.
*
* @param program - the Program containing the target Type
* @param target - the type to get the pattern data for
* @returns the pattern data, if any was set
*/
export function getPatternData(program: Program, target: Type): PatternData | undefined {
return program.stateMap(patternValuesKey).get(target);
}

Expand Down
73 changes: 73 additions & 0 deletions packages/compiler/test/decorators/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
getKnownValues,
getOverloadedOperation,
getOverloads,
getPattern,
getPatternData,
getReturnsDoc,
isErrorModel,
resolveEncodedName,
Expand Down Expand Up @@ -225,6 +227,77 @@ describe("compiler: built-in decorators", () => {
});
});

describe("@pattern", () => {
it("applies @pattern to scalar", async () => {
const { A } = (await runner.compile(
`
@test
@pattern("^[a-z]+$")
scalar A extends string;
`
)) as { A: Scalar };

strictEqual(getPattern(runner.program, A), "^[a-z]+$");
});

it("applies @pattern to model property", async () => {
const { A } = (await runner.compile(
`
@test
model A {
@test
@pattern("^[a-z]+$")
prop: string;
}
`
)) as { A: Model };

const prop = A.properties.get("prop") as ModelProperty;
strictEqual(prop.kind, "ModelProperty");
strictEqual(getPattern(runner.program, prop), "^[a-z]+$");
});

it("emit diagnostic if pattern is not a string", async () => {
const diagnostics = await runner.diagnose(`
model A {
@pattern(123)
prop: string;
}
`);

expectDiagnostics(diagnostics, {
code: "invalid-argument",
message: `Argument '123' is not assignable to parameter of type 'valueof string'`,
});
});

it("optionally allows specifying a pattern validation message", async () => {
const { A, B } = (await runner.compile(
`
@test
@pattern("^[a-z]+$", "Must be all lowercase.")
scalar A extends string;
@test
@pattern("^[a-z]+$")
scalar B extends string;
`
)) as { A: Scalar; B: Scalar };

const pattern = getPattern(runner.program, A);
strictEqual(pattern, "^[a-z]+$");
const data = getPatternData(runner.program, A);
strictEqual(data?.pattern, pattern);
strictEqual(data?.validationMessage, "Must be all lowercase.");

const pattern2 = getPattern(runner.program, B);
strictEqual(pattern2, "^[a-z]+$");
const data2 = getPatternData(runner.program, B);
strictEqual(data2?.pattern, pattern2);
strictEqual(data2?.validationMessage, undefined);
});
});

describe("@returnsDoc", () => {
it("applies @returnsDoc on operation", async () => {
const { test } = (await runner.compile(
Expand Down

0 comments on commit 15f6dbe

Please sign in to comment.