Skip to content

Commit 6a942e8

Browse files
authored
Enforcing test pipeline
* Move Context classes to internal namespace * Remove internal methods from IDE autocompletion helper * Added array and object generic types support. * Added data converter tests * Improve retry policy casting and add protobuf casting tests * Suppress impure method call (side-effect in assertion)
1 parent 09e8338 commit 6a942e8

32 files changed

+1104
-202
lines changed

src/Activity/ActivityType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
class ActivityType
2020
{
2121
/**
22-
* @readonly
22+
* @psalm-readonly
2323
* @psalm-allow-private-mutation
2424
* @var string
2525
*/

src/Client/ClientOptions.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use JetBrains\PhpStorm\ExpectedValues;
1515
use JetBrains\PhpStorm\Pure;
1616
use Temporal\Api\Enums\V1\QueryRejectCondition;
17+
use Temporal\Internal\Assert;
1718

1819
/**
1920
* @psalm-type QueryRejectionConditionType = QueryRejectCondition::QUERY_REJECT_CONDITION_*
@@ -80,12 +81,16 @@ public function withIdentity(string $identity): self
8081
/**
8182
* @param QueryRejectionConditionType $condition
8283
* @return $this
84+
*
85+
* @psalm-suppress ImpureMethodCall
8386
*/
8487
#[Pure]
8588
public function withQueryRejectionCondition(
8689
#[ExpectedValues(valuesFromClass: QueryRejectCondition::class)]
8790
int $condition
8891
): self {
92+
assert(Assert::enum($condition, QueryRejectCondition::class));
93+
8994
$self = clone $this;
9095

9196
$self->queryRejectionCondition = $condition;

src/Client/WorkflowOptions.php

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
use Temporal\Internal\Marshaller\Type\DateIntervalType;
3131
use Temporal\Internal\Marshaller\Type\NullableType;
3232
use Temporal\Internal\Marshaller\Type\ObjectType;
33-
use Temporal\Internal\Support\Cron;
3433
use Temporal\Internal\Support\DateInterval;
3534
use Temporal\Internal\Support\Options;
3635
use Temporal\Worker\WorkerFactoryInterface;
@@ -375,6 +374,7 @@ public function toMemo(DataConverterInterface $converter): ?Memo
375374
}
376375

377376
$fields = [];
377+
378378
foreach ($this->memo as $key => $value) {
379379
$fields[$key] = $converter->toPayload($value);
380380
}
@@ -406,21 +406,4 @@ public function toSearchAttributes(DataConverterInterface $converter): ?SearchAt
406406

407407
return $search;
408408
}
409-
410-
/**
411-
* @return RetryPolicy
412-
* @internal
413-
*/
414-
public function toRetryPolicy(): RetryPolicy
415-
{
416-
$rt = new RetryPolicy();
417-
$rt->setInitialInterval(DateInterval::toDuration($this->retryOptions->initialInterval));
418-
$rt->setMaximumInterval(DateInterval::toDuration($this->retryOptions->maximumInterval));
419-
420-
$rt->setBackoffCoefficient($this->retryOptions->backoffCoefficient);
421-
$rt->setMaximumAttempts($this->retryOptions->maximumAttempts);
422-
$rt->setNonRetryableErrorTypes($this->retryOptions->nonRetryableExceptions);
423-
424-
return $rt;
425-
}
426409
}

