diff --git a/packages/cloud_firestore/cloud_firestore_types/.gitignore b/packages/cloud_firestore/cloud_firestore_types/.gitignore new file mode 100644 index 000000000000..3cceda557896 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/cloud_firestore/cloud_firestore_types/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore_types/CHANGELOG.md new file mode 100644 index 000000000000..effe43c82c8a --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/cloud_firestore/cloud_firestore_types/README.md b/packages/cloud_firestore/cloud_firestore_types/README.md new file mode 100644 index 000000000000..a214a3b4993f --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/README.md @@ -0,0 +1,7 @@ +# cloud_firestore_types + +Firestore types for use in the [`cloud_firestore`][1] plugin. + +These types are separate from the `cloud_firestore` plugin so they may be used in contexts that connot transitively depend on Flutter. + +[1]: ../cloud_firestore diff --git a/packages/cloud_firestore/cloud_firestore_types/lib/cloud_firestore_types.dart b/packages/cloud_firestore/cloud_firestore_types/lib/cloud_firestore_types.dart new file mode 100644 index 000000000000..423f1a88c49b --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/lib/cloud_firestore_types.dart @@ -0,0 +1,3 @@ +export 'src/blob.dart'; +export 'src/geo_point.dart'; +export 'src/timestamp.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_types/lib/src/blob.dart b/packages/cloud_firestore/cloud_firestore_types/lib/src/blob.dart new file mode 100644 index 000000000000..a91053a84dcc --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/lib/src/blob.dart @@ -0,0 +1,27 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2017, the Chromium project authors. Please see the AUTHORS file +// for details. 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:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +/// Represents binary data stored in [Uint8List]. +@immutable +class Blob { + /// Creates a blob. + const Blob(this.bytes); + + /// The bytes that are contained in this blob. + final Uint8List bytes; + + @override + bool operator ==(Object other) => + other is Blob && + const DeepCollectionEquality().equals(other.bytes, bytes); + + @override + int get hashCode => Object.hashAll(bytes); +} diff --git a/packages/cloud_firestore/cloud_firestore_types/lib/src/geo_point.dart b/packages/cloud_firestore/cloud_firestore_types/lib/src/geo_point.dart new file mode 100644 index 000000000000..3c9308773f2e --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/lib/src/geo_point.dart @@ -0,0 +1,27 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2017, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:meta/meta.dart'; + +/// Represents a geographical point by its longitude and latitude +@immutable +class GeoPoint { + /// Create [GeoPoint] instance. + const GeoPoint(this.latitude, this.longitude) + : assert(latitude >= -90 && latitude <= 90), + assert(longitude >= -180 && longitude <= 180); + + final double latitude; // ignore: public_member_api_docs + final double longitude; // ignore: public_member_api_docs + + @override + bool operator ==(Object other) => + other is GeoPoint && + other.latitude == latitude && + other.longitude == longitude; + + @override + int get hashCode => Object.hash(latitude, longitude); +} diff --git a/packages/cloud_firestore/cloud_firestore_types/lib/src/timestamp.dart b/packages/cloud_firestore/cloud_firestore_types/lib/src/timestamp.dart new file mode 100644 index 000000000000..21b1f877b451 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/lib/src/timestamp.dart @@ -0,0 +1,114 @@ +// Copyright 2018, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:meta/meta.dart'; + +const int _kThousand = 1000; +const int _kMillion = 1000000; +const int _kBillion = 1000000000; + +void _check(bool expr, String name, int value) { + if (!expr) { + throw ArgumentError('Timestamp $name out of range: $value'); + } +} + +/// A Timestamp represents a point in time independent of any time zone or calendar, +/// represented as seconds and fractions of seconds at nanosecond resolution in UTC +/// Epoch time. It is encoded using the Proleptic Gregorian Calendar which extends +/// the Gregorian calendar backwards to year one. It is encoded assuming all minutes +/// are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second table +/// is needed for interpretation. Range is from 0001-01-01T00:00:00Z to +/// 9999-12-31T23:59:59.999999999Z. By restricting to that range, we ensure that we +/// can convert to and from RFC 3339 date strings. +/// +/// For more information, see [the reference timestamp definition](https://github.com/google/protobuf/blob/main/src/google/protobuf/timestamp.proto) +@immutable +class Timestamp implements Comparable { + /// Creates a [Timestamp] + Timestamp(this._seconds, this._nanoseconds) { + _validateRange(_seconds, _nanoseconds); + } + + /// Create a [Timestamp] fromMillisecondsSinceEpoch + factory Timestamp.fromMillisecondsSinceEpoch(int milliseconds) { + int seconds = (milliseconds / _kThousand).floor(); + final int nanoseconds = (milliseconds - seconds * _kThousand) * _kMillion; + return Timestamp(seconds, nanoseconds); + } + + /// Create a [Timestamp] fromMicrosecondsSinceEpoch + factory Timestamp.fromMicrosecondsSinceEpoch(int microseconds) { + final int seconds = microseconds ~/ _kMillion; + final int nanoseconds = (microseconds - seconds * _kMillion) * _kThousand; + return Timestamp(seconds, nanoseconds); + } + + /// Create a [Timestamp] from [DateTime] instance + factory Timestamp.fromDate(DateTime date) { + return Timestamp.fromMicrosecondsSinceEpoch(date.microsecondsSinceEpoch); + } + + /// Create a [Timestamp] from [DateTime].now() + factory Timestamp.now() { + return Timestamp.fromMicrosecondsSinceEpoch( + DateTime.now().microsecondsSinceEpoch, + ); + } + + final int _seconds; + final int _nanoseconds; + + static const int _kStartOfTime = -62135596800; + static const int _kEndOfTime = 253402300800; + + // ignore: public_member_api_docs + int get seconds => _seconds; + + // ignore: public_member_api_docs + int get nanoseconds => _nanoseconds; + + // ignore: public_member_api_docs + int get millisecondsSinceEpoch => + seconds * _kThousand + nanoseconds ~/ _kMillion; + + // ignore: public_member_api_docs + int get microsecondsSinceEpoch => + seconds * _kMillion + nanoseconds ~/ _kThousand; + + /// Converts [Timestamp] to [DateTime] + DateTime toDate() { + return DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch); + } + + @override + int get hashCode => Object.hash(seconds, nanoseconds); + + @override + bool operator ==(Object other) => + other is Timestamp && + other.seconds == seconds && + other.nanoseconds == nanoseconds; + + @override + int compareTo(Timestamp other) { + if (seconds == other.seconds) { + return nanoseconds.compareTo(other.nanoseconds); + } + + return seconds.compareTo(other.seconds); + } + + @override + String toString() { + return 'Timestamp(seconds=$seconds, nanoseconds=$nanoseconds)'; + } + + static void _validateRange(int seconds, int nanoseconds) { + _check(nanoseconds >= 0, 'nanoseconds', nanoseconds); + _check(nanoseconds < _kBillion, 'nanoseconds', nanoseconds); + _check(seconds >= _kStartOfTime, 'seconds', seconds); + _check(seconds < _kEndOfTime, 'seconds', seconds); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_types/pubspec.yaml b/packages/cloud_firestore/cloud_firestore_types/pubspec.yaml new file mode 100644 index 000000000000..2a7862fea9f3 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/pubspec.yaml @@ -0,0 +1,15 @@ +name: cloud_firestore_types +description: Firestore types for use in the cloud_firestore plugin and Flutterless contexts. +version: 1.0.0 +homepage: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_types +repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_types + +environment: + sdk: ^3.0.0 + +dependencies: + collection: ^1.19.0 + meta: ^1.8.0 + +dev_dependencies: + test: ^1.24.0 diff --git a/packages/cloud_firestore/cloud_firestore_types/test/geo_point_test.dart b/packages/cloud_firestore/cloud_firestore_types/test/geo_point_test.dart new file mode 100644 index 000000000000..faf2c580bb1f --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/test/geo_point_test.dart @@ -0,0 +1,24 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_firestore_types/cloud_firestore_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('$GeoPoint', () { + test('equality', () { + expect(const GeoPoint(-80, 0), equals(const GeoPoint(-80, 0))); + expect(const GeoPoint(0, 0), equals(const GeoPoint(0, 0))); + expect(const GeoPoint(0, 100), equals(const GeoPoint(0, 100))); + }); + + test('throws if invalid values', () { + expect(() => GeoPoint(-100, 0), throwsA(isA)); + expect(() => GeoPoint(100, 0), throwsA(isA)); + expect(() => GeoPoint(0, -190), throwsA(isA)); + expect(() => GeoPoint(0, 190), throwsA(isA)); + }); + }); +} diff --git a/packages/cloud_firestore/cloud_firestore_types/test/timestamp_test.dart b/packages/cloud_firestore/cloud_firestore_types/test/timestamp_test.dart new file mode 100644 index 000000000000..b2668a5183f1 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_types/test/timestamp_test.dart @@ -0,0 +1,84 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_firestore_types/cloud_firestore_types.dart'; +import 'package:test/test.dart'; + +const int _kBillion = 1000000000; +const int _kStartOfTime = -62135596800; +const int _kEndOfTime = 253402300800; +const int int64MaxValue = 9223372036854775807; + +void main() { + group('$Timestamp', () { + test('equality', () { + expect(Timestamp(0, 0), equals(Timestamp(0, 0))); + expect(Timestamp(123, 456), equals(Timestamp(123, 456))); + }); + + test('validation', () { + expect(() => Timestamp(0, -1), throwsArgumentError); + expect(() => Timestamp(0, _kBillion + 1), throwsArgumentError); + expect(() => Timestamp(_kStartOfTime - 1, 123), throwsArgumentError); + expect(() => Timestamp(_kEndOfTime + 1, 123), throwsArgumentError); + }); + + test('returns properties', () { + Timestamp t = Timestamp(123, 456); + expect(t.seconds, equals(123)); + expect(t.nanoseconds, equals(456)); + }); + + // https://github.com/firebase/flutterfire/issues/1222 + test('does not exceed range', () { + Timestamp maxTimestamp = Timestamp(_kEndOfTime - 1, _kBillion - 1); + Timestamp.fromMicrosecondsSinceEpoch(maxTimestamp.microsecondsSinceEpoch); + }); + + test('fromMillisecondsSinceEpoch throws max out of range exception', () { + expect(() => Timestamp.fromMillisecondsSinceEpoch(int64MaxValue), + throwsArgumentError); + }); + + test('fromMillisecondsSinceEpoch can handle current timestamp', () { + int currentEpoch = DateTime.now().millisecondsSinceEpoch; + Timestamp t = Timestamp.fromMillisecondsSinceEpoch(currentEpoch); + + expect(t.toDate().year > 1970, equals(true)); + }); + + test('fromMillisecondsSinceEpoch can handle future date', () { + int currentEpoch = DateTime.now().millisecondsSinceEpoch + 999999999; + Timestamp t = Timestamp.fromMillisecondsSinceEpoch(currentEpoch); + + expect( + t.toDate().millisecondsSinceEpoch > + DateTime.now().millisecondsSinceEpoch, + equals(true)); + }); + + test('fromMillisecondsSinceEpoch can handle 0', () { + Timestamp t = Timestamp.fromMillisecondsSinceEpoch(0); + expect(t.toDate().toUtc().year, 1970); + expect(t.toDate().toUtc().month, 1); + expect(t.toDate().toUtc().day, 1); + }); + + test('fromMillisecondsSinceEpoch can handle negative millisecond values', + () { + Timestamp t = Timestamp.fromMillisecondsSinceEpoch(-9999999999); + + expect(t.toDate().toUtc().year, 1969); + expect(t.toDate().toUtc().month, 9); + }); + + test('millisecondsSinceEpoch returns correct negative epoch value', () { + Timestamp t = Timestamp.fromMillisecondsSinceEpoch(-9999999999); + int epoch = t.millisecondsSinceEpoch; + + expect(epoch, equals(-9999999999)); + }); + }); +}