Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/vendor/
|
||||
/.phpunit.*
|
||||
309
LICENSE
Normal file
309
LICENSE
Normal file
@@ -0,0 +1,309 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share
|
||||
and change it. By contrast, the GNU General Public License is intended to
|
||||
guarantee your freedom to share and change free software--to make sure the
|
||||
software is free for all its users. This General Public License applies to most
|
||||
of the Free Software Foundation's software and to any other program whose
|
||||
authors commit to using it. (Some other Free Software Foundation software is
|
||||
covered by the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our
|
||||
General Public Licenses are designed to make sure that you have the freedom to
|
||||
distribute copies of free software (and charge for this service if you wish),
|
||||
that you receive source code or can get it if you want it, that you can change
|
||||
the software or use pieces of it in new free programs; and that you know you can
|
||||
do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone to deny
|
||||
you these rights or to ask you to surrender the rights. These restrictions
|
||||
translate to certain responsibilities for you if you distribute copies of the
|
||||
software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a
|
||||
fee, you must give the recipients all the rights that you have. You must make
|
||||
sure that they, too, receive or can get the source code. And you must show them
|
||||
these terms so they know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and (2) offer
|
||||
you this license which gives you legal permission to copy, distribute and/or
|
||||
modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain that
|
||||
everyone understands that there is no warranty for this free software. If the
|
||||
software is modified by someone else and passed on, we want its recipients to
|
||||
know that what they have is not the original, so that any problems introduced by
|
||||
others will not reflect on the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents. We wish
|
||||
to avoid the danger that redistributors of a free program will individually
|
||||
obtain patent licenses, in effect making the program proprietary. To prevent
|
||||
this, we have made it clear that any patent must be licensed for everyone's free
|
||||
use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains a notice
|
||||
placed by the copyright holder saying it may be distributed under the terms of
|
||||
this General Public License. The "Program", below, refers to any such program or
|
||||
work, and a "work based on the Program" means either the Program or any
|
||||
derivative work under copyright law: that is to say, a work containing the
|
||||
Program or a portion of it, either verbatim or with modifications and/or
|
||||
translated into another language. (Hereinafter, translation is included without
|
||||
limitation in the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not covered by
|
||||
this License; they are outside its scope. The act of running the Program is not
|
||||
restricted, and the output from the Program is covered only if its contents
|
||||
constitute a work based on the Program (independent of having been made by
|
||||
running the Program). Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's source code as
|
||||
you receive it, in any medium, provided that you conspicuously and appropriately
|
||||
publish on each copy an appropriate copyright notice and disclaimer of warranty;
|
||||
keep intact all the notices that refer to this License and to the absence of any
|
||||
warranty; and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you may at
|
||||
your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion of it, thus
|
||||
forming a work based on the Program, and copy and distribute such modifications
|
||||
or work under the terms of Section 1 above, provided that you also meet all of
|
||||
these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices stating
|
||||
that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in whole or
|
||||
in part contains or is derived from the Program or any part thereof, to be
|
||||
licensed as a whole at no charge to all third parties under the terms of this
|
||||
License.
|
||||
|
||||
c) If the modified program normally reads commands interactively when run,
|
||||
you must cause it, when started running for such interactive use in the most
|
||||
ordinary way, to print or display an announcement including an appropriate
|
||||
copyright notice and a notice that there is no warranty (or else, saying that
|
||||
you provide a warranty) and that users may redistribute the program under these
|
||||
conditions, and telling the user how to view a copy of this License. (Exception:
|
||||
if the Program itself is interactive but does not normally print such an
|
||||
announcement, your work based on the Program is not required to print an
|
||||
announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable
|
||||
sections of that work are not derived from the Program, and can be reasonably
|
||||
considered independent and separate works in themselves, then this License, and
|
||||
its terms, do not apply to those sections when you distribute them as separate
|
||||
works. But when you distribute the same sections as part of a whole which is a
|
||||
work based on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the entire whole,
|
||||
and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your
|
||||
rights to work written entirely by you; rather, the intent is to exercise the
|
||||
right to control the distribution of derivative or collective works based on the
|
||||
Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program with the
|
||||
Program (or with a work based on the Program) on a volume of a storage or
|
||||
distribution medium does not bring the other work under the scope of this
|
||||
License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it, under Section
|
||||
2) in object code or executable form under the terms of Sections 1 and 2 above
|
||||
provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable source
|
||||
code, which must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three years, to
|
||||
give any third party, for a charge no more than your cost of physically
|
||||
performing source distribution, a complete machine-readable copy of the
|
||||
corresponding source code, to be distributed under the terms of Sections 1 and 2
|
||||
above on a medium customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer to
|
||||
distribute corresponding source code. (This alternative is allowed only for
|
||||
noncommercial distribution and only if you received the program in object code
|
||||
or executable form with such an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for making
|
||||
modifications to it. For an executable work, complete source code means all the
|
||||
source code for all modules it contains, plus any associated interface
|
||||
definition files, plus the scripts used to control compilation and installation
|
||||
of the executable. However, as a special exception, the source code distributed
|
||||
need not include anything that is normally distributed (in either source or
|
||||
binary form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component itself
|
||||
accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the source code
|
||||
from the same place counts as distribution of the source code, even though third
|
||||
parties are not compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program except as
|
||||
expressly provided under this License. Any attempt otherwise to copy, modify,
|
||||
sublicense or distribute the Program is void, and will automatically terminate
|
||||
your rights under this License. However, parties who have received copies, or
|
||||
rights, from you under this License will not have their licenses terminated so
|
||||
long as such parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not signed it.
|
||||
However, nothing else grants you permission to modify or distribute the Program
|
||||
or its derivative works. These actions are prohibited by law if you do not
|
||||
accept this License. Therefore, by modifying or distributing the Program (or any
|
||||
work based on the Program), you indicate your acceptance of this License to do
|
||||
so, and all its terms and conditions for copying, distributing or modifying the
|
||||
Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the Program),
|
||||
the recipient automatically receives a license from the original licensor to
|
||||
copy, distribute or modify the Program subject to these terms and conditions.
|
||||
You may not impose any further restrictions on the recipients' exercise of the
|
||||
rights granted herein. You are not responsible for enforcing compliance by third
|
||||
parties to this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent infringement
|
||||
or for any other reason (not limited to patent issues), conditions are imposed
|
||||
on you (whether by court order, agreement or otherwise) that contradict the
|
||||
conditions of this License, they do not excuse you from the conditions of this
|
||||
License. If you cannot distribute so as to satisfy simultaneously your
|
||||
obligations under this License and any other pertinent obligations, then as a
|
||||
consequence you may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by all those
|
||||
who receive copies directly or indirectly through you, then the only way you
|
||||
could satisfy both it and this License would be to refrain entirely from
|
||||
distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply and the
|
||||
section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents or
|
||||
other property right claims or to contest validity of any such claims; this
|
||||
section has the sole purpose of protecting the integrity of the free software
|
||||
distribution system, which is implemented by public license practices. Many
|
||||
people have made generous contributions to the wide range of software
|
||||
distributed through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing to
|
||||
distribute software through any other system and a licensee cannot impose that
|
||||
choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a
|
||||
consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in certain
|
||||
countries either by patents or by copyrighted interfaces, the original copyright
|
||||
holder who places the Program under this License may add an explicit
|
||||
geographical distribution limitation excluding those countries, so that
|
||||
distribution is permitted only in or among countries not thus excluded. In such
|
||||
case, this License incorporates the limitation as if written in the body of this
|
||||
License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions of the
|
||||
General Public License from time to time. Such new versions will be similar in
|
||||
spirit to the present version, but may differ in detail to address new problems
|
||||
or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies
|
||||
a version number of this License which applies to it and "any later version",
|
||||
you have the option of following the terms and conditions either of that version
|
||||
or of any later version published by the Free Software Foundation. If the
|
||||
Program does not specify a version number of this License, you may choose any
|
||||
version ever published by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free programs
|
||||
whose distribution conditions are different, write to the author to ask for
|
||||
permission. For software which is copyrighted by the Free Software Foundation,
|
||||
write to the Free Software Foundation; we sometimes make exceptions for this.
|
||||
Our decision will be guided by the two goals of preserving the free status of
|
||||
all derivatives of our free software and of promoting the sharing and reuse of
|
||||
software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE
|
||||
PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED
|
||||
IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS
|
||||
IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT
|
||||
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
|
||||
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
|
||||
PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
||||
SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
||||
TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
|
||||
THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use
|
||||
to the public, the best way to achieve this is to make it free software which
|
||||
everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach
|
||||
them to the start of each source file to most effectively convey the exclusion
|
||||
of warranty; and each file should have at least the "copyright" line and a
|
||||
pointer to where the full notice is found.
|
||||
|
||||
one line to give the program's name and an idea of what it does. Copyright
|
||||
(C) yyyy name of author
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by the Free
|
||||
Software Foundation; either version 2 of the License, or (at your option) any
|
||||
later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information
|
||||
on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this when it
|
||||
starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
|
||||
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
|
||||
and you are welcome to redistribute it under certain conditions; type `show c'
|
||||
for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may be
|
||||
called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. Here is
|
||||
a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Protocol Base for ReactPHP
|
||||
|
||||
Use this library to build protocols. It doesn't do any single protocol to any greater
|
||||
extent, but it provides callbacks and scaffolding for you to build and stream most
|
||||
protocol data.
|
||||
|
||||
## About the protocols
|
||||
|
||||
This library is intended to be generic, so only protocols that have no external
|
||||
dependencies will be included.
|
||||
|
||||
## Using
|
||||
|
||||
### Protocols
|
||||
|
||||
```php
|
||||
// Basic line-oriented protocol, think shell or SMTP
|
||||
$proto = new LineProtocol($stream, [
|
||||
'lineSeparator' => "\n",
|
||||
'fieldSeparator' => " ",
|
||||
'fieldQuote' => '"',
|
||||
]);
|
||||
|
||||
// encode your frames and send it however you desire
|
||||
$data = $proto->packFrame([ "mv", "foo.txt", "bar.txt" ]);
|
||||
// → mv foo.txt bar.txt\n
|
||||
|
||||
// Or use JSON if you so desire
|
||||
$proto = new JsonProtocol($stream, [
|
||||
'frameSeparator' => "\0",
|
||||
]);
|
||||
$data = $proto->packFrame([ 'foo' => 'bar' ]);
|
||||
// → {"foo":"bar"}\0
|
||||
```
|
||||
|
||||
Unpacking works as expected:
|
||||
|
||||
```php
|
||||
$proto = new LineProtocol($stream, [
|
||||
'lineSeparator' => "\n",
|
||||
'fieldSeparator' => " ",
|
||||
'fieldQuote' => '"',
|
||||
]);
|
||||
|
||||
// encode your frames and send it however you desire
|
||||
$cmdline = $proto->unpackFrame("mv foo.txt bar.txt\n" ]);
|
||||
// → [ "mv", "foo.txt", "bar.txt" ]
|
||||
```
|
||||
|
||||
### Protocols with ProtocolStream
|
||||
|
||||
ProtocolStream combines a ProtocolInterface and a DuplexStreamInterface into
|
||||
one package. It will emit an `message` event when a new frame has been
|
||||
decoded, an `error` event if something goes wrong, and an `overflow` event
|
||||
if the buffer overflows a configured max size.
|
||||
|
||||
```php
|
||||
$stream = new ProtocolStream($proto, $someduplexstream);
|
||||
$stream->on("message", function (array $message) {
|
||||
// ...
|
||||
});
|
||||
$stream->send([ 'data' => 'here' ]);
|
||||
```
|
||||
24
composer.json
Normal file
24
composer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "noccylabs/react-protocol",
|
||||
"description": "Generic wire protocols for ReactPHP applications",
|
||||
"type": "library",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"NoccyLabs\\React\\Protocol\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christopher Vagnetoft",
|
||||
"email": "labs@noccy.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"react/stream": "^1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^13.0",
|
||||
"phpstan/phpstan": "^2.1"
|
||||
}
|
||||
}
|
||||
2049
composer.lock
generated
Normal file
2049
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
doc/json-protos.md
Normal file
12
doc/json-protos.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# JSON Protocols
|
||||
|
||||
The JSON protocol simply serializes
|
||||
|
||||
|
||||
|
||||
|
||||
## Available sub-formats
|
||||
|
||||
### JsonRpcProtocol
|
||||
|
||||
### NativeMessagingProtocol
|
||||
34
doc/pack-ref.md
Normal file
34
doc/pack-ref.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# PHP pack/unpack reference
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------------------------------------------ |
|
||||
| a | NUL-padded string |
|
||||
| A | SPACE-padded string |
|
||||
| h | Hex string, low nibble first |
|
||||
| H | Hex string, high nibble first |
|
||||
| c | signed char |
|
||||
| C | unsigned char |
|
||||
| s | signed short (always 16 bit, machine byte order) |
|
||||
| S | unsigned short (always 16 bit, machine byte order) |
|
||||
| n | unsigned short (always 16 bit, big endian byte order) |
|
||||
| v | unsigned short (always 16 bit, little endian byte order) |
|
||||
| i | signed integer (machine dependent size and byte order) |
|
||||
| I | unsigned integer (machine dependent size and byte order) |
|
||||
| l | signed long (always 32 bit, machine byte order) |
|
||||
| L | unsigned long (always 32 bit, machine byte order) |
|
||||
| N | unsigned long (always 32 bit, big endian byte order) |
|
||||
| V | unsigned long (always 32 bit, little endian byte order) |
|
||||
| q | signed long long (always 64 bit, machine byte order) |
|
||||
| Q | unsigned long long (always 64 bit, machine byte order) |
|
||||
| J | unsigned long long (always 64 bit, big endian byte order) |
|
||||
| P | unsigned long long (always 64 bit, little endian byte order) |
|
||||
| f | float (machine dependent size and representation) |
|
||||
| g | float (machine dependent size, little endian byte order) |
|
||||
| G | float (machine dependent size, big endian byte order) |
|
||||
| d | double (machine dependent size and representation) |
|
||||
| e | double (machine dependent size, little endian byte order) |
|
||||
| E | double (machine dependent size, big endian byte order) |
|
||||
| x | NUL byte |
|
||||
| X | Back up one byte |
|
||||
| Z | NUL-terminated (ASCIIZ) string, will be NUL padded |
|
||||
| @ | NUL-fill to absolute position |
|
||||
48
doc/recipes.md
Normal file
48
doc/recipes.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Recipes
|
||||
|
||||
These are some suggestions and ideas for when building your own protocols using the
|
||||
various protocol classes.
|
||||
|
||||
## Marshalling objects
|
||||
|
||||
You can use the beforePackCb and afterUnpackCb to serialize and unserialize parameters
|
||||
as desired. Consider the security implications of this before you do anything stupid
|
||||
though, as a spoofed frame could have unexpected consequences when unserialized.
|
||||
|
||||
```php
|
||||
$proto = new JsonProtocol(
|
||||
beforePackCb: function (array $frame): array {
|
||||
$frame['obj'] = serialize($frame['obj']);
|
||||
return $frame;
|
||||
},
|
||||
afterUnpackCb: function (array $frame): array {
|
||||
$frame['obj'] = unserialize($frame['obj]);
|
||||
return $frame;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Building line-based messages
|
||||
|
||||
By using the `beforePackCb` callback, you can take messages in a structured format
|
||||
and return them as a list for line-based protocols. You can do the same in reverse
|
||||
with the `afterUnpackCb` callback.
|
||||
|
||||
```php
|
||||
$proto = new LineProtocol(
|
||||
beforePackCb: function (array $frame): array {
|
||||
switch ($frame['cmd']) {
|
||||
case 'search':
|
||||
return [ "search", $frame['query'] ];
|
||||
// ..
|
||||
}
|
||||
},
|
||||
afterUnpackCb: function (array $frame): array {
|
||||
switch ($frame[0]) {
|
||||
case 'search':
|
||||
return [ 'cmd' => $frame[0], 'query' => $frame[1] ];
|
||||
// ..
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
35
doc/text-protos.md
Normal file
35
doc/text-protos.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Text/Line Protocols
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Available sub-protocols
|
||||
|
||||
### HttpLikeProtocol
|
||||
|
||||
This is as the name says a HTTP-like parser. It can be used for HTTP, STOMP
|
||||
and more. All messages are expected to follow the following style:
|
||||
|
||||
```text
|
||||
query-line <$lineSeparator>
|
||||
header-line* <$lineSeparator>
|
||||
<$lineSeparator>
|
||||
optional-body
|
||||
```
|
||||
|
||||
Line separators default to `\n`, but can be set to any sequence or the value `true`
|
||||
to attempt to automatically detect the endings used.
|
||||
|
||||
The presence of a body is determined by consulting the headers. The `$contentLengthHeader`
|
||||
constructor parameter defaults to 'content-length', and can be set to null to disable parsing
|
||||
of bodies entirely.
|
||||
|
||||
```php
|
||||
$proto = new HttpLikeProtocol(
|
||||
queryFormat: '{command} {path}',
|
||||
lineSeparator: true,
|
||||
)
|
||||
|
||||
|
||||
```
|
||||
13
examples/jsonproto.php
Normal file
13
examples/jsonproto.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||
|
||||
|
||||
$proto = new JsonProtocol(
|
||||
frameSeparator: "\n",
|
||||
prependSizeBytes: 0,
|
||||
);
|
||||
|
||||
echo $proto->packFrame([ 'hello'=>'world', 'answer'=>42 ]);
|
||||
23
examples/jsonprotostream.php
Normal file
23
examples/jsonprotostream.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||
use NoccyLabs\React\Protocol\ProtocolStream;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
use React\Stream\WritableResourceStream;
|
||||
|
||||
$rs = new ThroughStream();
|
||||
$ws = new WritableResourceStream(STDOUT);
|
||||
$stream = new CompositeStream($rs,$ws);
|
||||
|
||||
$proto = new JsonProtocol(
|
||||
frameSeparator: "\n",
|
||||
prependSizeBytes: 0,
|
||||
);
|
||||
$stream = new ProtocolStream($stream, $proto);
|
||||
|
||||
$stream->send([ 'hello' => 'world' ]);
|
||||
$stream->send([ 'foo' => 'bar', 'answer' => 42 ]);
|
||||
|
||||
9
examples/lineproto.php
Normal file
9
examples/lineproto.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||
|
||||
$proto = new LineProtocol();
|
||||
|
||||
echo $proto->packFrame([ 'hello', 'world' ]);
|
||||
55
examples/upgrading.php
Normal file
55
examples/upgrading.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This example demonstrates upgrading a ProtocolStream to use a different protocol
|
||||
* than initially configured with.
|
||||
*
|
||||
* Run the example and try entering some things such as:
|
||||
* hello world
|
||||
* foo "bar baz"
|
||||
* and finally:
|
||||
* upgrade
|
||||
* After the upgrade command, it will only respond to JSON data:
|
||||
* {"foo":"bar"}
|
||||
*
|
||||
*/
|
||||
|
||||
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||
use NoccyLabs\React\Protocol\ProtocolStream;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use React\Stream\WritableResourceStream;
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
$stdin = new ReadableResourceStream(STDIN);
|
||||
$stdout = new WritableResourceStream(STDOUT);
|
||||
$stdio = new CompositeStream($stdin, $stdout);
|
||||
|
||||
$textProto = new LineProtocol(
|
||||
beforePackCb: function (array $msg): array {
|
||||
$out = [];
|
||||
$cmd = array_shift($msg);
|
||||
$out = [ $cmd, ...array_map("json_encode", $msg) ];
|
||||
return $out;
|
||||
}
|
||||
);
|
||||
$jsonProto = new JsonProtocol(
|
||||
frameSeparator: "\n"
|
||||
);
|
||||
|
||||
$stream = new ProtocolStream($stdio, $textProto);
|
||||
$stream->on("message", function (array $msg) use ($stream, $jsonProto) {
|
||||
if (array_is_list($msg)) {
|
||||
if (reset($msg) == 'upgrade') {
|
||||
$stream->send([ 'OK', 'Upgrading to JSON protocol']);
|
||||
$stream->upgrade($jsonProto);
|
||||
$stream->send([ 'info' => 'Now using JSON proto' ]);
|
||||
} else {
|
||||
$stream->send([ 'echo', json_encode($msg)]);
|
||||
}
|
||||
} else {
|
||||
$stream->send([ 'echo' => $msg ]);
|
||||
}
|
||||
});
|
||||
5
phpstan.neon
Normal file
5
phpstan.neon
Normal file
@@ -0,0 +1,5 @@
|
||||
parameters:
|
||||
level: 5
|
||||
paths:
|
||||
- src
|
||||
|
||||
25
phpunit.xml
Normal file
25
phpunit.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
requireCoverageMetadata="true"
|
||||
beStrictAboutCoverageMetadata="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnPhpunitDeprecations="true"
|
||||
failOnPhpunitDeprecation="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
142
src/Binary/BinaryProtocol.php
Normal file
142
src/Binary/BinaryProtocol.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Binary;
|
||||
|
||||
use Closure;
|
||||
use NoccyLabs\React\Protocol\ProtocolException;
|
||||
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||
|
||||
class BinaryProtocol implements ProtocolInterface
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $frameSeparator = "",
|
||||
public readonly int $prependSizeBytes = 0,
|
||||
public readonly string $prependSizeEndian = 'l',
|
||||
public readonly ?string $compression = null,
|
||||
private readonly ?Closure $beforePackCb = null,
|
||||
private readonly ?Closure $afterPackCb = null,
|
||||
private readonly ?Closure $beforeUnpackCb = null,
|
||||
private readonly ?Closure $afterUnpackCb = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function packFrame(array $frame): string
|
||||
{
|
||||
if (is_callable($this->beforePackCb))
|
||||
$frame = call_user_func($this->beforePackCb, $frame);
|
||||
|
||||
$data = $frame['payload'];
|
||||
|
||||
// append separator
|
||||
$data = $data . $this->frameSeparator;
|
||||
|
||||
// prepend size
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
$data = $this->packSizeBytes(strlen($data)) . $data;
|
||||
}
|
||||
|
||||
if (is_callable($this->afterPackCb))
|
||||
$data = call_user_func($this->afterPackCb, $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function unpackFrame(string $data): array
|
||||
{
|
||||
if (is_callable($this->beforeUnpackCb))
|
||||
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
if ($this->prependSizeBytes > strlen($data)) {
|
||||
// not enough data to parse size...
|
||||
throw new ProtocolException("BinaryProtocol: Not enough data to parse size");
|
||||
}
|
||||
$len = $this->unpackSizeBytes($data);
|
||||
if ($len <= 0) {
|
||||
// unparsable?
|
||||
throw new ProtocolException("BinaryProtocol: Invalid size decoded from frame");
|
||||
}
|
||||
if ($len > strlen($data) - $this->prependSizeBytes) {
|
||||
// insufficient data
|
||||
throw new ProtocolException("BinaryProtocol: Insufficient data for unpacking (want {$len} but got ".(strlen($data)).")");
|
||||
}
|
||||
$data = substr($data, $this->prependSizeBytes);
|
||||
}
|
||||
|
||||
if ($this->frameSeparator) {
|
||||
$data = substr($data, 0, -strlen($this->frameSeparator));
|
||||
}
|
||||
|
||||
$frame = [ 'payload' => $data ];
|
||||
// echo "[{$data}]"; var_dump($frame);
|
||||
if (!$frame) {
|
||||
// invalid json
|
||||
throw new ProtocolException("Unparsable frame received: {$data}");
|
||||
}
|
||||
|
||||
if (is_callable($this->afterUnpackCb))
|
||||
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
public function consumeFrame(string &$data): ?array
|
||||
{
|
||||
// check for $this->prependSizeBytes
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
$len = $this->unpackSizeBytes($data);
|
||||
// if size is greater than data (i.e. incomplete)
|
||||
if ($len > strlen($data) - $this->prependSizeBytes) return null;
|
||||
$p = $len;
|
||||
// $data = substr($data, $this->prependSizeBytes);
|
||||
} elseif ($this->frameSeparator) {
|
||||
// check for $this->frameSeparator
|
||||
$p = strpos($data, $this->frameSeparator);
|
||||
if ($p === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||
$data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||
|
||||
return $this->unpackFrame($frame);
|
||||
}
|
||||
|
||||
public function packSizeBytes(int $size): string
|
||||
{
|
||||
$endian = function($b,$l,$s) {
|
||||
return match ($this->prependSizeEndian) {
|
||||
'b' => $b,
|
||||
'l' => $l,
|
||||
default => $s
|
||||
};
|
||||
};
|
||||
return match ($this->prependSizeBytes) {
|
||||
1 => pack($endian("C","C","C"), $size),
|
||||
2 => pack($endian("n","v","S"), $size),
|
||||
4 => pack($endian("N","V","L"), $size),
|
||||
default => throw new ProtocolException("BinaryProtocol: Invalid message size length")
|
||||
};
|
||||
}
|
||||
|
||||
public function unpackSizeBytes(string $data): int
|
||||
{
|
||||
$bytes = substr($data, 0, $this->prependSizeBytes);
|
||||
$endian = function($b,$l,$s) {
|
||||
return match ($this->prependSizeEndian) {
|
||||
'b' => $b,
|
||||
'l' => $l,
|
||||
default => $s
|
||||
};
|
||||
};
|
||||
return match ($this->prependSizeBytes) {
|
||||
1 => unpack($endian("C","C","C")."len", $bytes)['len'],
|
||||
2 => unpack($endian("n","v","S")."len", $bytes)['len'],
|
||||
4 => unpack($endian("N","V","L")."len", $bytes)['len'],
|
||||
default => throw new ProtocolException("BinaryProtocol: Invalid message size length")
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
148
src/Json/JsonProtocol.php
Normal file
148
src/Json/JsonProtocol.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||
use Closure;
|
||||
use NoccyLabs\React\Protocol\ProtocolException;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class JsonProtocol implements ProtocolInterface
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $frameSeparator = "\0",
|
||||
public readonly int $prependSizeBytes = 0,
|
||||
public readonly string $prependSizeEndian = 'l',
|
||||
public readonly bool $unescapedSlashes = true,
|
||||
private readonly ?Closure $beforePackCb = null,
|
||||
private readonly ?Closure $afterPackCb = null,
|
||||
private readonly ?Closure $beforeUnpackCb = null,
|
||||
private readonly ?Closure $afterUnpackCb = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function packFrame(array $frame): string
|
||||
{
|
||||
if (is_callable($this->beforePackCb))
|
||||
$frame = call_user_func($this->beforePackCb, $frame);
|
||||
|
||||
$jsonOpts = ($this->unescapedSlashes?\JSON_UNESCAPED_SLASHES:0);
|
||||
$data = @json_encode($frame, $jsonOpts);
|
||||
if (!$data) {
|
||||
throw new ProtocolException("JsonProtocol: Empty data after serializing");
|
||||
}
|
||||
|
||||
// append separator
|
||||
$data = $data . $this->frameSeparator;
|
||||
|
||||
// prepend size
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
$data = $this->packSizeBytes(strlen($data)) . $data;
|
||||
}
|
||||
|
||||
if (is_callable($this->afterPackCb))
|
||||
$data = call_user_func($this->afterPackCb, $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function unpackFrame(string $data): array
|
||||
{
|
||||
if (is_callable($this->beforeUnpackCb))
|
||||
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
if ($this->prependSizeBytes > strlen($data)) {
|
||||
// not enough data to parse size...
|
||||
throw new ProtocolException("JsonProtocol: Not enough data to parse size");
|
||||
}
|
||||
$len = $this->unpackSizeBytes($data);
|
||||
if ($len <= 0) {
|
||||
// unparsable?
|
||||
throw new ProtocolException("JsonProtocol: Invalid size decoded from frame");
|
||||
}
|
||||
if ($len > strlen($data) - $this->prependSizeBytes) {
|
||||
// insufficient data
|
||||
throw new ProtocolException("JsonProtocol: Insufficient data for unpacking");
|
||||
}
|
||||
$data = substr($data, $this->prependSizeBytes);
|
||||
}
|
||||
|
||||
if ($this->frameSeparator) {
|
||||
$data = substr($data, 0, -strlen($this->frameSeparator));
|
||||
}
|
||||
|
||||
$frame = @json_decode($data, true);
|
||||
// echo "[{$data}]"; var_dump($frame);
|
||||
if (!$frame) {
|
||||
// invalid json
|
||||
throw new ProtocolException("Unparsable frame received: {$data}");
|
||||
}
|
||||
|
||||
if (is_callable($this->afterUnpackCb))
|
||||
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
public function consumeFrame(string &$data): ?array
|
||||
{
|
||||
// check for $this->prependSizeBytes
|
||||
if ($this->prependSizeBytes > 0) {
|
||||
$len = $this->unpackSizeBytes($data);
|
||||
// if size is greater than data (i.e. incomplete)
|
||||
if ($len > strlen($data) - $this->prependSizeBytes) return null;
|
||||
$p = $len;
|
||||
// $data = substr($data, $this->prependSizeBytes);
|
||||
} elseif ($this->frameSeparator) {
|
||||
// check for $this->frameSeparator
|
||||
$p = strpos($data, $this->frameSeparator);
|
||||
if ($p === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||
$data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||
|
||||
return $this->unpackFrame($frame);
|
||||
}
|
||||
|
||||
public function packSizeBytes(int $size): string
|
||||
{
|
||||
$endian = function($b,$l,$s) {
|
||||
return match ($this->prependSizeEndian) {
|
||||
'b' => $b,
|
||||
'l' => $l,
|
||||
default => $s
|
||||
};
|
||||
};
|
||||
return match ($this->prependSizeBytes) {
|
||||
1 => pack($endian("C","C","C"), $size),
|
||||
2 => pack($endian("n","v","S"), $size),
|
||||
4 => pack($endian("N","V","L"), $size),
|
||||
default => throw new ProtocolException("JsonProtocol: Invalid message size length")
|
||||
};
|
||||
}
|
||||
|
||||
public function unpackSizeBytes(string $data): int
|
||||
{
|
||||
$bytes = substr($data, 0, $this->prependSizeBytes);
|
||||
$endian = function($b,$l,$s) {
|
||||
return match ($this->prependSizeEndian) {
|
||||
'b' => $b,
|
||||
'l' => $l,
|
||||
default => $s
|
||||
};
|
||||
};
|
||||
return match ($this->prependSizeBytes) {
|
||||
1 => unpack($endian("C","C","C")."len", $bytes)['len'],
|
||||
2 => unpack($endian("n","v","S")."len", $bytes)['len'],
|
||||
4 => unpack($endian("N","V","L")."len", $bytes)['len'],
|
||||
default => throw new ProtocolException("JsonProtocol: Invalid message size length")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
43
src/Json/JsonRpcProtocol.php
Normal file
43
src/Json/JsonRpcProtocol.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use NoccyLabs\React\Protocol\ProtocolException;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
/**
|
||||
* Implementation of the JSON-RPC base protocol.
|
||||
*
|
||||
*
|
||||
*/
|
||||
class JsonRpcProtocol extends JsonProtocol
|
||||
{
|
||||
public function __construct(
|
||||
string $frameSeparator = "\n",
|
||||
)
|
||||
{
|
||||
parent::__construct(
|
||||
frameSeparator: $frameSeparator,
|
||||
prependSizeBytes: 0,
|
||||
unescapedSlashes: true,
|
||||
beforePackCb: $this->beforePack(...),
|
||||
afterPackCb: null,
|
||||
beforeUnpackCb: null,
|
||||
afterUnpackCb: $this->afterUnpack(...)
|
||||
);
|
||||
}
|
||||
|
||||
private function beforePack(array $frame): array
|
||||
{
|
||||
$frame['jsonrpc'] ??= "2.0";
|
||||
if (!(isset($frame['method']) || isset($frame['result']))) {
|
||||
throw new ProtocolException("JsonRpcProtocol: Either method or result key must be present");
|
||||
}
|
||||
return $frame;
|
||||
}
|
||||
|
||||
private function afterUnpack(array $frame): array
|
||||
{
|
||||
return $frame;
|
||||
}
|
||||
}
|
||||
28
src/Json/NativeMessagingProtocol.php
Normal file
28
src/Json/NativeMessagingProtocol.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
/**
|
||||
* Implementation of the common browser Native Messaging Protocol, used to
|
||||
* communicate with a browser script from local processes.
|
||||
*
|
||||
* For more see:
|
||||
* - https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging
|
||||
* - https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
|
||||
*/
|
||||
class NativeMessagingProtocol extends JsonProtocol
|
||||
{
|
||||
public function __construct(
|
||||
)
|
||||
{
|
||||
parent::__construct(
|
||||
frameSeparator: '',
|
||||
prependSizeBytes: 4,
|
||||
prependSizeEndian: 's',
|
||||
unescapedSlashes: true,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
87
src/Line/HttpLikeProtocol.php
Normal file
87
src/Line/HttpLikeProtocol.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Line;
|
||||
|
||||
use Closure;
|
||||
use NoccyLabs\React\Protocol\ProtocolException;
|
||||
|
||||
class HttpLikeProtocol extends LineProtocol
|
||||
{
|
||||
private ?int $wantedPayloadSize = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $lineSeparator = "\n",
|
||||
private readonly bool $capturePayloads = false,
|
||||
private readonly ?string $payloadLengthHeader = null,
|
||||
private readonly ?Closure $beforePackCb = null,
|
||||
private readonly ?Closure $afterPackCb = null,
|
||||
private readonly ?Closure $beforeUnpackCb = null,
|
||||
private readonly ?Closure $afterUnpackCb = null
|
||||
)
|
||||
{
|
||||
return parent::__construct(
|
||||
lineBreak: str_repeat($lineSeparator, 2),
|
||||
);
|
||||
}
|
||||
|
||||
public function packFrame(array $frame): string
|
||||
{
|
||||
if (is_callable($this->beforePackCb))
|
||||
$frame = call_user_func($this->beforePackCb, $frame);
|
||||
|
||||
$headers = [];
|
||||
foreach ($frame['headers']??[] as $header => $values) {
|
||||
foreach ($values as $value) {
|
||||
$headers[] = sprintf("%s: %s", $header, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable($this->afterPackCb))
|
||||
$frame = call_user_func($this->afterPackCb, $frame);
|
||||
|
||||
return join($this->lineSeparator, [
|
||||
$frame['query'],
|
||||
...$headers,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function unpackFrame(string $data): array
|
||||
{
|
||||
if (is_callable($this->beforeUnpackCb))
|
||||
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||
|
||||
$lines = explode($this->lineSeparator, $data);
|
||||
$query = array_shift($lines);
|
||||
$headers = [];
|
||||
foreach ($lines as $line) {
|
||||
if (!trim($line) && $line == end($lines)) continue;
|
||||
$line = trim($line);
|
||||
if (!str_contains($line, ":")) {
|
||||
throw new ProtocolException("Invalid header line: {$line}");
|
||||
}
|
||||
[$header, $value] = array_map(trim(...), explode(":", $line, 2));
|
||||
if (!array_key_exists($header, $headers)) $headers[$header] = [];
|
||||
$headers[$header][] = $value;
|
||||
}
|
||||
|
||||
$frame = [
|
||||
'query' => $query,
|
||||
'headers' => $headers,
|
||||
];
|
||||
|
||||
if (is_callable($this->afterUnpackCb))
|
||||
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||
|
||||
return $frame;
|
||||
}
|
||||
|
||||
public function consumeFrame(string &$data): ?array
|
||||
{
|
||||
if ($this->wantedPayloadSize > 0) {
|
||||
// consume and emit a "payload" event
|
||||
}
|
||||
return parent::consumeFrame($data);
|
||||
}
|
||||
}
|
||||
64
src/Line/LineProtocol.php
Normal file
64
src/Line/LineProtocol.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Line;
|
||||
|
||||
use Closure;
|
||||
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||
|
||||
class LineProtocol implements ProtocolInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $lineBreak = "\n",
|
||||
private readonly bool $quoteStrings = false,
|
||||
private readonly bool $escapeSpecial = false,
|
||||
private readonly ?Closure $beforePackCb = null,
|
||||
private readonly ?Closure $afterPackCb = null,
|
||||
private readonly ?Closure $beforeUnpackCb = null,
|
||||
private readonly ?Closure $afterUnpackCb = null,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function packFrame(array $frame): string
|
||||
{
|
||||
if (is_callable($this->beforePackCb))
|
||||
$frame = call_user_func($this->beforePackCb, $frame);
|
||||
|
||||
if ($this->escapeSpecial) {
|
||||
$frame = array_map(quotemeta(...), $frame);
|
||||
}
|
||||
|
||||
if ($this->quoteStrings) {
|
||||
$frame = array_map(fn($v) => is_string($v) ? ('"'.str_replace('"',"\\\"",$v).'"') : $v, $frame);
|
||||
}
|
||||
|
||||
$data = join(" ", $frame);
|
||||
$data .= $this->lineBreak;
|
||||
|
||||
if (is_callable($this->afterPackCb))
|
||||
$data = call_user_func($this->afterPackCb, $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function unpackFrame(string $data): array
|
||||
{
|
||||
return str_getcsv($data, ' ', '"', "\\");
|
||||
}
|
||||
|
||||
public function consumeFrame(string &$data): ?array
|
||||
{
|
||||
// check for $this->lineBreak
|
||||
$p = strpos($data, $this->lineBreak);
|
||||
if ($p === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// break on separator
|
||||
$frame = substr($data, 0, $p);
|
||||
$data = substr($data, $p + strlen($this->lineBreak));
|
||||
|
||||
return $this->unpackFrame($frame);
|
||||
}
|
||||
}
|
||||
10
src/ProtocolException.php
Normal file
10
src/ProtocolException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ProtocolException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
50
src/ProtocolInterface.php
Normal file
50
src/ProtocolInterface.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol;
|
||||
|
||||
interface ProtocolInterface
|
||||
{
|
||||
/**
|
||||
* Pack a frame, turning an array of data into the frame format.
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function packFrame(array $data): string;
|
||||
|
||||
/**
|
||||
* Unpack a frame, turning a string or binary blob into an array of data.
|
||||
*
|
||||
* @param string $data
|
||||
* @param array
|
||||
*/
|
||||
public function unpackFrame(string $data): array;
|
||||
|
||||
/**
|
||||
* Unpacks a frame from the buffer if possible. If a frame is unpacked, the
|
||||
* consumed bytes will be removed from the head of $data.
|
||||
*
|
||||
* @param string $data (byref)
|
||||
* @return array|null
|
||||
*/
|
||||
public function consumeFrame(string &$data): ?array;
|
||||
|
||||
/**
|
||||
* Peek at the buffer and determine if we can consume a frame.
|
||||
*
|
||||
* If peekFrame returns true, calling consumeFrame() should return the frame
|
||||
* detected by peekFrame(). The function may therefore prepare a frame to be
|
||||
* returned immediately on the next call to consumeFrame(). This also means
|
||||
* that you should NEVER call peekFrame() without intending to call
|
||||
* consumeFrame() immediately on a true result.
|
||||
*
|
||||
* The reasoning for this is that peekFrame() may need to parse a header to
|
||||
* determine a body length, and this parsing may be as involved as the
|
||||
* actual consumption/parsing.
|
||||
*
|
||||
* @param string $data
|
||||
* @return boolean
|
||||
*/
|
||||
// public function peekFrame(string $data): bool;
|
||||
|
||||
}
|
||||
72
src/ProtocolStream.php
Normal file
72
src/ProtocolStream.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
use Evenement\EventEmitterTrait;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
class ProtocolStream implements EventEmitterInterface
|
||||
{
|
||||
use EventEmitterTrait;
|
||||
|
||||
private string $readBuffer = '';
|
||||
|
||||
private ProtocolInterface $protocol;
|
||||
|
||||
public function __construct(
|
||||
private readonly DuplexStreamInterface $stream,
|
||||
ProtocolInterface $protocol,
|
||||
private readonly int $maxBuffer = 8192,
|
||||
private readonly bool $closeOnOverflow = false,
|
||||
)
|
||||
{
|
||||
$this->protocol = $protocol;
|
||||
$stream->on("data", $this->receive(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the protocol, switching from f.ex. a text based to a JSON
|
||||
* based protocol without having to re-create the ProtocolStream.
|
||||
*
|
||||
* @param ProtocolInterface $protocol
|
||||
* @return void
|
||||
*/
|
||||
public function upgrade(ProtocolInterface $protocol): void
|
||||
{
|
||||
$this->protocol = $protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @param array $frame
|
||||
* @return void
|
||||
*/
|
||||
public function send(array $frame): void
|
||||
{
|
||||
$data = $this->protocol->packFrame($frame);
|
||||
$this->stream->write($data);
|
||||
}
|
||||
|
||||
private function receive(string $data): void
|
||||
{
|
||||
$this->readBuffer .= $data;
|
||||
|
||||
try {
|
||||
while ($frame = $this->protocol->consumeFrame($this->readBuffer)) {
|
||||
$this->emit("message", [ $frame ]);
|
||||
}
|
||||
} catch (ProtocolException $e) {
|
||||
$this->emit("error", [ $e ]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strlen($this->readBuffer) > $this->maxBuffer) {
|
||||
$this->emit("overflow");
|
||||
if ($this->closeOnOverflow) {
|
||||
$this->stream->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
tests/Binary/BinaryProtocolTest.php
Normal file
92
tests/Binary/BinaryProtocolTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Binary;
|
||||
|
||||
use NoccyLabs\React\Protocol\ProtocolException;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(BinaryProtocol::class)]
|
||||
class BinaryProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testPackingFrames()
|
||||
{
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "\0"
|
||||
);
|
||||
$res1 = $proto->packFrame([ 'payload' => 'foobar' ]);
|
||||
$this->assertEquals("foobar\0", $res1);
|
||||
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "",
|
||||
prependSizeBytes: 2,
|
||||
);
|
||||
$res1 = $proto->packFrame([ 'payload' => 'foobar' ]);
|
||||
$this->assertEquals("\x06\x00foobar", $res1);
|
||||
|
||||
}
|
||||
|
||||
public function testUnpackingFrames()
|
||||
{
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "\0"
|
||||
);
|
||||
$res1 = $proto->unpackFrame("foobar\0");
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "",
|
||||
prependSizeBytes: 2,
|
||||
);
|
||||
$res1 = $proto->unpackFrame("\x06\x00foobar");
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||
|
||||
}
|
||||
|
||||
public function testUnpackingIncompleteFrames()
|
||||
{
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "\0"
|
||||
);
|
||||
$res1 = $proto->unpackFrame("foobar\0");
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "",
|
||||
prependSizeBytes: 2,
|
||||
);
|
||||
|
||||
$this->expectException(ProtocolException::class);
|
||||
$res0 = $proto->unpackFrame("\x06");
|
||||
$this->expectException(ProtocolException::class);
|
||||
$res1 = $proto->unpackFrame("\x00foo");
|
||||
$res2 = $proto->unpackFrame("bar");
|
||||
|
||||
$this->assertNull($res1);
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res2);
|
||||
}
|
||||
|
||||
public function testConsumingFrames()
|
||||
{
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "\0"
|
||||
);
|
||||
$res1 = $proto->unpackFrame("foobar\0");
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||
|
||||
$proto = new BinaryProtocol(
|
||||
frameSeparator: "",
|
||||
prependSizeBytes: 2,
|
||||
);
|
||||
|
||||
$buffer = "\x06\x00foobar\x06\x00foobaz";
|
||||
|
||||
$res1 = $proto->consumeFrame($buffer);
|
||||
$res2 = $proto->consumeFrame($buffer);
|
||||
|
||||
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||
$this->assertEquals([ 'payload' => 'foobaz' ], $res2);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
109
tests/Json/JsonProtocolTest.php
Normal file
109
tests/Json/JsonProtocolTest.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(JsonProtocol::class)]
|
||||
class JsonProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testFrameSeparators()
|
||||
{
|
||||
$proto = new JsonProtocol("\0");
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
$proto = new JsonProtocol("\n");
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("{\"foo\":\"bar\"}\n", $packed);
|
||||
|
||||
}
|
||||
|
||||
public function testFrameSizePrefixes()
|
||||
{
|
||||
$proto = new JsonProtocol(prependSizeBytes: 1);
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
$proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'l');
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("\x0E\x00{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
$proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'b');
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("\x00\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
$proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'l');
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("\x0E\x00\x00\x00{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
$proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'b');
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("\x00\x00\x00\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||
|
||||
}
|
||||
|
||||
public function testPackingCallbacks()
|
||||
{
|
||||
$proto = new JsonProtocol(
|
||||
prependSizeBytes: 0,
|
||||
beforePackCb: function (array $data) {
|
||||
$data['a'] = true;
|
||||
return $data;
|
||||
},
|
||||
afterPackCb: function (string $data) {
|
||||
return $data."a";
|
||||
},
|
||||
);
|
||||
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||
$this->assertEquals("{\"foo\":\"bar\",\"a\":true}\0a", $packed);
|
||||
}
|
||||
|
||||
public function testUnpackingCallbacks()
|
||||
{
|
||||
$proto = new JsonProtocol(
|
||||
prependSizeBytes: 0,
|
||||
beforeUnpackCb: function (string $data) {
|
||||
return strtolower($data);
|
||||
},
|
||||
afterUnpackCb: function (array $data) {
|
||||
unset($data['a']);
|
||||
return $data;
|
||||
},
|
||||
);
|
||||
$unpacked = $proto->unpackFrame("{\"FOO\":\"bar\",\"a\":true}\0");
|
||||
$expect = ['foo' => 'bar'];
|
||||
$this->assertEquals($expect, $unpacked);
|
||||
}
|
||||
|
||||
public function testConsumingFrames()
|
||||
{
|
||||
$proto = new JsonProtocol(
|
||||
frameSeparator: "\n"
|
||||
);
|
||||
|
||||
$buffer = '{"foo":true}'."\n".'{"bar":false}'."\n";
|
||||
$msg1 = $proto->consumeFrame($buffer);
|
||||
$msg2 = $proto->consumeFrame($buffer);
|
||||
|
||||
$this->assertEquals(['foo'=>true], $msg1);
|
||||
$this->assertEquals(['bar'=>false], $msg2);
|
||||
|
||||
$proto = new JsonProtocol(
|
||||
frameSeparator: "\n",
|
||||
prependSizeBytes: 4,
|
||||
prependSizeEndian: 'l',
|
||||
);
|
||||
|
||||
$buffer = "\x0c\x00\x00\x00".'{"foo":true}'."\n"."\x0d\x00\x00\x00".'{"bar":false}'."\n";
|
||||
$msg1 = $proto->consumeFrame($buffer);
|
||||
$msg2 = $proto->consumeFrame($buffer);
|
||||
|
||||
$this->assertEquals(['foo'=>true], $msg1);
|
||||
$this->assertEquals(['bar'=>false], $msg2);
|
||||
|
||||
}
|
||||
}
|
||||
25
tests/Json/JsonRpcProtocolTest.php
Normal file
25
tests/Json/JsonRpcProtocolTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(JsonProtocol::class)]
|
||||
#[CoversClass(JsonRpcProtocol::class)]
|
||||
class JsonRpcProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testProcessingCallbacks()
|
||||
{
|
||||
$proto = new JsonRpcProtocol(
|
||||
frameSeparator: "\n"
|
||||
);
|
||||
|
||||
$packed = $proto->packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]);
|
||||
|
||||
$this->assertEquals('{"method":"foo","params":{"bar":true},"jsonrpc":"2.0"}'."\n", $packed);
|
||||
}
|
||||
|
||||
}
|
||||
23
tests/Json/NativeMessagingProtocolTest.php
Normal file
23
tests/Json/NativeMessagingProtocolTest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Json;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(JsonProtocol::class)]
|
||||
#[CoversClass(NativeMessagingProtocol::class)]
|
||||
class NativeMessagingProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testFramePacking()
|
||||
{
|
||||
$proto = new NativeMessagingProtocol();
|
||||
|
||||
$packed = $proto->packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]);
|
||||
|
||||
$this->assertEquals("\x26\x00\x00\x00".'{"method":"foo","params":{"bar":true}}', $packed);
|
||||
}
|
||||
|
||||
}
|
||||
45
tests/Line/HttpLikeProtocolTest.php
Normal file
45
tests/Line/HttpLikeProtocolTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Line;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(HttpLikeProtocol::class)]
|
||||
class HttpLikeProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testPackingFrames()
|
||||
{
|
||||
$proto = new HttpLikeProtocol(
|
||||
lineSeparator: "\n"
|
||||
);
|
||||
$packed = $proto->packFrame(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]]);
|
||||
$this->assertEquals("GET / HTTP/1.1\nfoo: bar\n\n", $packed);
|
||||
}
|
||||
|
||||
public function testUnpackingFrames()
|
||||
{
|
||||
$proto = new HttpLikeProtocol(
|
||||
lineSeparator: "\n"
|
||||
);
|
||||
|
||||
$msg1 = $proto->unpackFrame("GET / HTTP/1.1\nfoo: bar\n\n");
|
||||
$this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1);
|
||||
}
|
||||
|
||||
public function testConsumingFrames()
|
||||
{
|
||||
$proto = new HttpLikeProtocol(
|
||||
lineSeparator: "\n"
|
||||
);
|
||||
|
||||
$buffer = "GET / HTTP/1.1\nfoo: bar\n\n";
|
||||
$msg1 = $proto->consumeFrame($buffer);
|
||||
// $msg2 = $proto->consumeFrame($buffer);
|
||||
|
||||
$this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1);
|
||||
// $this->assertEquals(['bar','false'], $msg2);
|
||||
}
|
||||
}
|
||||
33
tests/Line/LineProtocolTest.php
Normal file
33
tests/Line/LineProtocolTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol\Line;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(LineProtocol::class)]
|
||||
class LineProtocolTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testFrameSeparators()
|
||||
{
|
||||
$proto = new LineProtocol("\n");
|
||||
$packed = $proto->packFrame(['foo', 'bar']);
|
||||
$this->assertEquals("foo bar\n", $packed);
|
||||
}
|
||||
|
||||
public function testConsumingFrames()
|
||||
{
|
||||
$proto = new LineProtocol(
|
||||
lineBreak: "\n"
|
||||
);
|
||||
|
||||
$buffer = "foo true\nbar false\n";
|
||||
$msg1 = $proto->consumeFrame($buffer);
|
||||
$msg2 = $proto->consumeFrame($buffer);
|
||||
|
||||
$this->assertEquals(['foo','true'], $msg1);
|
||||
$this->assertEquals(['bar','false'], $msg2);
|
||||
}
|
||||
}
|
||||
82
tests/ProtocolStreamTest.php
Normal file
82
tests/ProtocolStreamTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\Protocol;
|
||||
|
||||
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use React\EventLoop\Loop;
|
||||
use React\Stream\CompositeStream;
|
||||
use React\Stream\ThroughStream;
|
||||
|
||||
#[CoversClass(ProtocolStream::class)]
|
||||
#[CoversClass(LineProtocol::class)]
|
||||
#[CoversClass(JsonProtocol::class)]
|
||||
class ProtocolStreamTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testSendingFrames()
|
||||
{
|
||||
$is = new ThroughStream();
|
||||
$os = new ThroughStream();
|
||||
$cs = new CompositeStream($is, $os);
|
||||
|
||||
$proto = new LineProtocol();
|
||||
$stream = new ProtocolStream($cs, $proto);
|
||||
|
||||
$written = null;
|
||||
$os->on("data", function ($v) use (&$written) { $written = $v; });
|
||||
|
||||
$stream->send(["helloworld"]);
|
||||
|
||||
$this->assertEquals("helloworld\n", $written);
|
||||
|
||||
$cs->close();
|
||||
}
|
||||
|
||||
public function testReceivingFrames()
|
||||
{
|
||||
$is = new ThroughStream();
|
||||
$os = new ThroughStream();
|
||||
$cs = new CompositeStream($is, $os);
|
||||
$ws = new CompositeStream($os, $is);
|
||||
|
||||
$proto = new LineProtocol();
|
||||
$stream = new ProtocolStream($cs, $proto);
|
||||
|
||||
$read = null;
|
||||
$stream->on("message", function ($v) use (&$read) { $read = $v; });
|
||||
|
||||
$ws->write("helloworld\n");
|
||||
|
||||
$this->assertEquals([ "helloworld" ], $read);
|
||||
|
||||
$cs->close();
|
||||
}
|
||||
|
||||
public function testPartialFrames()
|
||||
{
|
||||
$is = new ThroughStream();
|
||||
$os = new ThroughStream();
|
||||
$cs = new CompositeStream($is, $os);
|
||||
$ws = new CompositeStream($os, $is);
|
||||
|
||||
$proto = new JsonProtocol(
|
||||
frameSeparator: "\n"
|
||||
);
|
||||
$stream = new ProtocolStream($cs, $proto);
|
||||
|
||||
$read = null;
|
||||
$stream->on("message", function ($v) use (&$read) { $read = $v; });
|
||||
|
||||
$ws->write('{"hello":');
|
||||
$ws->write('"world"}');
|
||||
$ws->write("\n");
|
||||
|
||||
$this->assertEquals([ "hello" => "world" ], $read);
|
||||
|
||||
$cs->close();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user