Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
116dbdf
Initial plan
Copilot Sep 24, 2025
6fc0fce
Add @minValue/@maxValue support for datetime types
Copilot Sep 24, 2025
ffd7a03
Complete @minValue/@maxValue datetime support with comprehensive testing
Copilot Sep 24, 2025
6d18bf0
Rename validation function and update error messages per review feedback
Copilot Sep 24, 2025
b5c2c51
Fix test constructors and add value validation per review feedback
Copilot Sep 24, 2025
8aab5c1
Changes before error encountered
Copilot Sep 24, 2025
0670d5b
Fix unixTimestamp32 test to use fromISO constructor
Copilot Sep 24, 2025
82cf960
Fix CI failures: remove unused function and update datetime tests to …
Copilot Sep 24, 2025
f8a72da
progress
timotheeguerin Sep 26, 2025
8f1b411
fix
timotheeguerin Sep 26, 2025
8638de7
fixup
timotheeguerin Sep 26, 2025
f6bac2a
.
timotheeguerin Sep 26, 2025
a146841
Create minmax-value-more-types-2025-8-26-17-2-2.md
timotheeguerin Sep 26, 2025
fa120d3
fix openapi3
timotheeguerin Sep 26, 2025
52e6b38
.
timotheeguerin Sep 26, 2025
b3a512e
Create minmax-value-more-types-2025-8-26-17-58-29.md
timotheeguerin Sep 26, 2025
8d8bb16
tweak
timotheeguerin Sep 26, 2025
df8579d
Create minmax-value-more-types-2025-8-26-18-20-28.md
timotheeguerin Sep 26, 2025
ca6a659
rename
timotheeguerin Sep 26, 2025
be4aa50
Minvalue of 0 works
timotheeguerin Sep 26, 2025
3071549
fix
timotheeguerin Sep 26, 2025
7cbee19
fix
timotheeguerin Sep 26, 2025
c43637c
regen docs
timotheeguerin Sep 26, 2025
d798687
Merge branch 'main' into minmax-value-more-types
timotheeguerin Sep 26, 2025
b1f6af4
Merge branch 'main' into minmax-value-more-types
timotheeguerin Oct 6, 2025
dd80fdd
Apply suggestion from @timotheeguerin
timotheeguerin Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/minmax-value-more-types-2025-8-26-17-2-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/tspd"
---

Handle union of union correctly for target type in decorator signature generation
13 changes: 13 additions & 0 deletions .chronus/changes/minmax-value-more-types-2025-8-26-17-58-29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---

Add support for `@minValue`, `@maxValue` and their exclusive variant for datetime and duration types.
Expose as well the following APIs
- `getMinValueForScalar`
- `getMaxValueForScalar`
- `getMinValueExclusiveForScalar`
- `getMaxValueExclusiveForScalar`
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/openapi3"
---

Add support for min/max value for date time and duration types
9 changes: 5 additions & 4 deletions packages/compiler/generated-defs/TypeSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Numeric,
Operation,
Scalar,
ScalarValue,
Type,
Union,
UnionVariant,
Expand Down Expand Up @@ -380,7 +381,7 @@ export type MaxItemsDecorator = (
export type MinValueDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
value: Numeric,
value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue,
) => void;

/**
Expand All @@ -396,7 +397,7 @@ export type MinValueDecorator = (
export type MaxValueDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
value: Numeric,
value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue,
) => void;

/**
Expand All @@ -413,7 +414,7 @@ export type MaxValueDecorator = (
export type MinValueExclusiveDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
value: Numeric,
value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue,
) => void;

/**
Expand All @@ -430,7 +431,7 @@ export type MinValueExclusiveDecorator = (
export type MaxValueExclusiveDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
value: Numeric,
value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue,
) => void;

/**
Expand Down
29 changes: 25 additions & 4 deletions packages/compiler/lib/std/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ extern dec minLength(target: string | ModelProperty, value: valueof integer);
*/
extern dec maxLength(target: string | ModelProperty, value: valueof integer);

