| // 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; |
| }), |
| ); |
| } |
| } |