src/Common/RetryOptions.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use JetBrains\PhpStorm\Pure;
1515
use Temporal\Activity\ActivityOptions;
16+
use Temporal\Api\Common\V1\RetryPolicy;
1617
use Temporal\Internal\Assert;
1718
use Temporal\Internal\Marshaller\Meta\Marshal;
1819
use Temporal\Internal\Marshaller\Type\DateIntervalType;
@@ -200,4 +201,22 @@ public function withNonRetryableExceptions(array $exceptions): self
200201
$self->nonRetryableExceptions = $exceptions;
201202
return $self;
202203
}
204+
205+
/**
206+
* Converts DTO to protobuf object
207+
*
208+
* @return RetryPolicy
209+
*/
210+
public function toRetryPolicy(): RetryPolicy
211+
{
212+
$policy = new RetryPolicy();
213+
$policy->setInitialInterval(DateInterval::toDuration($this->initialInterval));
214+
$policy->setMaximumInterval(DateInterval::toDuration($this->maximumInterval));
215+
216+
$policy->setBackoffCoefficient($this->backoffCoefficient);
217+
$policy->setMaximumAttempts($this->maximumAttempts);
218+
$policy->setNonRetryableErrorTypes($this->nonRetryableExceptions);
219+
220+
return $policy;
221+
}
203222
}

src/DataConverter/BinaryConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function toPayload($value): ?Payload
3434
return null;
3535
}
3636

37-
return self::create($value->getData());
37+
return $this->create($value->getData());
3838
}
3939