/** Types that can have range limits */
alias RangeLimitableTypes =
| numeric
| utcDateTime
| offsetDateTime
| plainDate
| plainTime
| duration;

/**
* Specify the minimum number of items this array should have.
* @param value Minimum number
Expand Down Expand Up @@ -230,7 +239,10 @@ extern dec maxItems(target: unknown[] | ModelProperty, value: valueof integer);
* scalar Age is int32;
* ```
*/
extern dec minValue(target: numeric | ModelProperty, value: valueof numeric);
extern dec minValue(
target: RangeLimitableTypes | ModelProperty,
value: valueof RangeLimitableTypes
);

/**
* Specify the maximum value this numeric type should be.
Expand All @@ -242,7 +254,10 @@ extern dec minValue(target: numeric | ModelProperty, value: valueof numeric);
* scalar Age is int32;
* ```
*/
extern dec maxValue(target: numeric | ModelProperty, value: valueof numeric);
extern dec maxValue(
target: RangeLimitableTypes | ModelProperty,
value: valueof RangeLimitableTypes
);

/**
* Specify the minimum value this numeric type should be, exclusive of the given
Expand All @@ -255,7 +270,10 @@ extern dec maxValue(target: numeric | ModelProperty, value: valueof numeric);
* scalar distance is float64;
* ```
*/
extern dec minValueExclusive(target: numeric | ModelProperty, value: valueof numeric);
extern dec minValueExclusive(
target: RangeLimitableTypes | ModelProperty,
value: valueof RangeLimitableTypes
);

/**
* Specify the maximum value this numeric type should be, exclusive of the given
Expand All @@ -268,7 +286,10 @@ extern dec minValueExclusive(target: numeric | ModelProperty, value: valueof num
* scalar distance is float64;
* ```
*/
extern dec maxValueExclusive(target: numeric | ModelProperty, value: valueof numeric);
extern dec maxValueExclusive(
target: RangeLimitableTypes | ModelProperty,
value: valueof RangeLimitableTypes
);

/**
* Mark this value as a secret value that should be treated carefully to avoid exposure
Expand Down
29 changes: 26 additions & 3 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export interface Checker {
): T & TypePrototype;
finishType<T extends Type>(typeDef: T): T;
createLiteralType(value: string, node?: StringLiteralNode): StringLiteral;
createLiteralType(value: number, node?: NumericLiteralNode): NumericLiteral;
createLiteralType(value: number | Numeric, node?: NumericLiteralNode): NumericLiteral;
createLiteralType(value: boolean, node?: BooleanLiteralNode): BooleanLiteral;
createLiteralType(
value: string | number | boolean,
Expand All @@ -224,6 +224,20 @@ export interface Checker {
diagnosticTarget: DiagnosticTarget,
): [boolean, readonly Diagnostic[]];

/**
* Check if the value is assignable to the given type
* @param source Source value, should be assignable to the target type.
* @param target Target type
* @param diagnosticTarget Target for the diagnostic, unless something better can be inferred.
* @returns [related, list of diagnostics]
* @internal
*/
isValueOfType(
source: Value,
target: Type,
diagnosticTarget: DiagnosticTarget,
): [boolean, readonly Diagnostic[]];

