Skip to content

Commit 04b77ea

Browse files
author
Michael Hahn
authored
Merge pull request #29 from lexidor/multipleOf
Implement multipleOf
2 parents db2b262 + 2d3f4f5 commit 04b77ea

File tree

6 files changed

+251
-0
lines changed

6 files changed

+251
-0
lines changed

src/Codegen/Constraints/NumberBuilder.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
?'maximum' => int,
1313
?'minimum' => int,
1414
?'coerce' => bool,
15+
?'multipleOf' => num,
1516
...
1617
);
1718

@@ -39,6 +40,14 @@ public function build(): this {
3940
->setValue($minimum, HackBuilderValues::export());
4041
}
4142

43+
$devisor = $this->typed_schema['multipleOf'] ?? null;
44+
if ($devisor is nonnull) {
45+
$properties[] = $this->codegenProperty('devisorForMultipleOf')
46+
->setType('num')
47+
->setValue($devisor, HackBuilderValues::export());
48+
}
49+
50+
4251
$enum = $this->getEnumCodegenProperty();
4352
if ($enum is nonnull) {
4453
$properties[] = $enum;
@@ -90,6 +99,13 @@ protected function getCheckMethod(): CodegenMethod {
9099
);
91100
}
92101

102+
if (($this->typed_schema['multipleOf'] ?? null) is nonnull) {
103+
$hb->addMultilineCall(
104+
'Constraints\NumberMultipleOfConstraint::check',
105+
vec['$typed', 'self::$devisorForMultipleOf', '$pointer'],
106+
);
107+
}
108+
93109
$hb->addReturn('$typed', HackBuilderValues::literal());
94110

95111
return $this->codegenCheckMethod()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?hh // strict
2+
3+
namespace Slack\Hack\JsonSchema\Constraints;
4+
5+
use namespace HH\Lib\Math;
6+
use namespace Slack\Hack\JsonSchema;
7+
8+
// Because comparing floating point numbers using `===` will have undesierable results,
9+
// we will compare with a little bit of leeway.
10+
// We are looking for a remainer of 0 after the modulo.
11+
// `30 % 3` is 0, so 30 is a multiple of 3.
12+
// `fmod(6., .6)` is not 0, because of floating point rounding errors.
13+
// It is 2.220...E-16, but this is almost zero, so we pass the test.
14+
const float COMPARISON_LEEWAY = 10. ** -6;
15+
16+
class NumberMultipleOfConstraint {
17+
public static function check(num $dividend, num $devisor, string $pointer): void {
18+
invariant($devisor > 0, 'multipleOf 0 or a negative number does not make sense. Use a positive non-zero number.');
19+
20+
$remainer = Math\abs(
21+
$dividend is int && $devisor is int ? $dividend % $devisor : \fmod((float)$dividend, (float)$devisor),
22+
);
23+
24+
$error = shape(
25+
'code' => JsonSchema\FieldErrorCode::FAILED_CONSTRAINT,
26+
'constraint' => shape(
27+
'type' => JsonSchema\FieldErrorConstraint::MULTIPLE_OF,
28+
'expected' => $devisor,
29+
'got' => $dividend,
30+
),
31+
'message' => "must be a multiple of {$devisor}",
32+
);
33+
34+
if ($remainer is int && $remainer !== 0) {
35+
throw new JsonSchema\InvalidFieldException($pointer, vec[$error]);
36+
}
37+
if ($remainer is float) {
38+
// If we are closer to 0 than to the devisor, we check assert that our remainer
39+
// is less than our COMPARISON_LEEWAY.
40+
if ($remainer < $devisor / 2 && $remainer > COMPARISON_LEEWAY) {
41+
throw new JsonSchema\InvalidFieldException($pointer, vec[$error]);
42+
// However, sometimes the remainer is very close to the devisor.
43+
// That could also indicate a multiple of the devisor.
44+
} else if ($remainer > $devisor / 2 && ($devisor - $remainer) > COMPARISON_LEEWAY) {
45+
throw new JsonSchema\InvalidFieldException($pointer, vec[$error]);
46+
}
47+
}
48+
}
49+
}

