Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloud_firestore): Create a cloud_firestore_types package that can be used in dart-only contexts #13215

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/.gitignore
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version.
7 changes: 7 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/README.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'src/blob.dart';
export 'src/geo_point.dart';
export 'src/timestamp.dart';
27 changes: 27 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/lib/src/blob.dart
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
114 changes: 114 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/lib/src/timestamp.dart
Original file line number Diff line number Diff line change
@@ -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<Timestamp> {
/// 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);
}
}
15 changes: 15 additions & 0 deletions packages/cloud_firestore/cloud_firestore_types/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<AssertionError>));
expect(() => GeoPoint(100, 0), throwsA(isA<AssertionError>));
expect(() => GeoPoint(0, -190), throwsA(isA<AssertionError>));
expect(() => GeoPoint(0, 190), throwsA(isA<AssertionError>));
});
});
}
Original file line number Diff line number Diff line change
@@ -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));
});
});
}