4040
/**

src/DataConverter/DataConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public static function createDefault(): DataConverterInterface
8383
new NullConverter(),
8484
new BinaryConverter(),
8585
new ProtoJsonConverter(),
86-
new JsonDTOConverter()
86+
new JsonConverter()
8787
);
8888
}
8989
}

src/DataConverter/JsonConverter.php

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,40 @@
1111

1212
namespace Temporal\DataConverter;
1313

14+
use Doctrine\Common\Annotations\Reader;
15+
use Spiral\Attributes\AnnotationReader;
16+
use Spiral\Attributes\AttributeReader;
17+
use Spiral\Attributes\Composite\SelectiveReader;
18+
use Spiral\Attributes\ReaderInterface;
1419
use Temporal\Api\Common\V1\Payload;
1520
use Temporal\Exception\DataConverterException;
21+
use Temporal\Internal\Marshaller\Mapper\AttributeMapperFactory;
22+
use Temporal\Internal\Marshaller\Marshaller;
23+
use Temporal\Internal\Marshaller\MarshallerInterface;
1624

25+
/**
26+
* Json converter with the ability to serialize/unserialize DTO objects using JSON.
27+
*/
1728
class JsonConverter extends Converter
1829
{
30+
/**
31+
* @var int
32+
*/
33+
public const JSON_FLAGS = \JSON_THROW_ON_ERROR | \JSON_PRESERVE_ZERO_FRACTION;
34+
35+
/**
36+
* @var MarshallerInterface
37+
*/
38+
private MarshallerInterface $marshaller;
39+
40+
/**
41+
* @param MarshallerInterface|null $marshaller
42+
*/
43+
public function __construct(MarshallerInterface $marshaller = null)
44+
{
45+
$this->marshaller = $marshaller ?? self::createDefaultMarshaller();
46+
}
47+
1948
/**
2049
* @return string
2150
*/
@@ -27,12 +56,19 @@ public function getEncodingType(): string
2756
/**
2857
* @param mixed $value
2958
* @return Payload|null
30-
* @throws \JsonException
59+
* @throws DataConverterException
3160
*/
3261
public function toPayload($value): ?Payload
3362
{
63+
if (\is_object($value)) {
64+
$value = $value instanceof \stdClass
65+
? $value
66+
: $this->marshaller->marshal($value)
67+
;
68+
}
69+
3470
try {
35-
return self::create(\json_encode($value, \JSON_THROW_ON_ERROR));
71+
return $this->create(\json_encode($value, self::JSON_FLAGS));
3672
} catch (\Throwable $e) {
3773
throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
3874
}
@@ -42,13 +78,142 @@ public function toPayload($value): ?Payload
4278
* @param Payload $payload
4379
* @param Type $type
4480
* @return mixed|void
81+
* @throws DataConverterException
4582
*/
4683
public function fromPayload(Payload $payload, Type $type)
4784
{
4885
try {
49-
return \json_decode($payload->getData(), true, 512, \JSON_THROW_ON_ERROR);
86+
$data = \json_decode($payload->getData(), false, 512, self::JSON_FLAGS);
5087
} catch (\Throwable $e) {
5188
throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
5289
}
90+
91+
switch ($type->getName()) {
92+
case Type::TYPE_ANY:
93+
return $data;
94+
95+
case Type::TYPE_STRING:
96+
if (! \is_string($data)) {
97+
throw $this->errorInvalidType($type, $data);
98+
}
99+
100+
return $data;
101+
102+
case Type::TYPE_FLOAT:
103+
if (! \is_float($data)) {
104+
throw $this->errorInvalidType($type, $data);
105+
}
106+
107+
return $data;
108+
109+
case Type::TYPE_INT:
110+
if (! \is_int($data)) {
111+
throw $this->errorInvalidType($type, $data);
112+
}
113+
114+
return $data;
115+
116+
case Type::TYPE_BOOL:
117+
if (! \is_bool($data)) {
118+
throw $this->errorInvalidType($type, $data);
119+
}
120+
121+
return $data;
122+
123+
case Type::TYPE_ARRAY:
124+
if (! \is_array($data)) {
125+
throw $this->errorInvalidType($type, $data);
126+
}
127+
128+
return $data;
129+
130+
case Type::TYPE_OBJECT:
131+
if (! \is_object($data)) {
132+
throw $this->errorInvalidType($type, $data);
133+
}
134+
135+
return $data;
136+
}
137+
138+
if ((\is_object($data) || \is_array($data)) && $type->isClass()) {
139+
try {
140+
$instance = (new \ReflectionClass($type->getName()))
141+
->newInstanceWithoutConstructor()
142+
;
143+
} catch (\ReflectionException $e) {
144+
throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
145+
}
146+
147+
return $this->marshaller->unmarshal($this->toHashMap($data), $instance);
148+
}
149+
150+
throw $this->errorInvalidTypeName($type);
151+
}
152+
153+
/**
154+
* @param object|array $context
155+
* @return array
156+
*/
157+
private function toHashMap($context): array
158+
{
159+
if (\is_object($context)) {
160+
$context = (array)$context;
161+
}
162+
163+
foreach ($context as $key => $value) {
164+
if (\is_object($value) || \is_array($value)) {
165+
$context[$key] = $this->toHashMap($value);
166+
}
167+
}
168+
169+
return $context;
170+
}
171+
172+
/**
173+
* @param Type $type
174+
* @return DataConverterException
175+
*/
176+
private function errorInvalidTypeName(Type $type): DataConverterException
177+
{
178+
$message = \vsprintf('Type named "%s" is not a valid type name', [
179+
$type->getName()
180+
]);
181+
182+
return new DataConverterException($message);
183+
}
184+
185+
/**
186+
* @param Type $type
187+
* @param mixed $data
188+
* @return DataConverterException
189+
*/
190+
private function errorInvalidType(Type $type, $data): DataConverterException
191+
{
192+
$message = \vsprintf('The passed value of type "%s" can not be converted to required type "%s"', [
193+
\get_debug_type($data),
194+
$type->getName()
195+
]);
196+
197+
return new DataConverterException($message);
198+
}
199+
200+
/**
201+
* @return MarshallerInterface
202+
*/
203+
private static function createDefaultMarshaller(): MarshallerInterface
204+
{
205+
return new Marshaller(new AttributeMapperFactory(self::createDefaultReader()));
206+
}
207+
208+
/**
209+
* @return ReaderInterface
210+
*/
211+
private static function createDefaultReader(): ReaderInterface
212+
{
213+
if (\interface_exists(Reader::class)) {
214+
return new SelectiveReader([new AnnotationReader(), new AttributeReader()]);
215+
}
216+
217+
return new AttributeReader();
53218
}
54219
}

0 commit comments

Comments
 (0)