Skip to content

Commit 14a9c36

Browse files
committed
feat(runtime): add support for message aliases
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent d8fab7e commit 14a9c36

File tree

5 files changed

+143
-9
lines changed

5 files changed

+143
-9
lines changed

src/Runtime/Configuration.php

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Cel\Runtime;
66

7+
use Cel\Runtime\Exception\MisconfigurationException;
78
use Cel\Runtime\Message\MessageInterface;
9+
use Psl\Iter;
10+
use Psl\Str;
811

912
/**
1013
* Encapsulates the configuration for a CEL runtime environment.
@@ -15,6 +18,11 @@
1518
*/
1619
final readonly class Configuration
1720
{
21+
/**
22+
* @var array<class-string<MessageInterface>, list<string>> A reverse mapping of message class names to their aliases for quick lookup.
23+
*/
24+
public array $messageClassesToAliases;
25+
1826
/**
1927
* @param bool $enableMacros Whether to enable macro support (e.g., `has()`, `all()`).
2028
* @param bool $enableCoreExtension Whether to enable the core extension (e.g., `size()`, type conversions).
@@ -24,6 +32,13 @@
2432
* @param bool $enableListExtension Whether to enable the list extension (e.g., `sort()`, `chunk()`).
2533
* @param list<class-string<MessageInterface>> $allowedMessageClasses A security-focused allowlist of message classes that can be constructed within an expression.
2634
* Classes must implement `MessageInterface`. By default, no message construction is allowed.
35+
* @param array<string, class-string<MessageInterface>> $messageClassAliases An optional mapping of custom type names to fully qualified message class names.
36+
* This allows using shorter or more convenient names in expressions.
37+
* @param bool $enforceMessageClassAliases Whether to enforce the use of message class aliases when constructing messages.
38+
* If true, a class in `$allowedMessageClasses` that also has an alias in `$messageClassAliases`
39+
* can only be constructed using its alias.
40+
*
41+
* @throws MisconfigurationException if any alias in `$messageClassAliases` does not map to a class in `$allowedMessageClasses`.
2742
*/
2843
public function __construct(
2944
public bool $enableMacros = true,
@@ -33,7 +48,28 @@ public function __construct(
3348
public bool $enableStringExtension = true,
3449
public bool $enableListExtension = true,
3550
public array $allowedMessageClasses = [],
36-
) {}
51+
public array $messageClassAliases = [],
52+
public bool $enforceMessageClassAliases = false,
53+
) {
54+
foreach ($this->messageClassAliases as $messageAlias => $messageClassAlias) {
55+
if (!Iter\contains($this->allowedMessageClasses, $messageClassAlias)) {
56+
throw new MisconfigurationException(Str\format(
57+
'Message class alias "%s" ( `%s` ) does not map to an allowed message class. '
58+
. 'All aliases in $messageClassAliases must map to classes in $allowedMessageClasses.',
59+
$messageAlias,
60+
$messageClassAlias,
61+
));
62+
}
63+
}
64+
65+
/** @var array<class-string<MessageInterface>, list<string>> $messageClassesToAliases */
66+
$messageClassesToAliases = [];
67+
foreach ($this->messageClassAliases as $alias => $class) {
68+
$messageClassesToAliases[$class][] = $alias;
69+
}
70+
71+
$this->messageClassesToAliases = $messageClassesToAliases;
72+
}
3773

3874
/**
3975
* Creates a configuration that allows constructing only the specified message classes.
@@ -42,11 +78,22 @@ public function __construct(
4278
* from being instantiated within CEL expressions.
4379
*
4480
* @param list<class-string<MessageInterface>> $allowedMessageClasses The list of allowed message classes.
81+
* @param array<string, class-string<MessageInterface>> $messageClassAliases An optional mapping of custom type names to fully qualified message class names.
82+
* @param bool $enforceMessageClassAliases Whether to enforce the use of message class aliases when constructing messages.
4583
*
4684
* @return self The configuration instance with the specified allowed message classes.
85+
*
86+
* @throws MisconfigurationException if any alias in `$messageClassAliases` does not map to a class in `$allowedMessageClasses`.
4787
*/
48-
public static function forAllowedMessages(array $allowedMessageClasses): self
49-
{
50-
return new self(allowedMessageClasses: $allowedMessageClasses);
88+
public static function forAllowedMessages(
89+
array $allowedMessageClasses,
90+
array $messageClassAliases = [],
91+
bool $enforceMessageClassAliases = false,
92+
): self {
93+
return new self(
94+
allowedMessageClasses: $allowedMessageClasses,
95+
messageClassAliases: $messageClassAliases,
96+
enforceMessageClassAliases: $enforceMessageClassAliases,
97+
);
5198
}
5299
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cel\Runtime\Exception;
6+
7+
use InvalidArgumentException;
8+
9+
final class MisconfigurationException extends InvalidArgumentException implements ExceptionInterface
10+
{
11+
}

src/Runtime/Extension/DateTime/Function/TimestampFunction.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Cel\Runtime\Value\IntegerValue;
1111
use Cel\Runtime\Value\StringValue;
1212
use Cel\Runtime\Value\TimestampValue;
13+
use Cel\Runtime\Value\UnsignedIntegerValue;
1314
use Cel\Runtime\Value\Value;
1415
use Cel\Runtime\Value\ValueKind;
1516
use Cel\Syntax\Member\CallExpression;
@@ -19,9 +20,13 @@
1920
use Psl\DateTime\FormatPattern;
2021
use Psl\DateTime\Timestamp;
2122
use Psl\DateTime\Timezone;
23+
use Psl\Math;
2224
use Psl\Regex;
2325
use Psl\Str;
2426

27+
use function bccomp;
28+
use function is_string;
29+
2530
final readonly class TimestampFunction implements FunctionInterface
2631
{
2732
private const string RFC3339_PATTERN = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(.*)$/';
@@ -41,14 +46,14 @@ public function isIdempotent(): bool
4146
#[Override]
4247
public function getOverloads(): iterable
4348
{
44-
yield [ValueKind::Integer] => static function (CallExpression $expr, array $arguments): TimestampValue {
49+
yield [ValueKind::Integer] => static function (CallExpression $_expr, array $arguments): TimestampValue {
4550
/** @var IntegerValue $seconds */
4651
$seconds = $arguments[0];
4752

4853
return new TimestampValue(Timestamp::fromParts($seconds->value));
4954
};
5055

51-
yield [ValueKind::Float] => static function (CallExpression $expr, array $arguments): TimestampValue {
56+
yield [ValueKind::Float] => static function (CallExpression $_expr, array $arguments): TimestampValue {
5257
/** @var FloatValue $seconds */
5358
$seconds = $arguments[0];
5459

src/Runtime/Interpreter/TreeWalking/TreeWalkingInterpreter.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -885,13 +885,36 @@ private function message(MessageExpression $expression): Value
885885
}
886886

887887
$foundClassname = null;
888-
foreach ($this->configuration->allowedMessageClasses as $allowedClassname) {
889-
if (Byte\compare_ci($classname, $allowedClassname) === 0) {
890-
$foundClassname = $allowedClassname;
888+
$usingAlias = false;
889+
foreach ($this->configuration->messageClassAliases as $typeAlias => $targetClassname) {
890+
if (Byte\compare_ci($typename, $typeAlias) === 0) {
891+
$foundClassname = $targetClassname;
892+
891893
break;
892894
}
893895
}
894896

897+
if ($foundClassname === null) {
898+
foreach ($this->configuration->allowedMessageClasses as $allowedClassname) {
899+
if (Byte\compare_ci($classname, $allowedClassname) === 0) {
900+
$foundClassname = $allowedClassname;
901+
break;
902+
}
903+
}
904+
905+
if (
906+
$foundClassname !== null
907+
&& $this->configuration->enforceMessageClassAliases
908+
&& Iter\contains_key($this->configuration->messageClassesToAliases, $foundClassname)
909+
) {
910+
// Pretend the class does not exist if using an alias is enforced
911+
throw new NoSuchTypeException(
912+
Str\format('Message type `%s` does not exist or is not allowed per configuration.', $typename),
913+
$expression->getSpan(),
914+
);
915+
}
916+
}
917+
895918
if (null === $foundClassname) {
896919
throw new NoSuchTypeException(
897920
Str\format('Message type `%s` does not exist or is not allowed per configuration.', $typename),

tests/Runtime/RuntimeTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,54 @@ public static function provideEvaluationCases(): iterable
139139
Configuration::forAllowedMessages([CommentMessage::class]),
140140
];
141141

142+
yield 'Using Message Alias' => [
143+
'my_package.UserMessage { name: "azjezz", email: "azjezz@carthage.software" }',
144+
[],
145+
new MessageValue(new UserMessage('azjezz', 'azjezz@carthage.software'), [
146+
'name' => new StringValue('azjezz'),
147+
'email' => new StringValue('azjezz@carthage.software'),
148+
]),
149+
Configuration::forAllowedMessages([UserMessage::class], ['my_package.UserMessage' => UserMessage::class]),
150+
];
151+
152+
yield 'Enforced Message Alias Usage' => [
153+
'my_package.UserMessage { name: "azjezz", email: "azjezz@carthage.software" }',
154+
[],
155+
new MessageValue(new UserMessage('azjezz', 'azjezz@carthage.software'), [
156+
'name' => new StringValue('azjezz'),
157+
'email' => new StringValue('azjezz@carthage.software'),
158+
]),
159+
Configuration::forAllowedMessages(
160+
[UserMessage::class],
161+
['my_package.UserMessage' => UserMessage::class],
162+
true,
163+
),
164+
];
165+
166+
yield 'Using Message Without Alias' => [
167+
'cel.tests.fixture.UserMessage { name: "azjezz", email: "azjezz@carthage.software" }',
168+
[],
169+
new MessageValue(new UserMessage('azjezz', 'azjezz@carthage.software'), [
170+
'name' => new StringValue('azjezz'),
171+
'email' => new StringValue('azjezz@carthage.software'),
172+
]),
173+
Configuration::forAllowedMessages([UserMessage::class], ['my_package.UserMessage' => UserMessage::class]),
174+
];
175+
176+
yield 'Forbid Message FQCN Without Alias' => [
177+
'cel.tests.fixture.UserMessage { name: "azjezz", email: "azjezz@carthage.software" }',
178+
[],
179+
new NoSuchTypeException(
180+
'Message type `cel.tests.fixture.UserMessage` does not exist or is not allowed per configuration.',
181+
Span::zero(),
182+
),
183+
Configuration::forAllowedMessages(
184+
[UserMessage::class],
185+
['my_package.UserMessage' => UserMessage::class],
186+
true,
187+
),
188+
];
189+
142190
yield 'Division by zero: 10 / 0' => [
143191
'10 / 0',
144192
[],

0 commit comments

Comments
 (0)