diff --git a/pkgs/gcloud/CHANGELOG.md b/pkgs/gcloud/CHANGELOG.md index 5d4ab7e8..162953c5 100644 --- a/pkgs/gcloud/CHANGELOG.md +++ b/pkgs/gcloud/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0 + +- Add support for Cloud Tasks + ## 0.9.0 - Support `orderingKey` on pub/sub's `Message` type. diff --git a/pkgs/gcloud/lib/cloud_tasks.dart b/pkgs/gcloud/lib/cloud_tasks.dart new file mode 100644 index 00000000..5a49b370 --- /dev/null +++ b/pkgs/gcloud/lib/cloud_tasks.dart @@ -0,0 +1,68 @@ +/// This library provides a low-level API for accessing Google's Cloud +/// Tasks. +/// +/// For more information on Cloud Tasks, please refer to the following +/// developers page: https://cloud.google.com/tasks/docs +library; + +import 'package:googleapis/cloudtasks/v2.dart' as tasks; +import 'package:http/http.dart' as http; + +import 'service_scope.dart' as ss; + +import 'src/cloud_tasks_impl.dart' show CloudTasksImpl; + +const Symbol _tasksKey = #gcloud.tasks; + +/// Access the [CloudTasks] object available in the current service scope. +/// +/// The returned object will be the one which was previously registered with +/// [registerTasksService] within the current (or a parent) service scope. +/// +/// Accessing this getter outside of a service scope will result in an error. +/// See the `package:gcloud/service_scope.dart` library for more information. +CloudTasks get tasksService => ss.lookup(_tasksKey) as CloudTasks; + +/// Registers the [CloudTasks] object within the current service scope. +/// +/// The provided `tasks` object will be available via the top-level +/// `tasksService` getter. +/// +/// Calling this function outside of a service scope will result in an error. +/// Calling this function more than once inside the same service scope is not +/// allowed. +void registerTasksService(CloudTasks tasks) { + ss.register(_tasksKey, tasks); +} + +/// Interface used to talk to the Google Cloud Tasks service. +abstract class CloudTasks { + /// List of required OAuth2 scopes for Cloud Tasks operations. + // ignore: constant_identifier_names + static const Scopes = [tasks.CloudTasksApi.cloudPlatformScope]; + + /// Access Cloud Tasks using an authenticated client. + /// + /// The [client] is an authenticated HTTP client. This client must + /// provide access to at least the scopes in [CloudTasks.Scopes]. + factory CloudTasks(http.Client client, String project, String location) { + return CloudTasksImpl(client, project, location); + } + + /// Creates a new task on the specified [queue]. When the task is run, + /// [request] will be executed. + /// + /// If [scheduleTime] is provided, the task will be scheduled to run at the + /// specified time. If not provided, the task will be scheduled to run + /// immediately. + /// + /// If [name] is provided, the task will be given that name. + /// + /// Returns a [Future] which completes with the newly created task. + Future createTask( + String queue, + http.Request request, { + String? name, + DateTime? scheduleTime, + }); +} diff --git a/pkgs/gcloud/lib/src/cloud_tasks_impl.dart b/pkgs/gcloud/lib/src/cloud_tasks_impl.dart new file mode 100644 index 00000000..a5ae68dc --- /dev/null +++ b/pkgs/gcloud/lib/src/cloud_tasks_impl.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; + +import 'package:googleapis/cloudtasks/v2.dart' as api; +import 'package:http/http.dart' as http; + +import '../cloud_tasks.dart' as tasks; + +class CloudTasksImpl implements tasks.CloudTasks { + final api.CloudTasksApi _api; + final String _project; + final String _location; + + CloudTasksImpl(http.Client client, String project, String location) + : _api = api.CloudTasksApi(client), + _project = project, + _location = location; + + @override + Future createTask( + String queue, + http.Request request, { + DateTime? scheduleTime, + String? name, + }) async { + final task = api.Task( + name: name, + scheduleTime: scheduleTime?.toUtc().toIso8601String(), + httpRequest: _fromHttpRequest(request), + ); + final createTaskRequest = api.CreateTaskRequest(task: task); + return _api.projects.locations.queues.tasks + .create(createTaskRequest, _fullQueueName(queue)); + } + + api.HttpRequest _fromHttpRequest(http.Request request) { + return api.HttpRequest( + headers: request.headers, + body: base64.encode(utf8.encode(request.body)), + httpMethod: request.method, + url: request.url.toString(), + ); + } + + String _fullQueueName(String name) { + return 'projects/$_project/locations/$_location/queues/$name'; + } +} diff --git a/pkgs/gcloud/pubspec.yaml b/pkgs/gcloud/pubspec.yaml index 818774ed..6e21d6de 100644 --- a/pkgs/gcloud/pubspec.yaml +++ b/pkgs/gcloud/pubspec.yaml @@ -1,5 +1,5 @@ name: gcloud -version: 0.9.0 +version: 0.10.0 description: >- High level idiomatic Dart API for Google Cloud Storage, Pub-Sub and Datastore. repository: https://github.com/dart-lang/labs/tree/main/pkgs/gcloud diff --git a/pkgs/gcloud/test/cloud_tasks/cloud_tasks_test.dart b/pkgs/gcloud/test/cloud_tasks/cloud_tasks_test.dart new file mode 100644 index 00000000..9a8f2995 --- /dev/null +++ b/pkgs/gcloud/test/cloud_tasks/cloud_tasks_test.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import 'package:gcloud/cloud_tasks.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +import '../common.dart'; +import '../common_e2e.dart'; + +const _hostName = 'cloudtasks.googleapis.com'; +const _rootPath = '/v2/'; +const _defaultLocation = 'us-central1'; + +MockClient mockClient() => MockClient(_hostName, _rootPath); + +Future withMockClientAsync( + Future Function(MockClient client, CloudTasks tasks) function) async { + var mock = mockClient(); + await function(mock, CloudTasks(mock, testProject, _defaultLocation)); +} + +void main() { + group('task', () { + test('create with httpRequest', () async { + const taskName = 'my-task'; + const requestBody = 'hello world'; + const requestHeaders = { + 'content-type': 'application/json', + 'authorization': 'Bearer ....' + }; + final expectedTaskJson = { + 'name': taskName, + 'httpRequest': { + 'headers': requestHeaders, + 'body': base64.encode(utf8.encode(requestBody)), + 'httpMethod': 'GET', + 'url': 'https://www.google.com', + }, + }; + final responseJson = {'task': expectedTaskJson}; + + await withMockClientAsync((mock, tasks) async { + mock.register( + 'POST', + 'projects/test-project/locations/us-central1/queues/test-queue/tasks', + expectAsync1((request) { + final json = jsonDecode(request.body) as Map; + expect(json, responseJson); + return mock.respond(expectedTaskJson); + }), + ); + + final request = http.Request('GET', Uri.parse('https://www.google.com')) + ..body = requestBody + ..headers.addAll(requestHeaders); + + final task = + await tasks.createTask('test-queue', request, name: taskName); + expect(task.name, taskName); + final actualRequestJson = task.httpRequest!.toJson(); + expect(actualRequestJson, expectedTaskJson['httpRequest']); + }); + }); + }); +}