Skip to content

Commit db731bd

Browse files
author
Michael Hahn
authored
Merge pull request #53 from betsyalegria/alegria-support-minProperties-maxProperties
Support minProperties and maxProperties on objects
2 parents 263c625 + e9baaea commit db731bd

File tree

8 files changed

+466
-3
lines changed

8 files changed

+466
-3
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ SHELL := /bin/bash
22

33
.PHONY: test autoload composer
44

5-
HHVM_VERSION=4.80.5
6-
HHVM_NEXT_VERSION=4.80.5
5+
HHVM_VERSION=4.102.2
6+
HHVM_NEXT_VERSION=4.102.2
77

88
DOCKER_RUN=docker run -v $(shell pwd):/data --workdir /data --rm hhvm/hhvm:$(HHVM_VERSION)
99
CONTAINER_NAME=hack-json-schema-hhvm

src/Codegen/Constraints/ObjectBuilder.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
?'additionalProperties' => mixed,
2121
?'patternProperties' => dict<string, TSchema>,
2222
?'coerce' => bool,
23+
?'minProperties' => int,
24+
?'maxProperties' => int,
2325
...
2426
);
2527

@@ -74,6 +76,34 @@ public function build(): this {
7476
->setValue($hb->getCode(), HackBuilderValues::literal());
7577
}
7678

79+
$max_properties = $this->typed_schema['maxProperties'] ?? null;
80+
if ($max_properties is nonnull) {
81+
if ($max_properties < 0) {
82+
throw new \Exception('maxProperties must be a non-negative integer');
83+
}
84+
85+
$class_properties[] = $this->codegenProperty('maxProperties')
86+
->setType('int')
87+
->setValue($max_properties, HackBuilderValues::export());
88+
}
89+
90+
$min_properties = $this->typed_schema['minProperties'] ?? null;
91+
if ($min_properties is nonnull) {
92+
if ($min_properties < 0) {
93+
throw new \Exception('minProperties must be a non-negative integer');
94+
}
95+
96+
$class_properties[] = $this->codegenProperty('minProperties')
97+
->setType('int')
98+
->setValue($min_properties, HackBuilderValues::export());
99+
}
100+
101+
if ($min_properties is nonnull && $max_properties is nonnull) {
102+
if ($min_properties > $max_properties) {
103+
throw new \Exception('maxProperties must be greater than minProperties');
104+
}
105+
}
106+
77107
$class->addProperties($class_properties);
78108
$this->addBuilderClass($class);
79109

@@ -132,6 +162,29 @@ protected function getCheckMethodCode(
132162
$include_error_handling = true;
133163
}
134164

