Added code_buffer, combinator, pub_sub
This commit is contained in:
parent
8fab36de2b
commit
13ab076cab
87 changed files with 4973 additions and 0 deletions
1
packages/code_buffer/.travis.yml
Normal file
1
packages/code_buffer/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
12
packages/code_buffer/AUTHORS.md
Normal file
12
packages/code_buffer/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
26
packages/code_buffer/CHANGELOG.md
Normal file
26
packages/code_buffer/CHANGELOG.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Change Log
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Published as `belatuk_code_buffer` package
|
||||
|
||||
## 2.0.3
|
||||
|
||||
* Resolved static analysis warnings
|
||||
|
||||
## 2.0.2
|
||||
|
||||
* Updated README
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Fixed invalid homepage url in pubspec.yaml
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Added `CodeBuffer.noWhitespace()`.
|
29
packages/code_buffer/LICENSE
Normal file
29
packages/code_buffer/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
69
packages/code_buffer/README.md
Normal file
69
packages/code_buffer/README.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Belatuk Code Buffer
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_code_buffer)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/code_buffer/LICENSE)
|
||||
|
||||
**Replacement of `package:code_buffer` with breaking changes to support NNBD.**
|
||||
|
||||
An advanced StringBuffer geared toward generating code, and source maps.
|
||||
|
||||
## Installation
|
||||
|
||||
In your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
belatuk_code_buffer: ^3.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Use a `CodeBuffer` just like any regular `StringBuffer`:
|
||||
|
||||
```dart
|
||||
String someFunc() {
|
||||
var buf = CodeBuffer();
|
||||
buf
|
||||
..write('hello ')
|
||||
..writeln('world!');
|
||||
return buf.toString();
|
||||
}
|
||||
```
|
||||
|
||||
However, a `CodeBuffer` supports indentation.
|
||||
|
||||
```dart
|
||||
void someOtherFunc() {
|
||||
var buf = CodeBuffer();
|
||||
// Custom options...
|
||||
var buf = CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
|
||||
|
||||
// Any following lines will have an incremented indentation level...
|
||||
buf.indent();
|
||||
|
||||
// And vice-versa:
|
||||
buf.outdent();
|
||||
}
|
||||
```
|
||||
|
||||
`CodeBuffer` instances keep track of every `SourceSpan` they create.
|
||||
This makes them useful for codegen tools, or to-JS compilers.
|
||||
|
||||
```dart
|
||||
void someFunc(CodeBuffer buf) {
|
||||
buf.write('hello');
|
||||
expect(buf.lastLine.text, 'hello');
|
||||
|
||||
buf.writeln('world');
|
||||
expect(buf.lastLine.lastSpan.start.column, 5);
|
||||
}
|
||||
```
|
||||
|
||||
You can copy a `CodeBuffer` into another, heeding indentation rules:
|
||||
|
||||
```dart
|
||||
void yetAnotherFunc(CodeBuffer a, CodeBuffer b) {
|
||||
b.copyInto(a);
|
||||
}
|
||||
```
|
1
packages/code_buffer/analysis_options.yaml
Normal file
1
packages/code_buffer/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
46
packages/code_buffer/example/main.dart
Normal file
46
packages/code_buffer/example/main.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:belatuk_code_buffer/belatuk_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Use a `CodeBuffer` just like any regular `StringBuffer`:
|
||||
String someFunc() {
|
||||
var buf = CodeBuffer();
|
||||
buf
|
||||
..write('hello ')
|
||||
..writeln('world!');
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/// However, a `CodeBuffer` supports indentation.
|
||||
void someOtherFunc() {
|
||||
var buf = CodeBuffer();
|
||||
|
||||
// Custom options...
|
||||
// ignore: unused_local_variable
|
||||
var customBuf =
|
||||
CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
|
||||
|
||||
// Without whitespace..
|
||||
// ignore: unused_local_variable
|
||||
var minifyingBuf = CodeBuffer.noWhitespace();
|
||||
|
||||
// Any following lines will have an incremented indentation level...
|
||||
buf.indent();
|
||||
|
||||
// And vice-versa:
|
||||
buf.outdent();
|
||||
}
|
||||
|
||||
/// `CodeBuffer` instances keep track of every `SourceSpan` they create.
|
||||
//This makes them useful for codegen tools, or to-JS compilers.
|
||||
void yetAnotherOtherFunc(CodeBuffer buf) {
|
||||
buf.write('hello');
|
||||
expect(buf.lastLine!.text, 'hello');
|
||||
|
||||
buf.writeln('world');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 5);
|
||||
}
|
||||
|
||||
/// You can copy a `CodeBuffer` into another, heeding indentation rules:
|
||||
void yetEvenAnotherFunc(CodeBuffer a, CodeBuffer b) {
|
||||
b.copyInto(a);
|
||||
}
|
231
packages/code_buffer/lib/belatuk_code_buffer.dart
Normal file
231
packages/code_buffer/lib/belatuk_code_buffer.dart
Normal file
|
@ -0,0 +1,231 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
|
||||
/// An advanced StringBuffer geared toward generating code, and source maps.
|
||||
class CodeBuffer implements StringBuffer {
|
||||
/// The character sequence used to represent a line break.
|
||||
final String newline;
|
||||
|
||||
/// The character sequence used to represent a space/tab.
|
||||
final String space;
|
||||
|
||||
/// The source URL to be applied to all generated [SourceSpan] instances.
|
||||
final dynamic sourceUrl;
|
||||
|
||||
/// If `true` (default: `false`), then an additional [newline] will be inserted at the end of the generated string.
|
||||
final bool trailingNewline;
|
||||
|
||||
final List<CodeBufferLine> _lines = [];
|
||||
CodeBufferLine? _currentLine, _lastLine;
|
||||
int _indentationLevel = 0;
|
||||
int _length = 0;
|
||||
|
||||
CodeBuffer(
|
||||
{this.space = ' ',
|
||||
this.newline = '\n',
|
||||
this.trailingNewline = false,
|
||||
this.sourceUrl});
|
||||
|
||||
/// Creates a [CodeBuffer] that does not emit additional whitespace.
|
||||
factory CodeBuffer.noWhitespace({sourceUrl}) => CodeBuffer(
|
||||
space: '', newline: '', trailingNewline: false, sourceUrl: sourceUrl);
|
||||
|
||||
/// The last line created within this buffer.
|
||||
CodeBufferLine? get lastLine => _lastLine;
|
||||
|
||||
/// Returns an immutable collection of the [CodeBufferLine]s within this instance.
|
||||
List<CodeBufferLine> get lines => List<CodeBufferLine>.unmodifiable(_lines);
|
||||
|
||||
@override
|
||||
bool get isEmpty => _lines.isEmpty;
|
||||
|
||||
@override
|
||||
bool get isNotEmpty => _lines.isNotEmpty;
|
||||
|
||||
@override
|
||||
int get length => _length;
|
||||
|
||||
CodeBufferLine _createLine() {
|
||||
var start = SourceLocation(
|
||||
_length,
|
||||
sourceUrl: sourceUrl,
|
||||
line: _lines.length,
|
||||
column: _indentationLevel * space.length,
|
||||
);
|
||||
var line = CodeBufferLine._(_indentationLevel, start).._end = start;
|
||||
_lines.add(_lastLine = line);
|
||||
return line;
|
||||
}
|
||||
|
||||
/// Increments the indentation level.
|
||||
void indent() {
|
||||
_indentationLevel++;
|
||||
}
|
||||
|
||||
/// Decrements the indentation level, if it is greater than `0`.
|
||||
void outdent() {
|
||||
if (_indentationLevel > 0) _indentationLevel--;
|
||||
}
|
||||
|
||||
/// Copies the contents of this [CodeBuffer] into another, preserving indentation and source mapping information.
|
||||
void copyInto(CodeBuffer other) {
|
||||
if (_lines.isEmpty) return;
|
||||
var i = 0;
|
||||
|
||||
for (var line in _lines) {
|
||||
// To compute offset:
|
||||
// 1. Find current length of other
|
||||
// 2. Add length of its newline
|
||||
// 3. Add indentation
|
||||
var column = (other._indentationLevel + line.indentationLevel) *
|
||||
other.space.length;
|
||||
var offset = other._length + other.newline.length + column;
|
||||
|
||||
// Re-compute start + end
|
||||
var start = SourceLocation(
|
||||
offset,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: other._lines.length + i,
|
||||
column: column,
|
||||
);
|
||||
|
||||
var end = SourceLocation(
|
||||
offset + line.span.length,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: start.line,
|
||||
column: column + line._buf.length,
|
||||
);
|
||||
|
||||
var clone = CodeBufferLine._(
|
||||
line.indentationLevel + other._indentationLevel, start)
|
||||
.._end = end
|
||||
.._buf.write(line._buf.toString());
|
||||
|
||||
// Adjust lastSpan
|
||||
if (line._lastSpan != null) {
|
||||
var s = line._lastSpan!.start;
|
||||
var lastSpanColumn =
|
||||
((line.indentationLevel + other._indentationLevel) *
|
||||
other.space.length) +
|
||||
line.text.indexOf(line._lastSpan!.text);
|
||||
clone._lastSpan = SourceSpan(
|
||||
SourceLocation(
|
||||
offset + s.offset,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: clone.span.start.line,
|
||||
column: lastSpanColumn,
|
||||
),
|
||||
SourceLocation(
|
||||
offset + s.offset + line._lastSpan!.length,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: clone.span.end.line,
|
||||
column: lastSpanColumn + line._lastSpan!.length,
|
||||
),
|
||||
line._lastSpan!.text,
|
||||
);
|
||||
}
|
||||
|
||||
other._lines.add(other._currentLine = other._lastLine = clone);
|
||||
|
||||
// Adjust length accordingly...
|
||||
other._length = offset + clone.span.length;
|
||||
i++;
|
||||
}
|
||||
|
||||
other.writeln();
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_lines.clear();
|
||||
_length = _indentationLevel = 0;
|
||||
_currentLine = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void writeCharCode(int charCode) {
|
||||
_currentLine ??= _createLine();
|
||||
|
||||
_currentLine!._buf.writeCharCode(charCode);
|
||||
var end = _currentLine!._end;
|
||||
_currentLine!._end = SourceLocation(
|
||||
end.offset + 1,
|
||||
sourceUrl: end.sourceUrl,
|
||||
line: end.line,
|
||||
column: end.column + 1,
|
||||
);
|
||||
_length++;
|
||||
_currentLine!._lastSpan =
|
||||
SourceSpan(end, _currentLine!._end, String.fromCharCode(charCode));
|
||||
}
|
||||
|
||||
@override
|
||||
void write(Object? obj) {
|
||||
var msg = obj.toString();
|
||||
_currentLine ??= _createLine();
|
||||
_currentLine!._buf.write(msg);
|
||||
var end = _currentLine!._end;
|
||||
_currentLine!._end = SourceLocation(
|
||||
end.offset + msg.length,
|
||||
sourceUrl: end.sourceUrl,
|
||||
line: end.line,
|
||||
column: end.column + msg.length,
|
||||
);
|
||||
_length += msg.length;
|
||||
_currentLine!._lastSpan = SourceSpan(end, _currentLine!._end, msg);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeln([Object? obj = '']) {
|
||||
if (obj != null && obj != '') write(obj);
|
||||
_currentLine = null;
|
||||
_length++;
|
||||
}
|
||||
|
||||
@override
|
||||
void writeAll(Iterable objects, [String separator = '']) {
|
||||
write(objects.join(separator));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var buf = StringBuffer();
|
||||
var i = 0;
|
||||
|
||||
for (var line in lines) {
|
||||
if (i++ > 0) buf.write(newline);
|
||||
for (var j = 0; j < line.indentationLevel; j++) {
|
||||
buf.write(space);
|
||||
}
|
||||
buf.write(line._buf.toString());
|
||||
}
|
||||
|
||||
if (trailingNewline == true) buf.write(newline);
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a line of text within a [CodeBuffer].
|
||||
class CodeBufferLine {
|
||||
/// Mappings from one [SourceSpan] to another, to aid with generating dynamic source maps.
|
||||
final Map<SourceSpan, SourceSpan> sourceMappings = {};
|
||||
|
||||
/// The level of indentation preceding this line.
|
||||
final int indentationLevel;
|
||||
|
||||
final SourceLocation _start;
|
||||
final StringBuffer _buf = StringBuffer();
|
||||
late SourceLocation _end;
|
||||
SourceSpan? _lastSpan;
|
||||
|
||||
CodeBufferLine._(this.indentationLevel, this._start);
|
||||
|
||||
/// The [SourceSpan] corresponding to the last text written to this line.
|
||||
SourceSpan? get lastSpan => _lastSpan;
|
||||
|
||||
/// The [SourceSpan] corresponding to this entire line.
|
||||
SourceSpan get span => SourceSpan(_start, _end, _buf.toString());
|
||||
|
||||
/// The text within this line.
|
||||
String get text => _buf.toString();
|
||||
}
|
12
packages/code_buffer/pubspec.yaml
Normal file
12
packages/code_buffer/pubspec.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
name: belatuk_code_buffer
|
||||
version: 3.0.0
|
||||
description: An advanced StringBuffer geared toward generating code, and source maps.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/code_buffer
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
charcode: ^1.2.0
|
||||
source_span: ^1.8.1
|
||||
dev_dependencies:
|
||||
test: ^1.17.3
|
||||
lints: ^1.0.0
|
47
packages/code_buffer/test/copy_test.dart
Normal file
47
packages/code_buffer/test/copy_test.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:belatuk_code_buffer/belatuk_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var a = CodeBuffer(), b = CodeBuffer();
|
||||
|
||||
setUp(() {
|
||||
a.writeln('outer block 1');
|
||||
b
|
||||
..writeln('inner block 1')
|
||||
..writeln('inner block 2');
|
||||
b.copyInto(a..indent());
|
||||
a
|
||||
..outdent()
|
||||
..writeln('outer block 2');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
a.clear();
|
||||
b.clear();
|
||||
});
|
||||
|
||||
test('sets correct text', () {
|
||||
expect(
|
||||
a.toString(),
|
||||
[
|
||||
'outer block 1',
|
||||
' inner block 1',
|
||||
' inner block 2',
|
||||
'outer block 2',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('sets lastLine+lastSpan', () {
|
||||
var c = CodeBuffer()
|
||||
..indent()
|
||||
..write('>')
|
||||
..writeln('innermost');
|
||||
c.copyInto(a);
|
||||
expect(a.lastLine!.text, '>innermost');
|
||||
expect(a.lastLine!.span.start.column, 2);
|
||||
expect(a.lastLine!.lastSpan!.start.line, 4);
|
||||
expect(a.lastLine!.lastSpan!.start.column, 3);
|
||||
expect(a.lastLine!.lastSpan!.end.line, 4);
|
||||
expect(a.lastLine!.lastSpan!.end.column, 12);
|
||||
});
|
||||
}
|
46
packages/code_buffer/test/span_test.dart
Normal file
46
packages/code_buffer/test/span_test.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:charcode/charcode.dart';
|
||||
import 'package:belatuk_code_buffer/belatuk_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var buf = CodeBuffer();
|
||||
tearDown(buf.clear);
|
||||
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 0);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 1);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('foo');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 0);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 3);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('multiple writes in one line', () {
|
||||
buf
|
||||
..write('foo')
|
||||
..write('baz');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 3);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 6);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('multiple lines', () {
|
||||
buf
|
||||
..writeln('foo')
|
||||
..write('bar')
|
||||
..write('+')
|
||||
..writeln('baz');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 4);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 1);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 7);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 1);
|
||||
});
|
||||
}
|
90
packages/code_buffer/test/write_test.dart
Normal file
90
packages/code_buffer/test/write_test.dart
Normal file
|
@ -0,0 +1,90 @@
|
|||
import 'package:charcode/charcode.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:belatuk_code_buffer/belatuk_code_buffer.dart';
|
||||
|
||||
void main() {
|
||||
var buf = CodeBuffer();
|
||||
tearDown(buf.clear);
|
||||
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.toString(), 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.toString(), 'hello world');
|
||||
});
|
||||
|
||||
test('custom space', () {
|
||||
var b = CodeBuffer(space: '+')
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('baz');
|
||||
expect(b.toString(), 'foo\n+baz');
|
||||
});
|
||||
|
||||
test('custom newline', () {
|
||||
var b = CodeBuffer(newline: 'N')
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('baz');
|
||||
expect(b.toString(), 'fooN baz');
|
||||
});
|
||||
|
||||
test('trailing newline', () {
|
||||
var b = CodeBuffer(trailingNewline: true)..writeln('foo');
|
||||
expect(b.toString(), 'foo\n');
|
||||
});
|
||||
|
||||
group('multiple lines', () {
|
||||
setUp(() {
|
||||
buf
|
||||
..writeln('foo')
|
||||
..writeln('bar')
|
||||
..writeln('baz');
|
||||
expect(buf.lines, hasLength(3));
|
||||
expect(buf.lines[0].text, 'foo');
|
||||
expect(buf.lines[1].text, 'bar');
|
||||
expect(buf.lines[2].text, 'baz');
|
||||
});
|
||||
});
|
||||
|
||||
test('indent', () {
|
||||
buf
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('bar')
|
||||
..indent()
|
||||
..writeln('baz')
|
||||
..outdent()
|
||||
..writeln('quux')
|
||||
..outdent()
|
||||
..writeln('end');
|
||||
expect(buf.toString(), 'foo\n bar\n baz\n quux\nend');
|
||||
});
|
||||
|
||||
group('sets lastLine text', () {
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.text, 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.lastLine!.text, 'hello world');
|
||||
});
|
||||
});
|
||||
|
||||
group('sets lastLine lastSpan', () {
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.lastSpan!.text, 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.lastLine!.lastSpan!.text, 'hello world');
|
||||
});
|
||||
});
|
||||
}
|
4
packages/combinator/.travis.yml
Normal file
4
packages/combinator/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- stable
|
||||
- dev
|
12
packages/combinator/AUTHORS.md
Normal file
12
packages/combinator/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
33
packages/combinator/CHANGELOG.md
Normal file
33
packages/combinator/CHANGELOG.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Change Log
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Published as `belatuk_combinator` package
|
||||
|
||||
## 2.0.2
|
||||
|
||||
* Resolve static analysis warnings
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Add `tupleX` parsers. Hooray for strong typing!
|
||||
|
||||
## 1.0.0+3
|
||||
|
||||
* `then` now *always* returns `dynamic`.
|
||||
|
||||
## 1.0.0+2
|
||||
|
||||
* `star` now includes with a call to `opt`.
|
||||
* Added comments.
|
||||
* Enforce generics on `separatedBy`.
|
||||
* Enforce Dart 2 semantics.
|
29
packages/combinator/LICENSE
Normal file
29
packages/combinator/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
128
packages/combinator/README.md
Normal file
128
packages/combinator/README.md
Normal file
|
@ -0,0 +1,128 @@
|
|||
# Belatuk Combinator
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_combinator)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/combinator/LICENSE)
|
||||
|
||||
Packrat parser combinators that support static typing, generics, file spans, memoization, and more.
|
||||
|
||||
**RECOMMENDED:**
|
||||
Check `example/` for examples.
|
||||
The examples contain examples of using:
|
||||
|
||||
* Generic typing
|
||||
* Reading `FileSpan` from `ParseResult`
|
||||
* More...
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
// Parse a Pattern (usually String or RegExp).
|
||||
var foo = match('foo');
|
||||
var number = match(RegExp(r'[0-9]+'), errorMessage: 'Expected a number.');
|
||||
|
||||
// Set a value.
|
||||
var numWithValue = number.map((r) => int.parse(r.span.text));
|
||||
|
||||
// Expect a pattern, or nothing.
|
||||
var optional = numWithValue.opt();
|
||||
|
||||
// Expect a pattern zero or more times.
|
||||
var star = optional.star();
|
||||
|
||||
// Expect one or more times.
|
||||
var plus = optional.plus();
|
||||
|
||||
// Expect an arbitrary number of times.
|
||||
var threeTimes = optional.times(3);
|
||||
|
||||
// Expect a sequence of patterns.
|
||||
var doraTheExplorer = chain([
|
||||
match('Dora').space(),
|
||||
match('the').space(),
|
||||
match('Explorer').space(),
|
||||
]);
|
||||
|
||||
// Choose exactly one of a set of patterns, whichever
|
||||
// appears first.
|
||||
var alt = any([
|
||||
match('1'),
|
||||
match('11'),
|
||||
match('111'),
|
||||
]);
|
||||
|
||||
// Choose the *longest* match for any of the given alternatives.
|
||||
var alt2 = longest([
|
||||
match('1'),
|
||||
match('11'),
|
||||
match('111'),
|
||||
]);
|
||||
|
||||
// Friendly operators
|
||||
var fooOrNumber = foo | number;
|
||||
var fooAndNumber = foo & number;
|
||||
var notFoo = ~foo;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
|
||||
Parsers without descriptive error messages can lead to frustrating dead-ends
|
||||
for end-users. Fortunately, `belatuk_combinator` is built with error handling in mind.
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
// Append an arbitrary error message to a parser if it is not matched.
|
||||
var withError = parser.error(errorMessage: 'Hey!!! Wrong!!!');
|
||||
|
||||
// You can also set the severity of an error.
|
||||
var asHint = parser.error(severity: SyntaxErrorSeverity.hint);
|
||||
|
||||
// Constructs like `any`, `chain`, and `longest` support this as well.
|
||||
var foo = longest([
|
||||
parser.error(errorMessage: 'foo'),
|
||||
parser.error(errorMessage: 'bar')
|
||||
], errorMessage: 'Expected a "foo" or a "bar"');
|
||||
|
||||
// If multiple errors are present at one location,
|
||||
// it can create a lot of noise.
|
||||
//
|
||||
// Use `foldErrors` to only take one error at a given location.
|
||||
var lessNoise = parser.foldErrors();
|
||||
}
|
||||
```
|
||||
|
||||
## Whitespaces
|
||||
|
||||
Handling optional whitespace is dead-easy:
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
var optionalSpace = parser.space();
|
||||
}
|
||||
```
|
||||
|
||||
## For Programming Languages
|
||||
|
||||
`belatuk_combinator` was conceived to make writing parsers for complex grammars easier,
|
||||
namely programming languages. Thus, there are functions built-in to make common constructs
|
||||
easier:
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
var array = parser
|
||||
.separatedByComma()
|
||||
.surroundedBySquareBrackets(defaultValue: []);
|
||||
|
||||
var braces = parser.surroundedByCurlyBraces();
|
||||
|
||||
var sep = parser.separatedBy(match('!').space());
|
||||
}
|
||||
```
|
||||
|
||||
## Differences between this and Petitparser
|
||||
|
||||
* `belatuk_combinator` makes extensive use of Dart's dynamic typing
|
||||
* `belatuk_combinator` supports detailed error messages (with configurable severity)
|
||||
* `belatuk_combinator` keeps track of locations (ex. `line 1: 3`)
|
1
packages/combinator/analysis_options.yaml
Normal file
1
packages/combinator/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
14
packages/combinator/combinator.iml
Normal file
14
packages/combinator/combinator.iml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
56
packages/combinator/example/basic_auth.dart
Normal file
56
packages/combinator/example/basic_auth.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Run this with "Basic QWxhZGRpbjpPcGVuU2VzYW1l"
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Parse a part of a decoded Basic auth string.
|
||||
///
|
||||
/// Namely, the `username` or `password` in `{username}:{password}`.
|
||||
final Parser<String> string =
|
||||
match<String>(RegExp(r'[^:$]+'), errorMessage: 'Expected a string.')
|
||||
.value((r) => r.span!.text);
|
||||
|
||||
/// Transforms `{username}:{password}` to `{"username": username, "password": password}`.
|
||||
final Parser<Map<String, String>> credentials = chain<String>([
|
||||
string.opt(),
|
||||
match<String>(':'),
|
||||
string.opt(),
|
||||
]).map<Map<String, String>>(
|
||||
(r) => {'username': r.value![0], 'password': r.value![2]});
|
||||
|
||||
/// We can actually embed a parser within another parser.
|
||||
///
|
||||
/// This is used here to BASE64URL-decode a string, and then
|
||||
/// parse the decoded string.
|
||||
final Parser credentialString = match<Map<String, String>?>(
|
||||
RegExp(r'([^\n$]+)'),
|
||||
errorMessage: 'Expected a credential string.')
|
||||
.value((r) {
|
||||
var decoded = utf8.decode(base64Url.decode(r.span!.text));
|
||||
var scanner = SpanScanner(decoded);
|
||||
return credentials.parse(scanner).value;
|
||||
});
|
||||
|
||||
final Parser basic = match<Null>('Basic').space();
|
||||
|
||||
final Parser basicAuth = basic.then(credentialString).index(1);
|
||||
|
||||
void main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a basic auth value: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = basicAuth.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
71
packages/combinator/example/calculator.dart
Normal file
71
packages/combinator/example/calculator.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Note: This grammar does not handle precedence, for the sake of simplicity.
|
||||
Parser<num> calculatorGrammar() {
|
||||
var expr = reference<num>();
|
||||
|
||||
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'))
|
||||
.value((r) => num.parse(r.span!.text));
|
||||
|
||||
var hex = match<int>(RegExp(r'0x([A-Fa-f0-9]+)'))
|
||||
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 16));
|
||||
|
||||
var binary = match<int>(RegExp(r'([0-1]+)b'))
|
||||
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 2));
|
||||
|
||||
var alternatives = <Parser<num>>[];
|
||||
|
||||
void registerBinary(String op, num Function(num, num) f) {
|
||||
alternatives.add(
|
||||
chain<num>([
|
||||
expr.space(),
|
||||
match<Null>(op).space() as Parser<num>,
|
||||
expr.space(),
|
||||
]).map((r) => f(r.value![0], r.value![2])),
|
||||
);
|
||||
}
|
||||
|
||||
registerBinary('**', (a, b) => pow(a, b));
|
||||
registerBinary('*', (a, b) => a * b);
|
||||
registerBinary('/', (a, b) => a / b);
|
||||
registerBinary('%', (a, b) => a % b);
|
||||
registerBinary('+', (a, b) => a + b);
|
||||
registerBinary('-', (a, b) => a - b);
|
||||
registerBinary('^', (a, b) => a.toInt() ^ b.toInt());
|
||||
registerBinary('&', (a, b) => a.toInt() & b.toInt());
|
||||
registerBinary('|', (a, b) => a.toInt() | b.toInt());
|
||||
|
||||
alternatives.addAll([
|
||||
number,
|
||||
hex,
|
||||
binary,
|
||||
expr.parenthesized(),
|
||||
]);
|
||||
|
||||
expr.parser = longest(alternatives);
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
void main() {
|
||||
var calculator = calculatorGrammar();
|
||||
|
||||
while (true) {
|
||||
stdout.write('Enter an expression: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = calculator.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
stderr.writeln(error.toolString);
|
||||
stderr.writeln(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
29
packages/combinator/example/delimiter.dart
Normal file
29
packages/combinator/example/delimiter.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser<String> id =
|
||||
match<String>(RegExp(r'[A-Za-z]+')).value((r) => r.span!.text);
|
||||
|
||||
// We can use `separatedBy` to easily construct parser
|
||||
// that can be matched multiple times, separated by another
|
||||
// pattern.
|
||||
//
|
||||
// This is useful for parsing arrays or map literals.
|
||||
void main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a string (ex "a,b,c"): ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = id.separatedBy(match(',').space()).parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
71
packages/combinator/example/json.dart
Normal file
71
packages/combinator/example/json.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
Parser jsonGrammar() {
|
||||
var expr = reference();
|
||||
|
||||
// Parse a number
|
||||
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'),
|
||||
errorMessage: 'Expected a number.')
|
||||
.value(
|
||||
(r) => num.parse(r.span!.text),
|
||||
);
|
||||
|
||||
// Parse a string (no escapes supported, because lazy).
|
||||
var string =
|
||||
match(RegExp(r'"[^"]*"'), errorMessage: 'Expected a string.').value(
|
||||
(r) => r.span!.text.substring(1, r.span!.text.length - 1),
|
||||
);
|
||||
|
||||
// Parse an array
|
||||
var array = expr
|
||||
.space()
|
||||
.separatedByComma()
|
||||
.surroundedBySquareBrackets(defaultValue: []);
|
||||
|
||||
// KV pair
|
||||
var keyValuePair = chain([
|
||||
string.space(),
|
||||
match(':').space(),
|
||||
expr.error(errorMessage: 'Missing expression.'),
|
||||
]).castDynamic().cast<Map>().value((r) => {r.value![0]: r.value![2]});
|
||||
|
||||
// Parse an object.
|
||||
var object = keyValuePair
|
||||
.separatedByComma()
|
||||
.castDynamic()
|
||||
.surroundedByCurlyBraces(defaultValue: {});
|
||||
|
||||
expr.parser = longest(
|
||||
[
|
||||
array,
|
||||
number,
|
||||
string,
|
||||
object.error(),
|
||||
],
|
||||
errorMessage: 'Expected an expression.',
|
||||
).space();
|
||||
|
||||
return expr.foldErrors();
|
||||
}
|
||||
|
||||
void main() {
|
||||
var JSON = jsonGrammar();
|
||||
|
||||
while (true) {
|
||||
stdout.write('Enter some JSON: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = JSON.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
38
packages/combinator/example/main.dart
Normal file
38
packages/combinator/example/main.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser minus = match('-');
|
||||
|
||||
final Parser<int> digit =
|
||||
match(RegExp(r'[0-9]'), errorMessage: 'Expected a number');
|
||||
|
||||
final Parser digits = digit.plus();
|
||||
|
||||
final Parser dot = match('.');
|
||||
|
||||
final Parser decimal = ( // digits, (dot, digits)?
|
||||
digits & (dot & digits).opt() //
|
||||
);
|
||||
|
||||
final Parser number = //
|
||||
(minus.opt() & decimal) // minus?, decimal
|
||||
.map<num>((r) => num.parse(r.span!.text));
|
||||
|
||||
void main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a number: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = number.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
stderr.writeln(error.toolString);
|
||||
stderr.writeln(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
45
packages/combinator/example/query_string.dart
Normal file
45
packages/combinator/example/query_string.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
// For some reason, this cannot be run in checked mode???
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser<String> key =
|
||||
match<String>(RegExp(r'[^=&\n]+'), errorMessage: 'Missing k/v')
|
||||
.value((r) => r.span!.text);
|
||||
|
||||
final Parser value = key.map((r) => Uri.decodeQueryComponent(r.value!));
|
||||
|
||||
final Parser pair = chain([
|
||||
key,
|
||||
match('='),
|
||||
value,
|
||||
]).map((r) {
|
||||
return {
|
||||
r.value![0]: r.value![2],
|
||||
};
|
||||
});
|
||||
|
||||
final Parser pairs = pair
|
||||
.separatedBy(match(r'&'))
|
||||
.map((r) => r.value!.reduce((a, b) => a..addAll(b)));
|
||||
|
||||
final Parser queryString = pairs.opt();
|
||||
|
||||
void main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a query string: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = pairs.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
85
packages/combinator/example/sexp.dart
Normal file
85
packages/combinator/example/sexp.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
void main() {
|
||||
var expr = reference();
|
||||
var symbols = <String, dynamic>{};
|
||||
|
||||
void registerFunction(String name, int nArgs, Function(List<num>) f) {
|
||||
symbols[name] = Tuple2(nArgs, f);
|
||||
}
|
||||
|
||||
registerFunction('**', 2, (args) => pow(args[0], args[1]));
|
||||
registerFunction('*', 2, (args) => args[0] * args[1]);
|
||||
registerFunction('/', 2, (args) => args[0] / args[1]);
|
||||
registerFunction('%', 2, (args) => args[0] % args[1]);
|
||||
registerFunction('+', 2, (args) => args[0] + args[1]);
|
||||
registerFunction('-', 2, (args) => args[0] - args[1]);
|
||||
registerFunction('.', 1, (args) => args[0].toDouble());
|
||||
registerFunction('print', 1, (args) {
|
||||
print(args[0]);
|
||||
return args[0];
|
||||
});
|
||||
|
||||
var number =
|
||||
match(RegExp(r'[0-9]+(\.[0-9]+)?'), errorMessage: 'Expected a number.')
|
||||
.map((r) => num.parse(r.span!.text));
|
||||
|
||||
var id = match(
|
||||
RegExp(
|
||||
r'[A-Za-z_!\\$",\\+-\\./:;\\?<>%&\\*@\[\]\\{\}\\|`\\^~][A-Za-z0-9_!\\$",\\+-\\./:;\\?<>%&\*@\[\]\\{\}\\|`\\^~]*'),
|
||||
errorMessage: 'Expected an ID')
|
||||
.map((r) => symbols[r.span!.text] ??=
|
||||
throw "Undefined symbol: '${r.span!.text}'");
|
||||
|
||||
var atom = number.castDynamic().or(id);
|
||||
|
||||
var list = expr.space().times(2, exact: false).map((r) {
|
||||
try {
|
||||
var out = [];
|
||||
var q = Queue.from(r.value!.reversed);
|
||||
|
||||
while (q.isNotEmpty) {
|
||||
var current = q.removeFirst();
|
||||
if (current is! Tuple2) {
|
||||
out.insert(0, current);
|
||||
} else {
|
||||
var args = [];
|
||||
for (var i = 0; i < (current.item1 as num); i++) {
|
||||
args.add(out.removeLast());
|
||||
}
|
||||
out.add(current.item2(args));
|
||||
}
|
||||
}
|
||||
|
||||
return out.length == 1 ? out.first : out;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
expr.parser = longest([
|
||||
list,
|
||||
atom,
|
||||
expr.parenthesized(),
|
||||
]); //list | atom | expr.parenthesized();
|
||||
|
||||
while (true) {
|
||||
stdout.write('> ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var result = expr.parse(SpanScanner(line));
|
||||
|
||||
if (result.errors.isNotEmpty) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.message);
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
14
packages/combinator/example/tuple.dart
Normal file
14
packages/combinator/example/tuple.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
void main() {
|
||||
var pub = match('pub').map((r) => r.span!.text).space();
|
||||
var dart = match('dart').map((r) => 24).space();
|
||||
var lang = match('lang').map((r) => true).space();
|
||||
|
||||
// Parses a Tuple3<String, int, bool>
|
||||
var grammar = tuple3(pub, dart, lang);
|
||||
|
||||
var scanner = SpanScanner('pub dart lang');
|
||||
print(grammar.parse(scanner).value);
|
||||
}
|
2
packages/combinator/lib/belatuk_combinator.dart
Normal file
2
packages/combinator/lib/belatuk_combinator.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/combinator/combinator.dart';
|
||||
export 'src/error.dart';
|
26
packages/combinator/lib/src/combinator/advance.dart
Normal file
26
packages/combinator/lib/src/combinator/advance.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Advance<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final int amount;
|
||||
|
||||
_Advance(this.parser, this.amount);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
if (result.successful) args.scanner.position += amount;
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('advance($amount) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
85
packages/combinator/lib/src/combinator/any.dart
Normal file
85
packages/combinator/lib/src/combinator/any.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Matches any one of the given [parsers].
|
||||
///
|
||||
/// If [backtrack] is `true` (default), a failed parse will not modify the scanner state.
|
||||
///
|
||||
/// You can provide a custom [errorMessage]. You can set it to `false` to not
|
||||
/// generate any error at all.
|
||||
Parser<T> any<T>(Iterable<Parser<T>> parsers,
|
||||
{bool backtrack = true, errorMessage, SyntaxErrorSeverity? severity}) {
|
||||
return _Any(parsers, backtrack != false, errorMessage,
|
||||
severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Any<T> extends Parser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final bool backtrack;
|
||||
final errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Any(this.parsers, this.backtrack, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var inactive = parsers
|
||||
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
|
||||
|
||||
if (inactive.isEmpty) {
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
var errors = <SyntaxError>[];
|
||||
var replay = args.scanner.position;
|
||||
|
||||
for (var parser in inactive) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful) {
|
||||
return result;
|
||||
} else {
|
||||
if (backtrack) args.scanner.position = replay;
|
||||
if (parser is _Alt) errors.addAll(result.errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != false) {
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
// Never called
|
||||
throw ArgumentError('[Combinator] Invalid method call');
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('any(${parsers.length}) (')
|
||||
..indent();
|
||||
var i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
26
packages/combinator/lib/src/combinator/cache.dart
Normal file
26
packages/combinator/lib/src/combinator/cache.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Cache<T> extends Parser<T> {
|
||||
final Map<int, ParseResult<T>> _cache = {};
|
||||
final Parser<T> parser;
|
||||
|
||||
_Cache(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
return _cache.putIfAbsent(args.scanner.position, () {
|
||||
return parser._parse(args.increaseDepth());
|
||||
}).change(parser: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cache(${_cache.length}) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
63
packages/combinator/lib/src/combinator/cast.dart
Normal file
63
packages/combinator/lib/src/combinator/cast.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Cast<T, U extends T> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
|
||||
_Cast(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<U>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.value as U?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cast<$U> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _CastDynamic<T> extends Parser<dynamic> {
|
||||
final Parser<T> parser;
|
||||
|
||||
_CastDynamic(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<dynamic> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<dynamic>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cast<dynamic> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
111
packages/combinator/lib/src/combinator/chain.dart
Normal file
111
packages/combinator/lib/src/combinator/chain.dart
Normal file
|
@ -0,0 +1,111 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Expects to parse a sequence of [parsers].
|
||||
///
|
||||
/// If [failFast] is `true` (default), then the first failure to parse will abort the parse.
|
||||
ListParser<T> chain<T>(Iterable<Parser<T>> parsers,
|
||||
{bool failFast = true, SyntaxErrorSeverity? severity}) {
|
||||
return _Chain<T>(
|
||||
parsers, failFast != false, severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Alt<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Alt(this.parser, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return result.successful
|
||||
? result
|
||||
: result.addErrors([
|
||||
SyntaxError(
|
||||
severity, errorMessage, result.span ?? args.scanner.emptySpan),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
parser.stringify(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chain<T> extends ListParser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final bool failFast;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Chain(this.parsers, this.failFast, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <T>[];
|
||||
var spans = <FileSpan>[];
|
||||
var successful = true;
|
||||
|
||||
for (var parser in parsers) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
if (failFast) {
|
||||
return ParseResult(
|
||||
args.trampoline, args.scanner, this, false, result.errors);
|
||||
}
|
||||
|
||||
successful = false;
|
||||
}
|
||||
|
||||
if (result.value != null) {
|
||||
results.add(result.value!);
|
||||
} else {
|
||||
results.add('NULL' as T);
|
||||
}
|
||||
|
||||
if (result.span != null) {
|
||||
spans.add(result.span!);
|
||||
}
|
||||
}
|
||||
|
||||
FileSpan? span;
|
||||
|
||||
if (spans.isNotEmpty) {
|
||||
span = spans.reduce((a, b) => a.expand(b));
|
||||
}
|
||||
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
successful,
|
||||
errors,
|
||||
span: span,
|
||||
value: List<T>.unmodifiable(results),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('chain(${parsers.length}) (')
|
||||
..indent();
|
||||
var i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
42
packages/combinator/lib/src/combinator/check.dart
Normal file
42
packages/combinator/lib/src/combinator/check.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Check<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final Matcher matcher;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Check(this.parser, this.matcher, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var matchState = {};
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
if (!result.successful) {
|
||||
return result;
|
||||
} else if (!matcher.matches(result.value, matchState)) {
|
||||
return result.change(successful: false).addErrors([
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage ??
|
||||
matcher.describe(StringDescription('Expected ')).toString() + '.',
|
||||
result.span,
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
var d = matcher.describe(StringDescription());
|
||||
buffer
|
||||
..writeln('check($d) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
394
packages/combinator/lib/src/combinator/combinator.dart
Normal file
394
packages/combinator/lib/src/combinator/combinator.dart
Normal file
|
@ -0,0 +1,394 @@
|
|||
library lex.src.combinator;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
import 'package:matcher/matcher.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../error.dart';
|
||||
|
||||
part 'any.dart';
|
||||
|
||||
part 'advance.dart';
|
||||
|
||||
part 'cache.dart';
|
||||
|
||||
part 'cast.dart';
|
||||
|
||||
part 'chain.dart';
|
||||
|
||||
part 'check.dart';
|
||||
|
||||
part 'compare.dart';
|
||||
|
||||
part 'fold_errors.dart';
|
||||
|
||||
part 'index.dart';
|
||||
|
||||
part 'longest.dart';
|
||||
|
||||
part 'map.dart';
|
||||
|
||||
part 'match.dart';
|
||||
|
||||
part 'max_depth.dart';
|
||||
|
||||
part 'negate.dart';
|
||||
|
||||
part 'opt.dart';
|
||||
|
||||
part 'recursion.dart';
|
||||
|
||||
part 'reduce.dart';
|
||||
|
||||
part 'reference.dart';
|
||||
|
||||
part 'repeat.dart';
|
||||
|
||||
part 'safe.dart';
|
||||
|
||||
part 'to_list.dart';
|
||||
|
||||
part 'util.dart';
|
||||
|
||||
part 'value.dart';
|
||||
|
||||
class ParseArgs {
|
||||
final Trampoline trampoline;
|
||||
final SpanScanner scanner;
|
||||
final int depth;
|
||||
|
||||
ParseArgs(this.trampoline, this.scanner, this.depth);
|
||||
|
||||
ParseArgs increaseDepth() => ParseArgs(trampoline, scanner, depth + 1);
|
||||
}
|
||||
|
||||
/// A parser combinator, which can parse very complicated grammars in a manageable manner.
|
||||
abstract class Parser<T> {
|
||||
ParseResult<T> __parse(ParseArgs args);
|
||||
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var pos = args.scanner.position;
|
||||
|
||||
if (args.trampoline.hasMemoized(this, pos)) {
|
||||
return args.trampoline.getMemoized<T>(this, pos);
|
||||
}
|
||||
|
||||
if (args.trampoline.isActive(this, pos)) {
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
args.trampoline.enter(this, pos);
|
||||
var result = __parse(args);
|
||||
args.trampoline.memoize(this, pos, result);
|
||||
args.trampoline.exit(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Parses text from a [SpanScanner].
|
||||
ParseResult<T> parse(SpanScanner scanner, [int depth = 1]) {
|
||||
var args = ParseArgs(Trampoline(), scanner, depth);
|
||||
return _parse(args);
|
||||
}
|
||||
|
||||
/// Skips forward a certain amount of steps after parsing, if it was successful.
|
||||
Parser<T> forward(int amount) => _Advance<T>(this, amount);
|
||||
|
||||
/// Moves backward a certain amount of steps after parsing, if it was successful.
|
||||
Parser<T> back(int amount) => _Advance<T>(this, amount * -1);
|
||||
|
||||
/// Casts this parser to produce [U] objects.
|
||||
Parser<U> cast<U extends T>() => _Cast<T, U>(this);
|
||||
|
||||
/// Casts this parser to produce [dynamic] objects.
|
||||
Parser<dynamic> castDynamic() => _CastDynamic<T>(this);
|
||||
|
||||
/// Runs the given function, which changes the returned [ParseResult] into one relating to a [U] object.
|
||||
Parser<U> change<U>(ParseResult<U> Function(ParseResult<T>) f) {
|
||||
return _Change<T, U>(this, f);
|
||||
}
|
||||
|
||||
/// Validates the parse result against a [Matcher].
|
||||
///
|
||||
/// You can provide a custom [errorMessage].
|
||||
Parser<T> check(Matcher matcher,
|
||||
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Check<T>(
|
||||
this, matcher, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
/// Binds an [errorMessage] to a copy of this parser.
|
||||
Parser<T> error({String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Alt<T>(this, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
/// Removes multiple errors that occur in the same spot; this can reduce noise in parser output.
|
||||
Parser<T> foldErrors({bool Function(SyntaxError a, SyntaxError b)? equal}) {
|
||||
equal ??= (b, e) => b.span?.start.offset == e.span?.start.offset;
|
||||
return _FoldErrors<T>(this, equal);
|
||||
}
|
||||
|
||||
/// Transforms the parse result using a unary function.
|
||||
Parser<U> map<U>(U Function(ParseResult<T>) f) {
|
||||
return _Map<T, U>(this, f);
|
||||
}
|
||||
|
||||
/// Prevents recursion past a certain [depth], preventing stack overflow errors.
|
||||
Parser<T> maxDepth(int depth) => _MaxDepth<T>(this, depth);
|
||||
|
||||
Parser<T> operator ~() => negate();
|
||||
|
||||
/// Ensures this pattern is not matched.
|
||||
///
|
||||
/// You can provide an [errorMessage].
|
||||
Parser<T> negate(
|
||||
{String errorMessage = 'Negate error',
|
||||
SyntaxErrorSeverity severity = SyntaxErrorSeverity.error}) =>
|
||||
_Negate<T>(this, errorMessage, severity);
|
||||
|
||||
/// Caches the results of parse attempts at various locations within the source text.
|
||||
///
|
||||
/// Use this to prevent excessive recursion.
|
||||
Parser<T> cache() => _Cache<T>(this);
|
||||
|
||||
Parser<T> operator &(Parser<T> other) => and(other);
|
||||
|
||||
/// Consumes `this` and another parser, but only considers the result of `this` parser.
|
||||
Parser<T> and(Parser other) => then(other).change<T>((r) {
|
||||
return ParseResult<T>(
|
||||
r.trampoline,
|
||||
r.scanner,
|
||||
this,
|
||||
r.successful,
|
||||
r.errors,
|
||||
span: r.span,
|
||||
value: (r.value != null ? r.value![0] : r.value) as T?,
|
||||
);
|
||||
});
|
||||
|
||||
Parser<T> operator |(Parser<T> other) => or(other);
|
||||
|
||||
/// Shortcut for [or]-ing two parsers.
|
||||
Parser<T> or<U>(Parser<T> other) => any<T>([this, other]);
|
||||
|
||||
/// Parses this sequence one or more times.
|
||||
ListParser<T> plus() => times(1, exact: false);
|
||||
|
||||
/// Safely escapes this parser when an error occurs.
|
||||
///
|
||||
/// The generated parser only runs once; repeated uses always exit eagerly.
|
||||
Parser<T> safe(
|
||||
{bool backtrack = true,
|
||||
String errorMessage = 'error',
|
||||
SyntaxErrorSeverity? severity}) =>
|
||||
_Safe<T>(
|
||||
this, backtrack, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
Parser<List<T>> separatedByComma() =>
|
||||
separatedBy(match<List<T>>(',').space());
|
||||
|
||||
/// Expects to see an infinite amounts of the pattern, separated by the [other] pattern.
|
||||
///
|
||||
/// Use this as a shortcut to parse arrays, parameter lists, etc.
|
||||
Parser<List<T>> separatedBy(Parser other) {
|
||||
var suffix = other.then(this).index(1).cast<T>();
|
||||
return then(suffix.star()).map((r) {
|
||||
var v = r.value;
|
||||
if (v == null || v.length < 2) {
|
||||
return [];
|
||||
}
|
||||
var preceding = v.isEmpty ? [] : (v[0] == null ? [] : [v[0]]);
|
||||
var out = List<T>.from(preceding);
|
||||
if (v[1] != null && v[1] != 'NULL') {
|
||||
v[1].forEach((element) {
|
||||
out.add(element as T);
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
Parser<T> surroundedByCurlyBraces({required T defaultValue}) => opt()
|
||||
.surroundedBy(match('{').space(), match('}').space())
|
||||
.map((r) => r.value ?? defaultValue);
|
||||
|
||||
Parser<T> surroundedBySquareBrackets({required T defaultValue}) => opt()
|
||||
.surroundedBy(match('[').space(), match(']').space())
|
||||
.map((r) => r.value ?? defaultValue);
|
||||
|
||||
/// Expects to see the pattern, surrounded by the others.
|
||||
///
|
||||
/// If no [right] is provided, it expects to see the same pattern on both sides.
|
||||
/// Use this parse things like parenthesized expressions, arrays, etc.
|
||||
Parser<T> surroundedBy(Parser left, [Parser? right]) {
|
||||
return chain([
|
||||
left,
|
||||
this,
|
||||
right ?? left,
|
||||
]).index(1).castDynamic().cast<T>();
|
||||
}
|
||||
|
||||
/// Parses `this`, either as-is or wrapped in parentheses.
|
||||
Parser<T> maybeParenthesized() {
|
||||
return any([parenthesized(), this]);
|
||||
}
|
||||
|
||||
/// Parses `this`, wrapped in parentheses.
|
||||
Parser<T> parenthesized() =>
|
||||
surroundedBy(match('(').space(), match(')').space());
|
||||
|
||||
/// Consumes any trailing whitespace.
|
||||
Parser<T> space() => trail(RegExp(r'[ \n\r\t]+'));
|
||||
|
||||
/// Consumes 0 or more instance(s) of this parser.
|
||||
ListParser<T> star({bool backtrack = true}) =>
|
||||
times(1, exact: false, backtrack: backtrack).opt();
|
||||
|
||||
/// Shortcut for [chain]-ing two parsers together.
|
||||
ListParser<dynamic> then(Parser other) => chain<dynamic>([this, other]);
|
||||
|
||||
/// Casts this instance into a [ListParser].
|
||||
ListParser<T> toList() => _ToList<T>(this);
|
||||
|
||||
/// Consumes and ignores any trailing occurrences of [pattern].
|
||||
Parser<T> trail(Pattern pattern) =>
|
||||
then(match(pattern).opt()).first().cast<T>();
|
||||
|
||||
/// Expect this pattern a certain number of times.
|
||||
///
|
||||
/// If [exact] is `false` (default: `true`), then the generated parser will accept
|
||||
/// an infinite amount of occurrences after the specified [count].
|
||||
///
|
||||
/// You can provide custom error messages for when there are [tooFew] or [tooMany] occurrences.
|
||||
ListParser<T> times(int count,
|
||||
{bool exact = true,
|
||||
String tooFew = 'Too few',
|
||||
String tooMany = 'Too many',
|
||||
bool backtrack = true,
|
||||
SyntaxErrorSeverity? severity}) {
|
||||
return _Repeat<T>(this, count, exact, tooFew, tooMany, backtrack,
|
||||
severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
/// Produces an optional copy of this parser.
|
||||
///
|
||||
/// If [backtrack] is `true` (default), then a failed parse will not
|
||||
/// modify the scanner state.
|
||||
Parser<T> opt({bool backtrack = true}) => _Opt(this, backtrack);
|
||||
|
||||
/// Sets the value of the [ParseResult].
|
||||
Parser<T> value(T Function(ParseResult<T?>) f) {
|
||||
return _Value<T>(this, f);
|
||||
}
|
||||
|
||||
/// Prints a representation of this parser, ideally without causing a stack overflow.
|
||||
void stringify(CodeBuffer buffer);
|
||||
}
|
||||
|
||||
/// A [Parser] that produces [List]s of a type [T].
|
||||
abstract class ListParser<T> extends Parser<List<T>> {
|
||||
/// Shortcut for calling [index] with `0`.
|
||||
Parser<T> first() => index(0);
|
||||
|
||||
/// Modifies this parser to only return the value at the given index [i].
|
||||
Parser<T> index(int i) => _Index<T>(this, i);
|
||||
|
||||
/// Shortcut for calling [index] with the greatest-possible index.
|
||||
Parser<T> last() => index(-1);
|
||||
|
||||
/// Modifies this parser to call `List.reduce` on the parsed values.
|
||||
Parser<T> reduce(T Function(T, T) combine) => _Reduce<T>(this, combine);
|
||||
|
||||
/// Sorts the parsed values, using the given [Comparator].
|
||||
ListParser<T> sort(Comparator<T> compare) => _Compare(this, compare);
|
||||
|
||||
@override
|
||||
ListParser<T> opt({bool backtrack = true}) => _ListOpt(this, backtrack);
|
||||
|
||||
/// Modifies this parser, returning only the values that match a predicate.
|
||||
Parser<List<T>> where(bool Function(T) f) =>
|
||||
map<List<T>>((r) => r.value?.where(f).toList() ?? []);
|
||||
|
||||
/// Condenses a [ListParser] into having a value of the combined span's text.
|
||||
Parser<String> flatten() => map<String>((r) => r.span?.text ?? '');
|
||||
}
|
||||
|
||||
/// Prevents stack overflow in recursive parsers.
|
||||
class Trampoline {
|
||||
final Map<Parser, Queue<int>> _active = {};
|
||||
final Map<Parser, List<Tuple2<int, ParseResult>>> _memo = {};
|
||||
|
||||
bool hasMemoized(Parser parser, int position) {
|
||||
var list = _memo[parser];
|
||||
return list?.any((t) => t.item1 == position) == true;
|
||||
}
|
||||
|
||||
ParseResult<T> getMemoized<T>(Parser parser, int position) {
|
||||
return _memo[parser]?.firstWhere((t) => t.item1 == position).item2
|
||||
as ParseResult<T>;
|
||||
}
|
||||
|
||||
void memoize(Parser parser, int position, ParseResult? result) {
|
||||
if (result != null) {
|
||||
var list = _memo.putIfAbsent(parser, () => []);
|
||||
var tuple = Tuple2(position, result);
|
||||
if (!list.contains(tuple)) list.add(tuple);
|
||||
}
|
||||
}
|
||||
|
||||
bool isActive(Parser parser, int position) {
|
||||
if (!_active.containsKey(parser)) {
|
||||
return false;
|
||||
}
|
||||
var q = _active[parser]!;
|
||||
if (q.isEmpty) return false;
|
||||
//return q.contains(position);
|
||||
return q.first == position;
|
||||
}
|
||||
|
||||
void enter(Parser parser, int position) {
|
||||
_active.putIfAbsent(parser, () => Queue()).addFirst(position);
|
||||
}
|
||||
|
||||
void exit(Parser parser) {
|
||||
if (_active.containsKey(parser)) _active[parser]?.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/// The result generated by a [Parser].
|
||||
class ParseResult<T> {
|
||||
final Parser<T> parser;
|
||||
final bool successful;
|
||||
final Iterable<SyntaxError> errors;
|
||||
final FileSpan? span;
|
||||
final T? value;
|
||||
final SpanScanner scanner;
|
||||
final Trampoline trampoline;
|
||||
|
||||
ParseResult(
|
||||
this.trampoline, this.scanner, this.parser, this.successful, this.errors,
|
||||
{this.span, this.value});
|
||||
|
||||
ParseResult<T> change(
|
||||
{Parser<T>? parser,
|
||||
bool? successful,
|
||||
Iterable<SyntaxError> errors = const [],
|
||||
FileSpan? span,
|
||||
T? value}) {
|
||||
return ParseResult<T>(
|
||||
trampoline,
|
||||
scanner,
|
||||
parser ?? this.parser,
|
||||
successful ?? this.successful,
|
||||
errors.isNotEmpty ? errors : this.errors,
|
||||
span: span ?? this.span,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
|
||||
ParseResult<T> addErrors(Iterable<SyntaxError> errors) {
|
||||
return change(
|
||||
errors: List<SyntaxError>.from(this.errors)..addAll(errors),
|
||||
);
|
||||
}
|
||||
}
|
38
packages/combinator/lib/src/combinator/compare.dart
Normal file
38
packages/combinator/lib/src/combinator/compare.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Compare<T> extends ListParser<T> {
|
||||
final ListParser<T> parser;
|
||||
final Comparator<T> compare;
|
||||
|
||||
_Compare(this.parser, this.compare);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
if (!result.successful) return result;
|
||||
|
||||
result = result.change(
|
||||
value: result.value?.isNotEmpty == true ? result.value : []);
|
||||
result = result.change(value: List<T>.from(result.value!));
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: result.span,
|
||||
value: result.value?..sort(compare),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('sort($compare) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
29
packages/combinator/lib/src/combinator/fold_errors.dart
Normal file
29
packages/combinator/lib/src/combinator/fold_errors.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _FoldErrors<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final bool Function(SyntaxError, SyntaxError) equal;
|
||||
|
||||
_FoldErrors(this.parser, this.equal);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
var errors = result.errors.fold<List<SyntaxError>>([], (out, e) {
|
||||
if (!out.any((b) => equal(e, b))) out.add(e);
|
||||
return out;
|
||||
});
|
||||
return result.change(errors: errors);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('fold errors (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
52
packages/combinator/lib/src/combinator/index.dart
Normal file
52
packages/combinator/lib/src/combinator/index.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Index<T> extends Parser<T> {
|
||||
final ListParser<T> parser;
|
||||
final int index;
|
||||
|
||||
_Index(this.parser, this.index);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
Object? value;
|
||||
|
||||
if (result.successful) {
|
||||
var vList = result.value;
|
||||
if (vList == null) {
|
||||
throw ArgumentError('ParseResult is null');
|
||||
}
|
||||
if (index == -1) {
|
||||
value = vList.last;
|
||||
} else {
|
||||
if (index < vList.length) {
|
||||
// print(">>>>Index: $index, Size: ${vList.length}");
|
||||
// value =
|
||||
// index == -1 ? result.value!.last : result.value!.elementAt(index);
|
||||
value = result.value!.elementAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: value as T?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('index($index) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
115
packages/combinator/lib/src/combinator/longest.dart
Normal file
115
packages/combinator/lib/src/combinator/longest.dart
Normal file
|
@ -0,0 +1,115 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Matches any one of the given [parsers].
|
||||
///
|
||||
/// You can provide a custom [errorMessage].
|
||||
Parser<T> longest<T>(Iterable<Parser<T>> parsers,
|
||||
{Object? errorMessage, SyntaxErrorSeverity? severity}) {
|
||||
return _Longest(parsers, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Longest<T> extends Parser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final Object? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Longest(this.parsers, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var inactive = parsers
|
||||
.toList()
|
||||
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
|
||||
|
||||
if (inactive.isEmpty) {
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
var replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <ParseResult<T>>[];
|
||||
|
||||
for (var parser in inactive) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful && result.span != null) {
|
||||
results.add(result);
|
||||
} else if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
args.scanner.position = replay;
|
||||
}
|
||||
|
||||
if (results.isNotEmpty) {
|
||||
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
|
||||
args.scanner.scan(results.first.span!.text);
|
||||
return results.first;
|
||||
}
|
||||
|
||||
if (errorMessage != false) {
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <ParseResult<T>>[];
|
||||
|
||||
for (var parser in parsers) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful) {
|
||||
results.add(result);
|
||||
} else if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
args.scanner.position = replay;
|
||||
}
|
||||
|
||||
if (results.isNotEmpty) {
|
||||
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
|
||||
args.scanner.scan(results.first.span!.text);
|
||||
return results.first;
|
||||
}
|
||||
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('longest(${parsers.length}) (')
|
||||
..indent();
|
||||
var i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
56
packages/combinator/lib/src/combinator/map.dart
Normal file
56
packages/combinator/lib/src/combinator/map.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Map<T, U> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
final U Function(ParseResult<T>) f;
|
||||
|
||||
_Map(this.parser, this.f);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<U>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.successful ? f(result) : null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('map<$U> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _Change<T, U> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
final ParseResult<U> Function(ParseResult<T>) f;
|
||||
|
||||
_Change(this.parser, this.f);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
return f(parser._parse(args.increaseDepth())).change(parser: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('change($f) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
41
packages/combinator/lib/src/combinator/match.dart
Normal file
41
packages/combinator/lib/src/combinator/match.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Expects to match a given [pattern]. If it is not matched, you can provide a custom [errorMessage].
|
||||
Parser<T> match<T>(Pattern pattern,
|
||||
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Match<T>(pattern, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
class _Match<T> extends Parser<T> {
|
||||
final Pattern pattern;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Match(this.pattern, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var scanner = args.scanner;
|
||||
if (!scanner.scan(pattern)) {
|
||||
return ParseResult(args.trampoline, scanner, this, false, [
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage ?? 'Expected "$pattern".',
|
||||
scanner.emptySpan,
|
||||
),
|
||||
]);
|
||||
}
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: scanner.lastSpan,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer.writeln('match($pattern)');
|
||||
}
|
||||
}
|
28
packages/combinator/lib/src/combinator/max_depth.dart
Normal file
28
packages/combinator/lib/src/combinator/max_depth.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _MaxDepth<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final int cap;
|
||||
|
||||
_MaxDepth(this.parser, this.cap);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
if (args.depth > cap) {
|
||||
return ParseResult<T>(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
return parser._parse(args.increaseDepth());
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('max depth($cap) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
51
packages/combinator/lib/src/combinator/negate.dart
Normal file
51
packages/combinator/lib/src/combinator/negate.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Negate<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Negate(this.parser, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
|
||||
if (!result.successful) {
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: result.span ?? args.scanner.lastSpan ?? args.scanner.emptySpan,
|
||||
value: result.value,
|
||||
);
|
||||
}
|
||||
|
||||
result = result.change(successful: false);
|
||||
|
||||
if (errorMessage != null) {
|
||||
result = result.addErrors([
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage,
|
||||
result.span,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('negate (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
57
packages/combinator/lib/src/combinator/opt.dart
Normal file
57
packages/combinator/lib/src/combinator/opt.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Opt<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final bool backtrack;
|
||||
|
||||
_Opt(this.parser, this.backtrack);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) args.scanner.position = replay;
|
||||
|
||||
return result.change(parser: this, successful: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('optional (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _ListOpt<T> extends ListParser<T> {
|
||||
final ListParser<T> parser;
|
||||
final bool backtrack;
|
||||
|
||||
_ListOpt(this.parser, this.backtrack);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) args.scanner.position = replay;
|
||||
|
||||
return result.change(parser: this, successful: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('optional (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
142
packages/combinator/lib/src/combinator/recursion.dart
Normal file
142
packages/combinator/lib/src/combinator/recursion.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/*
|
||||
/// Handles left recursion in a grammar using the Pratt algorithm.
|
||||
class Recursion<T> {
|
||||
Iterable<Parser<T>> prefix;
|
||||
Map<Parser, T Function(T, T, ParseResult<T>)> infix;
|
||||
Map<Parser, T Function(T, T, ParseResult<T>)> postfix;
|
||||
|
||||
Recursion({this.prefix, this.infix, this.postfix}) {
|
||||
prefix ??= [];
|
||||
infix ??= {};
|
||||
postfix ??= {};
|
||||
}
|
||||
|
||||
Parser<T> precedence(int p) => _Precedence(this, p);
|
||||
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('recursion (')
|
||||
..indent()
|
||||
..writeln('prefix(${prefix.length}')
|
||||
..writeln('infix(${infix.length}')
|
||||
..writeln('postfix(${postfix.length}')
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _Precedence<T> extends Parser<T> {
|
||||
final Recursion r;
|
||||
final int precedence;
|
||||
|
||||
_Precedence(this.r, this.precedence);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
int replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var start = args.scanner.state;
|
||||
var reversedKeys = r.infix.keys.toList().reversed;
|
||||
|
||||
for (var pre in r.prefix) {
|
||||
var result = pre._parse(args.increaseDepth()), originalResult = result;
|
||||
|
||||
if (!result.successful) {
|
||||
if (pre is _Alt) errors.addAll(result.errors);
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
var left = result.value;
|
||||
replay = args.scanner.position;
|
||||
//print('${result.span.text}:\n' + scanner.emptySpan.highlight());
|
||||
|
||||
while (true) {
|
||||
bool matched = false;
|
||||
|
||||
//for (int i = 0; i < r.infix.length; i++) {
|
||||
for (int i = r.infix.length - 1; i >= 0; i--) {
|
||||
//var fix = r.infix.keys.elementAt(r.infix.length - i - 1);
|
||||
var fix = reversedKeys.elementAt(i);
|
||||
|
||||
if (i < precedence) continue;
|
||||
|
||||
var result = fix._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (fix is _Alt) errors.addAll(result.errors);
|
||||
// If this is the last alternative and it failed, don't continue looping.
|
||||
//if (true || i + 1 < r.infix.length)
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
//print('FOUND $fix when left was $left');
|
||||
//print('$i vs $precedence\n${originalResult.span.highlight()}');
|
||||
result = r.precedence(i)._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
} else {
|
||||
matched = false;
|
||||
var old = left;
|
||||
left = r.infix[fix](left, result.value, result);
|
||||
print(
|
||||
'$old $fix ${result.value} = $left\n${result.span.highlight()}');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) break;
|
||||
}
|
||||
|
||||
replay = args.scanner.position;
|
||||
//print('f ${result.span.text}');
|
||||
|
||||
for (var post in r.postfix.keys) {
|
||||
var result = pre._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (post is _Alt) errors.addAll(result.errors);
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
left = r.infix[post](left, originalResult.value, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.scanner.isDone) {
|
||||
// If we're not done scanning, then we need some sort of guard to ensure the
|
||||
// that this exact parser does not run again in the exact position.
|
||||
}
|
||||
return ParseResult(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
errors,
|
||||
value: left,
|
||||
span: args.scanner.spanFrom(start),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ParseResult(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
false,
|
||||
errors,
|
||||
span: args.scanner.spanFrom(start),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('precedence($precedence) (')
|
||||
..indent();
|
||||
r.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
*/
|
46
packages/combinator/lib/src/combinator/reduce.dart
Normal file
46
packages/combinator/lib/src/combinator/reduce.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Reduce<T> extends Parser<T> {
|
||||
final ListParser<T> parser;
|
||||
final T Function(T, T) combine;
|
||||
|
||||
_Reduce(this.parser, this.combine);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
false,
|
||||
result.errors,
|
||||
);
|
||||
}
|
||||
|
||||
result = result.change(
|
||||
value: result.value?.isNotEmpty == true ? result.value : []);
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
[],
|
||||
span: result.span,
|
||||
value: result.value!.isEmpty ? null : result.value!.reduce(combine),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('reduce($combine) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
44
packages/combinator/lib/src/combinator/reference.dart
Normal file
44
packages/combinator/lib/src/combinator/reference.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
Reference<T> reference<T>() => Reference<T>._();
|
||||
|
||||
class Reference<T> extends Parser<T> {
|
||||
Parser<T>? _parser;
|
||||
bool printed = false;
|
||||
|
||||
Reference._();
|
||||
|
||||
set parser(Parser<T> value) {
|
||||
if (_parser != null) {
|
||||
throw StateError('There is already a parser assigned to this reference.');
|
||||
}
|
||||
_parser = value;
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
if (_parser == null) {
|
||||
throw StateError('There is no parser assigned to this reference.');
|
||||
}
|
||||
return _parser!._parse(args);
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
if (_parser == null) {
|
||||
throw StateError('There is no parser assigned to this reference.');
|
||||
}
|
||||
return _parser!._parse(args);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
if (_parser == null) {
|
||||
buffer.writeln('(undefined reference <$T>)');
|
||||
} else if (!printed) {
|
||||
_parser!.stringify(buffer);
|
||||
}
|
||||
printed = true;
|
||||
buffer.writeln('(previously printed reference)');
|
||||
}
|
||||
}
|
89
packages/combinator/lib/src/combinator/repeat.dart
Normal file
89
packages/combinator/lib/src/combinator/repeat.dart
Normal file
|
@ -0,0 +1,89 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Repeat<T> extends ListParser<T> {
|
||||
final Parser<T> parser;
|
||||
final int count;
|
||||
final bool exact, backtrack;
|
||||
final String tooFew;
|
||||
final String tooMany;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Repeat(this.parser, this.count, this.exact, this.tooFew, this.tooMany,
|
||||
this.backtrack, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <T>[];
|
||||
var spans = <FileSpan>[];
|
||||
var success = 0;
|
||||
var replay = args.scanner.position;
|
||||
ParseResult<T> result;
|
||||
|
||||
do {
|
||||
result = parser._parse(args.increaseDepth());
|
||||
if (result.successful) {
|
||||
success++;
|
||||
if (result.value != null) {
|
||||
results.add(result.value!);
|
||||
}
|
||||
replay = args.scanner.position;
|
||||
} else if (backtrack) args.scanner.position = replay;
|
||||
|
||||
if (result.span != null) {
|
||||
spans.add(result.span!);
|
||||
}
|
||||
} while (result.successful);
|
||||
|
||||
if (success < count) {
|
||||
errors.addAll(result.errors);
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
tooFew,
|
||||
result.span ?? args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
|
||||
if (backtrack) args.scanner.position = replay;
|
||||
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline, args.scanner, this, false, errors);
|
||||
} else if (success > count && exact) {
|
||||
if (backtrack) args.scanner.position = replay;
|
||||
|
||||
return ParseResult<List<T>>(args.trampoline, args.scanner, this, false, [
|
||||
SyntaxError(
|
||||
severity,
|
||||
tooMany,
|
||||
result.span ?? args.scanner.emptySpan,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
var span = spans.reduce((a, b) => a.expand(b));
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: span,
|
||||
value: results,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
var r = StringBuffer('{$count');
|
||||
if (!exact) r.write(',');
|
||||
r.write('}');
|
||||
buffer
|
||||
..writeln('repeat($r) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
47
packages/combinator/lib/src/combinator/safe.dart
Normal file
47
packages/combinator/lib/src/combinator/safe.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Safe<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final bool backtrack;
|
||||
final String errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
bool _triggered = false;
|
||||
|
||||
_Safe(this.parser, this.backtrack, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
|
||||
try {
|
||||
if (_triggered) throw Exception();
|
||||
return parser._parse(args.increaseDepth());
|
||||
} catch (_) {
|
||||
_triggered = true;
|
||||
if (backtrack) args.scanner.position = replay;
|
||||
var errors = <SyntaxError>[];
|
||||
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage,
|
||||
args.scanner.lastSpan ?? args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
|
||||
return ParseResult<T>(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
var t = _triggered ? 'triggered' : 'not triggered';
|
||||
buffer
|
||||
..writeln('safe($t) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
41
packages/combinator/lib/src/combinator/to_list.dart
Normal file
41
packages/combinator/lib/src/combinator/to_list.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _ToList<T> extends ListParser<T> {
|
||||
final Parser<T> parser;
|
||||
|
||||
_ToList(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.value is List) {
|
||||
return (result as ParseResult<List<T>>).change(parser: this);
|
||||
}
|
||||
|
||||
var values = <T>[];
|
||||
if (result.value != null) {
|
||||
values.add(result.value!);
|
||||
}
|
||||
return ParseResult(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: values,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('to list (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
57
packages/combinator/lib/src/combinator/util.dart
Normal file
57
packages/combinator/lib/src/combinator/util.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// A typed parser that parses a sequence of 2 values of different types.
|
||||
Parser<Tuple2<A, B>> tuple2<A, B>(Parser<A> a, Parser<B> b) {
|
||||
return chain([a, b]).map((r) {
|
||||
return Tuple2(r.value?[0] as A, r.value?[1] as B);
|
||||
});
|
||||
}
|
||||
|
||||
/// A typed parser that parses a sequence of 3 values of different types.
|
||||
Parser<Tuple3<A, B, C>> tuple3<A, B, C>(Parser<A> a, Parser<B> b, Parser<C> c) {
|
||||
return chain([a, b, c]).map((r) {
|
||||
return Tuple3(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C);
|
||||
});
|
||||
}
|
||||
|
||||
/// A typed parser that parses a sequence of 4 values of different types.
|
||||
Parser<Tuple4<A, B, C, D>> tuple4<A, B, C, D>(
|
||||
Parser<A> a, Parser<B> b, Parser<C> c, Parser<D> d) {
|
||||
return chain([a, b, c, d]).map((r) {
|
||||
return Tuple4(
|
||||
r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, r.value?[3] as D);
|
||||
});
|
||||
}
|
||||
|
||||
/// A typed parser that parses a sequence of 5 values of different types.
|
||||
Parser<Tuple5<A, B, C, D, E>> tuple5<A, B, C, D, E>(
|
||||
Parser<A> a, Parser<B> b, Parser<C> c, Parser<D> d, Parser<E> e) {
|
||||
return chain([a, b, c, d, e]).map((r) {
|
||||
return Tuple5(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
|
||||
r.value?[3] as D, r.value?[4] as E);
|
||||
});
|
||||
}
|
||||
|
||||
/// A typed parser that parses a sequence of 6 values of different types.
|
||||
Parser<Tuple6<A, B, C, D, E, F>> tuple6<A, B, C, D, E, F>(Parser<A> a,
|
||||
Parser<B> b, Parser<C> c, Parser<D> d, Parser<E> e, Parser<F> f) {
|
||||
return chain([a, b, c, d, e, f]).map((r) {
|
||||
return Tuple6(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
|
||||
r.value?[3] as D, r.value?[4] as E, r.value?[5] as F);
|
||||
});
|
||||
}
|
||||
|
||||
/// A typed parser that parses a sequence of 7 values of different types.
|
||||
Parser<Tuple7<A, B, C, D, E, F, G>> tuple7<A, B, C, D, E, F, G>(
|
||||
Parser<A> a,
|
||||
Parser<B> b,
|
||||
Parser<C> c,
|
||||
Parser<D> d,
|
||||
Parser<E> e,
|
||||
Parser<F> f,
|
||||
Parser<G> g) {
|
||||
return chain([a, b, c, d, e, f, g]).map((r) {
|
||||
return Tuple7(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
|
||||
r.value?[3] as D, r.value?[4] as E, r.value?[5] as F, r.value?[6] as G);
|
||||
});
|
||||
}
|
25
packages/combinator/lib/src/combinator/value.dart
Normal file
25
packages/combinator/lib/src/combinator/value.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Value<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final T Function(ParseResult<T>) f;
|
||||
|
||||
_Value(this.parser, this.f);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
return result.successful ? result.change(value: f(result)) : result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('set value($f) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
23
packages/combinator/lib/src/error.dart
Normal file
23
packages/combinator/lib/src/error.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
|
||||
class SyntaxError implements Exception {
|
||||
final SyntaxErrorSeverity severity;
|
||||
final String? message;
|
||||
final FileSpan? span;
|
||||
String? _toolString;
|
||||
|
||||
SyntaxError(this.severity, this.message, this.span);
|
||||
|
||||
String? get toolString {
|
||||
if (_toolString != null) return _toolString;
|
||||
var type = severity == SyntaxErrorSeverity.warning ? 'warning' : 'error';
|
||||
return _toolString = '$type: ${span!.start.toolString}: $message';
|
||||
}
|
||||
}
|
||||
|
||||
enum SyntaxErrorSeverity {
|
||||
warning,
|
||||
error,
|
||||
info,
|
||||
hint,
|
||||
}
|
15
packages/combinator/pubspec.yaml
Normal file
15
packages/combinator/pubspec.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: belatuk_combinator
|
||||
version: 3.0.0
|
||||
description: Packrat parser combinators that support static typing, generics, file spans, memoization, and more.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/combinator
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
belatuk_code_buffer: ^3.0.0
|
||||
matcher: ^0.12.10
|
||||
source_span: ^1.8.1
|
||||
string_scanner: ^1.1.0
|
||||
tuple: ^2.0.0
|
||||
dev_dependencies:
|
||||
test: ^1.17.4
|
||||
lints: ^1.0.0
|
12
packages/combinator/test/all.dart
Normal file
12
packages/combinator/test/all.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'list_test.dart' as list;
|
||||
import 'match_test.dart' as match;
|
||||
import 'misc_test.dart' as misc;
|
||||
import 'value_test.dart' as value;
|
||||
|
||||
void main() {
|
||||
group('list', list.main);
|
||||
group('match', match.main);
|
||||
group('value', value.main);
|
||||
misc.main();
|
||||
}
|
3
packages/combinator/test/common.dart
Normal file
3
packages/combinator/test/common.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
SpanScanner scan(String text) => SpanScanner(text);
|
22
packages/combinator/test/list_test.dart
Normal file
22
packages/combinator/test/list_test.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
var number = chain([
|
||||
match(RegExp(r'[0-9]+')).value((r) => int.parse(r.span!.text)),
|
||||
match(',').opt(),
|
||||
]).first().cast<int>();
|
||||
|
||||
var numbers = number.plus();
|
||||
|
||||
test('sort', () {
|
||||
var parser = numbers.sort((a, b) => a.compareTo(b));
|
||||
expect(parser.parse(scan('21,2,3,34,20')).value, [2, 3, 20, 21, 34]);
|
||||
});
|
||||
test('reduce', () {
|
||||
var parser = numbers.reduce((a, b) => a + b);
|
||||
expect(parser.parse(scan('21,2,3,34,20')).value, 80);
|
||||
expect(parser.parse(scan('not numbers')).value, isNull);
|
||||
});
|
||||
}
|
16
packages/combinator/test/match_test.dart
Normal file
16
packages/combinator/test/match_test.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
test('match string', () {
|
||||
expect(match('hello').parse(scan('hello world')).successful, isTrue);
|
||||
});
|
||||
test('match start only', () {
|
||||
expect(match('hello').parse(scan('goodbye hello')).successful, isFalse);
|
||||
});
|
||||
|
||||
test('fail if no match', () {
|
||||
expect(match('hello').parse(scan('world')).successful, isFalse);
|
||||
});
|
||||
}
|
66
packages/combinator/test/misc_test.dart
Normal file
66
packages/combinator/test/misc_test.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:matcher/matcher.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
test('advance', () {
|
||||
var scanner = scan('hello world');
|
||||
|
||||
// Casted -> dynamic just for the sake of coverage.
|
||||
var parser = match('he').forward(2).castDynamic();
|
||||
parser.parse(scanner);
|
||||
expect(scanner.position, 4);
|
||||
});
|
||||
|
||||
test('change', () {
|
||||
var parser = match('hello').change((r) => r.change(value: 23));
|
||||
expect(parser.parse(scan('helloworld')).value, 23);
|
||||
});
|
||||
|
||||
test('check', () {
|
||||
var parser = match<int>(RegExp(r'[A-Za-z]+'))
|
||||
.value((r) => r.span!.length)
|
||||
.check(greaterThan(3));
|
||||
expect(parser.parse(scan('helloworld')).successful, isTrue);
|
||||
expect(parser.parse(scan('yo')).successful, isFalse);
|
||||
});
|
||||
|
||||
test('map', () {
|
||||
var parser = match(RegExp(r'[A-Za-z]+')).map<int>((r) => r.span!.length);
|
||||
expect(parser.parse(scan('hello')).value, 5);
|
||||
});
|
||||
|
||||
test('negate', () {
|
||||
var parser = match('hello').negate(errorMessage: 'world');
|
||||
expect(parser.parse(scan('goodbye world')).successful, isTrue);
|
||||
expect(parser.parse(scan('hello world')).successful, isFalse);
|
||||
expect(parser.parse(scan('hello world')).errors.first.message, 'world');
|
||||
});
|
||||
|
||||
group('opt', () {
|
||||
var single = match('hello').opt(backtrack: true);
|
||||
var list = match('hel').then(match('lo')).opt();
|
||||
|
||||
test('succeeds if present', () {
|
||||
expect(single.parse(scan('hello')).successful, isTrue);
|
||||
expect(list.parse(scan('hello')).successful, isTrue);
|
||||
});
|
||||
|
||||
test('succeeds if not present', () {
|
||||
expect(single.parse(scan('goodbye')).successful, isTrue);
|
||||
expect(list.parse(scan('goodbye')).successful, isTrue);
|
||||
});
|
||||
|
||||
test('backtracks if not present', () {
|
||||
for (var parser in [single, list]) {
|
||||
var scanner = scan('goodbye');
|
||||
var pos = scanner.position;
|
||||
parser.parse(scanner);
|
||||
expect(scanner.position, pos);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('safe', () {});
|
||||
}
|
53
packages/combinator/test/recursion_test.dart
Normal file
53
packages/combinator/test/recursion_test.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
void main() {}
|
||||
|
||||
/*
|
||||
void main() {
|
||||
var number = match( RegExp(r'-?[0-9]+(\.[0-9]+)?'))
|
||||
.map<num>((r) => num.parse(r.span.text));
|
||||
|
||||
var term = reference<num>();
|
||||
|
||||
var r = Recursion<num>();
|
||||
|
||||
r.prefix = [number];
|
||||
|
||||
r.infix.addAll({
|
||||
match('*'): (l, r, _) => l * r,
|
||||
match('/'): (l, r, _) => l / r,
|
||||
match('+'): (l, r, _) => l + r,
|
||||
match('-'): (l, r, _) => l - r,
|
||||
|
||||
|
||||
match('-'): (l, r, _) => l - r,
|
||||
match('+'): (l, r, _) => l + r,
|
||||
match('/'): (l, r, _) => l / r,
|
||||
match('*'): (l, r, _) => l * r,
|
||||
|
||||
});
|
||||
|
||||
term.parser = r.precedence(0);
|
||||
|
||||
num parse(String text) {
|
||||
var scanner = SpanScanner(text);
|
||||
var result = term.parse(scanner);
|
||||
print(result.span.highlight());
|
||||
return result.value;
|
||||
}
|
||||
|
||||
test('prefix', () {
|
||||
expect(parse('24'), 24);
|
||||
});
|
||||
|
||||
test('infix', () {
|
||||
expect(parse('12/6'), 2);
|
||||
expect(parse('24+23'), 47);
|
||||
expect(parse('24-23'), 1);
|
||||
expect(parse('4*3'), 12);
|
||||
});
|
||||
|
||||
test('precedence', () {
|
||||
expect(parse('2+3*5*2'), 15);
|
||||
//expect(parse('2+3+5-2*2'), 15);
|
||||
});
|
||||
}
|
||||
*/
|
15
packages/combinator/test/value_test.dart
Normal file
15
packages/combinator/test/value_test.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
import 'package:angel3_combinator/belatuk_combinator.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
var parser = match('hello').value((r) => 'world');
|
||||
|
||||
test('sets value', () {
|
||||
expect(parser.parse(scan('hello world')).value, 'world');
|
||||
});
|
||||
|
||||
test('no value if no match', () {
|
||||
expect(parser.parse(scan('goodbye world')).value, isNull);
|
||||
});
|
||||
}
|
1
packages/pub_sub/.travis.yml
Normal file
1
packages/pub_sub/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
12
packages/pub_sub/AUTHORS.md
Normal file
12
packages/pub_sub/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
36
packages/pub_sub/CHANGELOG.md
Normal file
36
packages/pub_sub/CHANGELOG.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Change Log
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Published as `belatuk_combinator` package
|
||||
|
||||
## 3.0.2
|
||||
|
||||
* Resolved static analysis warnings
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Resolved static analysis warnings
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to work with Dart SDK 2.12.x NNBD
|
||||
|
||||
## 2.3.0
|
||||
|
||||
* Allow `2.x` versions of `stream_channel`.
|
||||
* Apply `package:pedantic` lints.
|
||||
|
||||
## 2.2.0
|
||||
|
||||
* Upgrade `uuid`.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
* Allow for "trusted clients," which are implicitly-registered clients.
|
||||
This makes using `package:pub_sub` easier, as well making it easier to scale.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Dart 2 updates.
|
29
packages/pub_sub/LICENSE
Normal file
29
packages/pub_sub/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
230
packages/pub_sub/README.md
Normal file
230
packages/pub_sub/README.md
Normal file
|
@ -0,0 +1,230 @@
|
|||
# Belatuk Pub Sub
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_pub_sub)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/pub_sub/LICENSE)
|
||||
|
||||
Keep application instances in sync with a simple pub/sub API.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `belatuk_pub_sub` as a dependency in your `pubspec.yaml` file:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
belatuk_pub_sub: ^4.0.0
|
||||
```
|
||||
|
||||
Then, be sure to run `pub get` in your terminal.
|
||||
|
||||
## Usage
|
||||
|
||||
`belatuk_pub_sub` is your typical pub/sub API. However, `belatuk_pub_sub` enforces authentication of every
|
||||
request. It is very possible that `belatuk_pub_sub` will run on both servers and in the browser,
|
||||
or on a platform belatuk_pub_sublike Flutter. Thus, there are provisions available to limit
|
||||
access.
|
||||
|
||||
**Be careful to not leak any `belatuk_pub_sub` client ID's if operating over a network.**
|
||||
If you do, you risk malicious users injecting events into your application, which
|
||||
could ultimately spell *disaster*.
|
||||
|
||||
A `belatuk_pub_sub` server can operate across multiple *adapters*, which take care of interfacing data over different
|
||||
media. For example, a single server can handle pub/sub between multiple Isolates and TCP Sockets, as well as
|
||||
WebSockets, simultaneously.
|
||||
|
||||
```dart
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
|
||||
|
||||
main() async {
|
||||
var server = pub_sub.Server([
|
||||
FooAdapter(...),
|
||||
BarAdapter(...)
|
||||
]);
|
||||
|
||||
server.addAdapter( BazAdapter(...));
|
||||
|
||||
// Call `start` to activate adapters, and begin handling requests.
|
||||
server.start();
|
||||
}
|
||||
```
|
||||
|
||||
### Trusted Clients
|
||||
|
||||
You can use `package:belatuk_pub_sub` without explicitly registering
|
||||
clients, *if and only if* those clients come from trusted sources.
|
||||
|
||||
Clients via `Isolate` are always trusted.
|
||||
|
||||
Clients via `package:json_rpc_2` must be explicitly marked
|
||||
as trusted (i.e. using an IP whitelist mechanism):
|
||||
|
||||
```dart
|
||||
JsonRpc2Adapter(..., isTrusted: false);
|
||||
|
||||
// Pass `null` as Client ID when trusted...
|
||||
pub_sub.IsolateClient(null);
|
||||
```
|
||||
|
||||
### Access Control
|
||||
|
||||
The ID's of all *untrusted* clients who will connect to the server must be known at start-up time.
|
||||
You may not register new clients after the server has started. This is mostly a security consideration;
|
||||
if it is impossible to register new clients, then malicious users cannot grant themselves additional
|
||||
privileges within the system.
|
||||
|
||||
```dart
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
|
||||
|
||||
void main() async {
|
||||
// ...
|
||||
server.registerClient(const ClientInfo('<client-id>'));
|
||||
|
||||
// Create a user who can subscribe, but not publish.
|
||||
server.registerClient(const ClientInfo('<client-id>', canPublish: false));
|
||||
|
||||
// Create a user who can publish, but not subscribe.
|
||||
server.registerClient(const ClientInfo('<client-id>', canSubscribe: false));
|
||||
|
||||
// Create a user with no privileges whatsoever.
|
||||
server.registerClient(const ClientInfo('<client-id>', canPublish: false, canSubscribe: false));
|
||||
|
||||
server.start();
|
||||
}
|
||||
```
|
||||
|
||||
### Isolates
|
||||
|
||||
If you are just running multiple instances of a server,
|
||||
use `package:belatuk_pub_sub/isolate.dart`.
|
||||
|
||||
You'll need one isolate to be the master. Typically this is the first isolate you create.
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:belatuk_pub_sub/isolate.dart' as pub_sub;
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
|
||||
|
||||
void main() async {
|
||||
// Easily bring up a server.
|
||||
var adapter = pub_sub.IsolateAdapter();
|
||||
var server = pub_sub.Server([adapter]);
|
||||
|
||||
// You then need to create a client that will connect to the adapter.
|
||||
// Each isolate in your application should contain a client.
|
||||
for (int i = 0; i < Platform.numberOfProcessors - 1; i++) {
|
||||
server.registerClient(pub_sub.ClientInfo('client$i'));
|
||||
}
|
||||
|
||||
// Start the server.
|
||||
server.start();
|
||||
|
||||
// Next, let's start isolates that interact with the server.
|
||||
//
|
||||
// Fortunately, we can send SendPorts over Isolates, so this is no hassle.
|
||||
for (int i = 0; i < Platform.numberOfProcessors - 1; i++)
|
||||
Isolate.spawn(isolateMain, [i, adapter.receivePort.sendPort]);
|
||||
|
||||
// It's possible that you're running your application in the server isolate as well:
|
||||
isolateMain([0, adapter.receivePort.sendPort]);
|
||||
}
|
||||
|
||||
void isolateMain(List args) {
|
||||
var client =
|
||||
pub_sub.IsolateClient('client${args[0]}', args[1] as SendPort);
|
||||
|
||||
// The client will connect automatically. In the meantime, we can start subscribing to events.
|
||||
client.subscribe('user::logged_in').then((sub) {
|
||||
// The `ClientSubscription` class extends `Stream`. Hooray for asynchrony!
|
||||
sub.listen((msg) {
|
||||
print('Logged in: $msg');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### JSON RPC 2.0
|
||||
|
||||
If you are not running on isolates, you need to import
|
||||
`package:belatuk_pub_sub/json_rpc_2.dart`. This library leverages `package:json_rpc_2` and
|
||||
`package:stream_channel` to create clients and servers that can hypothetically run on any
|
||||
medium, i.e. WebSockets, or TCP Sockets.
|
||||
|
||||
Check out `test/json_rpc_2_test.dart` for an example of serving `belatuk_pub_sub` over TCP sockets.
|
||||
|
||||
## Protocol
|
||||
|
||||
`belatuk_pub_sub` is built upon a simple RPC, and this package includes
|
||||
an implementation that runs via `SendPort`s and `ReceivePort`s, as
|
||||
well as one that runs on any `StreamChannel<String>`.
|
||||
|
||||
Data sent over the wire looks like the following:
|
||||
|
||||
```typescript
|
||||
// Sent by a client to initiate an exchange.
|
||||
interface Request {
|
||||
// This is an arbitrary string, assigned by your client, but in every case,
|
||||
// the client uses this to match your requests with asynchronous responses.
|
||||
request_id: string,
|
||||
|
||||
// The ID of the client to authenticate as.
|
||||
//
|
||||
// As you can imagine, this should be kept secret, to prevent breaches.
|
||||
client_id: string,
|
||||
|
||||
// Required for *every* request.
|
||||
params: {
|
||||
// A value to be `publish`ed.
|
||||
value?: any,
|
||||
|
||||
// The name of an event to `publish`.
|
||||
event_name?: string,
|
||||
|
||||
// The ID of a subscription to be cancelled.
|
||||
subscription_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server in response to a request.
|
||||
interface Response {
|
||||
// `true` for success, `false` for failures.
|
||||
status: boolean,
|
||||
|
||||
// Only appears if `status` is `false`; explains why an operation failed.
|
||||
error_message?: string,
|
||||
|
||||
// Matches the request_id sent by the client.
|
||||
request_id: string,
|
||||
|
||||
result?: {
|
||||
// The number of other clients to whom an event was `publish`ed.
|
||||
listeners:? number,
|
||||
|
||||
// The ID of a created subscription.
|
||||
subscription_id?: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When sending via JSON_RPC 2.0, the `params` of a `Request` are simply folded into the object
|
||||
itself, for simplicity's sake. In this case, a response will be sent as a notification whose
|
||||
name is the `request_id`.
|
||||
|
||||
In the case of Isolate clients/servers, events will be simply sent as Lists:
|
||||
|
||||
```dart
|
||||
['<event-name>', value]
|
||||
```
|
||||
|
||||
Clients can send the following (3) methods:
|
||||
|
||||
* `subscribe` (`event_name`:string): Subscribe to an event.
|
||||
* `unsubscribe` (`subscription_id`:string): Unsubscribe from an event you previously subscribed to.
|
||||
* `publish` (`event_name`:string, `value`:any): Publish an event to all other clients who are subscribed.
|
||||
|
||||
The client and server in `package:belatuk_pub_sub/isolate.dart` must make extra
|
||||
provisions to keep track of client ID's. Since `SendPort`s and `ReceivePort`s
|
||||
do not have any sort of guaranteed-unique ID's, new clients must send their
|
||||
`SendPort` to the server before sending any requests. The server then responds
|
||||
with an `id` that must be used to identify a `SendPort` to send a response to.
|
1
packages/pub_sub/analysis_options.yaml
Normal file
1
packages/pub_sub/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
44
packages/pub_sub/example/main.dart
Normal file
44
packages/pub_sub/example/main.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:belatuk_pub_sub/isolate.dart' as pub_sub;
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart' as pub_sub;
|
||||
|
||||
void main() async {
|
||||
// Easily bring up a server.
|
||||
var adapter = pub_sub.IsolateAdapter();
|
||||
var server = pub_sub.Server([adapter]);
|
||||
|
||||
// You then need to create a client that will connect to the adapter.
|
||||
// Every untrusted client in your application should be pre-registered.
|
||||
//
|
||||
// In the case of Isolates, however, those are always implicitly trusted.
|
||||
for (var i = 0; i < Platform.numberOfProcessors - 1; i++) {
|
||||
server.registerClient(pub_sub.ClientInfo('client$i'));
|
||||
}
|
||||
|
||||
// Start the server.
|
||||
server.start();
|
||||
|
||||
// Next, let's start isolates that interact with the server.
|
||||
//
|
||||
// Fortunately, we can send SendPorts over Isolates, so this is no hassle.
|
||||
for (var i = 0; i < Platform.numberOfProcessors - 1; i++) {
|
||||
await Isolate.spawn(isolateMain, [i, adapter.receivePort.sendPort]);
|
||||
}
|
||||
|
||||
// It's possible that you're running your application in the server isolate as well:
|
||||
isolateMain([0, adapter.receivePort.sendPort]);
|
||||
}
|
||||
|
||||
void isolateMain(List args) {
|
||||
// Isolates are always trusted, so technically we don't need to pass a client iD.
|
||||
var client = pub_sub.IsolateClient('client${args[0]}', args[1] as SendPort);
|
||||
|
||||
// The client will connect automatically. In the meantime, we can start subscribing to events.
|
||||
client.subscribe('user::logged_in').then((sub) {
|
||||
// The `ClientSubscription` class extends `Stream`. Hooray for asynchrony!
|
||||
sub.listen((msg) {
|
||||
print('Logged in: $msg');
|
||||
});
|
||||
});
|
||||
}
|
1
packages/pub_sub/lib/belatuk_pub_sub.dart
Normal file
1
packages/pub_sub/lib/belatuk_pub_sub.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/protocol/protocol.dart';
|
2
packages/pub_sub/lib/isolate.dart
Normal file
2
packages/pub_sub/lib/isolate.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/isolate/client.dart';
|
||||
export 'src/isolate/server.dart';
|
2
packages/pub_sub/lib/json_rpc_2.dart
Normal file
2
packages/pub_sub/lib/json_rpc_2.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/json_rpc/client.dart';
|
||||
export 'src/json_rpc/server.dart';
|
186
packages/pub_sub/lib/src/isolate/client.dart
Normal file
186
packages/pub_sub/lib/src/isolate/client.dart
Normal file
|
@ -0,0 +1,186 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:isolate';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../belatuk_pub_sub.dart';
|
||||
|
||||
/// A [Client] implementation that communicates via [SendPort]s and [ReceivePort]s.
|
||||
class IsolateClient extends Client {
|
||||
final Queue<Completer<String>> _onConnect = Queue<Completer<String>>();
|
||||
final Map<String, Completer<Map>> _requests = {};
|
||||
final List<_IsolateClientSubscription> _subscriptions = [];
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
String? _id;
|
||||
|
||||
/// The ID of the client we are authenticating as.
|
||||
///
|
||||
/// May be `null`, if and only if we are marked as a trusted source on
|
||||
/// the server side.
|
||||
String? get clientId => _clientId;
|
||||
String? _clientId;
|
||||
|
||||
/// A server's [SendPort] that messages should be sent to.
|
||||
final SendPort serverSendPort;
|
||||
|
||||
/// A [ReceivePort] that receives messages from the server.
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
|
||||
IsolateClient(String? clientId, this.serverSendPort) {
|
||||
_clientId = clientId;
|
||||
receivePort.listen((data) {
|
||||
if (data is Map && data['request_id'] is String) {
|
||||
var requestId = data['request_id'] as String?;
|
||||
var c = _requests.remove(requestId);
|
||||
|
||||
if (c != null && !c.isCompleted) {
|
||||
if (data['status'] is! bool) {
|
||||
c.completeError(
|
||||
FormatException('The server sent an invalid response.'));
|
||||
} else if (!(data['status'] as bool)) {
|
||||
c.completeError(PubSubException(data['error_message']?.toString() ??
|
||||
'The server sent a failure response, but did not provide an error message.'));
|
||||
} else if (data['result'] is! Map) {
|
||||
c.completeError(FormatException(
|
||||
'The server sent a success response, but did not include a result.'));
|
||||
} else {
|
||||
c.complete(data['result'] as Map?);
|
||||
}
|
||||
}
|
||||
} else if (data is Map && data['id'] is String && _id == null) {
|
||||
_id = data['id'] as String?;
|
||||
|
||||
for (var c in _onConnect) {
|
||||
if (!c.isCompleted) c.complete(_id);
|
||||
}
|
||||
|
||||
_onConnect.clear();
|
||||
} else if (data is List && data.length == 2 && data[0] is String) {
|
||||
var eventName = data[0] as String;
|
||||
var event = data[1];
|
||||
for (var s in _subscriptions.where((s) => s.eventName == eventName)) {
|
||||
if (!s._stream.isClosed) s._stream.add(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
serverSendPort.send(receivePort.sendPort);
|
||||
}
|
||||
|
||||
Future<T> _whenConnected<T>(FutureOr<T> Function() callback) {
|
||||
if (_id != null) {
|
||||
return Future<T>.sync(callback);
|
||||
} else {
|
||||
var c = Completer<String>();
|
||||
_onConnect.add(c);
|
||||
return c.future.then<T>((_) => callback());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future publish(String eventName, value) {
|
||||
return _whenConnected(() {
|
||||
var c = Completer<Map>();
|
||||
var requestId = _uuid.v4();
|
||||
_requests[requestId] = c;
|
||||
serverSendPort.send({
|
||||
'id': _id,
|
||||
'request_id': requestId,
|
||||
'method': 'publish',
|
||||
'params': {
|
||||
'client_id': clientId,
|
||||
'event_name': eventName,
|
||||
'value': value
|
||||
}
|
||||
});
|
||||
return c.future.then((result) {
|
||||
_clientId = result['client_id'] as String?;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ClientSubscription> subscribe(String eventName) {
|
||||
return _whenConnected<ClientSubscription>(() {
|
||||
var c = Completer<Map>();
|
||||
var requestId = _uuid.v4();
|
||||
_requests[requestId] = c;
|
||||
serverSendPort.send({
|
||||
'id': _id,
|
||||
'request_id': requestId,
|
||||
'method': 'subscribe',
|
||||
'params': {'client_id': clientId, 'event_name': eventName}
|
||||
});
|
||||
return c.future.then<ClientSubscription>((result) {
|
||||
_clientId = result['client_id'] as String?;
|
||||
var s = _IsolateClientSubscription(
|
||||
eventName, result['subscription_id'] as String?, this);
|
||||
_subscriptions.add(s);
|
||||
return s;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
receivePort.close();
|
||||
|
||||
for (var c in _onConnect) {
|
||||
if (!c.isCompleted) {
|
||||
c.completeError(StateError(
|
||||
'The client was closed before the server ever accepted the connection.'));
|
||||
}
|
||||
}
|
||||
|
||||
for (var c in _requests.values) {
|
||||
if (!c.isCompleted) {
|
||||
c.completeError(StateError(
|
||||
'The client was closed before the server responded to this request.'));
|
||||
}
|
||||
}
|
||||
|
||||
for (var s in _subscriptions) {
|
||||
s._close();
|
||||
}
|
||||
|
||||
_requests.clear();
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateClientSubscription extends ClientSubscription {
|
||||
final StreamController _stream = StreamController();
|
||||
final String? eventName, id;
|
||||
final IsolateClient client;
|
||||
|
||||
_IsolateClientSubscription(this.eventName, this.id, this.client);
|
||||
|
||||
void _close() {
|
||||
if (!_stream.isClosed) _stream.close();
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSubscription listen(void Function(dynamic event)? onData,
|
||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
||||
return _stream.stream.listen(onData,
|
||||
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future unsubscribe() {
|
||||
return client._whenConnected(() {
|
||||
var c = Completer<Map>();
|
||||
var requestId = client._uuid.v4();
|
||||
client._requests[requestId] = c;
|
||||
client.serverSendPort.send({
|
||||
'id': client._id,
|
||||
'request_id': requestId,
|
||||
'method': 'unsubscribe',
|
||||
'params': {'client_id': client.clientId, 'subscription_id': id}
|
||||
});
|
||||
|
||||
return c.future.then((_) {
|
||||
_close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
253
packages/pub_sub/lib/src/isolate/server.dart
Normal file
253
packages/pub_sub/lib/src/isolate/server.dart
Normal file
|
@ -0,0 +1,253 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../belatuk_pub_sub.dart';
|
||||
|
||||
/// A [Adapter] implementation that communicates via [SendPort]s and [ReceivePort]s.
|
||||
class IsolateAdapter extends Adapter {
|
||||
final Map<String, SendPort> _clients = {};
|
||||
final StreamController<PublishRequest> _onPublish =
|
||||
StreamController<PublishRequest>();
|
||||
final StreamController<SubscriptionRequest> _onSubscribe =
|
||||
StreamController<SubscriptionRequest>();
|
||||
final StreamController<UnsubscriptionRequest> _onUnsubscribe =
|
||||
StreamController<UnsubscriptionRequest>();
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
/// A [ReceivePort] on which to listen for incoming data.
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
|
||||
@override
|
||||
Stream<PublishRequest> get onPublish => _onPublish.stream;
|
||||
|
||||
@override
|
||||
Stream<SubscriptionRequest> get onSubscribe => _onSubscribe.stream;
|
||||
|
||||
@override
|
||||
Stream<UnsubscriptionRequest> get onUnsubscribe => _onUnsubscribe.stream;
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
receivePort.close();
|
||||
_clients.clear();
|
||||
_onPublish.close();
|
||||
_onSubscribe.close();
|
||||
_onUnsubscribe.close();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void start() {
|
||||
receivePort.listen((data) {
|
||||
if (data is SendPort) {
|
||||
var id = _uuid.v4();
|
||||
_clients[id] = data;
|
||||
data.send({'status': true, 'id': id});
|
||||
} else if (data is Map &&
|
||||
data['id'] is String &&
|
||||
data['request_id'] is String &&
|
||||
data['method'] is String &&
|
||||
data['params'] is Map) {
|
||||
var id = data['id'] as String?,
|
||||
requestId = data['request_id'] as String?,
|
||||
method = data['method'] as String?;
|
||||
var params = data['params'] as Map?;
|
||||
var sp = _clients[id!];
|
||||
|
||||
if (sp == null) {
|
||||
// There's nobody to respond to, so don't send anything to anyone. Oops.
|
||||
} else if (method == 'publish') {
|
||||
if (_isValidClientId(params!['client_id']) &&
|
||||
params['event_name'] is String &&
|
||||
params.containsKey('value')) {
|
||||
var clientId = params['client_id'] as String?,
|
||||
eventName = params['event_name'] as String?;
|
||||
var value = params['value'];
|
||||
var rq = _IsolatePublishRequestImpl(
|
||||
requestId, clientId, eventName, value, sp);
|
||||
_onPublish.add(rq);
|
||||
} else {
|
||||
sp.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': 'Expected client_id, event_name, and value.'
|
||||
});
|
||||
}
|
||||
} else if (method == 'subscribe') {
|
||||
if (_isValidClientId(params!['client_id']) &&
|
||||
params['event_name'] is String) {
|
||||
var clientId = params['client_id'] as String?,
|
||||
eventName = params['event_name'] as String?;
|
||||
var rq = _IsolateSubscriptionRequestImpl(
|
||||
clientId, eventName, sp, requestId, _uuid);
|
||||
_onSubscribe.add(rq);
|
||||
} else {
|
||||
sp.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': 'Expected client_id, and event_name.'
|
||||
});
|
||||
}
|
||||
} else if (method == 'unsubscribe') {
|
||||
if (_isValidClientId(params!['client_id']) &&
|
||||
params['subscription_id'] is String) {
|
||||
var clientId = params['client_id'] as String?,
|
||||
subscriptionId = params['subscription_id'] as String?;
|
||||
var rq = _IsolateUnsubscriptionRequestImpl(
|
||||
clientId, subscriptionId, sp, requestId);
|
||||
_onUnsubscribe.add(rq);
|
||||
} else {
|
||||
sp.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': 'Expected client_id, and subscription_id.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
sp.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message':
|
||||
'Unrecognized method "$method". Or, you omitted id, request_id, method, or params.'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _isValidClientId(id) => id == null || id is String;
|
||||
|
||||
@override
|
||||
bool isTrustedPublishRequest(PublishRequest request) {
|
||||
// Isolate clients are considered trusted, because they are
|
||||
// running in the same process as the central server.
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isTrustedSubscriptionRequest(SubscriptionRequest request) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolatePublishRequestImpl extends PublishRequest {
|
||||
@override
|
||||
final String? clientId;
|
||||
|
||||
@override
|
||||
final String? eventName;
|
||||
|
||||
@override
|
||||
final dynamic value;
|
||||
|
||||
final SendPort sendPort;
|
||||
|
||||
final String? requestId;
|
||||
|
||||
_IsolatePublishRequestImpl(
|
||||
this.requestId, this.clientId, this.eventName, this.value, this.sendPort);
|
||||
|
||||
@override
|
||||
void accept(PublishResponse response) {
|
||||
sendPort.send({
|
||||
'status': true,
|
||||
'request_id': requestId,
|
||||
'result': {
|
||||
'listeners': response.listeners,
|
||||
'client_id': response.clientId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
sendPort.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateSubscriptionRequestImpl extends SubscriptionRequest {
|
||||
@override
|
||||
final String? clientId;
|
||||
|
||||
@override
|
||||
final String? eventName;
|
||||
|
||||
final SendPort sendPort;
|
||||
|
||||
final String? requestId;
|
||||
|
||||
final Uuid _uuid;
|
||||
|
||||
_IsolateSubscriptionRequestImpl(
|
||||
this.clientId, this.eventName, this.sendPort, this.requestId, this._uuid);
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
sendPort.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Subscription> accept(String? clientId) {
|
||||
var id = _uuid.v4();
|
||||
sendPort.send({
|
||||
'status': true,
|
||||
'request_id': requestId,
|
||||
'result': {'subscription_id': id, 'client_id': clientId}
|
||||
});
|
||||
return _IsolateSubscriptionImpl(clientId, id, eventName, sendPort);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateSubscriptionImpl extends Subscription {
|
||||
@override
|
||||
final String? clientId, id;
|
||||
|
||||
final String? eventName;
|
||||
|
||||
final SendPort sendPort;
|
||||
|
||||
_IsolateSubscriptionImpl(
|
||||
this.clientId, this.id, this.eventName, this.sendPort);
|
||||
|
||||
@override
|
||||
void dispatch(event) {
|
||||
sendPort.send([eventName, event]);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateUnsubscriptionRequestImpl extends UnsubscriptionRequest {
|
||||
@override
|
||||
final String? clientId;
|
||||
|
||||
@override
|
||||
final String? subscriptionId;
|
||||
|
||||
final SendPort sendPort;
|
||||
|
||||
final String? requestId;
|
||||
|
||||
_IsolateUnsubscriptionRequestImpl(
|
||||
this.clientId, this.subscriptionId, this.sendPort, this.requestId);
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
sendPort.send({
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void accept() {
|
||||
sendPort.send({'status': true, 'request_id': requestId, 'result': {}});
|
||||
}
|
||||
}
|
145
packages/pub_sub/lib/src/json_rpc/client.dart
Normal file
145
packages/pub_sub/lib/src/json_rpc/client.dart
Normal file
|
@ -0,0 +1,145 @@
|
|||
import 'dart:async';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../belatuk_pub_sub.dart';
|
||||
|
||||
/// A [Client] implementation that communicates via JSON RPC 2.0.
|
||||
class JsonRpc2Client extends Client {
|
||||
final Map<String, Completer<Map>> _requests = {};
|
||||
final List<_JsonRpc2ClientSubscription> _subscriptions = [];
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
json_rpc_2.Peer? _peer;
|
||||
|
||||
/// The ID of the client we are authenticating as.
|
||||
///
|
||||
/// May be `null`, if and only if we are marked as a trusted source on
|
||||
/// the server side.
|
||||
String? get clientId => _clientId;
|
||||
String? _clientId;
|
||||
|
||||
JsonRpc2Client(String? clientId, StreamChannel<String> channel) {
|
||||
_clientId = clientId;
|
||||
_peer = json_rpc_2.Peer(channel);
|
||||
|
||||
_peer!.registerMethod('event', (json_rpc_2.Parameters params) {
|
||||
String? eventName = params['event_name'].asString;
|
||||
var event = params['value'].value;
|
||||
for (var s in _subscriptions.where((s) => s.eventName == eventName)) {
|
||||
if (!s._stream.isClosed) s._stream.add(event);
|
||||
}
|
||||
});
|
||||
|
||||
_peer!.registerFallback((json_rpc_2.Parameters params) {
|
||||
var c = _requests.remove(params.method);
|
||||
|
||||
if (c == null) {
|
||||
throw json_rpc_2.RpcException.methodNotFound(params.method);
|
||||
} else {
|
||||
var data = params.asMap;
|
||||
|
||||
if (data['status'] is! bool) {
|
||||
c.completeError(
|
||||
FormatException('The server sent an invalid response.'));
|
||||
} else if (!(data['status'] as bool)) {
|
||||
c.completeError(PubSubException(data['error_message']?.toString() ??
|
||||
'The server sent a failure response, but did not provide an error message.'));
|
||||
} else {
|
||||
c.complete(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_peer!.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
Future publish(String eventName, value) {
|
||||
var c = Completer<Map>();
|
||||
var requestId = _uuid.v4();
|
||||
_requests[requestId] = c;
|
||||
_peer!.sendNotification('publish', {
|
||||
'request_id': requestId,
|
||||
'client_id': clientId,
|
||||
'event_name': eventName,
|
||||
'value': value
|
||||
});
|
||||
return c.future.then((data) {
|
||||
_clientId = data['result']['client_id'] as String?;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ClientSubscription> subscribe(String eventName) {
|
||||
var c = Completer<Map>();
|
||||
var requestId = _uuid.v4();
|
||||
_requests[requestId] = c;
|
||||
_peer!.sendNotification('subscribe', {
|
||||
'request_id': requestId,
|
||||
'client_id': clientId,
|
||||
'event_name': eventName
|
||||
});
|
||||
return c.future.then<ClientSubscription>((result) {
|
||||
_clientId = result['client_id'] as String?;
|
||||
var s = _JsonRpc2ClientSubscription(
|
||||
eventName, result['subscription_id'] as String?, this);
|
||||
_subscriptions.add(s);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
if (_peer?.isClosed != true) _peer!.close();
|
||||
|
||||
for (var c in _requests.values) {
|
||||
if (!c.isCompleted) {
|
||||
c.completeError(StateError(
|
||||
'The client was closed before the server responded to this request.'));
|
||||
}
|
||||
}
|
||||
|
||||
for (var s in _subscriptions) {
|
||||
s._close();
|
||||
}
|
||||
|
||||
_requests.clear();
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
class _JsonRpc2ClientSubscription extends ClientSubscription {
|
||||
final StreamController _stream = StreamController();
|
||||
final String? eventName, id;
|
||||
final JsonRpc2Client client;
|
||||
|
||||
_JsonRpc2ClientSubscription(this.eventName, this.id, this.client);
|
||||
|
||||
void _close() {
|
||||
if (!_stream.isClosed) _stream.close();
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSubscription listen(void Function(dynamic event)? onData,
|
||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
||||
return _stream.stream.listen(onData,
|
||||
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future unsubscribe() {
|
||||
var c = Completer<Map>();
|
||||
var requestId = client._uuid.v4();
|
||||
client._requests[requestId] = c;
|
||||
client._peer!.sendNotification('unsubscribe', {
|
||||
'request_id': requestId,
|
||||
'client_id': client.clientId,
|
||||
'subscription_id': id
|
||||
});
|
||||
|
||||
return c.future.then((_) {
|
||||
_close();
|
||||
});
|
||||
}
|
||||
}
|
220
packages/pub_sub/lib/src/json_rpc/server.dart
Normal file
220
packages/pub_sub/lib/src/json_rpc/server.dart
Normal file
|
@ -0,0 +1,220 @@
|
|||
import 'dart:async';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../belatuk_pub_sub.dart';
|
||||
|
||||
/// A [Adapter] implementation that communicates via JSON RPC 2.0.
|
||||
class JsonRpc2Adapter extends Adapter {
|
||||
final StreamController<PublishRequest> _onPublish =
|
||||
StreamController<PublishRequest>();
|
||||
final StreamController<SubscriptionRequest> _onSubscribe =
|
||||
StreamController<SubscriptionRequest>();
|
||||
final StreamController<UnsubscriptionRequest> _onUnsubscribe =
|
||||
StreamController<UnsubscriptionRequest>();
|
||||
|
||||
final List<json_rpc_2.Peer> _peers = [];
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
json_rpc_2.Peer? _peer;
|
||||
|
||||
/// A [Stream] of incoming clients, who can both send and receive string data.
|
||||
final Stream<StreamChannel<String>> clientStream;
|
||||
|
||||
/// If `true`, clients can connect through this endpoint, *without* providing a client ID.
|
||||
///
|
||||
/// This can be a security vulnerability if you don't know what you're doing.
|
||||
/// If you *must* use this over the Internet, use an IP whitelist.
|
||||
final bool isTrusted;
|
||||
|
||||
JsonRpc2Adapter(this.clientStream, {this.isTrusted = false});
|
||||
|
||||
@override
|
||||
Stream<PublishRequest> get onPublish => _onPublish.stream;
|
||||
|
||||
@override
|
||||
Stream<SubscriptionRequest> get onSubscribe => _onSubscribe.stream;
|
||||
|
||||
@override
|
||||
Stream<UnsubscriptionRequest> get onUnsubscribe => _onUnsubscribe.stream;
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
if (_peer?.isClosed != true) _peer?.close();
|
||||
|
||||
Future.wait(_peers.where((s) => !s.isClosed).map((s) => s.close()))
|
||||
.then((_) => _peers.clear());
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
String? _getClientId(json_rpc_2.Parameters params) {
|
||||
try {
|
||||
return params['client_id'].asString;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void start() {
|
||||
clientStream.listen((client) {
|
||||
var peer = _peer = json_rpc_2.Peer(client);
|
||||
|
||||
peer.registerMethod('publish', (json_rpc_2.Parameters params) async {
|
||||
var requestId = params['request_id'].asString;
|
||||
var clientId = _getClientId(params);
|
||||
var eventName = params['event_name'].asString;
|
||||
var value = params['value'].value;
|
||||
var rq = _JsonRpc2PublishRequestImpl(
|
||||
requestId, clientId, eventName, value, peer);
|
||||
_onPublish.add(rq);
|
||||
});
|
||||
|
||||
peer.registerMethod('subscribe', (json_rpc_2.Parameters params) async {
|
||||
var requestId = params['request_id'].asString;
|
||||
var clientId = _getClientId(params);
|
||||
var eventName = params['event_name'].asString;
|
||||
var rq = _JsonRpc2SubscriptionRequestImpl(
|
||||
clientId, eventName, requestId, peer, _uuid);
|
||||
_onSubscribe.add(rq);
|
||||
});
|
||||
|
||||
peer.registerMethod('unsubscribe', (json_rpc_2.Parameters params) async {
|
||||
var requestId = params['request_id'].asString;
|
||||
var clientId = _getClientId(params);
|
||||
var subscriptionId = params['subscription_id'].asString;
|
||||
var rq = _JsonRpc2UnsubscriptionRequestImpl(
|
||||
clientId, subscriptionId, peer, requestId);
|
||||
_onUnsubscribe.add(rq);
|
||||
});
|
||||
|
||||
peer.listen();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool isTrustedPublishRequest(PublishRequest request) {
|
||||
return isTrusted;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isTrustedSubscriptionRequest(SubscriptionRequest request) {
|
||||
return isTrusted;
|
||||
}
|
||||
}
|
||||
|
||||
class _JsonRpc2PublishRequestImpl extends PublishRequest {
|
||||
final String requestId;
|
||||
|
||||
@override
|
||||
final String? clientId, eventName;
|
||||
|
||||
@override
|
||||
final dynamic value;
|
||||
|
||||
final json_rpc_2.Peer peer;
|
||||
|
||||
_JsonRpc2PublishRequestImpl(
|
||||
this.requestId, this.clientId, this.eventName, this.value, this.peer);
|
||||
|
||||
@override
|
||||
void accept(PublishResponse response) {
|
||||
peer.sendNotification(requestId, {
|
||||
'status': true,
|
||||
'request_id': requestId,
|
||||
'result': {
|
||||
'listeners': response.listeners,
|
||||
'client_id': response.clientId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
peer.sendNotification(requestId, {
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _JsonRpc2SubscriptionRequestImpl extends SubscriptionRequest {
|
||||
@override
|
||||
final String? clientId, eventName;
|
||||
|
||||
final String requestId;
|
||||
|
||||
final json_rpc_2.Peer peer;
|
||||
|
||||
final Uuid _uuid;
|
||||
|
||||
_JsonRpc2SubscriptionRequestImpl(
|
||||
this.clientId, this.eventName, this.requestId, this.peer, this._uuid);
|
||||
|
||||
@override
|
||||
FutureOr<Subscription> accept(String? clientId) {
|
||||
var id = _uuid.v4();
|
||||
peer.sendNotification(requestId, {
|
||||
'status': true,
|
||||
'request_id': requestId,
|
||||
'subscription_id': id,
|
||||
'client_id': clientId
|
||||
});
|
||||
return _JsonRpc2SubscriptionImpl(clientId, id, eventName, peer);
|
||||
}
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
peer.sendNotification(requestId, {
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _JsonRpc2SubscriptionImpl extends Subscription {
|
||||
@override
|
||||
final String? clientId, id;
|
||||
|
||||
final String? eventName;
|
||||
|
||||
final json_rpc_2.Peer peer;
|
||||
|
||||
_JsonRpc2SubscriptionImpl(this.clientId, this.id, this.eventName, this.peer);
|
||||
|
||||
@override
|
||||
void dispatch(event) {
|
||||
peer.sendNotification('event', {'event_name': eventName, 'value': event});
|
||||
}
|
||||
}
|
||||
|
||||
class _JsonRpc2UnsubscriptionRequestImpl extends UnsubscriptionRequest {
|
||||
@override
|
||||
final String? clientId;
|
||||
|
||||
@override
|
||||
final String subscriptionId;
|
||||
|
||||
final json_rpc_2.Peer peer;
|
||||
|
||||
final String requestId;
|
||||
|
||||
_JsonRpc2UnsubscriptionRequestImpl(
|
||||
this.clientId, this.subscriptionId, this.peer, this.requestId);
|
||||
|
||||
@override
|
||||
void accept() {
|
||||
peer.sendNotification(requestId, {'status': true, 'result': {}});
|
||||
}
|
||||
|
||||
@override
|
||||
void reject(String errorMessage) {
|
||||
peer.sendNotification(requestId, {
|
||||
'status': false,
|
||||
'request_id': requestId,
|
||||
'error_message': errorMessage
|
||||
});
|
||||
}
|
||||
}
|
30
packages/pub_sub/lib/src/protocol/client/client.dart
Normal file
30
packages/pub_sub/lib/src/protocol/client/client.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Queries a `pub_sub` server.
|
||||
abstract class Client {
|
||||
/// Publishes an event to the server.
|
||||
Future publish(String eventName, value);
|
||||
|
||||
/// Request a [ClientSubscription] to the desired [eventName] from the server.
|
||||
Future<ClientSubscription> subscribe(String eventName);
|
||||
|
||||
/// Disposes of this client.
|
||||
Future close();
|
||||
}
|
||||
|
||||
/// A client-side implementation of a subscription, which acts as a [Stream], and can be cancelled easily.
|
||||
abstract class ClientSubscription extends Stream {
|
||||
/// Stops listening for new events, and instructs the server to cancel the subscription.
|
||||
Future unsubscribe();
|
||||
}
|
||||
|
||||
/// Thrown as the result of an invalid request, or an attempt to perform an action without the correct privileges.
|
||||
class PubSubException implements Exception {
|
||||
/// The error message sent by the server.
|
||||
final String message;
|
||||
|
||||
const PubSubException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => '`pub_sub` exception: $message';
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export 'client.dart';
|
2
packages/pub_sub/lib/src/protocol/protocol.dart
Normal file
2
packages/pub_sub/lib/src/protocol/protocol.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'client/sync_client.dart';
|
||||
export 'server/sync_server.dart';
|
29
packages/pub_sub/lib/src/protocol/server/adapter.dart
Normal file
29
packages/pub_sub/lib/src/protocol/server/adapter.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'publish.dart';
|
||||
import 'subscription.dart';
|
||||
|
||||
/// Adapts an abstract medium to serve the `pub_sub` RPC protocol.
|
||||
abstract class Adapter {
|
||||
/// Determines if a given [request] comes from a trusted source.
|
||||
///
|
||||
/// If so, the request does not have to provide a pre-established ID,
|
||||
/// and instead will be assigned one.
|
||||
bool isTrustedPublishRequest(PublishRequest request);
|
||||
|
||||
bool isTrustedSubscriptionRequest(SubscriptionRequest request);
|
||||
|
||||
/// Fires an event whenever a client tries to publish data.
|
||||
Stream<PublishRequest> get onPublish;
|
||||
|
||||
/// Fires whenever a client tries to subscribe to an event.
|
||||
Stream<SubscriptionRequest> get onSubscribe;
|
||||
|
||||
/// Fires whenever a client cancels a subscription.
|
||||
Stream<UnsubscriptionRequest> get onUnsubscribe;
|
||||
|
||||
/// Disposes of this adapter.
|
||||
Future close();
|
||||
|
||||
/// Start listening for incoming clients.
|
||||
void start();
|
||||
}
|
14
packages/pub_sub/lib/src/protocol/server/client.dart
Normal file
14
packages/pub_sub/lib/src/protocol/server/client.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
/// Represents information about a client that will be accessing
|
||||
/// this `angel_sync` server.
|
||||
class ClientInfo {
|
||||
/// A unique identifier for this client.
|
||||
final String id;
|
||||
|
||||
/// If `true` (default), then the client is allowed to publish events.
|
||||
final bool canPublish;
|
||||
|
||||
/// If `true` (default), then the client can subscribe to events.
|
||||
final bool canSubscribe;
|
||||
|
||||
const ClientInfo(this.id, {this.canPublish = true, this.canSubscribe = true});
|
||||
}
|
28
packages/pub_sub/lib/src/protocol/server/publish.dart
Normal file
28
packages/pub_sub/lib/src/protocol/server/publish.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
/// Represents a request to publish information to other clients.
|
||||
abstract class PublishRequest {
|
||||
/// The ID of the client sending this request.
|
||||
String? get clientId;
|
||||
|
||||
/// The name of the event to be sent.
|
||||
String? get eventName;
|
||||
|
||||
/// The value to be published as an event.
|
||||
dynamic get value;
|
||||
|
||||
/// Accept the request, with a response.
|
||||
void accept(PublishResponse response);
|
||||
|
||||
/// Deny the request with an error message.
|
||||
void reject(String errorMessage);
|
||||
}
|
||||
|
||||
/// A response to a publish request. Informs the caller of how much clients received the event.
|
||||
class PublishResponse {
|
||||
/// The number of unique listeners to whom this event was propogated.
|
||||
final int listeners;
|
||||
|
||||
/// The client ID returned the server. Significant in cases where an ad-hoc client was registered.
|
||||
final String? clientId;
|
||||
|
||||
const PublishResponse(this.listeners, this.clientId);
|
||||
}
|
160
packages/pub_sub/lib/src/protocol/server/server.dart
Normal file
160
packages/pub_sub/lib/src/protocol/server/server.dart
Normal file
|
@ -0,0 +1,160 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'adapter.dart';
|
||||
import 'client.dart';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'publish.dart';
|
||||
import 'subscription.dart';
|
||||
|
||||
/// A server that implements the `pub_sub` protocol.
|
||||
///
|
||||
/// It can work using multiple [Adapter]s, to simultaneously
|
||||
/// serve local and remote clients alike.
|
||||
class Server {
|
||||
final List<Adapter> _adapters = [];
|
||||
final List<ClientInfo> _clients = [];
|
||||
final _rnd = Random.secure();
|
||||
final Map<String?, List<Subscription>> _subscriptions = {};
|
||||
bool _started = false;
|
||||
int _adHocIds = 0;
|
||||
|
||||
/// Initialize a server, optionally with a number of [adapters].
|
||||
Server([Iterable<Adapter> adapters = const []]) {
|
||||
_adapters.addAll(adapters);
|
||||
}
|
||||
|
||||
/// Adds a new [Adapter] to adapt incoming clients from a new interface.
|
||||
void addAdapter(Adapter adapter) {
|
||||
if (_started) {
|
||||
throw StateError(
|
||||
'You cannot add new adapters after the server has started listening.');
|
||||
} else {
|
||||
_adapters.add(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a new client with the server.
|
||||
void registerClient(ClientInfo client) {
|
||||
if (_started) {
|
||||
throw StateError(
|
||||
'You cannot register new clients after the server has started listening.');
|
||||
} else {
|
||||
_clients.add(client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes of this server, and closes all of its adapters.
|
||||
Future close() {
|
||||
Future.wait(_adapters.map((a) => a.close()));
|
||||
_adapters.clear();
|
||||
_clients.clear();
|
||||
_subscriptions.clear();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
String _newClientId() {
|
||||
// Create an unpredictable-enough ID. The harder it is for an attacker to guess, the better.
|
||||
var id =
|
||||
'pub_sub::adhoc_client${_rnd.nextDouble()}::${_adHocIds++}:${DateTime.now().millisecondsSinceEpoch * _rnd.nextDouble()}';
|
||||
|
||||
// This client is coming from a trusted source, and can therefore both publish and subscribe.
|
||||
_clients.add(ClientInfo(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (_adapters.isEmpty) {
|
||||
throw StateError(
|
||||
'Cannot start a SyncServer that has no adapters attached.');
|
||||
} else if (_started) {
|
||||
throw StateError('A SyncServer may only be started once.');
|
||||
}
|
||||
|
||||
_started = true;
|
||||
|
||||
for (var adapter in _adapters) {
|
||||
adapter.start();
|
||||
}
|
||||
|
||||
for (var adapter in _adapters) {
|
||||
// Handle publishes
|
||||
adapter.onPublish.listen((rq) {
|
||||
ClientInfo? client;
|
||||
String? clientId;
|
||||
|
||||
if (rq.clientId?.isNotEmpty == true ||
|
||||
adapter.isTrustedPublishRequest(rq)) {
|
||||
clientId =
|
||||
rq.clientId?.isNotEmpty == true ? rq.clientId : _newClientId();
|
||||
client = _clients.firstWhereOrNull((c) => c.id == clientId);
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
rq.reject('Unrecognized client ID "${clientId ?? '<missing>'}".');
|
||||
} else if (!client.canPublish) {
|
||||
rq.reject('You are not allowed to publish events.');
|
||||
} else {
|
||||
var listeners = _subscriptions[rq.eventName]
|
||||
?.where((s) => s.clientId != clientId) ??
|
||||
[];
|
||||
|
||||
if (listeners.isEmpty) {
|
||||
rq.accept(PublishResponse(0, clientId));
|
||||
} else {
|
||||
for (var listener in listeners) {
|
||||
listener.dispatch(rq.value);
|
||||
}
|
||||
|
||||
rq.accept(PublishResponse(listeners.length, clientId));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for incoming subscriptions
|
||||
adapter.onSubscribe.listen((rq) async {
|
||||
ClientInfo? client;
|
||||
String? clientId;
|
||||
|
||||
if (rq.clientId?.isNotEmpty == true ||
|
||||
adapter.isTrustedSubscriptionRequest(rq)) {
|
||||
clientId =
|
||||
rq.clientId?.isNotEmpty == true ? rq.clientId : _newClientId();
|
||||
client = _clients.firstWhereOrNull((c) => c.id == clientId);
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
rq.reject('Unrecognized client ID "${clientId ?? '<missing>'}".');
|
||||
} else if (!client.canSubscribe) {
|
||||
rq.reject('You are not allowed to subscribe to events.');
|
||||
} else {
|
||||
var sub = await rq.accept(clientId);
|
||||
var list = _subscriptions.putIfAbsent(rq.eventName, () => []);
|
||||
list.add(sub);
|
||||
}
|
||||
});
|
||||
|
||||
// Unregister subscriptions on unsubscribe
|
||||
adapter.onUnsubscribe.listen((rq) {
|
||||
Subscription? toRemove;
|
||||
late List<Subscription> sourceList;
|
||||
|
||||
for (var list in _subscriptions.values) {
|
||||
toRemove = list.firstWhereOrNull((s) => s.id == rq.subscriptionId);
|
||||
if (toRemove != null) {
|
||||
sourceList = list;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove == null) {
|
||||
rq.reject('The specified subscription does not exist.');
|
||||
} else if (toRemove.clientId != rq.clientId) {
|
||||
rq.reject('That is not your subscription to cancel.');
|
||||
} else {
|
||||
sourceList.remove(toRemove);
|
||||
rq.accept();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
47
packages/pub_sub/lib/src/protocol/server/subscription.dart
Normal file
47
packages/pub_sub/lib/src/protocol/server/subscription.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Represents a request to subscribe to an event.
|
||||
abstract class SubscriptionRequest {
|
||||
/// The ID of the client requesting to subscribe.
|
||||
String? get clientId;
|
||||
|
||||
/// The name of the event the client wants to subscribe to.
|
||||
String? get eventName;
|
||||
|
||||
/// Accept the request, and grant the client access to subscribe to the event.
|
||||
///
|
||||
/// Includes the client's ID, which is necessary for ad-hoc clients.
|
||||
FutureOr<Subscription> accept(String? clientId);
|
||||
|
||||
/// Deny the request with an error message.
|
||||
void reject(String errorMessage);
|
||||
}
|
||||
|
||||
/// Represents a request to unsubscribe to an event.
|
||||
abstract class UnsubscriptionRequest {
|
||||
/// The ID of the client requesting to unsubscribe.
|
||||
String? get clientId;
|
||||
|
||||
/// The name of the event the client wants to unsubscribe from.
|
||||
String? get subscriptionId;
|
||||
|
||||
/// Accept the request.
|
||||
FutureOr<void> accept();
|
||||
|
||||
/// Deny the request with an error message.
|
||||
void reject(String errorMessage);
|
||||
}
|
||||
|
||||
/// Represents a client's subscription to an event.
|
||||
///
|
||||
/// Also provides a means to fire an event.
|
||||
abstract class Subscription {
|
||||
/// A unique identifier for this subscription.
|
||||
String? get id;
|
||||
|
||||
/// The ID of the client who requested this subscription.
|
||||
String? get clientId;
|
||||
|
||||
/// Alerts a client of an event.
|
||||
void dispatch(event);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export 'adapter.dart';
|
||||
export 'client.dart';
|
||||
export 'publish.dart';
|
||||
export 'server.dart';
|
||||
export 'subscription.dart';
|
14
packages/pub_sub/pubspec.yaml
Normal file
14
packages/pub_sub/pubspec.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: belatuk_pub_sub
|
||||
version: 4.0.0
|
||||
description: Keep application instances in sync with a simple pub/sub API.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/pub_sub
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
json_rpc_2: ^3.0.0
|
||||
stream_channel: ^2.1.0
|
||||
uuid: ^3.0.4
|
||||
collection: ^1.15.0
|
||||
dev_dependencies:
|
||||
lints: ^1.0.0
|
||||
test: ^1.17.4
|
122
packages/pub_sub/test/isolate_test.dart
Normal file
122
packages/pub_sub/test/isolate_test.dart
Normal file
|
@ -0,0 +1,122 @@
|
|||
import 'dart:async';
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart';
|
||||
import 'package:belatuk_pub_sub/isolate.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Server server;
|
||||
late Client client1, client2, client3;
|
||||
late IsolateClient trustedClient;
|
||||
late IsolateAdapter adapter;
|
||||
|
||||
setUp(() async {
|
||||
adapter = IsolateAdapter();
|
||||
client1 =
|
||||
IsolateClient('isolate_test::secret', adapter.receivePort.sendPort);
|
||||
client2 =
|
||||
IsolateClient('isolate_test::secret2', adapter.receivePort.sendPort);
|
||||
client3 =
|
||||
IsolateClient('isolate_test::secret3', adapter.receivePort.sendPort);
|
||||
trustedClient = IsolateClient(null, adapter.receivePort.sendPort);
|
||||
|
||||
server = Server([adapter])
|
||||
..registerClient(const ClientInfo('isolate_test::secret'))
|
||||
..registerClient(const ClientInfo('isolate_test::secret2'))
|
||||
..registerClient(const ClientInfo('isolate_test::secret3'))
|
||||
..registerClient(
|
||||
const ClientInfo('isolate_test::no_publish', canPublish: false))
|
||||
..registerClient(
|
||||
const ClientInfo('isolate_test::no_subscribe', canSubscribe: false))
|
||||
..start();
|
||||
|
||||
var sub = await client3.subscribe('foo');
|
||||
sub.listen((data) {
|
||||
print('Client3 caught foo: $data');
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
Future.wait([
|
||||
server.close(),
|
||||
client1.close(),
|
||||
client2.close(),
|
||||
client3.close(),
|
||||
trustedClient.close()
|
||||
]);
|
||||
});
|
||||
|
||||
group('trusted', () {
|
||||
test('can publish', () async {
|
||||
await trustedClient.publish('hey', 'bye');
|
||||
expect(trustedClient.clientId, isNotNull);
|
||||
});
|
||||
test('can sub/unsub', () async {
|
||||
String? clientId;
|
||||
await trustedClient.publish('heyaaa', 'byeaa');
|
||||
expect(clientId = trustedClient.clientId, isNotNull);
|
||||
|
||||
var sub = await trustedClient.subscribe('yeppp');
|
||||
expect(trustedClient.clientId, clientId);
|
||||
|
||||
await sub.unsubscribe();
|
||||
expect(trustedClient.clientId, clientId);
|
||||
});
|
||||
});
|
||||
|
||||
test('subscribers receive published events', () async {
|
||||
var sub = await client2.subscribe('foo');
|
||||
await client1.publish('foo', 'bar');
|
||||
expect(await sub.first, 'bar');
|
||||
});
|
||||
|
||||
test('subscribers are not sent their own events', () async {
|
||||
var sub = await client1.subscribe('foo');
|
||||
await client1.publish('foo',
|
||||
'<this should never be sent to client1, because client1 sent it.>');
|
||||
await sub.unsubscribe();
|
||||
expect(await sub.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('can unsubscribe', () async {
|
||||
var sub = await client2.subscribe('foo');
|
||||
await client1.publish('foo', 'bar');
|
||||
await sub.unsubscribe();
|
||||
await client1.publish('foo', '<client2 will not catch this!>');
|
||||
expect(await sub.length, 1);
|
||||
});
|
||||
|
||||
group('isolate_server', () {
|
||||
test('reject unknown client id', () async {
|
||||
try {
|
||||
var client = IsolateClient(
|
||||
'isolate_test::invalid', adapter.receivePort.sendPort);
|
||||
await client.publish('foo', 'bar');
|
||||
throw 'Invalid client ID\'s should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
|
||||
test('reject unprivileged publish', () async {
|
||||
try {
|
||||
var client = IsolateClient(
|
||||
'isolate_test::no_publish', adapter.receivePort.sendPort);
|
||||
await client.publish('foo', 'bar');
|
||||
throw 'Unprivileged publishes should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
|
||||
test('reject unprivileged subscribe', () async {
|
||||
try {
|
||||
var client = IsolateClient(
|
||||
'isolate_test::no_subscribe', adapter.receivePort.sendPort);
|
||||
await client.subscribe('foo');
|
||||
throw 'Unprivileged subscribes should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
187
packages/pub_sub/test/json_rpc_2_test.dart
Normal file
187
packages/pub_sub/test/json_rpc_2_test.dart
Normal file
|
@ -0,0 +1,187 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:belatuk_pub_sub/belatuk_pub_sub.dart';
|
||||
import 'package:belatuk_pub_sub/json_rpc_2.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late ServerSocket serverSocket;
|
||||
late Server server;
|
||||
late Client client1, client2, client3;
|
||||
late JsonRpc2Client trustedClient;
|
||||
JsonRpc2Adapter adapter;
|
||||
|
||||
setUp(() async {
|
||||
serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
|
||||
adapter = JsonRpc2Adapter(
|
||||
serverSocket.map<StreamChannel<String>>(streamSocket),
|
||||
isTrusted: true);
|
||||
|
||||
var socket1 =
|
||||
await Socket.connect(InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var socket2 =
|
||||
await Socket.connect(InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var socket3 =
|
||||
await Socket.connect(InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var socket4 =
|
||||
await Socket.connect(InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
|
||||
client1 = JsonRpc2Client('json_rpc_2_test::secret', streamSocket(socket1));
|
||||
client2 = JsonRpc2Client('json_rpc_2_test::secret2', streamSocket(socket2));
|
||||
client3 = JsonRpc2Client('json_rpc_2_test::secret3', streamSocket(socket3));
|
||||
trustedClient = JsonRpc2Client(null, streamSocket(socket4));
|
||||
|
||||
server = Server([adapter])
|
||||
..registerClient(const ClientInfo('json_rpc_2_test::secret'))
|
||||
..registerClient(const ClientInfo('json_rpc_2_test::secret2'))
|
||||
..registerClient(const ClientInfo('json_rpc_2_test::secret3'))
|
||||
..registerClient(
|
||||
const ClientInfo('json_rpc_2_test::no_publish', canPublish: false))
|
||||
..registerClient(const ClientInfo('json_rpc_2_test::no_subscribe',
|
||||
canSubscribe: false))
|
||||
..start();
|
||||
|
||||
var sub = await client3.subscribe('foo');
|
||||
sub.listen((data) {
|
||||
print('Client3 caught foo: $data');
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
Future.wait(
|
||||
[server.close(), client1.close(), client2.close(), client3.close()]);
|
||||
});
|
||||
|
||||
group('trusted', () {
|
||||
test('can publish', () async {
|
||||
await trustedClient.publish('hey', 'bye');
|
||||
expect(trustedClient.clientId, isNotNull);
|
||||
});
|
||||
test('can sub/unsub', () async {
|
||||
String? clientId;
|
||||
await trustedClient.publish('heyaaa', 'byeaa');
|
||||
expect(clientId = trustedClient.clientId, isNotNull);
|
||||
|
||||
var sub = await trustedClient.subscribe('yeppp');
|
||||
expect(trustedClient.clientId, clientId);
|
||||
|
||||
await sub.unsubscribe();
|
||||
expect(trustedClient.clientId, clientId);
|
||||
});
|
||||
});
|
||||
|
||||
test('subscribers receive published events', () async {
|
||||
var sub = await client2.subscribe('foo');
|
||||
await client1.publish('foo', 'bar');
|
||||
expect(await sub.first, 'bar');
|
||||
});
|
||||
|
||||
test('subscribers are not sent their own events', () async {
|
||||
var sub = await client1.subscribe('foo');
|
||||
await client1.publish('foo',
|
||||
'<this should never be sent to client1, because client1 sent it.>');
|
||||
await sub.unsubscribe();
|
||||
expect(await sub.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('can unsubscribe', () async {
|
||||
var sub = await client2.subscribe('foo');
|
||||
await client1.publish('foo', 'bar');
|
||||
await sub.unsubscribe();
|
||||
await client1.publish('foo', '<client2 will not catch this!>');
|
||||
expect(await sub.length, 1);
|
||||
});
|
||||
|
||||
group('json_rpc_2_server', () {
|
||||
test('reject unknown client id', () async {
|
||||
try {
|
||||
var sock = await Socket.connect(
|
||||
InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var client =
|
||||
JsonRpc2Client('json_rpc_2_test::invalid', streamSocket(sock));
|
||||
await client.publish('foo', 'bar');
|
||||
throw 'Invalid client ID\'s should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
|
||||
test('reject unprivileged publish', () async {
|
||||
try {
|
||||
var sock = await Socket.connect(
|
||||
InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var client =
|
||||
JsonRpc2Client('json_rpc_2_test::no_publish', streamSocket(sock));
|
||||
await client.publish('foo', 'bar');
|
||||
throw 'Unprivileged publishes should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
|
||||
test('reject unprivileged subscribe', () async {
|
||||
try {
|
||||
var sock = await Socket.connect(
|
||||
InternetAddress.loopbackIPv4, serverSocket.port);
|
||||
var client =
|
||||
JsonRpc2Client('json_rpc_2_test::no_subscribe', streamSocket(sock));
|
||||
await client.subscribe('foo');
|
||||
throw 'Unprivileged subscribes should throw an error, but they do not.';
|
||||
} on PubSubException catch (e) {
|
||||
print('Expected exception was thrown: ${e.message}');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
StreamChannel<String> streamSocket(Socket socket) {
|
||||
var channel = _SocketStreamChannel(socket);
|
||||
return channel
|
||||
.cast<List<int>>()
|
||||
.transform(StreamChannelTransformer.fromCodec(utf8));
|
||||
}
|
||||
|
||||
class _SocketStreamChannel extends StreamChannelMixin<List<int>> {
|
||||
_SocketSink? _sink;
|
||||
final Socket socket;
|
||||
|
||||
_SocketStreamChannel(this.socket);
|
||||
|
||||
@override
|
||||
StreamSink<List<int>> get sink => _sink ??= _SocketSink(socket);
|
||||
|
||||
@override
|
||||
Stream<List<int>> get stream => socket;
|
||||
}
|
||||
|
||||
class _SocketSink extends StreamSink<List<int>> {
|
||||
final Socket socket;
|
||||
|
||||
_SocketSink(this.socket);
|
||||
|
||||
@override
|
||||
void add(List<int> event) {
|
||||
socket.add(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void addError(Object error, [StackTrace? stackTrace]) {
|
||||
Zone.current.errorCallback(error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future addStream(Stream<List<int>> stream) {
|
||||
return socket.addStream(stream);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
return socket.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future get done => socket.done;
|
||||
}
|
Loading…
Reference in a new issue