Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance the monitor verb syntax #301

Merged
merged 5 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/at_commons/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.0.43
- feat: Enhanced the monitor verb syntax
1. added `strict` flag to allow client to request that only regex-matching notifications are sent -
e.g. do not send other 'control' type notifications like the 'statsNotifications'
2. added `multiplexed` flag to allow client to indicate that
this socket is also being used for request-response interactions
## 3.0.42
- fix: Tightened the validation of 'public' key names. Keys like this: `public:@bob:foo.bar@alice` will now correctly be identified as not being valid.
## 3.0.41
Expand Down
10 changes: 7 additions & 3 deletions packages/at_commons/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ include: package:lints/recommended.yaml

# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
# Uncomment to specify additional rules.
# linter:
# rules:
# - camel_case_types
linter:
rules:
camel_case_types : true
unnecessary_string_interpolations : true
await_only_futures : true
unawaited_futures: true
depend_on_referenced_packages : false

analyzer:
# exclude:
Expand Down
3 changes: 3 additions & 0 deletions packages/at_commons/lib/src/at_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const String FROM = 'from';
const String TO = 'to';
const String KEY = 'key';
const String EPOCH_MILLIS = 'epochMillis';
const String MONITOR_STRICT_MODE = 'strict';
const String MONITOR_MULTIPLEXED_MODE = 'multiplexed';
const String MONITOR_REGEX = 'regex';
const String ID = 'id';
const String OPERATION = 'operation';
const String SET_OPERATION = 'setOperation';
Expand Down
94 changes: 83 additions & 11 deletions packages/at_commons/lib/src/verb/monitor_verb_builder.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
import 'dart:collection';

import 'package:at_commons/at_commons.dart';
import 'package:at_commons/src/verb/verb_builder.dart';