165+
$max_properties = $this->typed_schema['maxProperties'] ?? null;
166+
$min_properties = $this->typed_schema['minProperties'] ?? null;
167+
168+
if ($max_properties is nonnull || $min_properties is nonnull) {
169+
$hb->addAssignment('$length', '\HH\Lib\C\count($typed)', HackBuilderValues::literal())->ensureEmptyLine();
170+
}
171+
172+
if ($max_properties is nonnull) {
173+
$hb->addMultilineCall(
174+
'Constraints\ObjectMaxPropertiesConstraint::check',
175+
vec['$length', 'self::$maxProperties', '$pointer'],
176+
)
177+
->ensureEmptyLine();
178+
}
179+
180+
if ($min_properties is nonnull) {
181+
$hb->addMultilineCall(
182+
'Constraints\ObjectMinPropertiesConstraint::check',
183+
vec['$length', 'self::$minProperties', '$pointer'],
184+
)
185+
->ensureEmptyLine();
186+
}
187+
135188
$defaults = $this->getDefaults();
136189
if (!C\is_empty($defaults)) {
137190
$hb->ensureEmptyLine();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?hh // strict
2+
3+
namespace Slack\Hack\JsonSchema\Constraints;
4+
5+
use namespace Slack\Hack\JsonSchema;
6+
7+
class ObjectMaxPropertiesConstraint {
8+
public static function check(int $num_properties, int $max_properties, string $pointer): void {
9+
if ($num_properties > $max_properties) {
10+
$error = shape(
11+
'code' => JsonSchema\FieldErrorCode::FAILED_CONSTRAINT,
12+
'constraint' => shape(
13+
'type' => JsonSchema\FieldErrorConstraint::MAX_PROPERTIES,
14+
'expected' => $max_properties,
15+
'got' => $num_properties,
16+
),
17+
'message' => "no more than {$max_properties} properties allowed",
18+
);
19+
throw new JsonSchema\InvalidFieldException($pointer, vec[$error]);
20+
}
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?hh // strict
2+
3+
namespace Slack\Hack\JsonSchema\Constraints;
4+
5+
use namespace Slack\Hack\JsonSchema;
6+
7+
class ObjectMinPropertiesConstraint {
8+
public static function check(int $num_properties, int $min_properties, string $pointer): void {
9+
if ($num_properties < $min_properties) {
10+
$error = shape(
11+
'code' => JsonSchema\FieldErrorCode::FAILED_CONSTRAINT,
12+
'constraint' => shape(
13+
'type' => JsonSchema\FieldErrorConstraint::MIN_PROPERTIES,
14+
'expected' => $min_properties,
15+
'got' => $num_properties,
16+
),
17+
'message' => "must have minimum {$min_properties} properties",
18+
);
19+
throw new JsonSchema\InvalidFieldException($pointer, vec[$error]);
20+
}
21+
}
22+
}

src/Exceptions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ enum FieldErrorConstraint: string {
1515
MAX_ITEMS = 'max_items';
1616
MAX_LENGTH = 'max_length';
1717
MIN_LENGTH = 'min_length';
18+
MAX_PROPERTIES = 'max_properties';
19+
MIN_PROPERTIES = 'min_properties';
1820
MAXIMUM = 'maximum';
1921
MINIMUM = 'minimum';
2022
MULTIPLE_OF = 'multiple_of';

tests/ObjectSchemaValidatorTest.php

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use namespace Facebook\TypeAssert;
88
use function Facebook\FBExpect\expect;
99

10+
use type Slack\Hack\JsonSchema\{FieldErrorCode, FieldErrorConstraint};
1011
use type Slack\Hack\JsonSchema\Tests\Generated\ObjectSchemaValidator;
1112

1213
final class ObjectSchemaValidatorTest extends BaseCodegenTestCase {
@@ -530,4 +531,165 @@ public function testAdditionalProperitesRef(): void {
530531
expect($validator->isValid())->toBeFalse();
531532
}
532533

534+
public function testMinPropertiesWithValidLength(): void {
535+
$validator = new ObjectSchemaValidator(dict[
536+
'only_min_properties' => dict[
537+
'a' => 0,
538+
],
539+
]);
540+
541+
$validator->validate();
542+
expect($validator->isValid())->toBeTrue();
543+
}
544+
545+
public function testMinPropertiesWithInvalidLength(): void {
546+
$validator = new ObjectSchemaValidator(dict[
547+
'only_min_properties' => dict[],
548+
]);
549+
550+
$validator->validate();
551+
expect($validator->isValid())->toBeFalse();
552+
553+
$errors = $validator->getErrors();
554+
expect(C\count($errors))->toEqual(1);
555+
556+
$error = C\firstx($errors);
557+
expect($error['code'])->toEqual(FieldErrorCode::FAILED_CONSTRAINT);
558+
expect($error['message'])->toEqual('must have minimum 1 properties');
559+
560+
$constraint = Shapes::at($error, 'constraint');
561+
expect($constraint['type'])->toEqual(FieldErrorConstraint::MIN_PROPERTIES);
562+
expect($constraint['got'] ?? null)->toEqual(0);
563+
}
564+
565+
public function testMaxPropertiesWithValidLength(): void {
566+
// maxProperties is set to 1, so having 1 value should be fine
567+
$validator = new ObjectSchemaValidator(dict[
568+
'only_max_properties' => dict[
569+
'a' => 0,
570+
],
571+
]);
572+
573+
$validator->validate();
574+
expect($validator->isValid())->toBeTrue();
575+
576+
// maxProperties is set to 1, so having no values should also be fine
577+
$validator = new ObjectSchemaValidator(dict[
578+
'only_max_properties' => dict[],
579+
]);
580+
581+
$validator->validate();
582+
expect($validator->isValid())->toBeTrue();
583+
}
584+
585+
public function testMaxPropertiesWithInvalidLength(): void {
586+
$validator = new ObjectSchemaValidator(dict[
587+
'only_max_properties' => dict[
588+
'a' => 0,
589+
'b' => 1,
590+
],
591+
]);
592+
593+
$validator->validate();
594+
expect($validator->isValid())->toBeFalse();
595+
596+
$errors = $validator->getErrors();
597+
expect(C\count($errors))->toEqual(1);
598+
599+
$error = C\firstx($errors);
600+
expect($error['code'])->toEqual(FieldErrorCode::FAILED_CONSTRAINT);
601+
expect($error['message'])->toEqual('no more than 1 properties allowed');
602+
603+
$constraint = Shapes::at($error, 'constraint');
604+
expect($constraint['type'])->toEqual(FieldErrorConstraint::MAX_PROPERTIES);
605+
expect($constraint['got'] ?? null)->toEqual(2);
606+
}
607+
608+
public function testMinAndMaxPropertiesWithValidLength(): void {
609+
$validator = new ObjectSchemaValidator(dict[
610+
'min_and_max_properties' => dict[
611+
'a' => 0,
612+
],
613+
]);
614+
615+
$validator->validate();
616+
expect($validator->isValid())->toBeTrue();
617+
618+
$validator = new ObjectSchemaValidator(dict[
619+
'min_and_max_properties' => dict[
620+
'a' => 0,
621+
'b' => 1,
622+
],
623+
]);
624+
625+
$validator->validate();
626+
expect($validator->isValid())->toBeTrue();
627+
}
628+
629+
public function testMinAndMaxPropertiesWithInvalidLength(): void {
630+
// minProperties is set to 1, violate it
631+
$validator = new ObjectSchemaValidator(dict[
632+
'min_and_max_properties' => dict[],
633+
]);
634+
635+
$validator->validate();
636+
expect($validator->isValid())->toBeFalse();
637+
638+
$errors = $validator->getErrors();
639+
expect(C\count($errors))->toEqual(1);
640+
641+
$error = C\firstx($errors);
642+
expect($error['code'])->toEqual(FieldErrorCode::FAILED_CONSTRAINT);
643+
expect($error['message'])->toEqual('must have minimum 1 properties');
644+
645+
$constraint = Shapes::at($error, 'constraint');
646+
expect($constraint['type'])->toEqual(FieldErrorConstraint::MIN_PROPERTIES);
647+
expect($constraint['got'] ?? null)->toEqual(0);
648+
649+
// maxProperties is set to 2, violate it
650+
$validator = new ObjectSchemaValidator(dict[
651+
'min_and_max_properties' => dict[
652+
'a' => 0,
653+
'b' => 1,
654+
'c' => 2,
655+
],
656+
]);
657+
658+
$validator->validate();
659+
expect($validator->isValid())->toBeFalse();
660+
661+
$errors = $validator->getErrors();
662+
expect(C\count($errors))->toEqual(1);
663+
664+
$error = C\firstx($errors);
665+
expect($error['code'])->toEqual(FieldErrorCode::FAILED_CONSTRAINT);
666+
expect($error['message'])->toEqual('no more than 2 properties allowed');
667+
668+
$constraint = Shapes::at($error, 'constraint');
669+
expect($constraint['type'])->toEqual(FieldErrorConstraint::MAX_PROPERTIES);
670+
expect($constraint['got'] ?? null)->toEqual(3);
671+
}
672+
673+
public function testInvalidMinPropertiesWithNoAdditionalProperties(): void {
674+
$validator = new ObjectSchemaValidator(dict[
675+
'invalid_min_properties_with_no_additional_properties' => dict[
676+
'a' => 0,
677+
],
678+
]);
679+
680+
$validator->validate();
681+
expect($validator->isValid())->toBeFalse();
682+
683+
$errors = $validator->getErrors();
684+
expect(C\count($errors))->toEqual(1);
685+
686+
$error = C\firstx($errors);
687+
expect($error['code'])->toEqual(FieldErrorCode::FAILED_CONSTRAINT);
688+
expect($error['message'])->toEqual('invalid additional property: a');
689+
690+
$constraint = Shapes::at($error, 'constraint');
691+
expect($constraint['type'])->toEqual(FieldErrorConstraint::ADDITIONAL_PROPERTIES);
692+
expect($constraint['got'] ?? null)->toEqual('a');
693+
}
694+
533695
}

0 commit comments

Comments
 (0)