Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,21 @@ $nc->onError(static function (\Throwable $e): void {
});
```

#### Validation

Subjects and names are checked before anything is sent, following the rules of nats-server and nats.go:

- A subject is non-empty, without whitespace and empty tokens (a leading or trailing `.`, `..`): `Nats\Exception\InvalidSubject`.
`publish()`, `request()` and reply-to subjects must not contain the wildcards `*` and `>`, `subscribe()` and stream, filter and micro endpoint subjects
may contain `*` as a whole token and `>` as the last token. A wildcard inside a token, as in `foo*`, is a literal character.
- A queue group is non-empty and without whitespace: `Nats\Exception\InvalidSubject`.
- A stream, consumer or counter name is non-empty, at most 255 bytes, without `.`, `*`, `>`, `/`, `\`, whitespace and control characters: `Nats\Exception\InvalidName`.
- A priority group of a pull consumer matches `^[a-zA-Z0-9/_=-]{1,16}$`, as nats-server requires: `Nats\Exception\InvalidName`.
- A key-value or object store bucket name matches `^[a-zA-Z0-9_-]+$`: `Nats\Exception\InvalidBucketName`.
- A key-value key matches `^[-/_=.a-zA-Z0-9]+$` and is a valid subject; the keys to watch may contain wildcards: `Nats\Exception\InvalidKey`.
- An object name is non-empty: `Nats\Exception\ObjectIsInvalid`.
- A micro endpoint name matches `^[A-Za-z0-9_-]+$` (`Nats\Exception\InvalidName`), a group name is a subject without `>` (`Nats\Exception\InvalidSubject`).

## Nats JetStream

JetStream is the built-in NATS persistence system. The library provides both JetStream entity management (streams, consumers) and message publishing/consumption capabilities.
Expand Down Expand Up @@ -835,7 +850,8 @@ $srv = $nc->createService(new Micro\ServiceConfig('EchoService', '1.0.0'));

#### Service endpoints

After a service is created, endpoints can be added. By default, an endpoint is available via its name.
After a service is created, endpoints can be added. An endpoint name is an identifier matching `^[A-Za-z0-9_-]+$`, it is reported in the service info and stats.
The subject the endpoint listens on is set separately and defaults to the name.

```php
<?php
Expand All @@ -854,14 +870,14 @@ $nc = new Nats\Client(
$srv = $nc->createService(new Micro\ServiceConfig('EchoService', '1.0.0'));

$srv
->addEndpoint(new Micro\EndpointConfig('srv.echo'), static function (Micro\Request $request): void {
->addEndpoint(new Micro\EndpointConfig('echo', subject: 'srv.echo'), static function (Micro\Request $request): void {
$request->respond(new Micro\Response($request->data));
});

dump($nc->request('srv.echo', new Nats\Message('ping'))->message->payload);
```

If the subject for the endpoint is more complex (e.g., contains a `*` or `>`), the subject can be specified separately from the name.
The subject may contain wildcards: `*` as a whole token and `>` as the last token.

```php
<?php
Expand All @@ -880,7 +896,7 @@ $nc = new Nats\Client(
$srv = $nc->createService(new Micro\ServiceConfig('EchoService', '1.0.0'));

$srv
->addEndpoint(new Micro\EndpointConfig('srv.echo', subject: 'srv.echo.*'), static function (Micro\Request $request): void {
->addEndpoint(new Micro\EndpointConfig('echo', subject: 'srv.echo.*'), static function (Micro\Request $request): void {
$request->respond(new Micro\Response($request->subject));
});

Expand All @@ -890,6 +906,7 @@ dump($nc->request('srv.echo.x', new Nats\Message('ping'))->message->payload);
#### Groups

Endpoints can also be aggregated using groups. A group represents a common subject prefix used by all endpoints associated with it.
The prefix is prepended to the endpoint's subject (to its name, if no subject is set), the endpoint name stays the same.

```php
<?php
Expand Down
49 changes: 49 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Thesis\Nats\Internal\Rpc;
use Thesis\Nats\Internal\Subscription\Subscriber;
use Thesis\Nats\Internal\Subscription\SubscriptionHandler;
use Thesis\Nats\Internal\Validation\Validate;
use Thesis\Nats\JetStream\Delivery as JetStreamDelivery;
use Thesis\Nats\JetStream\Internal\Acks;
use Thesis\Nats\JetStream\Metadata;
Expand Down Expand Up @@ -115,6 +116,30 @@ public function publish(
Message $message = new Message(),
?string $replyTo = null,
?Cancellation $cancellation = null,
): void {
Validate::publishSubject($subject);

if ($replyTo !== null) {
Validate::publishSubject($replyTo);
}

$this->rawPublish($subject, $message, $replyTo, $cancellation);
}

/**
* Publishes without validating the subjects: the JetStream API embeds subject filters, wildcards included,
* into its request subjects, e.g. $JS.API.CONSUMER.CREATE.<stream>.<consumer>.<filter>.
*
* @internal
* @param non-empty-string $subject
* @param ?non-empty-string $replyTo
* @throws NatsException
*/
public function rawPublish(
string $subject,
Message $message = new Message(),
?string $replyTo = null,
?Cancellation $cancellation = null,
): void {
$connection = $this->connection($cancellation);

Expand Down Expand Up @@ -185,6 +210,12 @@ public function subscribe(
int $bufferSize = 1_000,
?Cancellation $cancellation = null,
): Subscription {
Validate::subscribeSubject($subject);

if ($queueGroup !== null) {
Validate::queueGroup($queueGroup);
}

$subscriptionId = $this->subscriptionIdGenerator->nextId();
$unsubscribe = $this->unsubscribe(...);

Expand Down Expand Up @@ -216,11 +247,29 @@ public function subscribe(

/**
* @param non-empty-string $subject
* @throws NatsException
*/
public function request(
string $subject,
Message $message = new Message(),
?Cancellation $cancellation = null,
): Delivery {
Validate::publishSubject($subject);

return $this->rawRequest($subject, $message, $cancellation);
}

/**
* Requests without validating the subject, see {@see rawPublish()}.
*
* @internal
* @param non-empty-string $subject
* @throws NatsException
*/
public function rawRequest(
string $subject,
Message $message = new Message(),
?Cancellation $cancellation = null,
): Delivery {
$subscribe = $this->subscribeCallback(...);
$this->rpc ??= async(function () use ($subscribe): Rpc\Handler {
Expand Down
20 changes: 20 additions & 0 deletions src/Exception/InvalidBucketName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Thesis\Nats\Exception;

use Thesis\Nats\NatsException;

/**
* Thrown before anything is sent when a key-value or object store bucket name is malformed.
*
* @api
*/
final class InvalidBucketName extends NatsException
{
public static function because(string $value, string $reason): self
{
return new self("Invalid bucket name '{$value}': {$reason}.");
}
}
20 changes: 20 additions & 0 deletions src/Exception/InvalidKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Thesis\Nats\Exception;

use Thesis\Nats\NatsException;

/**
* Thrown before anything is sent when a key-value key or key filter is malformed.
*
* @api
*/
final class InvalidKey extends NatsException
{
public static function because(string $value, string $reason): self
{
return new self("Invalid key '{$value}': {$reason}.");
}
}
20 changes: 20 additions & 0 deletions src/Exception/InvalidName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Thesis\Nats\Exception;

use Thesis\Nats\NatsException;

/**
* Thrown before anything is sent when a stream, consumer, counter or micro endpoint name is malformed.
*
* @api
*/
final class InvalidName extends NatsException
{
public static function because(string $value, string $reason): self
{
return new self("Invalid name '{$value}': {$reason}.");
}
}
17 changes: 15 additions & 2 deletions src/Exception/InvalidSubject.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@
use Thesis\Nats\NatsException;

/**
* Thrown before anything is sent when a subject or a queue group is malformed,
* and for the server's "-ERR 'Invalid Subject'".
*
* @api
*/
final class InvalidSubject extends NatsException
{
public function __construct(int $code = 0, ?\Throwable $previous = null)
public function __construct(string $message = 'Invalid Subject', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct('Invalid Subject', $code, $previous);
parent::__construct($message, $code, $previous);
}

public static function because(string $subject, string $reason): self
{
return new self("Invalid subject '{$subject}': {$reason}.");
}

public static function forQueueGroup(string $queueGroup, string $reason): self
{
return new self("Invalid queue group '{$queueGroup}': {$reason}.");
}
}
2 changes: 1 addition & 1 deletion src/Internal/Rpc/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public function request(
);

try {
$client->publish($subject, $message, $replyTo->subject, $cancellation);
$client->rawPublish($subject, $message, $replyTo->subject, $cancellation);

return $deferred->getFuture()->await($cancellation);
} finally {
Expand Down
Loading
Loading