/**
* Check if the given type is one of the built-in standard TypeSpec Types.
* @param type Type to check
Expand Down Expand Up @@ -393,10 +407,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
getValueExactType,
getTemplateParameterUsageMap,
isTypeAssignableTo: undefined!,
isValueOfType: undefined!,
stats,
};
const relation = createTypeRelationChecker(program, checker);
checker.isTypeAssignableTo = relation.isTypeAssignableTo;
checker.isValueOfType = relation.isValueOfType;

return checker;

Expand Down Expand Up @@ -6099,14 +6115,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
| StringTemplateMiddleNode
| StringTemplateTailNode,
): StringLiteral;
function createLiteralType(value: number, node?: NumericLiteralNode): NumericLiteral;
function createLiteralType(value: number | Numeric, node?: NumericLiteralNode): NumericLiteral;
function createLiteralType(value: boolean, node?: BooleanLiteralNode): BooleanLiteral;
function createLiteralType(
value: string | number | boolean,
node?: LiteralNode,
): StringLiteral | NumericLiteral | BooleanLiteral;
function createLiteralType(
value: string | number | boolean,
value: string | number | boolean | Numeric,
node?: LiteralNode,
): StringLiteral | NumericLiteral | BooleanLiteral {
if (program.literalTypes.has(value)) {
Expand Down Expand Up @@ -6139,6 +6155,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
numericValue: Numeric(valueAsString),
});
break;
default:
type = createType({
kind: "Number",
value: value.asNumber() ?? 0,
valueAsString: value.toString(),
numericValue: value,
});
}
program.literalTypes.set(value, type);
return finishType(type);
Expand Down
104 changes: 90 additions & 14 deletions packages/compiler/src/core/intrinsic-type-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import { DiscriminatedOptions } from "../../generated-defs/TypeSpec.js";
import { createStateSymbol } from "../lib/utils.js";
import { useStateMap } from "../utils/state-accessor.js";
import type { Numeric } from "./numeric.js";
import { isNumeric, type Numeric } from "./numeric.js";
import type { Program } from "./program.js";
import type { Model, Type, Union } from "./types.js";
import type { Model, ScalarValue, Type, Union } from "./types.js";

const stateKeys = {
minValues: createStateSymbol("minValues"),
Expand All @@ -27,57 +27,133 @@ const stateKeys = {

// #region @minValue

export function setMinValue(program: Program, target: Type, value: Numeric): void {
program.stateMap(stateKeys.minValues).set(target, value);
const [
/** Get the min value for numeric or scalar types like date times */
getMinValueRaw,
setMinValue,
] = useStateMap<Type, Numeric | ScalarValue>(stateKeys.minValues);

export { setMinValue };

/** Get the minimum value for a scalar type(datetime, duration, etc.). */
export function getMinValueForScalar(program: Program, target: Type): ScalarValue | undefined {
const value = getMinValueRaw(program, target);
return !isNumeric(value) ? value : undefined;
}

/** Get the minimum value for a numeric type. If the value cannot be represented as a JS number(Overflow) undefined will be returned */
export function getMinValueAsNumeric(program: Program, target: Type): Numeric | undefined {
return program.stateMap(stateKeys.minValues).get(target);
const value = getMinValueRaw(program, target);
return isNumeric(value) ? value : undefined;
}

/**
* Get the minimum value for a numeric type.
* If the value cannot be represented as a JS number(Overflow) undefined will be returned
* See {@link getMinValueAsNumeric} to get the precise value
*/
export function getMinValue(program: Program, target: Type): number | undefined {
return getMinValueAsNumeric(program, target)?.asNumber() ?? undefined;
}
// #endregion @minValue

// #region @maxValue

