blob: f5d6be432e6ac95845bdbdbbe2a5bcc6dae0db39 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:mirrors';
import 'dart:typed_data';
import 'package:appengine/appengine.dart';
import 'package:appengine/appengine.dart' as gae show context;
import 'package:fixnum/fixnum.dart';
import 'package:gcloud/db.dart';
import 'package:meta/meta.dart';
import 'allowed_account.dart';
import 'commit.dart';
import 'github_build_status_update.dart';
import 'key_helper.pb.dart';
import 'task.dart';
const Set<Type> _defaultTypes = <Type>{
Commit,
GithubBuildStatusUpdate,
Task,
AllowedAccount,
};
/// Class used to encode and decode [Key] objects.
///
/// The encoding uses binary-encoded protocol buffers that are then base-64 URL
/// encoded (and the decoding reverses that process).
///
/// This encoding scheme is necessary to match the behavior of the Go AppEngine
/// datastore library. This parity is required while Cocoon operates with
/// two backends, because the serialized values vended by one backend must
/// be deserializable by the other backend.
@immutable
class KeyHelper {
KeyHelper({
AppEngineContext? applicationContext,
Set<Type> types = _defaultTypes,
}) : applicationContext = applicationContext ?? gae.context.applicationContext,
types = _populateTypes(types);
/// Metadata about the App Engine application.
final AppEngineContext applicationContext;
/// Maps Dart [Model] classes to their corresponding App Engine datastore
/// type names.
///
/// This is initialized when the [KeyHelper] is created by iterating over
/// the `types` argument to the [KeyHelper.new] constructor and looking for
/// `@`[Kind] annotations on those classes.
final Map<Type, Kind> types;
/// Encodes the specified [key] as a base-64 encoded protocol buffer
/// representation of the key.
///
/// See also:
///
/// * <https://github.com/golang/appengine/blob/b2f4a3cf3c67576a2ee09e1fe62656a5086ce880/datastore/key.go#L231>
String encode(Key<dynamic> key) {
final Reference reference = Reference()
..app = applicationContext.applicationID
..path = _asPath(key);
if (applicationContext.partition.isNotEmpty) {
reference.nameSpace = applicationContext.partition;
}
final Uint8List buffer = reference.writeToBuffer();
final String base64Encoded = base64Url.encode(buffer);
return base64Encoded.split('=').first;
}
/// Decodes the specified [encoded] string into its [Key] representation.
///
/// See also:
///
/// * [encode], which is the complement to this method.
/// * <https://github.com/golang/appengine/blob/b2f4a3cf3c67576a2ee09e1fe62656a5086ce880/datastore/key.go#L244>
Key<dynamic> decode(String encoded) {
// Re-add padding.
final int remainder = encoded.length % 4;
if (remainder != 0) {
final String padding = '=' * (4 - remainder);
encoded += padding;
}
final Uint8List decoded = base64Url.decode(encoded);
final Reference reference = Reference.fromBuffer(decoded);
return reference.path.element.fold<Key<dynamic>>(
Key<int>.emptyKey(Partition(reference.nameSpace.isEmpty ? null : reference.nameSpace)),
(Key<dynamic> previous, Path_Element element) {
final Iterable<MapEntry<Type, Kind>> entries =
types.entries.where((MapEntry<Type, Kind> entry) => entry.value.name == element.type);
if (entries.isEmpty) {
throw StateError('Unknown type: ${element.type}');
}
final MapEntry<Type, Kind> entry = entries.single;
if (entry.value.idType == IdType.String) {
return previous.append<String>(entry.key, id: element.name);
} else {
return previous.append<int>(entry.key, id: element.id.toInt());
}
},
);
}
static Map<Type, Kind> _populateTypes(Set<Type> types) {
final Map<Type, Kind> result = <Type, Kind>{};
for (Type type in types) {
final ClassMirror classMirror = reflectClass(type);
final List<InstanceMirror> kindAnnotations = classMirror.metadata
.where((InstanceMirror annotation) => annotation.hasReflectee)
.where((InstanceMirror annotation) => annotation.reflectee.runtimeType == Kind)
.toList();
if (kindAnnotations.isEmpty) {
throw StateError('Class $type has no @Kind annotation');
}
final Kind annotation = kindAnnotations.single.reflectee as Kind;
result[type] = Kind(
name: annotation.name ?? type.toString(),
idType: annotation.idType,
);
}
return Map<Type, Kind>.unmodifiable(result);
}
Path _asPath(Key<dynamic> key) {
final List<Key<dynamic>> path = <Key<dynamic>>[];
for (Key<dynamic>? current = key; current != null && !current.isEmpty; current = current.parent) {
path.insert(0, current);
}
return Path()
..element.addAll(
path.map<Path_Element>((Key<dynamic> key) {
final Path_Element element = Path_Element();
if (key.type != null) {
element.type = types.containsKey(key.type) ? types[key.type!]!.name! : key.type.toString();
}
final Object? id = key.id;
if (id is String) {
element.name = id;
} else if (id is int) {
element.id = Int64(id);
}
return element;
}),
);
}
}