Skip to content

Commit

Permalink
Feat: Global segments (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Aug 11, 2022
1 parent 1c467d1 commit f4e841d
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 17 deletions.
3 changes: 3 additions & 0 deletions src/Configuration/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public function getSessionId(): ?string;

public function getCustomProperty(string $name): string;

/**
* @todo make $value nullable
*/
public function setCustomProperty(string $name, string $value): self;

public function hasCustomProperty(string $name): bool;
Expand Down
4 changes: 2 additions & 2 deletions src/Configuration/UnleashContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public function getCustomProperty(string $name): string
return $this->customContext[$name];
}

public function setCustomProperty(string $name, string $value): self
public function setCustomProperty(string $name, ?string $value): self
{
$this->customContext[$name] = $value;
$this->customContext[$name] = $value ?? '';

return $this;
}
Expand Down
25 changes: 25 additions & 0 deletions src/DTO/DefaultSegment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Unleash\Client\DTO;

final class DefaultSegment implements Segment
{
/**
* @param array<Constraint> $constraints
*/
public function __construct(
private readonly int $id,
private readonly array $constraints,
) {
}

public function getId(): int
{
return $this->id;
}

public function getConstraints(): array
{
return $this->constraints;
}
}
16 changes: 16 additions & 0 deletions src/DTO/DefaultStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ final class DefaultStrategy implements Strategy
/**
* @param array<string,string> $parameters
* @param array<Constraint> $constraints
* @param array<Segment> $segments
*/
public function __construct(
private readonly string $name,
private readonly array $parameters = [],
private readonly array $constraints = [],
private readonly array $segments = [],
private readonly bool $nonexistentSegments = false,
) {
}

Expand All @@ -35,4 +38,17 @@ public function getConstraints(): array
{
return $this->constraints;
}

/**
* @return array<Segment>
*/
public function getSegments(): array
{
return $this->segments;
}

public function hasNonexistentSegments(): bool
{
return $this->nonexistentSegments;
}
}
13 changes: 13 additions & 0 deletions src/DTO/Segment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Unleash\Client\DTO;

interface Segment
{
public function getId(): int;

/**
* @return array<Constraint>
*/
public function getConstraints(): array;
}
4 changes: 4 additions & 0 deletions src/DTO/Strategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace Unleash\Client\DTO;

