Implementation of Persistent Storage API to the flutter homescreen 67/30167/4
authorLudwig Schwiedrzik <ludwig.schwiedrzik@d-fine.com>
Tue, 13 Aug 2024 15:23:45 +0000 (17:23 +0200)
committerScott Murray <scott.murray@konsulko.com>
Tue, 10 Sep 2024 19:52:10 +0000 (19:52 +0000)
Added protobuf definition of Persistent Storage API.
Generated grpc-compliant code from protobuf API definition.
Set up storage_client based on similar radio_client.
Updated app_confi_provider and app_provider to include new API features
and prepare for implementation of persistent storage of users, user
preferences.
Added unit tests for all API rpcs.

Bug-AGL: [SPEC-5227]
Change-Id: I759501bcb9de3a70a14718f8b3a87bedcf811baa
Signed-off-by: Tom Kronsbein <tom.kronsbein@d-fine.com>
Signed-off-by: Ludwig Schwiedrzik <ludwig.schwiedrzik@d-fine.com>
lib/data/data_providers/app_config_provider.dart
lib/data/data_providers/app_provider.dart
lib/data/data_providers/storage_client.dart [new file with mode: 0644]
protos/lib/src/generated/storage_api.pb.dart [new file with mode: 0644]
protos/lib/src/generated/storage_api.pbenum.dart [new file with mode: 0644]
protos/lib/src/generated/storage_api.pbgrpc.dart [new file with mode: 0644]
protos/lib/src/generated/storage_api.pbjson.dart [new file with mode: 0644]
protos/lib/storage-api.dart [new file with mode: 0644]
protos/protos/storage_api.proto [new file with mode: 0644]
test/StorageClient_test.dart [new file with mode: 0644]

index 6a4ea02..b82eb54 100644 (file)
@@ -56,6 +56,23 @@ class RadioConfig {
   }
 }
 
