Skip to content
This repository was archived by the owner on Aug 25, 2025. It is now read-only.

Commit db6d9c5

Browse files
mwildehahnfredemmott
authored andcommitted
Add support for optional fields within shapes (#76) (#89)
* Add support for optional shape fields & nested shapes * Simplify CodegenShapeMember * Don't use public class properties * Add support for CodegenShape_FUTURE * Remove old codegen * Use correct codegen file * Prefer vec * Don't accept old shape
1 parent 355d40b commit db6d9c5

8 files changed

+267
-2
lines changed

src/CodegenFactoryTrait.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ final public function codegenShape(
168168
return new CodegenShape($this->getConfig(), $attrs);
169169
}
170170

171+
final public function codegenShape_FUTURE(
172+
CodegenShapeMember ...$members
173+
): CodegenShape_FUTURE {
174+
return new CodegenShape_FUTURE($this->getConfig(), vec($members));
175+
}
176+
171177
final public function codegenType(string $name): CodegenType {
172178
return new CodegenType($this->getConfig(), $name);
173179
}

src/CodegenShapeMember.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?hh // strict
2+
/*
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the MIT license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*
9+
*/
10+
11+
namespace Facebook\HackCodegen;
12+
13+
final class CodegenShapeMember {
14+
15+
private bool $isOptional = false;
16+
17+
public function __construct(
18+
private string $name,
19+
private mixed $type,
20+
) {
21+
invariant(
22+
is_string($type) || $type instanceof CodegenShape_FUTURE,
23+
"You must provide either a string or shape",
24+
);
25+
}
26+
27+
public function setIsOptional(bool $value = true): this {
28+
$this->isOptional = $value;
29+
return $this;
30+
}
31+
32+
public function getIsOptional(): bool {
33+
return $this->isOptional;
34+
}
35+
36+
public function getName(): string {
37+
return $this->name;
38+
}
39+
40+
public function getType(): string {
41+
if ($this->type instanceof ICodeBuilderRenderer) {
42+
return $this->type->render();
43+
}
44+
45+
invariant($this->type !== null, "Somehow type is null");
46+
invariant(is_string($this->type), "Somehow type is not a string");
47+
return $this->type;
48+
}
49+
}

src/CodegenShape_FUTURE.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?hh // strict
2+
/*
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the MIT license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*
9+
*/
10+
11+
namespace Facebook\HackCodegen;
12+
13+
/**
14+
* Generate code for a shape. Please don't use this class directly; instead use
15+
* the function codegenShape_FUTURE. E.g.:
16+
*
17+
* ```
18+
* codegenShape_FUTURE(
19+
* new CodegenShapeMember('x', 'int'),
20+
* (new CodegenShapeMember('y', 'int'))->setIsOptional(),
21+
* )
22+
* ```
23+
*
24+
*/
25+
final class CodegenShape_FUTURE implements ICodeBuilderRenderer {
26+
27+
use HackBuilderRenderer;
28+
29+
private ?string $manualAttrsID = null;
30+
31+
public function __construct(
32+
protected IHackCodegenConfig $config,
33+
private vec<CodegenShapeMember> $members,
34+
) {
35+
}
36+
37+
public function setManualAttrsID(?string $id = null): this {
38+
$this->manualAttrsID = $id;
39+
return $this;
40+
}
41+
42+
public function appendToBuilder(HackBuilder $builder): HackBuilder {
43+
$builder->addLine('shape(')->indent();
44+
45+
foreach ($this->members as $member) {
46+
$prefix = $member->getIsOptional() ? '?' : '';
47+
$builder->addLinef(
48+
"%s'%s' => %s,",
49+
$prefix,
50+
$member->getName(),
51+
$member->getType(),
52+
);
53+
}
54+
55+
$manual_id = $this->manualAttrsID;
56+
if ($manual_id !== null) {
57+
$builder
58+
->ensureNewLine()
59+
->startManualSection($manual_id)
60+
->ensureEmptyLine()
61+
->endManualSection();
62+
}
63+
64+
return $builder->unindent()->add(')');
65+
}
66+
}

src/CodegenType.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class CodegenType implements ICodeBuilderRenderer {
2222
use HackBuilderRenderer;
2323

2424
private ?string $type;
25-
private ?CodegenShape $codegenShape;
25+
private ?ICodeBuilderRenderer $codegenShape;
2626
private string $keyword = 'type';
2727

2828
public function __construct(
@@ -40,11 +40,16 @@ public function setType(string $type): this {
4040
return $this;
4141
}
4242

43-
public function setShape(CodegenShape $codegen_shape): this {
43+
public function setShape(mixed $codegen_shape): this {
4444
invariant(
4545
$this->type === null,
4646
"You can't set both the type and the shape.",
4747
);
48+
invariant(
49+
$codegen_shape instanceof ICodeBuilderRenderer && ($codegen_shape instanceof CodegenShape_FUTURE || $codegen_shape instanceof CodegenShape),
50+
"You must provide either CodegenShape_FUTURE or CodegenShape",
51+
);
52+
4853
$this->codegenShape = $codegen_shape;
4954
return $this;
5055
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@generated
2+
!@#$%codegentest:testMultipleNestedShapes
3+
shape(
4+
'test' => shape(
5+
?'point' => shape(
6+
'x' => int,
7+
'y' => int,
8+
),
9+
'url' => string,
10+
),
11+
?'key' => string,
12+
)
13+
!@#$%codegentest:testNestedShape
14+
shape(
15+
'url' => string,
16+
?'point' => shape(
17+
'x' => int,
18+
'y' => int,
19+
),
20+
)
21+
!@#$%codegentest:testNestedShapeLegacy
22+
shape(
23+
'url' => string,
24+
?'point' => shape(
25+
'x' => int,
26+
'y' => int,
27+
),
28+
)
29+
!@#$%codegentest:testShape
30+
shape(
31+
'x' => int,
32+
'y' => int,
33+
'url' => string,
34+
)
35+
!@#$%codegentest:testShapeOptionalFields
36+
shape(
37+
?'x' => int,
38+
?'y' => int,
39+
'url' => string,
40+
)

test/CodegenShape_FUTURETest.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?hh // strict
2+
/*
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the MIT license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*
9+
*/
10+
11+
namespace Facebook\HackCodegen;
12+
13+
final class CodegenShapeFutureTest extends CodegenBaseTest {
14+
15+
public function testShape(): void {
16+
$shape = $this
17+
->getCodegenFactory()
18+
->codegenShape_FUTURE(
19+
new CodegenShapeMember('x', 'int'),
20+
new CodegenShapeMember('y', 'int'),
21+
new CodegenShapeMember('url', 'string'),
22+
);
23+
24+
$this->assertUnchanged($shape->render());
25+
}
26+
27+
public function testShapeOptionalFields(): void {
28+
$shape = $this
29+
->getCodegenFactory()
30+
->codegenShape_FUTURE(
31+
(new CodegenShapeMember('x', 'int'))->setIsOptional(),
32+
(new CodegenShapeMember('y', 'int'))->setIsOptional(),
33+
new CodegenShapeMember('url', 'string'),
34+
);
35+
36+
$this->assertUnchanged($shape->render());
37+
}
38+
39+
public function testNestedShape(): void {
40+
$nested = $this
41+
->getCodegenFactory()
42+
->codegenShape_FUTURE(
43+
new CodegenShapeMember('x', 'int'),
44+
new CodegenShapeMember('y', 'int'),
45+
);
46+
47+
$shape = $this
48+
->getCodegenFactory()
49+
->codegenShape_FUTURE(
50+
new CodegenShapeMember('url', 'string'),
51+
(new CodegenShapeMember('point', $nested))->setIsOptional(),
52+
);
53+
54+
$this->assertUnchanged($shape->render());
55+
}
56+
57+
public function testMultipleNestedShapes(): void {
58+
$first = $this
59+
->getCodegenFactory()
60+
->codegenShape_FUTURE(
61+
new CodegenShapeMember('x', 'int'),
62+
new CodegenShapeMember('y', 'int'),
63+
);
64+
65+
$second = $this
66+
->getCodegenFactory()
67+
->codegenShape_FUTURE(
68+
(new CodegenShapeMember('point', $first))->setIsOptional(),
69+
new CodegenShapeMember('url', 'string'),
70+
);
71+
72+
$shape = $this
73+
->getCodegenFactory()
74+
->codegenShape_FUTURE(
75+
new CodegenShapeMember('test', $second),
76+
(new CodegenShapeMember('key', 'string'))->setIsOptional(),
77+
);
78+
79+
$this->assertUnchanged($shape->render());
80+
}
81+
}

test/CodegenTypeTest.codegen

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ type Point = shape(
88
'y' => int,
99
);
1010

11+
!@#$%codegentest:testShape_FUTURE
12+
type Point = shape(
13+
'x' => int,
14+
'y' => int,
15+
);
16+
1117
!@#$%codegentest:testType
1218
type Point = (int, int);
1319

test/CodegenTypeTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,16 @@ public function testShape(): void {
3232

3333
$this->assertUnchanged($type->render());
3434
}
35+
36+
public function testShape_FUTURE(): void {
37+
$cgf = $this->getCodegenFactory();
38+
$type = $cgf
39+
->codegenType('Point')
40+
->setShape($cgf->codegenShape_FUTURE(
41+
new CodegenShapeMember('x', 'int'),
42+
new CodegenShapeMember('y', 'int'),
43+
));
44+
45+
$this->assertUnchanged($type->render());
46+
}
3547
}

0 commit comments

Comments
 (0)