Skip to content

Commit

Permalink
feat: add hint repository (#341)
Browse files Browse the repository at this point in the history
* docs: update db client docs

* feat: add hint model

* feat: add hint repository to api

* feat: create hint repository

* test: hint model

* test: hint repository
  • Loading branch information
jsgalarraga authored Apr 20, 2024
1 parent 5bf8297 commit 49003c4
Show file tree
Hide file tree
Showing 18 changed files with 511 additions and 2 deletions.
3 changes: 3 additions & 0 deletions api/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import 'package:crossword_repository/crossword_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:db_client/db_client.dart';
import 'package:firebase_cloud_storage/firebase_cloud_storage.dart';
import 'package:hint_repository/hint_repository.dart';
import 'package:jwt_middleware/jwt_middleware.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:logging/logging.dart';

final gameUrl = GameUrl(_gameUrl);
late CrosswordRepository crosswordRepository;
late HintRepository hintRepository;
late BoardRenderer boardRenderer;
late LeaderboardRepository leaderboardRepository;
late FirebaseCloudStorage firebaseCloudStorage;
Expand All @@ -21,6 +23,7 @@ Future<void> init(InternetAddress ip, int port) async {
final dbClient = DbClient.initialize(_appId, useEmulator: _useEmulator);

crosswordRepository = CrosswordRepository(dbClient: dbClient);
hintRepository = HintRepository(dbClient: dbClient);
boardRenderer = const BoardRenderer();

leaderboardRepository = LeaderboardRepository(
Expand Down
6 changes: 4 additions & 2 deletions api/packages/db_client/lib/src/db_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class DbClient {
}
}

/// Updates a record with the given data.
/// Create or update a document. In case of an update, fields not referenced
/// in the payload will remain unchanged.
Future<void> update(String entity, DbEntityRecord record) =>
_update(entity, record, 0);

Expand All @@ -105,7 +106,8 @@ class DbClient {
}
}

/// Creates or updates a record with the given data and document id.
/// Create or update a document. In the case of an update, any fields not
/// referenced in the payload will be deleted.
Future<void> set(String entity, DbEntityRecord record) =>
_set(entity, record, 0);

Expand Down
44 changes: 44 additions & 0 deletions api/packages/game_domain/lib/src/models/hint.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'hint.g.dart';

/// {@template hint}
/// A model that represents a hint for a crossword word.
/// {@endtemplate}
@JsonSerializable(ignoreUnannotated: true)
class Hint extends Equatable {
/// {@macro hint}
const Hint({required this.question, required this.response});

/// {@macro hint}
factory Hint.fromJson(Map<String, dynamic> json) => _$HintFromJson(json);

/// Question asked by the user to get the hint.
@JsonKey()
final String question;

/// Response generated by Gemini to the question.
@JsonKey()
final HintResponse response;

/// Returns a json representation from this instance.
Map<String, dynamic> toJson() => _$HintToJson(this);

@override
List<Object?> get props => [question, response];
}

/// Enum representing the possible responses to a hint question.
enum HintResponse {
/// The question is related to the word and an affirmative response helps
/// the user to guess the word.
yes,

/// The question is related to the word and a negative response helps
/// the user to guess the word.
no,

/// Most likely, the question is not related to the word being guessed.
notApplicable,
}
23 changes: 23 additions & 0 deletions api/packages/game_domain/lib/src/models/hint.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/packages/game_domain/lib/src/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'dart:math' show Point;

export 'answer.dart';
export 'board_section.dart';
export 'hint.dart';
export 'mascots.dart';
export 'player.dart';
export 'point_converter.dart';
Expand Down
50 changes: 50 additions & 0 deletions api/packages/game_domain/test/src/models/hint_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// ignore_for_file: prefer_const_constructors

import 'package:game_domain/game_domain.dart';
import 'package:test/test.dart';

void main() {
group('Hint', () {
test('creates correct json object from Hint object', () {
final hint = Hint(question: 'to be?', response: HintResponse.yes);
final json = hint.toJson();

expect(
json,
equals({
'question': 'to be?',
'response': 'yes',
}),
);
});

test('creates correct Hint object from json object', () {
final json = {
'question': 'or not to be?',
'response': 'notApplicable',
};
final hint = Hint.fromJson(json);
expect(
hint,
equals(
Hint(
question: 'or not to be?',
response: HintResponse.notApplicable,
),
),
);
});

test('supports equality', () {
final firstHint = Hint(
question: 'to be?',
response: HintResponse.no,
);
final secondHint = Hint(
question: 'to be?',
response: HintResponse.no,
);
expect(firstHint, equals(secondHint));
});
});
}
7 changes: 7 additions & 0 deletions api/packages/hint_repository/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
62 changes: 62 additions & 0 deletions api/packages/hint_repository/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Hint Repository

[![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 handle the hints.

## Installation 💻

**❗ In order to start using Hint Repository you must have the [Dart SDK][dart_install_link] installed on your machine.**

Install via `dart pub add`:

```sh
dart pub add hint_repository
```

---

## Continuous Integration 🤖

Hint 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.

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 🧪

To run all unit tests:

```sh
dart pub global activate coverage 1.2.0
dart test --coverage=coverage
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info
```

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

[dart_install_link]: https://dart.dev/get-dart
[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_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 api/packages/hint_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 api/packages/hint_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.
5 changes: 5 additions & 0 deletions api/packages/hint_repository/lib/hint_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// A repository to handle the hints.
library;

export 'src/hint_exception.dart';
export 'src/hint_repository.dart';
23 changes: 23 additions & 0 deletions api/packages/hint_repository/lib/src/hint_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:hint_repository/hint_repository.dart';

/// {@template hint_exception}
/// Exception thrown when an error occurs in the [HintRepository].
/// {@endtemplate}
class HintException implements Exception {
/// {@macro hint_exception}
HintException(this.cause, this.stackTrace);

/// Error cause.
final dynamic cause;

/// The stack trace of the error.
final StackTrace stackTrace;

@override
String toString() {
return '''
cause: $cause
stackTrace: $stackTrace
''';
}
}
82 changes: 82 additions & 0 deletions api/packages/hint_repository/lib/src/hint_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:db_client/db_client.dart';
import 'package:game_domain/game_domain.dart';
import 'package:hint_repository/hint_repository.dart';

/// {@template hint_repository}
/// A repository to handle the hints.
/// {@endtemplate}
class HintRepository {
/// {@macro hint_repository}
const HintRepository({
required DbClient dbClient,
}) : _dbClient = dbClient;

final DbClient _dbClient;

static const _answersCollection = 'answers';
static const _hintsCollection = 'hints';

String _hintsPath(String wordId) =>
'$_answersCollection/$wordId/$_hintsCollection';

/// Generates a new hint for the given word, having the context from previous
/// hints.
Future<Hint> generateHint({
required String wordAnswer,
required String question,
required List<Hint> previousHints,
}) async {
// TODO(jaime): Call the hint generation service.
final hint = Hint(
question: question,
response: HintResponse.yes,
);

return hint;
}

/// Fetches the previous asked hints for the given word.
Future<List<Hint>> getPreviousHints({
required String userId,
required String wordId,
}) async {
try {
final hintDoc = await _dbClient.getById(_hintsPath(wordId), userId);
if (hintDoc == null) {
return [];
}

final hintsData = hintDoc.data['hints'] as List<dynamic>;
final hints = hintsData
.map((element) => Hint.fromJson(element as Map<String, dynamic>))
.toList();
return hints;
} catch (e, stackTrace) {
final message = 'Error getting previous hints for word $wordId '
'by user $userId: $e';
throw HintException(message, stackTrace);
}
}

/// Saves the hints for the given word.
Future<void> saveHints({
required String userId,
required String wordId,
required List<Hint> hints,
}) async {
try {
await _dbClient.set(
_hintsPath(wordId),
DbEntityRecord(
id: userId,
data: {
'hints': hints.map((e) => e.toJson()).toList(),
},
),
);
} catch (e, stackTrace) {
final message = 'Error saving hints for word $wordId by user $userId: $e';
throw HintException(message, stackTrace);
}
}
}
18 changes: 18 additions & 0 deletions api/packages/hint_repository/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: hint_repository
description: A repository to handle the hints.
version: 0.1.0+1
publish_to: none

environment:
sdk: "^3.3.0"

dependencies:
db_client:
path: ../db_client
game_domain:
path: ../game_domain

dev_dependencies:
mocktail: ^1.0.3
test: ^1.25.2
very_good_analysis: ^5.1.0
Loading

0 comments on commit 49003c4

Please sign in to comment.