src/Exceptions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum FieldErrorConstraint: string {
1717
MIN_LENGTH = 'min_length';
1818
MAXIMUM = 'maximum';
1919
MINIMUM = 'minimum';
20+
MULTIPLE_OF = 'multiple_of';
2021
ENUM = 'enum';
2122
PATTERN = 'pattern';
2223
FORMAT = 'format';

tests/MultipleOfConstraintTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?hh // partial
2+
3+
namespace Slack\Hack\JsonSchema\Tests;
4+
5+
use function Facebook\FBExpect\expect;
6+
use type Facebook\HackTest\DataProvider;
7+
use namespace Slack\Hack\JsonSchema;
8+
9+
final class MultipleOfConstraintTest extends BaseCodegenTestCase {
10+
11+
<<__Override>>
12+
public static async function beforeFirstTestAsync(): Awaitable<void> {
13+
$ret = self::getBuilder('multiple-of.json', 'MultipleOfValidator');
14+
$ret['codegen']->build();
15+
require_once($ret['path']);
16+
}
17+
18+
public function provideCasesForIsValid(
19+
): dict<
20+
string,
21+
(shape(?'a_multiple_of_five_int' => int, ?'a_multiple_of_1_point_one_repeating_number' => num), bool),
22+
> {
23+
return dict[
24+
'zero is a multiple of 5' => tuple(shape('a_multiple_of_five_int' => 0), true),
25+
'one is not a multiple of 5' => tuple(shape('a_multiple_of_five_int' => 1), false),
26+
'five is a multiple of 5' => tuple(shape('a_multiple_of_five_int' => 5), true),
27+
'fifty is a multiple of 5' => tuple(shape('a_multiple_of_five_int' => 50), true),
28+
'zero is a mutliple of 1.1...' => tuple(shape('a_multiple_of_1_point_one_repeating_number' => 0), true),
29+
'one is not a mutliple of 1.1...' => tuple(shape('a_multiple_of_1_point_one_repeating_number' => 1), false),
30+
'5.5... is a multiple of 1.1...' =>
31+
// This will have an modulus of 1.1111111111056, testing if I can deal with it being slightly below the devidor.
32+
tuple(shape('a_multiple_of_1_point_one_repeating_number' => 5.55555555555), true),
33+
'5.5...6 is a multiple of 1.1... if you place the 6 far enough back' =>
34+
// This will have an modulus of 4.4444449986969E-7, testing if I can deal with it being slightly above zero.
35+
tuple(shape('a_multiple_of_1_point_one_repeating_number' => 5.555556), true),
36+
'5555555.5... is a multiple of 1.1...' =>
37+
tuple(shape('a_multiple_of_1_point_one_repeating_number' => 55555555.55555555555), true),
38+
// I arbitrarily choose to check for 6 digits. These tests need updating if we choose a different number of digits.
39+
'1.11111 <- 5 times a one behind the period is a multiple of 1.1...' =>
40+
tuple(shape('a_multiple_of_1_point_one_repeating_number' => 1.11111), false),
41+
'1.111111 <- 6 times a one behind the period is a multiple of 1.1...' =>
42+
tuple(shape('a_multiple_of_1_point_one_repeating_number' => 1.111111), true),
43+
];
44+
}
45+
46+
<<DataProvider('provideCasesForIsValid')>>
47+
public function testIsValid(shape(...) $value, bool $is_valid): void {
48+
$schema = new Generated\MultipleOfValidator($value);
49+
$schema->validate();
50+
expect($schema->isValid())->toBeSame($is_valid);
51+
}
52+
53+
public function testForError(): void {
54+
$schema = new Generated\MultipleOfValidator(shape('a_multiple_of_five_int' => 1));
55+
$schema->validate();
56+
$err = $schema->getErrors()[0];
57+
expect(Shapes::idx($err, 'constraint')as nonnull['type'])->toBeSame(JsonSchema\FieldErrorConstraint::MULTIPLE_OF);
58+
expect(Shapes::idx($err, 'constraint') |> Shapes::idx($$ as nonnull, 'got'))->toBeSame(1);
59+
expect(Shapes::idx($err, 'constraint') |> Shapes::idx($$ as nonnull, 'expected'))->toBeSame(5);
60+
expect($err['message'])->toBeSame('must be a multiple of 5');
61+
}
62+
63+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?hh // strict
2+
/**
3+
* This file is generated. Do not modify it manually!
4+
*
5+
* To re-generate this file run `make test`
6+
*
7+
*
8+
* @generated SignedSource<<2a4a4e3e9c399de1de58724be9cf4435>>
9+
*/
10+
namespace Slack\Hack\JsonSchema\Tests\Generated;
11+
use namespace Slack\Hack\JsonSchema;
12+
use namespace Slack\Hack\JsonSchema\Constraints;
13+
14+
type TMultipleOfValidator = shape(
15+
?'a_multiple_of_five_int' => int,
16+
?'a_multiple_of_1_point_one_repeating_number' => num,
17+
...
18+
);
19+
20+
final class MultipleOfValidatorPropertiesAMultipleOfFiveInt {
21+
22+
private static num $devisorForMultipleOf = 5;
23+
private static bool $coerce = false;
24+
25+
public static function check(mixed $input, string $pointer): int {
26+
$typed =
27+
Constraints\IntegerConstraint::check($input, $pointer, self::$coerce);
28+
29+
Constraints\NumberMultipleOfConstraint::check(
30+
$typed,
31+
self::$devisorForMultipleOf,
32+
$pointer,
33+
);
34+
return $typed;
35+
}
36+
}
37+
38+
final class MultipleOfValidatorPropertiesAMultipleOf1PointOneRepeatingNumber {
39+
40+
private static num $devisorForMultipleOf = 1.1111111111111;
41+
private static bool $coerce = false;
42+
43+
public static function check(mixed $input, string $pointer): num {
44+
$typed = Constraints\NumberConstraint::check($input, $pointer, self::$coerce);
45+
46+
Constraints\NumberMultipleOfConstraint::check(
47+
$typed,
48+
self::$devisorForMultipleOf,
49+
$pointer,
50+
);
51+
return $typed;
52+
}
53+
}
54+
55+
final class MultipleOfValidator
56+
extends JsonSchema\BaseValidator<TMultipleOfValidator> {
57+
58+
private static bool $coerce = false;
59+
60+
public static function check(
61+
mixed $input,
62+
string $pointer,
63+
): TMultipleOfValidator {
64+
$typed = Constraints\ObjectConstraint::check($input, $pointer, self::$coerce);
65+
66+
$errors = vec[];
67+
$output = shape();
68+
69+
/*HHAST_IGNORE_ERROR[UnusedVariable] Some loops generated with this statement do not use their $value*/
70+
foreach ($typed as $key => $value) {
71+
/* HH_IGNORE_ERROR[4051] allow dynamic access to preserve input. See comment in the codegen lib for reasoning and alternatives if needed. */
72+
$output[$key] = $value;
73+
}
74+
75+
if (\HH\Lib\C\contains_key($typed, 'a_multiple_of_five_int')) {
76+
try {
77+
$output['a_multiple_of_five_int'] = MultipleOfValidatorPropertiesAMultipleOfFiveInt::check(
78+
$typed['a_multiple_of_five_int'],
79+
JsonSchema\get_pointer($pointer, 'a_multiple_of_five_int'),
80+
);
81+
} catch (JsonSchema\InvalidFieldException $e) {
82+
$errors = \HH\Lib\Vec\concat($errors, $e->errors);
83+
}
84+
}
85+
86+
if (\HH\Lib\C\contains_key($typed, 'a_multiple_of_1_point_one_repeating_number')) {
87+
try {
88+
$output['a_multiple_of_1_point_one_repeating_number'] = MultipleOfValidatorPropertiesAMultipleOf1PointOneRepeatingNumber::check(
89+
$typed['a_multiple_of_1_point_one_repeating_number'],
90+
JsonSchema\get_pointer($pointer, 'a_multiple_of_1_point_one_repeating_number'),
91+
);
92+
} catch (JsonSchema\InvalidFieldException $e) {
93+
$errors = \HH\Lib\Vec\concat($errors, $e->errors);
94+
}
95+
}
96+
97+
if (\HH\Lib\C\count($errors)) {
98+
throw new JsonSchema\InvalidFieldException($pointer, $errors);
99+
}
100+
101+
/* HH_IGNORE_ERROR[4163] */
102+
return $output;
103+
}
104+
105+
<<__Override>>
106+
protected function process(): TMultipleOfValidator {
107+
return self::check($this->input, $this->pointer);
108+
}
109+
}

tests/examples/multiple-of.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"a_multiple_of_five_int": {
5+
"type": "integer",
6+
"multipleOf": 5
7+
},
8+
"a_multiple_of_1_point_one_repeating_number": {
9+
"type": "number",
10+
"multipleOf": 1.1111111111111111
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)