From e283d168513bf30562932f0abd9c5adc283bc8c3 Mon Sep 17 00:00:00 2001 From: kevinmcody <55589482+kevinmcody@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:48:12 -0400 Subject: [PATCH] Update oas_models.py to support multipleOf Support multipleOf constraint on an Integer or Number. Strategy: Use the ceiling of Min divided by multipleOf, floor of Max divided by multipleOf, for the random min/max, then multiply by multipleOf --- src/OpenApiLibCore/models/oas_models.py | 63 +++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/OpenApiLibCore/models/oas_models.py b/src/OpenApiLibCore/models/oas_models.py index 6f72be8..1da3347 100644 --- a/src/OpenApiLibCore/models/oas_models.py +++ b/src/OpenApiLibCore/models/oas_models.py @@ -7,6 +7,7 @@ from functools import cached_property from random import choice, randint, sample, shuffle, uniform from sys import float_info +from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR from typing import ( Annotated, Any, @@ -331,7 +332,7 @@ class IntegerSchema(SchemaBase[int], frozen=True): exclusiveMaximum: int | bool | None = None minimum: int | None = None exclusiveMinimum: int | bool | None = None - multipleOf: int | None = None # TODO: implement support + multipleOf: int | None = None const: int | None = None enum: list[int] | None = None nullable: bool = False @@ -390,7 +391,29 @@ def get_valid_value( if self.enum is not None: return choice(self.enum), self - return randint(self._min_value, self._max_value), self + if self.multipleOf is None: + return randint(self._min_value, self._max_value), self + + step = self.multipleOf + if step <= 0: + logger.debug(f"multipleOf must be > 0, got {self.multipleOf}") + return randint(self._min_value, self._max_value), self + + # k_min and k_max are the bounds for the integer k, which will be chosen randomly, + # then multiplied by the step (multipleOf) to get a valid value. + k_min = -(-self._min_value // step) # the "double negative" essentially turns floor division into ceiling division + k_max = self._max_value // step # floor division + + if k_min > k_max: + logger.debug( + f"No number satisfies bounds [{self._min_value}, {self._max_value}] " + f"and multipleOf {self.multipleOf}" + ) + return randint(self._min_value, self._max_value), self + + # choose a k randomly between k_min and k_max, then multiply by step + value = randint(k_min, k_max) * step + return value, self def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: disable=unused-argument invalid_values: list[int] = [] @@ -401,6 +424,8 @@ def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: if self._max_value < self._max_int: invalid_values.append(self._max_value + 1) + # TODO: handle multipleOf for out of bounds values + if invalid_values: return invalid_values @@ -452,7 +477,7 @@ class NumberSchema(SchemaBase[float], frozen=True): exclusiveMaximum: int | float | bool | None = None minimum: int | float | None = None exclusiveMinimum: int | float | bool | None = None - multipleOf: int | None = None # TODO: implement support + multipleOf: int | float | None = None const: int | float | None = None enum: list[int | float] | None = None nullable: bool = False @@ -507,7 +532,35 @@ def get_valid_value( if self.enum is not None: return choice(self.enum), self - return uniform(self._min_value, self._max_value), self + if self.multipleOf is None: + return uniform(self._min_value, self._max_value), self + + #Convert multipleOf and bounds to Decimal to avoid float rounding errors. + step = Decimal(str(self.multipleOf)) + if step <= 0: + logger.debug(f"multipleOf must be > 0, got {self.multipleOf}") + return uniform(self._min_value, self._max_value), self + + min_value = Decimal(str(self._min_value)) + max_value = Decimal(str(self._max_value)) + + #k_min and k_max are the bounds for the integer k, which will be chosen randomly, + # then multiplied by the step (multipleOf) to get a valid value. + # dividing by step and using ceiling/floor ensures then rounding ensures that k_min and k_max + # are the smallest/largest integers that satisfy the bounds when multiplied by step. + k_min = int((min_value / step).to_integral_value(rounding=ROUND_CEILING)) + k_max = int((max_value / step).to_integral_value(rounding=ROUND_FLOOR)) + + if k_min > k_max: + logger.debug( + f"No number satisfies bounds [{self._min_value}, {self._max_value}] " + f"and multipleOf {self.multipleOf}" + ) + return uniform(self._min_value, self._max_value), self + + #choose a k randomly between k_min and k_max, then multiply by step + value = float(Decimal(randint(k_min, k_max)) * step) + return value, self def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pylint: disable=unused-argument invalid_values: list[float] = [] @@ -518,6 +571,8 @@ def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pyli if self._max_value < self._max_float: invalid_values.append(self._max_value + 0.000000001) + # TODO: handle multipleOf for out of bounds values + if invalid_values: return invalid_values