/// Monitor builder generates a command that streams incoming notifications from the secondary server to
/// the current client.
/// ```
/// // Receives all of the notifications
/// var builder = MonitorVerbBuilder();
///
/// // Receives notifications for those keys that matches a specific regex
/// var builder = MonitorVerbBuilder()..regex = '.alice';
/// ```
/// the current client. See also [VerbSyntax.monitor]
class MonitorVerbBuilder implements VerbBuilder {
@Deprecated('not used')
bool auth = true;
String? regex;

String? _regex;

/// The regular expression to be used when building the monitor command.
/// When [regex] is supplied, server will send notifications which match the regex. If [strict]
/// is true, then only those regex-matching notifications will be sent. If [strict] is false,
/// then other 'control' notifications (e.g. the statsNotification) which don't necessarily
/// match the [regex] will also be sent
String? get regex => _regex;
set regex (String? r) {
if (r != null && r.trim().isEmpty) {
r = null;
}
_regex = r;
}

/// The timestamp, in milliseconds since epoch, to be used when building the monitor command.
/// When [lastNotificationTime] is supplied, server will only send notifications received at
/// or after that timestamp
int? lastNotificationTime;

/// Whether this monitor command is to be built with the 'strict' flag or not.
/// When [strict] is true, server will only send notifications which match the [regex]; no other
/// 'control' notifications such as statsNotifications will be sent on this connection unless
/// they match the [regex]
bool strict = false;

/// Whether this monitor command is to be built with the 'multiplexed' flag or not.
/// When [multiplexed] is true, the server will understand that this is a connection
/// which the client is using not just for notifications but also for request-response
/// interactions. In this case, the server will only send notifications once there is
/// no request currently in progress
bool multiplexed = false;

@override
String buildCommand() {
var monitorCommand = 'monitor';
if (strict) {
monitorCommand += ':strict';
}
if (multiplexed) {
monitorCommand += ':multiplexed';
}
if (lastNotificationTime != null) {
monitorCommand += ':${lastNotificationTime.toString()}';
monitorCommand += ':$lastNotificationTime';
}
if (regex != null) {
if (regex != null && regex!.trim().isNotEmpty) {
monitorCommand += ' $regex';
}
monitorCommand += '\n';
Expand All @@ -31,4 +65,42 @@ class MonitorVerbBuilder implements VerbBuilder {
bool checkParams() {
return true;
}

/// Create a MonitorVerbBuilder from an atProtocol command string
static MonitorVerbBuilder getBuilder(String command) {
if (command != command.trim()) {
throw IllegalArgumentException(
'Commands may not have leading or trailing whitespace');
}
HashMap<String, String?>? verbParams = (VerbUtil.getVerbParam(VerbSyntax.monitor, command));
if (verbParams == null) {
throw InvalidSyntaxException('Command does not match the monitor syntax');
}

var builder = MonitorVerbBuilder();
builder.strict = verbParams[MONITOR_STRICT_MODE] == MONITOR_STRICT_MODE;
builder.multiplexed = verbParams[MONITOR_MULTIPLEXED_MODE] == MONITOR_MULTIPLEXED_MODE;
builder.regex = verbParams[MONITOR_REGEX];
builder.lastNotificationTime = verbParams[EPOCH_MILLIS] == null ? null : int.parse(verbParams[EPOCH_MILLIS]!);

return builder;
}

@override
String toString() {
return 'MonitorVerbBuilder{regex: $regex, lastNotificationTime: $lastNotificationTime, strict: $strict, multiplexed: $multiplexed}';
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MonitorVerbBuilder &&
runtimeType == other.runtimeType &&
regex == other.regex &&
lastNotificationTime == other.lastNotificationTime &&
strict == other.strict &&
multiplexed == other.multiplexed;

@override
int get hashCode => regex.hashCode ^ lastNotificationTime.hashCode ^ strict.hashCode ^ multiplexed.hashCode;
}
21 changes: 20 additions & 1 deletion packages/at_commons/lib/src/verb/syntax.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,26 @@ class VerbSyntax {
r':(?<atKey>(([^:@\s]+)|(privatekey:at_secret)))'
r'(@(?<atSign>[^:@\s]+))?'
r'$';
static const monitor = r'^monitor(:(?<epochMillis>\d+))?( (?<regex>.+))?$';

/// * When 'strict' is set, server will only send notifications which match the regex; no other
/// 'control' notifications such as statsNotifications will be sent on this connection unless
/// they match the regex
/// * When 'multiplexed' is set, the server will understand that this is a connection
/// which the client is using not just for notifications but also for request-response
/// interactions. In this case, the server will only send notifications when there is no request
/// currently being handled
/// * When 'epochMillis' is supplied, server will only send notifications received at or after
/// that timestamp
/// * When 'regex' is supplied, server will send notifications which match the regex. If 'strict'
/// is set, then only those regex-matching notifications will be sent. If 'strict' is not set,
/// then other 'control' notifications (e.g. the statsNotification) which don't necessarily
/// match the regex will also be sent
static const monitor = r'^monitor'
r'(:(?<strict>strict))?'
r'(:(?<multiplexed>multiplexed))?'
r'(:(?<epochMillis>\d+))?'
r'( (?<regex>.+))?'
r'$';
static const stream =
r'^stream:((?<operation>init|send|receive|done|resume))?((@(?<receiver>[^@:\s]+)))?( ?namespace:(?<namespace>[\w-]+))?( ?startByte:(?<startByte>\d+))?( (?<streamId>[\w-]*))?( (?<fileName>.* ))?((?<length>\d*))?$';

Expand Down
2 changes: 1 addition & 1 deletion packages/at_commons/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: at_commons
description: A library of Dart and Flutter utility classes that are used across other components of the atPlatform.
version: 3.0.42
version: 3.0.43
repository: https://github.com/atsign-foundation/at_tools
homepage: https://atsign.dev

Expand Down
184 changes: 184 additions & 0 deletions packages/at_commons/test/monitor_verb_builder_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import 'package:at_commons/at_builders.dart';
import 'package:test/test.dart';

void main() {
int nowEpochMillis = DateTime.now().millisecondsSinceEpoch;
group('Monitor builder to command to builder to command round trip tests', () {
test('no params', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder();
String c1 = b1.buildCommand();
expect(c1, 'monitor\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('empty regex', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..regex=' \n \t';
expect(b1.regex, null);
String c1 = b1.buildCommand();
expect(c1, 'monitor\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('just regex', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..regex=r'\.wavi';
String c1 = b1.buildCommand();
expect(c1, 'monitor \\.wavi\n');
expect(c1,
r'monitor \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('just lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:$nowEpochMillis\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('both regex and lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..regex=r'\.wavi'
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:$nowEpochMillis \\.wavi\n');
expect(c1,
r'monitor:'
'$nowEpochMillis'
r' \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('just strict', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true;
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('just multiplexed', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..multiplexed = true;
String c1 = b1.buildCommand();
expect(c1, 'monitor:multiplexed\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('strict and regex', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true
..regex=r'\.wavi';
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict \\.wavi\n');
expect(c1, r'monitor:strict \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('multiplexed and regex', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..multiplexed = true
..regex=r'\.wavi';
String c1 = b1.buildCommand();
expect(c1, 'monitor:multiplexed \\.wavi\n');
expect(
c1,
r'monitor:multiplexed \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('strict and lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict:$nowEpochMillis\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('multiplexed and lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..multiplexed = true
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:multiplexed:$nowEpochMillis\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('strict, multiplexed and regex', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true
..multiplexed = true
..regex=r'\.wavi';
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict:multiplexed \\.wavi\n');
expect(
c1,
r'monitor:strict:multiplexed \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('strict, multiplexed and lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true
..multiplexed = true
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict:multiplexed:$nowEpochMillis\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
test('strict, multiplexed, regex and lastNotificationTime', () {
MonitorVerbBuilder b1 = MonitorVerbBuilder()
..strict = true
..multiplexed = true
..regex=r'\.wavi'
..lastNotificationTime=nowEpochMillis;
String c1 = b1.buildCommand();
expect(c1, 'monitor:strict:multiplexed:$nowEpochMillis \\.wavi\n');
expect(
c1,
r'monitor:strict:multiplexed'
':$nowEpochMillis'
r' \.wavi'
'\n');
MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim());
expect(b2, b1);
String c2 = b2.buildCommand();
expect (c2, c1);
});
});
}