Skip to content
This repository has been archived by the owner on Mar 6, 2024. It is now read-only.

Commit

Permalink
feat: add leaderboard_repository package
Browse files Browse the repository at this point in the history
  • Loading branch information
marcossevilla committed Oct 30, 2023
1 parent dad7cb3 commit cb6202a
Show file tree
Hide file tree
Showing 14 changed files with 741 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/leaderboard_repository.yaml
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
44 changes: 44 additions & 0 deletions packages/leaderboard_repository/.gitignore
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
67 changes: 67 additions & 0 deletions packages/leaderboard_repository/README.md
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

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 1 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 1 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

[![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

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)

Check warning on line 7 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)

Check warning on line 7 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)

## 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

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 11 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 11 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Install via `flutter pub add`:

```sh
dart pub add leaderboard_repository

Check warning on line 16 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)

Check warning on line 16 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)

Check warning on line 16 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (leaderboard)
```

---

## 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

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 23 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

Check warning on line 23 in packages/leaderboard_repository/README.md

View workflow job for this annotation

GitHub Actions / spell-check / build

Unknown word (Leaderboard)

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
1 change: 1 addition & 0 deletions packages/leaderboard_repository/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
20 changes: 20 additions & 0 deletions packages/leaderboard_repository/coverage_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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;
}
}
50 changes: 50 additions & 0 deletions packages/leaderboard_repository/lib/src/models/exceptions.dart
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.

2 changes: 2 additions & 0 deletions packages/leaderboard_repository/lib/src/models/models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'exceptions.dart';
export 'leaderboard_entry_data.dart';
Loading

0 comments on commit cb6202a

Please sign in to comment.