export function setMaxValue(program: Program, target: Type, value: Numeric): void {
program.stateMap(stateKeys.maxValues).set(target, value);
const [
/** Get the max value for numeric or scalar types like date times */
getMaxValueRaw,
setMaxValue,
] = useStateMap<Type, Numeric | ScalarValue>(stateKeys.maxValues);

export { setMaxValue };

/** Get the maximum value for a scalar type(datetime, duration, etc.). */
export function getMaxValueForScalar(program: Program, target: Type): ScalarValue | undefined {
const value = getMaxValueRaw(program, target);
return !isNumeric(value) ? value : undefined;
}

/** Get the maximum value for a numeric type. If the value cannot be represented as a JS number(Overflow) undefined will be returned */
export function getMaxValueAsNumeric(program: Program, target: Type): Numeric | undefined {
return program.stateMap(stateKeys.maxValues).get(target);
const value = getMaxValueRaw(program, target);
return isNumeric(value) ? value : undefined;
}

/**
* Get the maximum value for a numeric type.
* If the value cannot be represented as a JS number(Overflow) undefined will be returned
* See {@link getMaxValueAsNumeric} to get the precise value
*/
export function getMaxValue(program: Program, target: Type): number | undefined {
return getMaxValueAsNumeric(program, target)?.asNumber() ?? undefined;
}
// #endregion @maxValue

// #region @minValueExclusive

export function setMinValueExclusive(program: Program, target: Type, value: Numeric): void {
program.stateMap(stateKeys.minValueExclusive).set(target, value);
const [
/** Get the min value exclusive for numeric or scalar types like date times */
getMinValueExclusiveRaw,
setMinValueExclusive,
] = useStateMap<Type, Numeric | ScalarValue>(stateKeys.minValueExclusive);

export { setMinValueExclusive };

/** Get the minimum value exclusive for a scalar type(datetime, duration, etc.). */
export function getMinValueExclusiveForScalar(
program: Program,
target: Type,
): ScalarValue | undefined {
const value = getMinValueExclusiveRaw(program, target);
return !isNumeric(value) ? value : undefined;
}

/** Get the minimum value exclusive for a numeric type. If the value cannot be represented as a JS number(Overflow) undefined will be returned */
export function getMinValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined {
return program.stateMap(stateKeys.minValueExclusive).get(target);
const value = getMinValueExclusiveRaw(program, target);
return isNumeric(value) ? value : undefined;
}

/**
* Get the minimum value exclusive for a numeric type.
* If the value cannot be represented as a JS number(Overflow) undefined will be returned
* See {@link getMinValueExclusiveAsNumeric} to get the precise value
*/
export function getMinValueExclusive(program: Program, target: Type): number | undefined {
return getMinValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined;
}
// #endregion @minValueExclusive

// #region @maxValueExclusive
export function setMaxValueExclusive(program: Program, target: Type, value: Numeric): void {
program.stateMap(stateKeys.maxValueExclusive).set(target, value);

const [
/** Get the max value exclusive for numeric or scalar types like date times */
getMaxValueExclusiveRaw,
setMaxValueExclusive,
] = useStateMap<Type, Numeric | ScalarValue>(stateKeys.maxValueExclusive);

export { setMaxValueExclusive };

/** Get the maximum value exclusive for a scalar type(datetime, duration, etc.). */
export function getMaxValueExclusiveForScalar(
program: Program,
target: Type,
): ScalarValue | undefined {
const value = getMaxValueExclusiveRaw(program, target);
return !isNumeric(value) ? value : undefined;
}

/** Get the maximum value exclusive for a numeric type. If the value cannot be represented as a JS number(Overflow) undefined will be returned */
export function getMaxValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined {
return program.stateMap(stateKeys.maxValueExclusive).get(target);
const value = getMaxValueExclusiveRaw(program, target);
return isNumeric(value) ? value : undefined;
}

/**
* Get the maximum value exclusive for a numeric type.
* If the value cannot be represented as a JS number(Overflow) undefined will be returned
* See {@link getMaxValueExclusiveAsNumeric} to get the precise value
*/
export function getMaxValueExclusive(program: Program, target: Type): number | undefined {
return getMaxValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { createLogger } from "./logger/index.js";
import { createTracer } from "./logger/tracer.js";
import { createDiagnostic } from "./messages.js";
import { NameResolver, createResolver } from "./name-resolver.js";
import { Numeric } from "./numeric.js";
import { CompilerOptions } from "./options.js";
import { parse, parseStandaloneTypeReference } from "./parser.js";
import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js";
Expand Down Expand Up @@ -82,7 +83,7 @@ export interface Program {
jsSourceFiles: Map<string, JsSourceFileNode>;

/** @internal */
literalTypes: Map<string | number | boolean, LiteralType>;
literalTypes: Map<string | number | boolean | Numeric, LiteralType>;
host: CompilerHost;
tracer: Tracer;
trace(area: string, message: string): void;
Expand Down
Loading
Loading