[client] Simplify response parsing

This commit is contained in:
Abhishek Shroff
2025-05-12 19:38:33 +05:30
parent e1d743c816
commit 1db2d1d0b8
16 changed files with 141 additions and 180 deletions

View File

@@ -7,8 +7,8 @@ abstract class ResourceAction extends PhylumAction {
String get resourceId => _resourceId;
@override
ResponseParser get parseResponse =>
(_, response) => parseJsonMapResponse(response, PartialResourceResponse.fromResponse);
ResponseParser get parseResponse => (_, response) =>
parseJsonMapResponse(response, (response) => ResourceResponse.fromResponse(response, username: ''));
ResourceAction({required String resourceId}) : _resourceId = resourceId;

View File

@@ -16,11 +16,11 @@ abstract class ResourceCreateAction extends ResourceBindAction {
@override
FutureOr<void> processResponse(ApiResponse response) async {
if (response is PartialResourceResponse) {
if (response.id != resourceId) {
if (response is ResourceResponse) {
if (response.resource.id != resourceId) {
account.actionQueue.updateActions(
(PhylumAction action) => action is ResourceAction && action.resourceId == resourceId,
(action) => (action as ResourceAction).setResourceId(response.id),
(action) => (action as ResourceAction).setResourceId(response.resource.id),
);
}
}

View File

@@ -89,6 +89,6 @@ class ResourceDeleteAction extends ResourceAction with JsonApiAction {
bool dependsOn(PhylumAction action) => action is ResourceAction && action.resourceId == resourceId;
@override
ResponseParser get parseResponse => (_, response) =>
parseJsonMapResponse(response, permanent ? EmptyResponse.fromResponse : PartialResourceResponse.fromResponse);
ResponseParser get parseResponse =>
permanent ? (_, response) => parseJsonMapResponse(response, EmptyResponse.fromResponse) : super.parseResponse;
}

View File

@@ -1,9 +1,4 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/util/permissions.dart';
part 'bookmarks.dart';
part 'publinks.dart';
part 'resources.dart';

View File

@@ -1,72 +0,0 @@
part of 'parsers.dart';
class FullResource {
final Resource resource;
final Iterable<Publink> publinks;
final Map<String, Permission> permissions;
FullResource({required this.resource, required this.publinks, required this.permissions});
}
class PartialResource {
final ResourcesCompanion resource;
final Iterable<Publink>? publinks;
PartialResource({required this.resource, required this.publinks});
}
FullResource parseFullResource(
Map<String, dynamic> data, {
required String username,
required String? parent,
Map<String, Permission>? inheritedPermissions,
}) {
inheritedPermissions ??= (data['inherited_permissions'] as Map).cast<String, Permission>();
final grants = (data['grants'] as Map)
.cast<String, Map>()
.map((k, v) => MapEntry(k, v.cast<String, dynamic>()['p'] as Permission));
final permissions = Map.of(inheritedPermissions)..addAll(grants);
final r = Resource(
id: data['id'],
parent: parent,
name: data['name'],
dir: data['dir'],
created: DateTime.fromMillisecondsSinceEpoch(data['created']),
modified: DateTime.fromMillisecondsSinceEpoch(data['modified']),
deleted: data['deleted'] == 0 ? null : DateTime.fromMillisecondsSinceEpoch(data['deleted']),
contentLength: data['c_len'],
contentSha256: data['c_sha256'],
contentType: data['c_type'],
inheritedPermissions: jsonEncode(inheritedPermissions),
grants: grants.isEmpty ? null : jsonEncode(grants),
userPermission: permissions[username] ?? 0,
);
final publinks = parsePublinks(data['publinks'], r.id);
return FullResource(resource: r, publinks: publinks, permissions: permissions);
}
ResourcesCompanion parseResourceAncestor(Map<String, dynamic> data) {
return ResourcesCompanion(
id: Value(data['id']),
name: Value(data['name']),
dir: Value(true),
userPermission: Value(data['user_permission']),
);
}
PartialResource parsePartialResource(Map<String, dynamic> data, [String? id]) {
final r = ResourcesCompanion(
id: Value(data['id'] ?? id),
name: Value(data['name']),
dir: Value(data['dir']),
created: Value(DateTime.fromMillisecondsSinceEpoch(data['created'])),
modified: Value(DateTime.fromMillisecondsSinceEpoch(data['modified'])),
deleted: data['deleted'] == 0 ? Value(null) : Value(DateTime.fromMillisecondsSinceEpoch(data['deleted'])),
contentLength: Value(data['c_len']),
contentSha256: Value(data['c_sha256']),
contentType: Value(data['c_type']),
grants: data['grants'].isEmpty ? Value('') : Value(jsonEncode(data['grants'])));
final publinks = data['publinks'] != null ? parsePublinks(data['publinks'], r.id.value) : null;
return PartialResource(resource: r, publinks: publinks);
}

View File

@@ -53,7 +53,7 @@ class ResourceRepository extends Repository<PhylumAccount, Resource> {
return _account.apiClient.sendRequest(
ResourceInfoRequest(id),
(request, response) =>
parseJsonMapResponse(response, (data) => FullResourceResponse.fromResponse(data, _account.userEmail)),
parseJsonMapResponse(response, (data) => ResourceInfoResponse.fromResponse(data, _account.userEmail)),
);
}

View File

@@ -11,7 +11,12 @@ class SharedResourcesRepository {
Future<ApiResult> refresh() async {
return account.apiClient.sendRequest(
SharedResourcesRequest(),
(request, response) => parseJsonMapResponse(response, SharedResourcesResponse.fromResponse),
(request, response) => parseJsonMapResponse(
response,
(response) => SharedResourcesResponse.fromResponse(
response,
account.userEmail,
)),
);
}
}

View File

@@ -51,8 +51,10 @@ class TrashedResourceRepository extends Repository<PhylumAccount, TrashedResourc
Future<ApiResult> refresh({String? cursor}) async {
return _account.apiClient.sendRequest(
TrashListRequest(cursor: cursor),
(request, response) => parseJsonMapResponse(response, TrashListResponse.fromResponse),
);
TrashListRequest(cursor: cursor),
(request, response) => parseJsonMapResponse(
response,
(response) => TrashListResponse.fromResponse(response, _account.userEmail),
));
}
}

View File

@@ -1,44 +0,0 @@
part of 'responses.dart';
class PartialResourceResponse extends PhylumApiSuccessResponse {
final String id;
final PartialResource updates;
PartialResourceResponse({
required this.id,
required this.updates,
});
factory PartialResourceResponse.fromResponse(Map<String, dynamic> data) {
final updates = parsePartialResource(data);
return PartialResourceResponse(
id: updates.resource.id.value,
updates: updates,
);
}
@override
Future process(PhylumAccount account) async {
final db = account.db;
final publinks = updates.publinks;
Iterable<String>? orphanedPublinks;
if (publinks != null) {
orphanedPublinks = Set.of(await (db.select(db.publinks)
..where((l) => l.root.equals(id) & l.id.isNotIn(publinks.map((l) => l.id))))
.map((r) => r.id)
.get());
}
await db.batch((batch) async {
batch.update(db.resources, updates.resource, where: (o) => o.id.equals(id));
if (publinks != null) {
for (final id in orphanedPublinks!) {
batch.update(db.publinks, PublinksCompanion(root: Value('')), where: (o) => o.id.equals(id));
}
batch.insertAll(db.publinks, publinks, mode: InsertMode.insertOrReplace);
}
});
await account.datastore.get<Resource>().reloadRemoteData(id);
}
}

View File

@@ -1,33 +1,31 @@
part of 'responses.dart';
class FullResourceResponse extends PhylumApiSuccessResponse {
final FullResource resource;
class ResourceInfoResponse extends PhylumApiSuccessResponse {
final ResourceResponse root;
final Iterable<ResourcesCompanion> ancestors;
final Iterable<FullResource> children;
final Iterable<ResourceResponse> children;
FullResourceResponse({
required this.resource,
ResourceInfoResponse({
required this.root,
required this.ancestors,
required this.children,
});
factory FullResourceResponse.fromResponse(Map<String, dynamic> data, String username) {
factory ResourceInfoResponse.fromResponse(Map<String, dynamic> data, String username) {
final ancestors =
(data['ancestors'] as List).cast<Map>().map((a) => parseResourceAncestor(a.cast<String, dynamic>()));
final resource = parseFullResource(
final resource = ResourceResponse.fromResponse(
data['resource'],
parent: ancestors.isNotEmpty ? ancestors.first.id.value : null,
username: username,
);
final children = (data['children'] as List).cast<Map>().map((c) => parseFullResource(
final children = (data['children'] as List).cast<Map>().map((c) => ResourceResponse.fromResponse(
c.cast<String, dynamic>(),
parent: resource.resource.id,
username: username,
inheritedPermissions: resource.permissions,
));
return FullResourceResponse(
resource: resource,
return ResourceInfoResponse(
root: resource,
ancestors: ancestors,
children: children,
);
@@ -37,7 +35,7 @@ class FullResourceResponse extends PhylumApiSuccessResponse {
Future process(PhylumAccount account) async {
final db = account.db;
final fullResources = [resource, ...children];
final fullResources = [root, ...children];
final publinks = fullResources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e));
final orphanedPubinks = Set.of(await (db.select(db.publinks)
..where(
@@ -45,8 +43,8 @@ class FullResourceResponse extends PhylumApiSuccessResponse {
.map((r) => r.id)
.get());
final existing = Set.of(
await (db.select(db.resources)..where((t) => t.parent.equals(resource.resource.id))).map((r) => r.id).get());
final existing =
Set.of(await (db.select(db.resources)..where((t) => t.parent.equals(root.resource.id))).map((r) => r.id).get());
existing.removeAll(children.map((r) => r.resource.id));
final removedChildren = List.of(existing, growable: false);
@@ -55,7 +53,7 @@ class FullResourceResponse extends PhylumApiSuccessResponse {
batch.update(db.resources, ResourcesCompanion(parent: Value(null)), where: (o) => o.id.equals(id));
}
batch.insertAllOnConflictUpdate(db.resources, ancestors);
batch.insert(db.resources, resource.resource, mode: InsertMode.insertOrReplace);
batch.insert(db.resources, root.resource, mode: InsertMode.insertOrReplace);
batch.insertAll(db.resources, children.map((r) => r.resource), mode: InsertMode.insertOrReplace);
for (final id in orphanedPubinks) {
@@ -73,9 +71,18 @@ class FullResourceResponse extends PhylumApiSuccessResponse {
await repo.reloadRemoteData(a.id.value);
}
// This resource and its children are fully parsed, so the incoming object can replace the existing one
await repo.replaceRemoteData(resource.resource.id, resource.resource);
await repo.replaceRemoteData(root.resource.id, root.resource);
for (final c in children) {
await repo.replaceRemoteData(c.resource.id, c.resource);
}
}
}
ResourcesCompanion parseResourceAncestor(Map<String, dynamic> data) {
return ResourcesCompanion(
id: Value(data['id']),
name: Value(data['name']),
dir: Value(true),
userPermission: Value(data['user_permission']),
);
}

View File

@@ -0,0 +1,62 @@
part of 'responses.dart';
class ResourceResponse extends PhylumApiSuccessResponse {
final Resource resource;
final Iterable<Publink> publinks;
final Map<String, Permission> permissions;
ResourceResponse({required this.resource, required this.publinks, required this.permissions});
factory ResourceResponse.fromResponse(
Map<String, dynamic> data, {
required String username,
String? parent,
Map<String, Permission>? inheritedPermissions,
}) {
inheritedPermissions ??= (data['inherited_permissions'] as Map).cast<String, Permission>();
final grants = (data['grants'] as Map)
.cast<String, Map>()
.map((k, v) => MapEntry(k, v.cast<String, dynamic>()['p'] as Permission));
final permissions = Map.of(inheritedPermissions)..addAll(grants);
final r = Resource(
id: data['id'],
parent: parent ?? data['parent'],
name: data['name'],
dir: data['dir'],
created: DateTime.fromMillisecondsSinceEpoch(data['created']),
modified: DateTime.fromMillisecondsSinceEpoch(data['modified']),
deleted: data['deleted'] == 0 ? null : DateTime.fromMillisecondsSinceEpoch(data['deleted']),
contentLength: data['c_len'],
contentSha256: data['c_sha256'],
contentType: data['c_type'],
inheritedPermissions: jsonEncode(inheritedPermissions),
grants: grants.isEmpty ? null : jsonEncode(grants),
userPermission: permissions[username] ?? 0,
);
final publinks = parsePublinks(data['publinks'], r.id);
return ResourceResponse(resource: r, publinks: publinks, permissions: permissions);
}
@override
Future process(PhylumAccount account) async {
final db = account.db;
final orphanedPubinks = Set.of(await (db.select(db.publinks)
..where((l) => l.root.equals(resource.id) & l.id.isNotIn(publinks.map((l) => l.id))))
.map((r) => r.id)
.get());
await db.batch((batch) async {
batch.insert(db.resources, resource, mode: InsertMode.insertOrReplace);
for (final id in orphanedPubinks) {
batch.update(db.publinks, PublinksCompanion(root: Value('')), where: (o) => o.id.equals(id));
}
batch.insertAll(db.publinks, publinks, mode: InsertMode.insertOrReplace);
});
final repo = account.datastore.get<Resource>();
await repo.replaceRemoteData(resource.id, resource);
}
}

View File

@@ -6,12 +6,13 @@ import 'package:offtheline/offtheline.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/parsers/parsers.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/util/permissions.dart';
part 'bookmark_list_response.dart';
part 'bookmark_response.dart';
part 'empty_response.dart';
part 'full_resource_response.dart';
part 'partial_resource_response.dart';
part 'resource_info_response.dart';
part 'resource_response.dart';
part 'search_response.dart';
part 'shared_resources_response.dart';
part 'trash_list_response.dart';

View File

@@ -1,23 +1,24 @@
part of 'responses.dart';
class SearchResponse extends PhylumApiSuccessResponse {
final List<PartialResource> resources;
final List<ResourceResponse> resources;
SearchResponse({required this.resources});
factory SearchResponse.fromResponse(List data) {
final results =
data.cast<Map>().map((u) => parsePartialResource(u.cast<String, dynamic>())).toList(growable: false);
factory SearchResponse.fromResponse(List data, String username) {
final results = data
.cast<Map>()
.map((u) => ResourceResponse.fromResponse(u.cast<String, dynamic>(), username: username))
.toList(growable: false);
return SearchResponse(resources: results);
}
@override
Future process(PhylumAccount account) async {
final db = account.db;
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e ?? const []));
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e));
final orphanedPublinks = Set.of(await (db.select(db.publinks)
..where(
(l) => l.root.isIn(resources.map((r) => r.resource.id.value)) & l.id.isNotIn(publinks.map((l) => l.id))))
..where((l) => l.root.isIn(resources.map((r) => r.resource.id)) & l.id.isNotIn(publinks.map((l) => l.id))))
.map((r) => r.id)
.get());
@@ -30,7 +31,7 @@ class SearchResponse extends PhylumApiSuccessResponse {
});
final repo = account.datastore.get<Resource>();
for (final r in resources) {
await repo.reloadRemoteData(r.resource.id.value);
await repo.reloadRemoteData(r.resource.id);
}
}
}

View File

@@ -1,16 +1,15 @@
part of 'responses.dart';
class SharedResourcesResponse extends PhylumApiSuccessResponse {
final Iterable<PartialResource> resources;
final Iterable<ResourceResponse> resources;
@override
Future<void> process(PhylumAccount account) async {
final db = account.db;
final shared = resources.indexed.map((r) => SharedCompanion.insert(resourceId: r.$2.resource.id.value, seq: r.$1));
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e ?? const []));
final shared = resources.indexed.map((r) => SharedCompanion.insert(resourceId: r.$2.resource.id, seq: r.$1));
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e));
final orphanedPublinks = Set.of(await (db.select(db.publinks)
..where(
(l) => l.root.isIn(resources.map((r) => r.resource.id.value)) & l.id.isNotIn(publinks.map((l) => l.id))))
..where((l) => l.root.isIn(resources.map((r) => r.resource.id)) & l.id.isNotIn(publinks.map((l) => l.id))))
.map((r) => r.id)
.get());
@@ -27,14 +26,17 @@ class SharedResourcesResponse extends PhylumApiSuccessResponse {
final repo = account.datastore.get<Resource>();
for (final r in resources) {
await repo.reloadRemoteData(r.resource.id.value);
await repo.reloadRemoteData(r.resource.id);
}
}
SharedResourcesResponse({required this.resources});
factory SharedResourcesResponse.fromResponse(Map<String, dynamic> data) {
final resources = (data["shared"] as List).cast<Map>().map((u) => parsePartialResource(u.cast<String, dynamic>()));
factory SharedResourcesResponse.fromResponse(Map<String, dynamic> data, String username) {
final resources = (data["shared"] as List).cast<Map>().map((u) => ResourceResponse.fromResponse(
u.cast<String, dynamic>(),
username: username,
));
return SharedResourcesResponse(resources: resources);
}
}

View File

@@ -1,7 +1,7 @@
part of 'responses.dart';
class TrashListResponse extends PhylumApiSuccessResponse {
final Iterable<PartialResource> resources;
final Iterable<ResourceResponse> resources;
final String? cursor;
final bool clear;
@@ -11,9 +11,10 @@ class TrashListResponse extends PhylumApiSuccessResponse {
required this.clear,
});
factory TrashListResponse.fromResponse(Map<String, dynamic> data) {
final resources =
(data['resources'] as List).cast<Map>().map((a) => parsePartialResource(a.cast<String, dynamic>()));
factory TrashListResponse.fromResponse(Map<String, dynamic> data, String username) {
final resources = (data['resources'] as List)
.cast<Map>()
.map((a) => ResourceResponse.fromResponse(a.cast<String, dynamic>(), username: username));
final cursor = data['cursor'] as String;
final clear = data['clear'] as bool;
@@ -27,10 +28,9 @@ class TrashListResponse extends PhylumApiSuccessResponse {
@override
Future process(PhylumAccount account) async {
final db = account.db;
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e ?? const []));
final publinks = resources.map((r) => r.publinks).fold<List<Publink>>([], (acc, e) => acc..addAll(e));
final orphanedPublinks = Set.of(await (db.select(db.publinks)
..where(
(l) => l.root.isIn(resources.map((r) => r.resource.id.value)) & l.id.isNotIn(publinks.map((l) => l.id))))
..where((l) => l.root.isIn(resources.map((r) => r.resource.id)) & l.id.isNotIn(publinks.map((l) => l.id))))
.map((r) => r.id)
.get());
@@ -39,7 +39,7 @@ class TrashListResponse extends PhylumApiSuccessResponse {
if (clear) {
batch.deleteAll(db.trashedResources);
}
batch.insertAll(db.trashedResources, resources.map((r) => TrashedResource(id: r.resource.id.value)),
batch.insertAll(db.trashedResources, resources.map((r) => TrashedResource(id: r.resource.id)),
mode: InsertMode.insertOrReplace);
for (final id in orphanedPublinks) {
batch.update(db.publinks, PublinksCompanion(root: Value('')), where: (o) => o.id.equals(id));
@@ -49,7 +49,7 @@ class TrashListResponse extends PhylumApiSuccessResponse {
final repo = account.datastore.get<Resource>();
for (final r in resources) {
await repo.reloadRemoteData(r.resource.id.value);
await repo.reloadRemoteData(r.resource.id);
}
}
}

View File

@@ -125,12 +125,14 @@ class ExplorerPageSearch extends ExplorerPage {
return account.apiClient
.sendRequest(
SearchRequest(query: query),
(request, response) => parseJsonListResponse(response, SearchResponse.fromResponse),
(request, response) => parseJsonListResponse(
response,
(response) => SearchResponse.fromResponse(response, account.userEmail),
),
)
.then((result) {
if (result is SearchResponse) {
// _streamController.add(result.resources.map((r) => r.resource).toList(growable: false));
_streamController.add(const []);
_streamController.add(result.resources.map((r) => r.resource).toList(growable: false));
}
return result is ApiSuccessResponse;
});