/**
* @method array<Segment> getSegments()
* @method bool hasNonexistentSegments()
*/
interface Strategy
{
public function getName(): string;
Expand Down
86 changes: 74 additions & 12 deletions src/Repository/DefaultUnleashRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,33 @@
use Psr\Http\Message\RequestFactoryInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Unleash\Client\Configuration\UnleashConfiguration;
use Unleash\Client\DTO\Constraint;
use Unleash\Client\DTO\DefaultConstraint;
use Unleash\Client\DTO\DefaultFeature;
use Unleash\Client\DTO\DefaultSegment;
use Unleash\Client\DTO\DefaultStrategy;
use Unleash\Client\DTO\DefaultVariant;
use Unleash\Client\DTO\DefaultVariantOverride;
use Unleash\Client\DTO\DefaultVariantPayload;
use Unleash\Client\DTO\Feature;
use Unleash\Client\DTO\Segment;
use Unleash\Client\Enum\CacheKey;
use Unleash\Client\Enum\Stickiness;
use Unleash\Client\Event\FetchingDataFailedEvent;
use Unleash\Client\Event\UnleashEvents;
use Unleash\Client\Exception\HttpResponseException;
use Unleash\Client\Exception\InvalidValueException;

/**
* @phpstan-type ConstraintArray array{
* contextName: string,
* operator: string,
* values?: array<string>,
* value?: string,
* inverted?: bool,
* caseInsensitive?: bool
* }
*/
final class DefaultUnleashRepository implements UnleashRepository
{
public function __construct(
Expand Down Expand Up @@ -65,7 +78,9 @@ public function getFeatures(): iterable
$request = $this->requestFactory
->createRequest('GET', $this->configuration->getUrl() . 'client/features')
->withHeader('UNLEASH-APPNAME', $this->configuration->getAppName())
->withHeader('UNLEASH-INSTANCEID', $this->configuration->getInstanceId());
->withHeader('UNLEASH-INSTANCEID', $this->configuration->getInstanceId())
->withHeader('Unleash-Client-Spec', '4.2.2')
;

foreach ($this->configuration->getHeaders() as $name => $value) {
$request = $request->withHeader($name, $value);
Expand Down Expand Up @@ -141,6 +156,8 @@ private function parseFeatures(string $rawBody): array
$body = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
assert(is_array($body));

$globalSegments = $this->parseSegments($body['segments'] ?? []);

if (!isset($body['features']) || !is_array($body['features'])) {
throw new InvalidValueException("The body isn't valid because it doesn't contain a 'features' key");
}
Expand All @@ -150,21 +167,24 @@ private function parseFeatures(string $rawBody): array
$variants = [];

foreach ($feature['strategies'] as $strategy) {
$constraints = [];
foreach ($strategy['constraints'] ?? [] as $constraint) {
$constraints[] = new DefaultConstraint(
$constraint['contextName'],
$constraint['operator'],
$constraint['values'] ?? null,
$constraint['value'] ?? null,
$constraint['inverted'] ?? false,
$constraint['caseInsensitive'] ?? false,
);
$constraints = $this->parseConstraints($strategy['constraints'] ?? []);

$hasNonexistentSegments = false;
$segments = [];
foreach ($strategy['segments'] ?? [] as $segment) {
if (isset($globalSegments[$segment])) {
$segments[] = $globalSegments[$segment];
} else {
$hasNonexistentSegments = true;
break;
}
}
$strategies[] = new DefaultStrategy(
$strategy['name'],
$strategy['parameters'] ?? [],
$constraints
$constraints,
$segments,
$hasNonexistentSegments,
);
}
foreach ($feature['variants'] ?? [] as $variant) {
Expand All @@ -183,6 +203,7 @@ private function parseFeatures(string $rawBody): array
$overrides,
);
}

$features[$feature['name']] = new DefaultFeature(
$feature['name'],
$feature['enabled'],
Expand Down Expand Up @@ -222,4 +243,45 @@ private function setLastValidState(string $data): void
$this->configuration->getStaleTtl(),
);
}

/**
* @param array<array{id: int, constraints: array<ConstraintArray>}> $segmentsRaw
*
* @return array<Segment>
*/
private function parseSegments(array $segmentsRaw): array
{
$result = [];
foreach ($segmentsRaw as $segmentRaw) {
$result[$segmentRaw['id']] = new DefaultSegment(
$segmentRaw['id'],
$this->parseConstraints($segmentRaw['constraints']),
);
}

return $result;
}

/**
* @param array<ConstraintArray> $constraintsRaw
*
* @return array<Constraint>
*/
private function parseConstraints(array $constraintsRaw): array
{
$constraints = [];

foreach ($constraintsRaw as $constraint) {
$constraints[] = new DefaultConstraint(
$constraint['contextName'],
$constraint['operator'],
$constraint['values'] ?? null,
$constraint['value'] ?? null,
$constraint['inverted'] ?? false,
$constraint['caseInsensitive'] ?? false,
);
}

return $constraints;
}
}
24 changes: 22 additions & 2 deletions src/Strategy/AbstractStrategyHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Unleash\Client\Strategy;

use Unleash\Client\Configuration\Context;
use Unleash\Client\DTO\Constraint;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\Helper\ConstraintValidatorTrait;

Expand All @@ -24,13 +25,32 @@ protected function findParameter(string $parameter, Strategy $strategy): ?string

protected function validateConstraints(Strategy $strategy, Context $context): bool
{
$constraints = $strategy->getConstraints();
if (method_exists($strategy, 'hasNonexistentSegments') && $strategy->hasNonexistentSegments()) {
return false;
}

$validator = $this->getValidator();
$constraints = $this->getConstraintsForStrategy($strategy);

foreach ($constraints as $constraint) {
if (!$this->getValidator()->validateConstraint($constraint, $context)) {
if (!$validator->validateConstraint($constraint, $context)) {
return false;
}
}

return true;
}

/**
* @return iterable<Constraint>
*/
private function getConstraintsForStrategy(Strategy $strategy): iterable
{
yield from $strategy->getConstraints();

$segments = method_exists($strategy, 'getSegments') ? $strategy->getSegments() : [];
foreach ($segments as $segment) {
yield from $segment->getConstraints();
}
}
}
19 changes: 19 additions & 0 deletions tests/CoverageOnlyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Unleash\Client\Tests;

use PHPUnit\Framework\TestCase;
use Unleash\Client\DTO\DefaultSegment;

/**
* This class is only for triggering code that doesn't really make sense to test and is here to achieve 100% code coverage.
* The reason is to catch potential problems during transpilation to lower versions of php.
*/
final class CoverageOnlyTest extends TestCase
{
public function testDefaultSegment(): void
{
$instance = new DefaultSegment(1, []);
self::assertEquals(1, $instance->getId());
}
}

0 comments on commit f4e841d

Please sign in to comment.