-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
name: leaderboard_repository | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
on: | ||
push: | ||
paths: | ||
- "packages/leaderboard_repository/**" | ||
- ".github/workflows/leaderboard_repository.yaml" | ||
|
||
pull_request: | ||
paths: | ||
- "packages/leaderboard_repository/**" | ||
- ".github/workflows/leaderboard_repository.yaml" | ||
|
||
jobs: | ||
semantic_pull_request: | ||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 | ||
|
||
spell-check: | ||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 | ||
with: | ||
includes: "**/*.md" | ||
modified_files_only: false | ||
|
||
build: | ||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 | ||
with: | ||
flutter_channel: stable | ||
working_directory: packages/leaderboard_repository |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Miscellaneous | ||
*.class | ||
*.log | ||
*.pyc | ||
*.swp | ||
.DS_Store | ||
.atom/ | ||
.buildlog/ | ||
.history | ||
.svn/ | ||
migrate_working_dir/ | ||
|
||
# IntelliJ related | ||
*.iml | ||
*.ipr | ||
*.iws | ||
.idea/ | ||
|
||
# VSCode related | ||
.vscode/* | ||
|
||
# Flutter/Dart/Pub related | ||
**/doc/api/ | ||
**/ios/Flutter/.last_build_id | ||
.dart_tool/ | ||
.flutter-plugins | ||
.flutter-plugins-dependencies | ||
.packages | ||
.pub-cache/ | ||
.pub/ | ||
/build/ | ||
pubspec.lock | ||
|
||
# Web related | ||
lib/generated_plugin_registrant.dart | ||
|
||
# Symbolication related | ||
app.*.symbols | ||
|
||
# Obfuscation related | ||
app.*.map.json | ||
|
||
# Test related | ||
coverage |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Leaderboard Repository | ||
Check warning on line 1 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
Check warning on line 1 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
|
||
|
||
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] | ||
[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) | ||
[![License: MIT][license_badge]][license_link] | ||
|
||
A repository to access leaderboard data in Firebase Cloud Firestore. | ||
Check warning on line 7 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
Check warning on line 7 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
|
||
|
||
## Installation 💻 | ||
|
||
**❗ In order to start using Leaderboard Repository you must have the [Flutter SDK][flutter_install_link] installed on your machine.** | ||
Check warning on line 11 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
Check warning on line 11 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
|
||
|
||
Install via `flutter pub add`: | ||
|
||
```sh | ||
dart pub add leaderboard_repository | ||
Check warning on line 16 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
Check warning on line 16 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
|
||
``` | ||
|
||
--- | ||
|
||
## Continuous Integration 🤖 | ||
|
||
Leaderboard Repository comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution. | ||
Check warning on line 23 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
Check warning on line 23 in packages/leaderboard_repository/README.md GitHub Actions / spell-check / build
|
||
|
||
Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link]. | ||
|
||
--- | ||
|
||
## Running Tests 🧪 | ||
|
||
For first time users, install the [very_good_cli][very_good_cli_link]: | ||
|
||
```sh | ||
dart pub global activate very_good_cli | ||
``` | ||
|
||
To run all unit tests: | ||
|
||
```sh | ||
very_good test --coverage | ||
``` | ||
|
||
To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). | ||
|
||
```sh | ||
# Generate Coverage Report | ||
genhtml coverage/lcov.info -o coverage/ | ||
|
||
# Open Coverage Report | ||
open coverage/index.html | ||
``` | ||
|
||
[flutter_install_link]: https://docs.flutter.dev/get-started/install | ||
[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions | ||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg | ||
[license_link]: https://opensource.org/licenses/MIT | ||
[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only | ||
[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only | ||
[mason_link]: https://github.com/felangel/mason | ||
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg | ||
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis | ||
[very_good_cli_link]: https://pub.dev/packages/very_good_cli | ||
[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage | ||
[very_good_ventures_link]: https://verygood.ventures | ||
[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only | ||
[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only | ||
[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include: package:very_good_analysis/analysis_options.5.1.0.yaml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// A repository to access leaderboard data in Firebase Cloud Firestore. | ||
library leaderboard_repository; | ||
|
||
export 'src/leaderboard_repository.dart'; | ||
export 'src/models/models.dart'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import 'package:cloud_firestore/cloud_firestore.dart'; | ||
import 'package:leaderboard_repository/leaderboard_repository.dart'; | ||
|
||
/// {@template leaderboard_repository} | ||
/// Repository to access leaderboard data in Firebase Cloud Firestore. | ||
/// {@endtemplate} | ||
class LeaderboardRepository { | ||
/// {@macro leaderboard_repository} | ||
const LeaderboardRepository( | ||
FirebaseFirestore firebaseFirestore, | ||
) : _firebaseFirestore = firebaseFirestore; | ||
|
||
final FirebaseFirestore _firebaseFirestore; | ||
|
||
static const _leaderboardLimit = 10; | ||
static const _leaderboardCollectionName = 'leaderboard'; | ||
static const _scoreFieldName = 'score'; | ||
|
||
/// Acquires top 10 [LeaderboardEntryData]s. | ||
Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async { | ||
try { | ||
final querySnapshot = await _firebaseFirestore | ||
.collection(_leaderboardCollectionName) | ||
.orderBy(_scoreFieldName, descending: true) | ||
.limit(_leaderboardLimit) | ||
.get(); | ||
final documents = querySnapshot.docs; | ||
return documents.toLeaderboard(); | ||
} on LeaderboardDeserializationException { | ||
rethrow; | ||
} on Exception catch (error, stackTrace) { | ||
throw FetchTop10LeaderboardException(error, stackTrace); | ||
} | ||
} | ||
|
||
/// Adds player's score entry to the leaderboard if it is within the top-10 | ||
Future<void> addLeaderboardEntry( | ||
LeaderboardEntryData entry, | ||
) async { | ||
final leaderboard = await _fetchLeaderboardSortedByScore(); | ||
if (leaderboard.length < 10) { | ||
await _saveScore(entry); | ||
} else { | ||
final tenthPositionScore = leaderboard[9].score; | ||
if (entry.score > tenthPositionScore) { | ||
await _saveScore(entry); | ||
} | ||
} | ||
} | ||
|
||
Future<List<LeaderboardEntryData>> _fetchLeaderboardSortedByScore() async { | ||
try { | ||
final querySnapshot = await _firebaseFirestore | ||
.collection(_leaderboardCollectionName) | ||
.orderBy(_scoreFieldName, descending: true) | ||
.get(); | ||
final documents = querySnapshot.docs; | ||
return documents.toLeaderboard(); | ||
} on Exception catch (error, stackTrace) { | ||
throw FetchLeaderboardException(error, stackTrace); | ||
} | ||
} | ||
|
||
Future<void> _saveScore(LeaderboardEntryData entry) { | ||
try { | ||
return _firebaseFirestore | ||
.collection(_leaderboardCollectionName) | ||
.add(entry.toJson()); | ||
} on Exception catch (error, stackTrace) { | ||
throw AddLeaderboardEntryException(error, stackTrace); | ||
} | ||
} | ||
} | ||
|
||
extension on List<QueryDocumentSnapshot> { | ||
List<LeaderboardEntryData> toLeaderboard() { | ||
final leaderboardEntries = <LeaderboardEntryData>[]; | ||
for (final document in this) { | ||
final data = document.data() as Map<String, dynamic>?; | ||
if (data != null) { | ||
try { | ||
leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); | ||
} catch (error, stackTrace) { | ||
throw LeaderboardDeserializationException(error, stackTrace); | ||
} | ||
} | ||
} | ||
return leaderboardEntries; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/// {@template leaderboard_exception} | ||
/// Base exception for leaderboard repository failures. | ||
/// {@endtemplate} | ||
abstract class LeaderboardException implements Exception { | ||
/// {@macro leaderboard_exception} | ||
const LeaderboardException(this.error, this.stackTrace); | ||
|
||
/// The error that was caught. | ||
final Object error; | ||
|
||
/// The Stacktrace associated with the [error]. | ||
final StackTrace stackTrace; | ||
} | ||
|
||
/// {@template leaderboard_deserialization_exception} | ||
/// Exception thrown when leaderboard data cannot be deserialized in the | ||
/// expected way. | ||
/// {@endtemplate} | ||
class LeaderboardDeserializationException extends LeaderboardException { | ||
/// {@macro leaderboard_deserialization_exception} | ||
const LeaderboardDeserializationException(Object error, StackTrace stackTrace) | ||
: super(error, stackTrace); | ||
} | ||
|
||
/// {@template fetch_top_10_leaderboard_exception} | ||
/// Exception thrown when failure occurs while fetching top 10 leaderboard. | ||
/// {@endtemplate} | ||
class FetchTop10LeaderboardException extends LeaderboardException { | ||
/// {@macro fetch_top_10_leaderboard_exception} | ||
const FetchTop10LeaderboardException(Object error, StackTrace stackTrace) | ||
: super(error, stackTrace); | ||
} | ||
|
||
/// {@template fetch_leaderboard_exception} | ||
/// Exception thrown when failure occurs while fetching the leaderboard. | ||
/// {@endtemplate} | ||
class FetchLeaderboardException extends LeaderboardException { | ||
/// {@macro fetch_top_10_leaderboard_exception} | ||
const FetchLeaderboardException(Object error, StackTrace stackTrace) | ||
: super(error, stackTrace); | ||
} | ||
|
||
/// {@template add_leaderboard_entry_exception} | ||
/// Exception thrown when failure occurs while adding entry to leaderboard. | ||
/// {@endtemplate} | ||
class AddLeaderboardEntryException extends LeaderboardException { | ||
/// {@macro add_leaderboard_entry_exception} | ||
const AddLeaderboardEntryException(Object error, StackTrace stackTrace) | ||
: super(error, stackTrace); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import 'package:equatable/equatable.dart'; | ||
import 'package:json_annotation/json_annotation.dart'; | ||
|
||
part 'leaderboard_entry_data.g.dart'; | ||
|
||
/// {@template leaderboard_entry_data} | ||
/// A model representing a leaderboard entry containing the player's initials, | ||
/// score, and chosen character. | ||
/// | ||
/// Stored in Firestore `leaderboard` collection. | ||
/// | ||
/// Example: | ||
/// ```json | ||
/// { | ||
/// "playerInitials" : "ABC", | ||
/// "score" : 1500, | ||
/// } | ||
/// ``` | ||
/// {@endtemplate} | ||
@JsonSerializable() | ||
class LeaderboardEntryData extends Equatable { | ||
/// {@macro leaderboard_entry_data} | ||
const LeaderboardEntryData({ | ||
required this.playerInitials, | ||
required this.score, | ||
}); | ||
|
||
/// Factory which converts a [Map] into a [LeaderboardEntryData]. | ||
factory LeaderboardEntryData.fromJson(Map<String, dynamic> json) { | ||
return _$LeaderboardEntryDataFromJson(json); | ||
} | ||
|
||
/// Converts the [LeaderboardEntryData] to [Map]. | ||
Map<String, dynamic> toJson() => _$LeaderboardEntryDataToJson(this); | ||
|
||
/// Player's chosen initials for [LeaderboardEntryData]. | ||
/// | ||
/// Example: 'ABC'. | ||
@JsonKey(name: 'playerInitials') | ||
final String playerInitials; | ||
|
||
/// Score for [LeaderboardEntryData]. | ||
/// | ||
/// Example: 1500. | ||
@JsonKey(name: 'score') | ||
final int score; | ||
|
||
/// An empty [LeaderboardEntryData] object. | ||
static const empty = LeaderboardEntryData( | ||
score: 0, | ||
playerInitials: '', | ||
); | ||
|
||
@override | ||
List<Object?> get props => [playerInitials, score]; | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export 'exceptions.dart'; | ||
export 'leaderboard_entry_data.dart'; |