+class StorageConfig {
+  final String hostname;
+  final int port;
+
+  static String defaultHostname = 'localhost';
+  static int defaultPort = 50054;
+
+  StorageConfig(
+      {required this.hostname, required this.port});
+
+  static StorageConfig defaultConfig() {
+    return StorageConfig(
+        hostname: StorageConfig.defaultHostname,
+        port: StorageConfig.defaultPort);
+  }
+}
+
 class MpdConfig {
   final String hostname;
   final int port;
@@ -77,6 +94,7 @@ class AppConfig {
   final bool randomHybridAnimation;
   final KuksaConfig kuksaConfig;
   final RadioConfig radioConfig;
+  final StorageConfig storageConfig;
   final MpdConfig mpdConfig;
 
   static String configFilePath = '/etc/xdg/AGL/ics-homescreen.yaml';
@@ -87,6 +105,7 @@ class AppConfig {
       required this.randomHybridAnimation,
       required this.kuksaConfig,
       required this.radioConfig,
+      required this.storageConfig,
       required this.mpdConfig});
 
   static KuksaConfig parseKuksaConfig(YamlMap kuksaMap) {
@@ -182,6 +201,25 @@ class AppConfig {
     }
   }
 
+    static StorageConfig parseStorageConfig(YamlMap storageMap) {
+    try {
+      String hostname = StorageConfig.defaultHostname;
+      if (storageMap.containsKey('hostname')) {
+        hostname = storageMap['hostname'];
+      }
+
+      int port = StorageConfig.defaultPort;
+      if (storageMap.containsKey('port')) {
+        port = storageMap['port'];
+      }
+
+      return StorageConfig(hostname: hostname, port: port);
+    } catch (_) {
+      debugPrint("Invalid storage configuration, using defaults");
+      return StorageConfig.defaultConfig();
+    }
+  }
+
   static MpdConfig parseMpdConfig(YamlMap mpdMap) {
     try {
       String hostname = MpdConfig.defaultHostname;
@@ -229,6 +267,13 @@ final appConfigProvider = Provider((ref) {
       radioConfig = RadioConfig.defaultConfig();
     }
 
+    StorageConfig storageConfig;
+    if (yamlMap.containsKey('storage')) {
+      storageConfig = AppConfig.parseStorageConfig(yamlMap['storage']);
+    } else {
+      storageConfig = StorageConfig.defaultConfig();
+    }
+
     MpdConfig mpdConfig;
     if (yamlMap.containsKey('mpd')) {
       mpdConfig = AppConfig.parseMpdConfig(yamlMap['mpd']);
@@ -266,6 +311,7 @@ final appConfigProvider = Provider((ref) {
         randomHybridAnimation: randomHybridAnimation,
         kuksaConfig: kuksaConfig,
         radioConfig: radioConfig,
+        storageConfig: storageConfig,
         mpdConfig: mpdConfig);
   } catch (_) {
     return AppConfig(
@@ -274,6 +320,7 @@ final appConfigProvider = Provider((ref) {
         randomHybridAnimation: false,
         kuksaConfig: KuksaConfig.defaultConfig(),
         radioConfig: RadioConfig.defaultConfig(),
+        storageConfig: StorageConfig.defaultConfig(),
         mpdConfig: MpdConfig.defaultConfig());
   }
 });
index ca2c3d4..0f7ed0c 100644 (file)
@@ -13,6 +13,7 @@ import 'package:flutter_ics_homescreen/data/data_providers/playlist_art_notifier
 import 'package:flutter_ics_homescreen/data/data_providers/val_client.dart';
 import 'package:flutter_ics_homescreen/data/data_providers/app_launcher.dart';
 import 'package:flutter_ics_homescreen/data/data_providers/radio_client.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/storage_client.dart';
 import 'package:flutter_ics_homescreen/data/data_providers/mpd_client.dart';
 import 'package:flutter_ics_homescreen/data/data_providers/play_controller.dart';
 import 'package:flutter_ics_homescreen/export.dart';
@@ -85,6 +86,13 @@ final radioClientProvider = Provider((ref) {
   return RadioClient(config: config, ref: ref);
 });
 
+
+final storageClientProvider = Provider((ref) {
+  StorageConfig config = ref.watch(appConfigProvider).storageConfig;
+  return StorageClient(config: config, ref: ref);
+});
+
+
 final mpdClientProvider = Provider((ref) {
   MpdConfig config = ref.watch(appConfigProvider).mpdConfig;
   return MpdClient(config: config, ref: ref);
@@ -134,9 +142,8 @@ final playControllerProvider = Provider((ref) {
   return PlayController(ref: ref);
 });
 
-final usersProvider = StateNotifierProvider<UsersNotifier, Users>((ref) {
-  return UsersNotifier(Users.initial());
-});
+final usersProvider =
+    NotifierProvider<UsersNotifier, Users>(UsersNotifier.new);
 
 final hybridStateProvider =
     StateNotifierProvider<HybridNotifier, Hybrid>((ref) {
diff --git a/lib/data/data_providers/storage_client.dart b/lib/data/data_providers/storage_client.dart
new file mode 100644 (file)
index 0000000..5c72785
--- /dev/null
@@ -0,0 +1,89 @@
+import 'package:flutter_ics_homescreen/export.dart';
+import 'package:protos/storage-api.dart' as storage_api;
+
+class StorageClient{
+  final StorageConfig config;
+  final Ref ref;
+  late storage_api.ClientChannel channel;
+  late storage_api.DatabaseClient stub;
+
+ StorageClient({required this.config, required this.ref}) {
+    debugPrint(
+        "Connecting to storage service at ${config.hostname}:${config.port}");
+    storage_api.ChannelCredentials creds = const storage_api.ChannelCredentials.insecure();
+    channel = storage_api.ClientChannel(config.hostname,
+        port: config.port, options: storage_api.ChannelOptions(credentials: creds));
+    stub = storage_api.DatabaseClient(channel);
+  }
+
+
+  Future<storage_api.StandardResponse> destroyDB() async {
+    try {
+      var response = await stub.destroyDB(storage_api.DestroyArguments());
+      return response;
+    } catch (e) {
+      print(e);
+      rethrow;
+    }
+  }
+
+  Future<storage_api.StandardResponse> write(storage_api.KeyValue keyValue) async {
+    try {
+      var response = await stub.write(keyValue);
+      return response;
+    } catch (e) {
+      print(e);
+      rethrow;
+    }
+  }
+  
+  Future<storage_api.ReadResponse> read(storage_api.Key key) async{
+    try{
+      var response = await stub.read(key);
+      return response;
+    } catch(e) {
+      print(e);
+      rethrow;
+    }
+  }
+
+  Future<storage_api.StandardResponse> delete(storage_api.Key key) async{
+    try{
+      var response = await stub.delete(key);
+      return response;
+    } catch(e) {
+      print(e);
+      rethrow;
+    }
+  }
+
+    Future<storage_api.ListResponse> search(storage_api.Key key) async{
+    try{
+      var response = await stub.search(key);
+      return response;
+    } catch(e) {
+      print(e);
+      rethrow;
+    }
+  }
+
+    Future<storage_api.StandardResponse> deleteNodes(storage_api.Key key) async{
+    try{
+      var response = await stub.deleteNodes(key);
+      return response;
+    } catch(e) {
+      print(e);
+      rethrow;
+    }
+  }
+
+    Future<storage_api.ListResponse> listNodes(storage_api.SubtreeInfo subtreeInfo) async{
+    try{
+      var response = await stub.listNodes(subtreeInfo);
+      return response;
+    } catch(e) {
+      print(e);
+      rethrow;
+    }
+  }
+}
\ No newline at end of file
diff --git a/protos/lib/src/generated/storage_api.pb.dart b/protos/lib/src/generated/storage_api.pb.dart
new file mode 100644 (file)
index 0000000..fb027be
--- /dev/null
@@ -0,0 +1,534 @@
+//
+//  Generated code. Do not modify.
+//  source: protos/protos/storage_api.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class Key extends $pb.GeneratedMessage {
+  factory Key({
+    $core.String? key,
+    $core.String? namespace,
+  }) {
+    final $result = create();
+    if (key != null) {
+      $result.key = key;
+    }
+    if (namespace != null) {
+      $result.namespace = namespace;
+    }
+    return $result;
+  }
+  Key._() : super();
+  factory Key.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory Key.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Key', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOS(1, _omitFieldNames ? '' : 'key')
+    ..aOS(2, _omitFieldNames ? '' : 'namespace')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  Key clone() => Key()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  Key copyWith(void Function(Key) updates) => super.copyWith((message) => updates(message as Key)) as Key;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static Key create() => Key._();
+  Key createEmptyInstance() => create();
+  static $pb.PbList<Key> createRepeated() => $pb.PbList<Key>();
+  @$core.pragma('dart2js:noInline')
+  static Key getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Key>(create);
+  static Key? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get key => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set key($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasKey() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearKey() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get namespace => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set namespace($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasNamespace() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearNamespace() => clearField(2);
+}
+
+class Value extends $pb.GeneratedMessage {
+  factory Value({
+    $core.String? value,
+  }) {
+    final $result = create();
+    if (value != null) {
+      $result.value = value;
+    }
+    return $result;
+  }
+  Value._() : super();
+  factory Value.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory Value.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Value', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOS(1, _omitFieldNames ? '' : 'value')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  Value clone() => Value()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  Value copyWith(void Function(Value) updates) => super.copyWith((message) => updates(message as Value)) as Value;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static Value create() => Value._();
+  Value createEmptyInstance() => create();
+  static $pb.PbList<Value> createRepeated() => $pb.PbList<Value>();
+  @$core.pragma('dart2js:noInline')
+  static Value getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Value>(create);
+  static Value? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get value => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set value($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasValue() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearValue() => clearField(1);
+}
+
+class KeyValue extends $pb.GeneratedMessage {
+  factory KeyValue({
+    $core.String? key,
+    $core.String? value,
+    $core.String? namespace,
+  }) {
+    final $result = create();
+    if (key != null) {
+      $result.key = key;
+    }
+    if (value != null) {
+      $result.value = value;
+    }
+    if (namespace != null) {
+      $result.namespace = namespace;
+    }
+    return $result;
+  }
+  KeyValue._() : super();
+  factory KeyValue.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory KeyValue.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'KeyValue', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOS(1, _omitFieldNames ? '' : 'key')
+    ..aOS(2, _omitFieldNames ? '' : 'value')
+    ..aOS(3, _omitFieldNames ? '' : 'namespace')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  KeyValue clone() => KeyValue()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  KeyValue copyWith(void Function(KeyValue) updates) => super.copyWith((message) => updates(message as KeyValue)) as KeyValue;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static KeyValue create() => KeyValue._();
+  KeyValue createEmptyInstance() => create();
+  static $pb.PbList<KeyValue> createRepeated() => $pb.PbList<KeyValue>();
+  @$core.pragma('dart2js:noInline')
+  static KeyValue getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<KeyValue>(create);
+  static KeyValue? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get key => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set key($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasKey() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearKey() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get value => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set value($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasValue() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearValue() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get namespace => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set namespace($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasNamespace() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearNamespace() => clearField(3);
+}
+
+class SubtreeInfo extends $pb.GeneratedMessage {
+  factory SubtreeInfo({
+    $core.String? node,
+    $core.int? layers,
+    $core.String? namespace,
+  }) {
+    final $result = create();
+    if (node != null) {
+      $result.node = node;
+    }
+    if (layers != null) {
+      $result.layers = layers;
+    }
+    if (namespace != null) {
+      $result.namespace = namespace;
+    }
+    return $result;
+  }
+  SubtreeInfo._() : super();
+  factory SubtreeInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory SubtreeInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SubtreeInfo', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOS(1, _omitFieldNames ? '' : 'node')
+    ..a<$core.int>(2, _omitFieldNames ? '' : 'layers', $pb.PbFieldType.O3)
+    ..aOS(3, _omitFieldNames ? '' : 'namespace')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  SubtreeInfo clone() => SubtreeInfo()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  SubtreeInfo copyWith(void Function(SubtreeInfo) updates) => super.copyWith((message) => updates(message as SubtreeInfo)) as SubtreeInfo;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static SubtreeInfo create() => SubtreeInfo._();
+  SubtreeInfo createEmptyInstance() => create();
+  static $pb.PbList<SubtreeInfo> createRepeated() => $pb.PbList<SubtreeInfo>();
+  @$core.pragma('dart2js:noInline')
+  static SubtreeInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SubtreeInfo>(create);
+  static SubtreeInfo? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get node => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set node($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasNode() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearNode() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.int get layers => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set layers($core.int v) { $_setSignedInt32(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasLayers() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearLayers() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get namespace => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set namespace($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasNamespace() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearNamespace() => clearField(3);
+}
+
+class DestroyArguments extends $pb.GeneratedMessage {
+  factory DestroyArguments() => create();
+  DestroyArguments._() : super();
+  factory DestroyArguments.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory DestroyArguments.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DestroyArguments', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  DestroyArguments clone() => DestroyArguments()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  DestroyArguments copyWith(void Function(DestroyArguments) updates) => super.copyWith((message) => updates(message as DestroyArguments)) as DestroyArguments;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static DestroyArguments create() => DestroyArguments._();
+  DestroyArguments createEmptyInstance() => create();
+  static $pb.PbList<DestroyArguments> createRepeated() => $pb.PbList<DestroyArguments>();
+  @$core.pragma('dart2js:noInline')
+  static DestroyArguments getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DestroyArguments>(create);
+  static DestroyArguments? _defaultInstance;
+}
+
+class StandardResponse extends $pb.GeneratedMessage {
+  factory StandardResponse({
+    $core.bool? success,
+    $core.String? message,
+  }) {
+    final $result = create();
+    if (success != null) {
+      $result.success = success;
+    }
+    if (message != null) {
+      $result.message = message;
+    }
+    return $result;
+  }
+  StandardResponse._() : super();
+  factory StandardResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory StandardResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StandardResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOB(1, _omitFieldNames ? '' : 'success')
+    ..aOS(2, _omitFieldNames ? '' : 'message')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  StandardResponse clone() => StandardResponse()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  StandardResponse copyWith(void Function(StandardResponse) updates) => super.copyWith((message) => updates(message as StandardResponse)) as StandardResponse;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static StandardResponse create() => StandardResponse._();
+  StandardResponse createEmptyInstance() => create();
+  static $pb.PbList<StandardResponse> createRepeated() => $pb.PbList<StandardResponse>();
+  @$core.pragma('dart2js:noInline')
+  static StandardResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StandardResponse>(create);
+  static StandardResponse? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.bool get success => $_getBF(0);
+  @$pb.TagNumber(1)
+  set success($core.bool v) { $_setBool(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasSuccess() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearSuccess() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get message => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set message($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasMessage() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearMessage() => clearField(2);
+}
+
+class ReadResponse extends $pb.GeneratedMessage {
+  factory ReadResponse({
+    $core.bool? success,
+    $core.String? message,
+    $core.String? result,
+  }) {
+    final $result = create();
+    if (success != null) {
+      $result.success = success;
+    }
+    if (message != null) {
+      $result.message = message;
+    }
+    if (result != null) {
+      $result.result = result;
+    }
+    return $result;
+  }
+  ReadResponse._() : super();
+  factory ReadResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ReadResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ReadResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOB(1, _omitFieldNames ? '' : 'success')
+    ..aOS(2, _omitFieldNames ? '' : 'message')
+    ..aOS(3, _omitFieldNames ? '' : 'result')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  ReadResponse clone() => ReadResponse()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  ReadResponse copyWith(void Function(ReadResponse) updates) => super.copyWith((message) => updates(message as ReadResponse)) as ReadResponse;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static ReadResponse create() => ReadResponse._();
+  ReadResponse createEmptyInstance() => create();
+  static $pb.PbList<ReadResponse> createRepeated() => $pb.PbList<ReadResponse>();
+  @$core.pragma('dart2js:noInline')
+  static ReadResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ReadResponse>(create);
+  static ReadResponse? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.bool get success => $_getBF(0);
+  @$pb.TagNumber(1)
+  set success($core.bool v) { $_setBool(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasSuccess() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearSuccess() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get message => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set message($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasMessage() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearMessage() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get result => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set result($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasResult() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearResult() => clearField(3);
+}
+
+class ListResponse extends $pb.GeneratedMessage {
+  factory ListResponse({
+    $core.bool? success,
+    $core.String? message,
+    $core.Iterable<$core.String>? result,
+  }) {
+    final $result = create();
+    if (success != null) {
+      $result.success = success;
+    }
+    if (message != null) {
+      $result.message = message;
+    }
+    if (result != null) {
+      $result.result.addAll(result);
+    }
+    return $result;
+  }
+  ListResponse._() : super();
+  factory ListResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory ListResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ListResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'storage_api'), createEmptyInstance: create)
+    ..aOB(1, _omitFieldNames ? '' : 'success')
+    ..aOS(2, _omitFieldNames ? '' : 'message')
+    ..pPS(3, _omitFieldNames ? '' : 'result')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  ListResponse clone() => ListResponse()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  ListResponse copyWith(void Function(ListResponse) updates) => super.copyWith((message) => updates(message as ListResponse)) as ListResponse;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static ListResponse create() => ListResponse._();
+  ListResponse createEmptyInstance() => create();
+  static $pb.PbList<ListResponse> createRepeated() => $pb.PbList<ListResponse>();
+  @$core.pragma('dart2js:noInline')
+  static ListResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListResponse>(create);
+  static ListResponse? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.bool get success => $_getBF(0);
+  @$pb.TagNumber(1)
+  set success($core.bool v) { $_setBool(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasSuccess() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearSuccess() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get message => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set message($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasMessage() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearMessage() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.List<$core.String> get result => $_getList(2);
+}
+
+
+const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
+const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
diff --git a/protos/lib/src/generated/storage_api.pbenum.dart b/protos/lib/src/generated/storage_api.pbenum.dart
new file mode 100644 (file)
index 0000000..8c2646e
--- /dev/null
@@ -0,0 +1,11 @@
+//
+//  Generated code. Do not modify.
+//  source: protos/protos/storage_api.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
diff --git a/protos/lib/src/generated/storage_api.pbgrpc.dart b/protos/lib/src/generated/storage_api.pbgrpc.dart
new file mode 100644 (file)
index 0000000..8bdd848
--- /dev/null
@@ -0,0 +1,179 @@
+//
+//  Generated code. Do not modify.
+//  source: protos/protos/storage_api.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:async' as $async;
+import 'dart:core' as $core;
+
+import 'package:grpc/service_api.dart' as $grpc;
+import 'package:protobuf/protobuf.dart' as $pb;
+
+import 'storage_api.pb.dart' as $0;
+
+export 'storage_api.pb.dart';
+
+@$pb.GrpcServiceName('storage_api.Database')
+class DatabaseClient extends $grpc.Client {
+  static final _$destroyDB = $grpc.ClientMethod<$0.DestroyArguments, $0.StandardResponse>(
+      '/storage_api.Database/DestroyDB',
+      ($0.DestroyArguments value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.StandardResponse.fromBuffer(value));
+  static final _$write = $grpc.ClientMethod<$0.KeyValue, $0.StandardResponse>(
+      '/storage_api.Database/Write',
+      ($0.KeyValue value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.StandardResponse.fromBuffer(value));
+  static final _$read = $grpc.ClientMethod<$0.Key, $0.ReadResponse>(
+      '/storage_api.Database/Read',
+      ($0.Key value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.ReadResponse.fromBuffer(value));
+  static final _$delete = $grpc.ClientMethod<$0.Key, $0.StandardResponse>(
+      '/storage_api.Database/Delete',
+      ($0.Key value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.StandardResponse.fromBuffer(value));
+  static final _$search = $grpc.ClientMethod<$0.Key, $0.ListResponse>(
+      '/storage_api.Database/Search',
+      ($0.Key value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.ListResponse.fromBuffer(value));
+  static final _$deleteNodes = $grpc.ClientMethod<$0.Key, $0.StandardResponse>(
+      '/storage_api.Database/DeleteNodes',
+      ($0.Key value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.StandardResponse.fromBuffer(value));
+  static final _$listNodes = $grpc.ClientMethod<$0.SubtreeInfo, $0.ListResponse>(
+      '/storage_api.Database/ListNodes',
+      ($0.SubtreeInfo value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.ListResponse.fromBuffer(value));
+
+  DatabaseClient($grpc.ClientChannel channel,
+      {$grpc.CallOptions? options,
+      $core.Iterable<$grpc.ClientInterceptor>? interceptors})
+      : super(channel, options: options,
+        interceptors: interceptors);
+
+  $grpc.ResponseFuture<$0.StandardResponse> destroyDB($0.DestroyArguments request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$destroyDB, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.StandardResponse> write($0.KeyValue request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$write, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.ReadResponse> read($0.Key request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$read, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.StandardResponse> delete($0.Key request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$delete, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.ListResponse> search($0.Key request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$search, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.StandardResponse> deleteNodes($0.Key request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$deleteNodes, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.ListResponse> listNodes($0.SubtreeInfo request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$listNodes, request, options: options);
+  }
+}
+
+@$pb.GrpcServiceName('storage_api.Database')
+abstract class DatabaseServiceBase extends $grpc.Service {
+  $core.String get $name => 'storage_api.Database';
+
+  DatabaseServiceBase() {
+    $addMethod($grpc.ServiceMethod<$0.DestroyArguments, $0.StandardResponse>(
+        'DestroyDB',
+        destroyDB_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.DestroyArguments.fromBuffer(value),
+        ($0.StandardResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.KeyValue, $0.StandardResponse>(
+        'Write',
+        write_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.KeyValue.fromBuffer(value),
+        ($0.StandardResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.Key, $0.ReadResponse>(
+        'Read',
+        read_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.Key.fromBuffer(value),
+        ($0.ReadResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.Key, $0.StandardResponse>(
+        'Delete',
+        delete_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.Key.fromBuffer(value),
+        ($0.StandardResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.Key, $0.ListResponse>(
+        'Search',
+        search_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.Key.fromBuffer(value),
+        ($0.ListResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.Key, $0.StandardResponse>(
+        'DeleteNodes',
+        deleteNodes_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.Key.fromBuffer(value),
+        ($0.StandardResponse value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.SubtreeInfo, $0.ListResponse>(
+        'ListNodes',
+        listNodes_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.SubtreeInfo.fromBuffer(value),
+        ($0.ListResponse value) => value.writeToBuffer()));
+  }
+
+  $async.Future<$0.StandardResponse> destroyDB_Pre($grpc.ServiceCall call, $async.Future<$0.DestroyArguments> request) async {
+    return destroyDB(call, await request);
+  }
+
+  $async.Future<$0.StandardResponse> write_Pre($grpc.ServiceCall call, $async.Future<$0.KeyValue> request) async {
+    return write(call, await request);
+  }
+
+  $async.Future<$0.ReadResponse> read_Pre($grpc.ServiceCall call, $async.Future<$0.Key> request) async {
+    return read(call, await request);
+  }
+
+  $async.Future<$0.StandardResponse> delete_Pre($grpc.ServiceCall call, $async.Future<$0.Key> request) async {
+    return delete(call, await request);
+  }
+
+  $async.Future<$0.ListResponse> search_Pre($grpc.ServiceCall call, $async.Future<$0.Key> request) async {
+    return search(call, await request);
+  }
+
+  $async.Future<$0.StandardResponse> deleteNodes_Pre($grpc.ServiceCall call, $async.Future<$0.Key> request) async {
+    return deleteNodes(call, await request);
+  }
+
+  $async.Future<$0.ListResponse> listNodes_Pre($grpc.ServiceCall call, $async.Future<$0.SubtreeInfo> request) async {
+    return listNodes(call, await request);
+  }
+
+  $async.Future<$0.StandardResponse> destroyDB($grpc.ServiceCall call, $0.DestroyArguments request);
+  $async.Future<$0.StandardResponse> write($grpc.ServiceCall call, $0.KeyValue request);
+  $async.Future<$0.ReadResponse> read($grpc.ServiceCall call, $0.Key request);
+  $async.Future<$0.StandardResponse> delete($grpc.ServiceCall call, $0.Key request);
+  $async.Future<$0.ListResponse> search($grpc.ServiceCall call, $0.Key request);
+  $async.Future<$0.StandardResponse> deleteNodes($grpc.ServiceCall call, $0.Key request);
+  $async.Future<$0.ListResponse> listNodes($grpc.ServiceCall call, $0.SubtreeInfo request);
+}
diff --git a/protos/lib/src/generated/storage_api.pbjson.dart b/protos/lib/src/generated/storage_api.pbjson.dart
new file mode 100644 (file)
index 0000000..f697b4b
--- /dev/null
@@ -0,0 +1,126 @@
+//
+//  Generated code. Do not modify.
+//  source: protos/protos/storage_api.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:convert' as $convert;
+import 'dart:core' as $core;
+import 'dart:typed_data' as $typed_data;
+
+@$core.Deprecated('Use keyDescriptor instead')
+const Key$json = {
+  '1': 'Key',
+  '2': [
+    {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
+    {'1': 'namespace', '3': 2, '4': 1, '5': 9, '10': 'namespace'},
+  ],
+};
+
+/// Descriptor for `Key`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List keyDescriptor = $convert.base64Decode(
+    'CgNLZXkSEAoDa2V5GAEgASgJUgNrZXkSHAoJbmFtZXNwYWNlGAIgASgJUgluYW1lc3BhY2U=');
+
+@$core.Deprecated('Use valueDescriptor instead')
+const Value$json = {
+  '1': 'Value',
+  '2': [
+    {'1': 'value', '3': 1, '4': 1, '5': 9, '10': 'value'},
+  ],
+};
+
+/// Descriptor for `Value`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List valueDescriptor = $convert.base64Decode(
+    'CgVWYWx1ZRIUCgV2YWx1ZRgBIAEoCVIFdmFsdWU=');
+
+@$core.Deprecated('Use keyValueDescriptor instead')
+const KeyValue$json = {
+  '1': 'KeyValue',
+  '2': [
+    {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
+    {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'},
+    {'1': 'namespace', '3': 3, '4': 1, '5': 9, '10': 'namespace'},
+  ],
+};
+
+/// Descriptor for `KeyValue`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List keyValueDescriptor = $convert.base64Decode(
+    'CghLZXlWYWx1ZRIQCgNrZXkYASABKAlSA2tleRIUCgV2YWx1ZRgCIAEoCVIFdmFsdWUSHAoJbm'
+    'FtZXNwYWNlGAMgASgJUgluYW1lc3BhY2U=');
+
+@$core.Deprecated('Use subtreeInfoDescriptor instead')
+const SubtreeInfo$json = {
+  '1': 'SubtreeInfo',
+  '2': [
+    {'1': 'node', '3': 1, '4': 1, '5': 9, '10': 'node'},
+    {'1': 'layers', '3': 2, '4': 1, '5': 5, '9': 0, '10': 'layers', '17': true},
+    {'1': 'namespace', '3': 3, '4': 1, '5': 9, '10': 'namespace'},
+  ],
+  '8': [
+    {'1': '_layers'},
+  ],
+};
+
+/// Descriptor for `SubtreeInfo`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List subtreeInfoDescriptor = $convert.base64Decode(
+    'CgtTdWJ0cmVlSW5mbxISCgRub2RlGAEgASgJUgRub2RlEhsKBmxheWVycxgCIAEoBUgAUgZsYX'
+    'llcnOIAQESHAoJbmFtZXNwYWNlGAMgASgJUgluYW1lc3BhY2VCCQoHX2xheWVycw==');
+
+@$core.Deprecated('Use destroyArgumentsDescriptor instead')
+const DestroyArguments$json = {
+  '1': 'DestroyArguments',
+};
+
+/// Descriptor for `DestroyArguments`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List destroyArgumentsDescriptor = $convert.base64Decode(
+    'ChBEZXN0cm95QXJndW1lbnRz');
+
+@$core.Deprecated('Use standardResponseDescriptor instead')
+const StandardResponse$json = {
+  '1': 'StandardResponse',
+  '2': [
+    {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'},
+    {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
+  ],
+};
+
+/// Descriptor for `StandardResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List standardResponseDescriptor = $convert.base64Decode(
+    'ChBTdGFuZGFyZFJlc3BvbnNlEhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSGAoHbWVzc2FnZR'
+    'gCIAEoCVIHbWVzc2FnZQ==');
+
+@$core.Deprecated('Use readResponseDescriptor instead')
+const ReadResponse$json = {
+  '1': 'ReadResponse',
+  '2': [
+    {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'},
+    {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
+    {'1': 'result', '3': 3, '4': 1, '5': 9, '10': 'result'},
+  ],
+};
+
+/// Descriptor for `ReadResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List readResponseDescriptor = $convert.base64Decode(
+    'CgxSZWFkUmVzcG9uc2USGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIYCgdtZXNzYWdlGAIgAS'
+    'gJUgdtZXNzYWdlEhYKBnJlc3VsdBgDIAEoCVIGcmVzdWx0');
+
+@$core.Deprecated('Use listResponseDescriptor instead')
+const ListResponse$json = {
+  '1': 'ListResponse',
+  '2': [
+    {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'},
+    {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
+    {'1': 'result', '3': 3, '4': 3, '5': 9, '10': 'result'},
+  ],
+};
+
+/// Descriptor for `ListResponse`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List listResponseDescriptor = $convert.base64Decode(
+    'CgxMaXN0UmVzcG9uc2USGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIYCgdtZXNzYWdlGAIgAS'
+    'gJUgdtZXNzYWdlEhYKBnJlc3VsdBgDIAMoCVIGcmVzdWx0');
+
diff --git a/protos/lib/storage-api.dart b/protos/lib/storage-api.dart
new file mode 100644 (file)
index 0000000..eccfb04
--- /dev/null
@@ -0,0 +1,8 @@
+library storage_api;
+
+export 'src/generated/storage_api.pb.dart';
+export 'src/generated/storage_api.pbenum.dart';
+export 'src/generated/storage_api.pbgrpc.dart';
+export 'src/generated/storage_api.pbjson.dart';
+
+export 'package:grpc/grpc.dart';
\ No newline at end of file
diff --git a/protos/protos/storage_api.proto b/protos/protos/storage_api.proto
new file mode 100644 (file)
index 0000000..90272e8
--- /dev/null
@@ -0,0 +1,66 @@
+syntax = "proto3";
+
+package storage_api;
+
+service Database {
+    // Deletes the entire data base.
+    rpc DestroyDB(DestroyArguments) returns (StandardResponse);
+
+    // Writes a key-value pair to the data base
+    rpc Write(KeyValue) returns (StandardResponse);
+
+    // Reads the value for the given key from the data base.
+    rpc Read(Key) returns (ReadResponse);
+
+    // Deletes the entry for the given key from the data base.
+    rpc Delete(Key) returns (StandardResponse);
+
+    // Lists any keys that contain the given string.
+    rpc Search(Key) returns (ListResponse);
+
+    // Deletes all keys in subtree of given root. Assumes that keys follow VSS-like tress structure.
+    rpc DeleteNodes(Key) returns (StandardResponse);
+
+    // Lists all nodes in subtree of given root and depth. Assumes that keys follow VSS-like tress structure.
+    rpc ListNodes(SubtreeInfo) returns (ListResponse);
+}
+
+message Key {
+    string key = 1;
+    string namespace = 2;
+}
+
+message Value {
+    string value = 1;
+}
+
+message KeyValue {
+    string key = 1;
+    string value = 2;
+    string namespace = 3;
+}
+
+message SubtreeInfo {
+    string node = 1;
+    optional int32 layers = 2;
+    string namespace = 3;
+}
+
+message DestroyArguments {}
+
+message StandardResponse {
+    bool success = 1;
+    string message = 2;
+}
+
+message ReadResponse {
+    bool success = 1;
+    string message = 2;
+    string result = 3;
+}
+
+message ListResponse {
+    bool success = 1;
+    string message = 2;
+    repeated string result = 3;
+}
diff --git a/test/StorageClient_test.dart b/test/StorageClient_test.dart
new file mode 100644 (file)
index 0000000..1c36809
--- /dev/null
@@ -0,0 +1,557 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:protos/storage-api.dart' as storage_api;
+import 'package:flutter_ics_homescreen/export.dart';
+
+import 'package:flutter_ics_homescreen/data/data_providers/storage_client.dart';
+
+// Mock implementation of Ref if necessary.
+class MockRef extends Ref {
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+void main() {
+  late StorageClient storageClient;
+
+   // Default namespace.
+  late storage_api.Key keyEmpty;
+  late storage_api.Key key1;
+  late storage_api.Key key2;
+  late storage_api.Key key3;
+  late storage_api.Key key4;
+  late storage_api.Key keyPartialInfotainment;
+  late storage_api.Key keyPartialVehicle;
+  
+  late storage_api.KeyValue keyValueEmpty;
+  late storage_api.KeyValue keyValue1;
+  late storage_api.KeyValue keyValue2;
+  late storage_api.KeyValue keyValue3;
+  late storage_api.KeyValue keyValue4;
+
+  // Non-default "testSpace" namespace (TS)
+  late storage_api.Key key1TS;
+  late storage_api.Key key2TS;
+  late storage_api.Key key3TS;
+  late storage_api.Key key4TS;
+  late storage_api.Key key4PartialInfotainmentTS;
+  late storage_api.Key keyPartialVehicleTS;
+  
+  late storage_api.KeyValue keyValue1TS;
+  late storage_api.KeyValue keyValue2TS;
+  late storage_api.KeyValue keyValue3TS;
+  late storage_api.KeyValue keyValue4TS;
+  
+
+  setUp(() {
+    storageClient = StorageClient(
+      config: StorageConfig.defaultConfig(),
+      ref: MockRef(),
+    );
+
+    // Default nameSpace.
+    keyEmpty = storage_api.Key(key: '');
+    keyValueEmpty = storage_api.KeyValue(key: '', value: 'valueEmpty');
+
+    key1 = storage_api.Key(key: 'Vehicle.Infotainment.Radio.CurrentStation');
+    keyValue1 = storage_api.KeyValue()
+    ..key = 'Vehicle.Infotainment.Radio.CurrentStation'
+    ..value = 'testStation';
+
+    key2 = storage_api.Key(key: 'Vehicle.Infotainment.Radio.Volume');
+    keyValue2 = storage_api.KeyValue()
+    ..key = 'Vehicle.Infotainment.Radio.Volume'
+    ..value = '42';
+
+    key3 = storage_api.Key(key: 'Vehicle.Infotainment.HVAC.OutdoorTemperature');
+    keyValue3 = storage_api.KeyValue()
+    ..key = 'Vehicle.Infotainment.HVAC.OutdoorTemperature'
+    ..value = '17';
+
+    key4 = storage_api.Key(key: 'Vehicle.Communication.Radio.Volume');
+    keyValue4 = storage_api.KeyValue()
+    ..key = 'Vehicle.Communication.Radio.Volume'
+    ..value = '40';
+
+    keyPartialInfotainment = storage_api.Key(key: 'Vehicle.Infotainment');
+    keyPartialVehicle = storage_api.Key(key: 'Vehicle');
+
+
+    // Non-default "testSpace" namespace (TS)
+    key1TS = storage_api.Key(key: 'VehicleTS.Infotainment.Radio.CurrentStation', namespace: 'testSpace');
+    keyValue1TS = storage_api.KeyValue()
+    ..key = 'VehicleTS.Infotainment.Radio.CurrentStation'
+    ..value = 'testStationTS'
+    ..namespace= 'testSpace';
+
+    key2TS = storage_api.Key(key: 'VehicleTS.Infotainment.Radio.Volume', namespace: 'testSpace');
+    keyValue2TS = storage_api.KeyValue()
+    ..key = 'VehicleTS.Infotainment.Radio.Volume'
+    ..value = '42TS'
+    ..namespace = 'testSpace';
+
+    key3TS = storage_api.Key(key: 'VehicleTS.Infotainment.HVAC.OutdoorTemperature', namespace: 'testSpace');
+    keyValue3TS = storage_api.KeyValue()
+    ..key = 'VehicleTS.Infotainment.HVAC.OutdoorTemperature'
+    ..value = '17TS'
+    ..namespace = 'testSpace';
+
+    key4TS = storage_api.Key(key: 'VehicleTS.Communication.Radio.Volume', namespace: 'testSpace');
+    keyValue4TS = storage_api.KeyValue()
+    ..key = 'VehicleTS.Communication.Radio.Volume'
+    ..value = '40TS'
+    ..namespace = 'testSpace';
+
+    key4PartialInfotainmentTS = storage_api.Key(key: 'VehicleTS.Infotainment', namespace: 'testSpace');
+    keyPartialVehicleTS = storage_api.Key(key: 'VehicleTS', namespace: 'testSpace');
+  });
+
+  // PrepareTree.
+  Future prepareTree() async{
+    await storageClient.destroyDB();
+
+    // Default namespace.
+    await storageClient.write(keyValue1);
+    await storageClient.write(keyValue2);
+    await storageClient.write(keyValue3);
+    await storageClient.write(keyValue4);
+    
+    // Non-default "testSpace" namespace
+    await storageClient.write(keyValue1TS);
+    await storageClient.write(keyValue2TS);
+    await storageClient.write(keyValue3TS);
+    await storageClient.write(keyValue4TS);
+  }
+  test('prepareTree ', () async{
+    await prepareTree();
+    // Default namespace.
+    final readResponse1 = await storageClient.read(key1);
+    final readResponse2 = await storageClient.read(key2);
+    final readResponse3 = await storageClient.read(key3);
+    final readResponse4 = await storageClient.read(key4);
+
+    // Non-default "testSpace" namespace
+    final readResponse1TS = await storageClient.read(key1TS);
+    final readResponse2TS = await storageClient.read(key2TS);
+    final readResponse3TS = await storageClient.read(key3TS);
+    final readResponse4TS = await storageClient.read(key4TS);
+    
+    // Default namespace.
+    expect(readResponse1, isA<storage_api.ReadResponse>());
+    expect(readResponse1.success, isTrue);
+    expect(readResponse1.result, equals(keyValue1.value));
+
+    expect(readResponse2, isA<storage_api.ReadResponse>());
+    expect(readResponse2.success, isTrue);
+    expect(readResponse2.result, equals(keyValue2.value));
+
+    expect(readResponse3, isA<storage_api.ReadResponse>());
+    expect(readResponse3.success, isTrue);
+    expect(readResponse3.result, equals(keyValue3.value));
+
+    expect(readResponse4, isA<storage_api.ReadResponse>());
+    expect(readResponse4.success, isTrue);
+    expect(readResponse4.result, equals(keyValue4.value));
+
+    // Non-default "testSpace" namespace
+    expect(readResponse1TS, isA<storage_api.ReadResponse>());
+    expect(readResponse1TS.success, isTrue);
+    expect(readResponse1TS.result, equals(keyValue1TS.value));
+
+    expect(readResponse2TS, isA<storage_api.ReadResponse>());
+    expect(readResponse2TS.success, isTrue);
+    expect(readResponse2TS.result, equals(keyValue2TS.value));
+
+    expect(readResponse3TS, isA<storage_api.ReadResponse>());
+    expect(readResponse3TS.success, isTrue);
+    expect(readResponse3TS.result, equals(keyValue3TS.value));
+
+    expect(readResponse4TS, isA<storage_api.ReadResponse>());
+    expect(readResponse4TS.success, isTrue);
+    expect(readResponse4TS.result, equals(keyValue4TS.value));
+    await storageClient.destroyDB();
+  });
+
+  // Read and Write.
+  test('write and read node', () async {
+    await storageClient.destroyDB();
+    // Act.
+    await storageClient.write(keyValue1);
+    final readResponse = await storageClient.read(key1);
+
+    // Assert.
+    expect(readResponse, isA<storage_api.ReadResponse>()); // Checks if object is of right instance
+    expect(readResponse.success, isTrue); // Checks if read was successful
+    expect(readResponse.result, equals(keyValue1.value)); // Checks the result value
+    await storageClient.destroyDB();
+  });
+  
+  test('write and read node (non-default namespace) ', () async {
+    await storageClient.destroyDB();
+    await storageClient.write(keyValue1TS);
+    final readResponse = await storageClient.read(key1TS);
+
+    expect(readResponse, isA<storage_api.ReadResponse>());
+    expect(readResponse.success, isTrue);
+    expect(readResponse.result, equals(keyValue1TS.value));
+    await storageClient.destroyDB();
+  });
+
+  test('write and read root', () async {
+    await storageClient.destroyDB();
+    final storage_api.Key keyRoot = storage_api.Key(key: 'Vehicle');
+    final storage_api.KeyValue keyValueRoot = storage_api.KeyValue(key: 'Vehicle', value: 'Car1');
+
+    await storageClient.write(keyValueRoot);
+    final readResponse = await storageClient.read(keyRoot);
+
+    expect(readResponse, isA<storage_api.ReadResponse>());
+    expect(readResponse.success, isTrue);
+    expect(readResponse.result, equals(keyValueRoot.value));
+    await storageClient.destroyDB();
+  });
+
+  test('write key with space', () async {
+    await storageClient.destroyDB();
+    final storage_api.Key keySpace = storage_api.Key(key: 'My Vehicle');
+    final storage_api.KeyValue keyValueSpace = storage_api.KeyValue(key: 'My Vehicle', value: 'my Car');
+
+    await storageClient.write(keyValueSpace);
+    final readResponse = await storageClient.read(keySpace);
+
+    expect(readResponse, isA<storage_api.ReadResponse>());
+    expect(readResponse.success, isTrue);
+    expect(readResponse.result, equals(keyValueSpace.value));
+    await storageClient.destroyDB();
+  });
+  
+    test('write with empty key', () async{
+      await storageClient.destroyDB();
+      final writeResponse = await storageClient.write(keyValueEmpty);
+      expect(writeResponse.success, isFalse);
+      expect(writeResponse.message, 'Error when trying to write key \'\' and value \'${keyValueEmpty.value}\': Key cannot be empty string.');
+      await storageClient.destroyDB();
+    });
+
+    test('read non-existent key', () async{
+      await storageClient.destroyDB();
+      final writeResponse = await storageClient.read(storage_api.Key(key: 'Key.Absent'));
+      expect(writeResponse.success, isFalse);
+      expect(writeResponse.result, '');
+      await storageClient.destroyDB();
+    });
+
+    // Search.
+    test('search substring: Vehicle.Infotainment.Radio', () async {
+    await prepareTree();
+
+    final searchResponse = await storageClient.search(storage_api.Key(key: 'Vehicle.Infotainment.Radio'));
+    expect(searchResponse, isA<storage_api.ListResponse>());
+    expect(searchResponse.success, isTrue);
+    expect(searchResponse.result, [
+            'Vehicle.Infotainment.Radio.CurrentStation',
+            'Vehicle.Infotainment.Radio.Volume']);
+    await storageClient.destroyDB();
+  });
+
+  test('search substring: Vehicle.Infotainment.Radio (non-default namespace)', () async {
+    await prepareTree();
+
+    final searchResponse = await storageClient.search(storage_api.Key(key: 'VehicleTS.Infotainment.Radio', namespace: 'testSpace'));
+    expect(searchResponse, isA<storage_api.ListResponse>());
+    expect(searchResponse.success, isTrue);
+    expect(searchResponse.result, [
+            'VehicleTS.Infotainment.Radio.CurrentStation',
+            'VehicleTS.Infotainment.Radio.Volume']);
+    await storageClient.destroyDB();
+  });
+
+  test('search substring: Radio', () async {
+    await prepareTree();
+
+    final searchResponse = await storageClient.search(storage_api.Key(key: 'Radio'));
+    expect(searchResponse, isA<storage_api.ListResponse>());
+    expect(searchResponse.success, isTrue);
+    expect(searchResponse.result, [
+            'Vehicle.Communication.Radio.Volume',
+            'Vehicle.Infotainment.Radio.CurrentStation',
+            'Vehicle.Infotainment.Radio.Volume'
+          ]);
+    await storageClient.destroyDB();
+  });
+
+  test('search full key', () async{
+    await storageClient.destroyDB();
+    await storageClient.write(keyValue1);
+
+    final searchResponse = await storageClient.search(storage_api.Key(key: 'Vehicle.Infotainment.Radio.CurrentStation'));
+    expect(searchResponse, isA<storage_api.ListResponse>());
+    expect(searchResponse.success, isTrue);
+
+    final keyList = searchResponse.result;
+    expect(keyList, contains('Vehicle.Infotainment.Radio.CurrentStation'));
+    await storageClient.destroyDB();
+  });
+
+  test('search empty key', () async{
+    await prepareTree();
+    final searchResponse = await storageClient.search(keyEmpty);
+
+    expect(searchResponse.success, isTrue);
+    // Every element of the namespace is returned.
+    expect((searchResponse.result), [
+            'Vehicle.Communication.Radio.Volume',
+            'Vehicle.Infotainment.HVAC.OutdoorTemperature',
+            'Vehicle.Infotainment.Radio.CurrentStation',
+            'Vehicle.Infotainment.Radio.Volume'
+          ]);
+    await storageClient.destroyDB();
+  });
+
+  // Delete.
+  test('delete verification of a single key with search ', () async {
+    await storageClient.destroyDB();
+    await storageClient.write(keyValue1);
+    
+    // Check before deletion.
+    final searchResponseBeforeDelete = await storageClient.search(storage_api.Key(key: 'Vehicle.Infotainment.Radio.CurrentStation'));
+    expect(searchResponseBeforeDelete, isA<storage_api.ListResponse>());
+    expect(searchResponseBeforeDelete.success, isTrue);
+
+    final keyListBeforeDelete = searchResponseBeforeDelete.result;
+    expect(keyListBeforeDelete, contains('Vehicle.Infotainment.Radio.CurrentStation'));
+
+    // Deletion.
+    await storageClient.delete(storage_api.Key(key: 'Vehicle.Infotainment.Radio.CurrentStation'));
+
+    // Check after deletion.
+    final searchResponseAfterDelete = await storageClient.search(storage_api.Key(key: 'Vehicle.Infotainment.Radio.CurrentStation'));
+    expect(searchResponseAfterDelete, isA<storage_api.ListResponse>());
+    expect(searchResponseAfterDelete.success, isTrue);
+
+    final keyListAfterDelete = searchResponseAfterDelete.result;
+    expect(keyListAfterDelete, isNot(contains('Vehicle.Infotainment.Radio.CurrentStation')));
+  });
+
+  test('delete verification of a single key with search (non-default namespace)', () async {
+    await storageClient.destroyDB();
+    await storageClient.write(keyValue1TS);
+    
+    // Check before deletion.
+    final searchResponseBeforeDelete = await storageClient.search(storage_api.Key(key: 'VehicleTS.Infotainment.Radio.CurrentStation', namespace: 'testSpace'));
+    expect(searchResponseBeforeDelete, isA<storage_api.ListResponse>());
+    expect(searchResponseBeforeDelete.success, isTrue);
+
+    final keyListBeforeDelete = searchResponseBeforeDelete.result;
+    expect(keyListBeforeDelete, contains('VehicleTS.Infotainment.Radio.CurrentStation'));
+
+    // Deletion.
+    await storageClient.delete(storage_api.Key(key: 'VehicleTS.Infotainment.Radio.CurrentStation', namespace: 'testSpace'));
+
+    // Check after deletion.
+    final searchResponseAfterDelete = await storageClient.search(storage_api.Key(key: 'VehicleTS.Infotainment.Radio.CurrentStation', namespace: 'testSpace'));
+    expect(searchResponseAfterDelete, isA<storage_api.ListResponse>());
+    expect(searchResponseAfterDelete.success, isTrue);
+
+    final keyListAfterDelete = searchResponseAfterDelete.result;
+    expect(keyListAfterDelete, isNot(contains('VehicleTS.Infotainment.Radio.CurrentStation')));
+    await storageClient.destroyDB();
+  });
+
+    test('delete-key does not exist ', () async{
+      await storageClient.destroyDB();
+      final deleteResponse = await storageClient.delete(key1);
+      expect(deleteResponse.success, isFalse);
+      expect(deleteResponse.message, 'Key \'${key1.key}\' does not exist in namespace \'${key1.namespace}\'!');
+      await storageClient.destroyDB();
+    });
+
+    // DestroyDB.
+    test('DestroyDB', () async{
+      await prepareTree();
+      await storageClient.destroyDB();
+      final searchResponse = await storageClient.search(keyPartialVehicle);
+      expect(searchResponse, isA<storage_api.ListResponse>());
+      expect(searchResponse.success, isTrue);
+      expect(searchResponse.result, []);
+      await storageClient.destroyDB();
+    });
+
+    // DeleteNodes.
+    test('Delete Infotainment node ', () async{
+      await prepareTree();
+      final deleteNodesResponse = await storageClient.deleteNodes(keyPartialInfotainment);
+      expect(deleteNodesResponse, isA<storage_api.StandardResponse>());
+      expect(deleteNodesResponse.success, isTrue);
+
+      final searchResponse = await storageClient.search(keyPartialVehicle);
+      expect(searchResponse, isA<storage_api.ListResponse>());
+      expect(searchResponse.success, isTrue);
+      expect(searchResponse.result, [
+        'Vehicle.Communication.Radio.Volume']);
+        await storageClient.destroyDB();
+    });
+
+    test('Delete Infotainment node (non-default namespace)', () async{
+      await prepareTree();
+      final deleteNodesResponse = await storageClient.deleteNodes(key4PartialInfotainmentTS);
+      expect(deleteNodesResponse, isA<storage_api.StandardResponse>());
+      expect(deleteNodesResponse.success, isTrue);
+
+      final searchResponse = await storageClient.search(keyPartialVehicleTS);
+      expect(searchResponse, isA<storage_api.ListResponse>());
+      expect(searchResponse.success, isTrue);
+      expect(searchResponse.result, [
+        'VehicleTS.Communication.Radio.Volume']);
+        await storageClient.destroyDB();
+    });
+
+    test('Delete Vehicle node ', () async{
+      await prepareTree();
+      final deleteNodesResponse = await storageClient.deleteNodes(keyPartialVehicle);
+      expect(deleteNodesResponse, isA<storage_api.StandardResponse>());
+      expect(deleteNodesResponse.success, isTrue);
+
+      final searchResponse = await storageClient.search(keyPartialVehicle);
+      expect(searchResponse, isA<storage_api.ListResponse>());
+      expect(searchResponse.success, isTrue);
+      expect(searchResponse.result, []);
+      await storageClient.destroyDB();
+    });
+
+    test('Delete node with empty key ', () async{
+      await prepareTree();
+      final deleteNodesResponse = await storageClient.deleteNodes(keyEmpty);
+      expect(deleteNodesResponse, isA<storage_api.StandardResponse>());
+      expect(deleteNodesResponse.success, isFalse);
+
+      final searchResponse = await storageClient.search(keyPartialVehicle);
+      expect(searchResponse, isA<storage_api.ListResponse>());
+      expect(searchResponse.success, isTrue);
+      expect(searchResponse.result, [
+        'Vehicle.Communication.Radio.Volume',
+        'Vehicle.Infotainment.HVAC.OutdoorTemperature',
+        'Vehicle.Infotainment.Radio.CurrentStation',
+        'Vehicle.Infotainment.Radio.Volume'
+          ]);
+      await storageClient.destroyDB();
+    });
+
+    // ListNodes.
+    test('listNodes Vehicle layer 1 ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(node:'Vehicle');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result, [
+        'Vehicle.Communication',
+        'Vehicle.Infotainment']);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle layer 1 (non-default namespace)', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(node: 'VehicleTS', namespace: 'testSpace');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result, [
+        'VehicleTS.Communication',
+        'VehicleTS.Infotainment']);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle layer 2 ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(layers: 2, node: 'Vehicle');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  [
+        'Vehicle.Communication.Radio',
+        'Vehicle.Infotainment.HVAC',
+        'Vehicle.Infotainment.Radio']);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle.infotainment layer 1 ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(node: 'Vehicle.Infotainment');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  [
+        'Vehicle.Infotainment.HVAC',
+        'Vehicle.Infotainment.Radio']);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle.infotainment layer -1 ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(layers: -1, node: 'Vehicle.Infotainment');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isFalse);
+      expect(listNodesResponse.result,  []);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes node non-existent ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(node: 'Vehicle.ADAS');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isFalse);
+      expect(listNodesResponse.result,  []);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes layer too large -> no children ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(layers: 3, node: 'Vehicle.Infotainment');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  []);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes empty node ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo( node: '');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  ['Vehicle']);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle layer 0  ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(layers: 0, node: 'Vehicle');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  [
+            'Vehicle.Communication.Radio.Volume',
+            'Vehicle.Infotainment.HVAC.OutdoorTemperature',
+            'Vehicle.Infotainment.Radio.CurrentStation',
+            'Vehicle.Infotainment.Radio.Volume'
+          ]);
+      await storageClient.destroyDB();
+    });
+
+    test('listNodes Vehicle.Infotainment layer 0  ', () async{
+      await prepareTree();
+      late storage_api.SubtreeInfo  subtreeInfo = storage_api.SubtreeInfo(layers: 0, node: 'Vehicle.Infotainment');
+      final listNodesResponse = await storageClient.listNodes(subtreeInfo);
+      expect(listNodesResponse, isA<storage_api.ListResponse>());
+      expect(listNodesResponse.success, isTrue);
+      expect(listNodesResponse.result,  [
+            'Vehicle.Infotainment.HVAC.OutdoorTemperature',
+            'Vehicle.Infotainment.Radio.CurrentStation',
+            'Vehicle.Infotainment.Radio.Volume'
+          ]);
+      await storageClient.destroyDB();
+    });
+}