Skip to content

Commit

Permalink
26 add cached recipe repository (#27)
Browse files Browse the repository at this point in the history
* [#25] feat: add get all recipes and ingredients

* [#25] feat: add indexed object

* [#25] chore: upgrade dependencies

* [#25] feat: add cached repository

* [#26] test: add model mapper test

* [#26] test: add matter unit test

* [#26] test: add cached ingredient repository test

* [#26] test: add cached recipe repository test

* [#26] refactor: change repository to allow datasource to be injected

* [@26] chore: fix typo

* [#26] fix: github test workflow
  • Loading branch information
Soogyo-In authored Jan 7, 2023
1 parent 41ab0da commit 0ca6608
Show file tree
Hide file tree
Showing 39 changed files with 2,314 additions and 193 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test_app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

- name: Run build runner for domain layer code generation
working-directory: ./domain
run: dart pub run build_runner build
run: dart pub run build_runner build -d

- name: Run domain layer tests
working-directory: ./domain
Expand All @@ -39,7 +39,7 @@ jobs:

- name: Run build runner for data layer code generation
working-directory: ./data
run: dart pub run build_runner build
run: dart pub run build_runner build -d

- name: Run data layer tests
working-directory: ./data
Expand All @@ -49,7 +49,7 @@ jobs:
run: flutter pub get

- name: Run build runner for interface layer code generation
run: flutter pub run build_runner build
run: flutter pub run build_runner build -d

- name: Run interface layer tests
run: flutter test
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ app.*.map.json
# Auto generated files
*.freezed.dart
*.g.dart

# code coverage files
coverage
3 changes: 2 additions & 1 deletion data/lib/data.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'exceptions.dart';
export 'datasource/datasource.dart';
export 'exceptions.dart';
export 'model/model.dart';
export 'repository/repository.dart';
129 changes: 67 additions & 62 deletions data/lib/datasource/local/recipe_local_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,33 @@ part of 'local_datasource.dart';

class RecipeLocalDatasource implements RecipeDatasource {
@override
Future<Recipe> addRecipe(Recipe recipe) => _upsertRecipe(recipe);
Future<IndexedRecipe> addRecipe(Recipe recipe) => _upsertRecipe(recipe);

@override
Future<Recipe> getRecipe(Id id) async {
Future<IndexedRecipe> getRecipe(Id id) async {
final isar = await Isar.open([RecipeDataSchema]);
final recipeData = await isar.recipeDatas.get(id);
if (recipeData == null) {
await isar.close();
throw DataNotFoundException();
}

final directions = recipeData.directions.map(
(directionData) {
final countByIngredientId = <int, Count>{};
final massByIngredientId = <int, Mass>{};
final volumeByIngredientId = <int, Volume>{};
final ingredientAmounts = directionData.ingredients ?? [];
for (final ingredientAmount in ingredientAmounts) {
final ingredientId = ingredientAmount.ingredientId;
final amount = ingredientAmount.toAmount();

if (amount is Count) countByIngredientId[ingredientId] = amount;
if (amount is Mass) massByIngredientId[ingredientId] = amount;
if (amount is Volume) volumeByIngredientId[ingredientId] = amount;
}

return Direction(
description: directionData.description,
temperature: directionData.temperature?.toDomainTemperature(),
time: Duration(seconds: directionData.timeInSeconds ?? 0),
countByIngredientId: countByIngredientId,
massByIngredientId: massByIngredientId,
volumeByIngredientId: volumeByIngredientId,
);
},
).toList();
await isar.close();

if (recipeData == null) throw DataNotFoundException();

return recipeData.toRecipe();
}

@override
Future<List<IndexedRecipe>> getAllRecipes() async {
final isar = await Isar.open([RecipeDataSchema]);
final recipeDatas = await isar.recipeDatas.where().findAll();

await isar.close();

return Recipe(
directions: directions,
name: recipeData.name,
description: recipeData.description ?? '',
id: recipeData.id,
servings: recipeData.servings,
);
return recipeDatas.map((recipeData) => recipeData.toRecipe()).toList();
}

@override
Future<Recipe> updateRecipe(Recipe recipe) => _upsertRecipe(recipe);
Future<IndexedRecipe> updateRecipe(IndexedRecipe recipe) =>
_upsertRecipe(recipe);

@override
Future<void> deleteRecipe(Id id) async {
Expand All @@ -61,29 +38,35 @@ class RecipeLocalDatasource implements RecipeDatasource {
}

@override
Future<Ingredient> addIngredient(Ingredient ingredient) =>
Future<IndexedIngredient> addIngredient(Ingredient ingredient) =>
_upsertIngredient(ingredient);

@override
Future<Ingredient> getIngredient(Id id) async {
Future<IndexedIngredient> getIngredient(Id id) async {
final isar = await Isar.open([IngredientDataSchema]);
final ingredient = await isar.ingredientDatas.get(id);
if (ingredient == null) {
await isar.close();
throw DataNotFoundException();
}
final ingredientData = await isar.ingredientDatas.get(id);

await isar.close();

return Ingredient(
name: ingredient.name,
description: ingredient.description ?? '',
id: ingredient.id,
);
if (ingredientData == null) throw DataNotFoundException();

return ingredientData.toIngredient();
}

@override
Future<Ingredient> updateIngredient(Ingredient ingredient) =>
Future<List<IndexedIngredient>> getAllIngredients() async {
final isar = await Isar.open([IngredientDataSchema]);
final ingredientDatas = await isar.ingredientDatas.where().findAll();

await isar.close();

return ingredientDatas
.map((ingredientData) => ingredientData.toIngredient())
.toList();
}

@override
Future<IndexedIngredient> updateIngredient(IndexedIngredient ingredient) =>
_upsertIngredient(ingredient);

@override
Expand All @@ -93,7 +76,7 @@ class RecipeLocalDatasource implements RecipeDatasource {
await isar.close();
}

Future<Recipe> _upsertRecipe(Recipe recipe) async {
Future<IndexedRecipe> _upsertRecipe(Recipe recipe) async {
final isar = await Isar.open([RecipeDataSchema]);
final directionData = <DirectionData>[];
for (final direction in recipe.directions) {
Expand All @@ -108,7 +91,7 @@ class RecipeLocalDatasource implements RecipeDatasource {
final amount = entry.value;
return IngredientAmountData()
..ingredientId = ingredientId
..unit = IngredientAmountMapper.fromAmount(amount)
..unit = amount.toMatterUnit()
..value = amount.value;
},
).toList();
Expand All @@ -117,14 +100,18 @@ class RecipeLocalDatasource implements RecipeDatasource {
DirectionData()
..description = direction.description
..ingredients = ingredientData
..temperature = direction.temperature?.toTemperature()
..temperature = direction.temperature?.toTemperatureData()
..timeInSeconds = direction.time.inSeconds,
);
}

final id = await isar.writeTxn(() {
final id = recipe.map(
(value) => Isar.autoIncrement,
indexed: (value) => value.id,
);
return isar.recipeDatas.put(
RecipeData(id: recipe.id ?? Isar.autoIncrement)
RecipeData(id: id)
..description = recipe.description
..directions = directionData
..name = recipe.name
Expand All @@ -134,23 +121,41 @@ class RecipeLocalDatasource implements RecipeDatasource {

await isar.close();

return recipe.copyWith(id: id);
return recipe.map(
(value) => IndexedRecipe(
id: id,
name: recipe.name,
directions: recipe.directions,
description: recipe.description,
servings: recipe.servings,
),
indexed: (value) => value,
);
}

Future<Ingredient> _upsertIngredient(
Ingredient ingredient,
) async {
Future<IndexedIngredient> _upsertIngredient(Ingredient ingredient) async {
final isar = await Isar.open([IngredientDataSchema]);
final id = await isar.writeTxn(() {
final id = ingredient.map(
(value) => Isar.autoIncrement,
indexed: (value) => value.id,
);
return isar.ingredientDatas.put(
IngredientData(id: ingredient.id ?? Isar.autoIncrement)
IngredientData(id: id)
..description = ingredient.description
..name = ingredient.name,
);
});

await isar.close();

return ingredient.copyWith(id: id);
return ingredient.map(
(value) => IndexedIngredient(
id: id,
name: ingredient.name,
description: ingredient.description,
),
indexed: (value) => value,
);
}
}
24 changes: 1 addition & 23 deletions data/lib/datasource/model_mapper/ingredient_amount_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,6 @@
import 'package:data/data.dart';
import 'package:domain/domain.dart';
part of 'model_mapper.dart';

extension IngredientAmountMapper on IngredientAmountData {
static MatterUnit fromAmount(Amount amount) {
if (amount is Milligram) return MatterUnit.milligram;
if (amount is Gram) return MatterUnit.gram;
if (amount is Kilogram) return MatterUnit.kilogram;
if (amount is Ounce) return MatterUnit.ounce;
if (amount is Pound) return MatterUnit.pound;
if (amount is CubicCentimeter) return MatterUnit.cubicCentimeter;
if (amount is Milliliter) return MatterUnit.milliliter;
if (amount is Liter) return MatterUnit.liter;
if (amount is Teaspoon) return MatterUnit.teaspoon;
if (amount is Tablespoon) return MatterUnit.tablespoon;
if (amount is FluidOunce) return MatterUnit.fluidOunce;
if (amount is Cup) return MatterUnit.cup;
if (amount is Count) return MatterUnit.count;
throw ArgumentError.value(
amount,
'amount',
'Unsupported type ${amount.runtimeType}',
);
}

Amount toAmount() {
if (unit.isMilligram) return Milligram(value);
if (unit.isGram) return Gram(value);
Expand Down
11 changes: 11 additions & 0 deletions data/lib/datasource/model_mapper/ingredient_data_mapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
part of 'model_mapper.dart';

extension IngredientDataMapper on IngredientData {
IndexedIngredient toIngredient() {
return IndexedIngredient(
name: name,
description: description ?? '',
id: id,
);
}
}
20 changes: 20 additions & 0 deletions data/lib/datasource/model_mapper/matter_unit_mapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
part of 'model_mapper.dart';

extension AmountMapper on Amount {
MatterUnit toMatterUnit() {
if (this is Milligram) return MatterUnit.milligram;
if (this is Gram) return MatterUnit.gram;
if (this is Kilogram) return MatterUnit.kilogram;
if (this is Ounce) return MatterUnit.ounce;
if (this is Pound) return MatterUnit.pound;
if (this is CubicCentimeter) return MatterUnit.cubicCentimeter;
if (this is Milliliter) return MatterUnit.milliliter;
if (this is Liter) return MatterUnit.liter;
if (this is Teaspoon) return MatterUnit.teaspoon;
if (this is Tablespoon) return MatterUnit.tablespoon;
if (this is FluidOunce) return MatterUnit.fluidOunce;
if (this is Cup) return MatterUnit.cup;
if (this is Count) return MatterUnit.count;
throw StateError('Unsupported type $runtimeType');
}
}
12 changes: 9 additions & 3 deletions data/lib/datasource/model_mapper/model_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export 'ingredient_amount_mapper.dart';
export 'temperature_data_mapper.dart';
export 'temperature_mapper.dart';
import 'package:data/data.dart';
import 'package:domain/domain.dart';

part 'ingredient_amount_mapper.dart';
part 'ingredient_data_mapper.dart';
part 'matter_unit_mapper.dart';
part 'recipe_data_mapper.dart';
part 'temperature_data_mapper.dart';
part 'temperature_mapper.dart';
39 changes: 39 additions & 0 deletions data/lib/datasource/model_mapper/recipe_data_mapper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
part of 'model_mapper.dart';

extension RecipDataMapper on RecipeData {
IndexedRecipe toRecipe() {
final directions = this.directions.map(
(directionData) {
final countByIngredientId = <int, Count>{};
final massByIngredientId = <int, Mass>{};
final volumeByIngredientId = <int, Volume>{};
final ingredientAmounts = directionData.ingredients ?? [];
for (final ingredientAmount in ingredientAmounts) {
final ingredientId = ingredientAmount.ingredientId;
final amount = ingredientAmount.toAmount();

if (amount is Count) countByIngredientId[ingredientId] = amount;
if (amount is Mass) massByIngredientId[ingredientId] = amount;
if (amount is Volume) volumeByIngredientId[ingredientId] = amount;
}

return Direction(
description: directionData.description,
temperature: directionData.temperature?.toTemperature(),
time: Duration(seconds: directionData.timeInSeconds ?? 0),
countByIngredientId: countByIngredientId,
massByIngredientId: massByIngredientId,
volumeByIngredientId: volumeByIngredientId,
);
},
).toList();

return IndexedRecipe(
directions: directions,
name: name,
description: description ?? '',
id: id,
servings: servings,
);
}
}
5 changes: 2 additions & 3 deletions data/lib/datasource/model_mapper/temperature_data_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:data/data.dart';
import 'package:domain/domain.dart';
part of 'model_mapper.dart';

extension TemperatureDataMapper on TemperatureData {
Temperature toDomainTemperature() {
Temperature toTemperature() {
if (unit.isCelsius) return Celsius(value);
if (unit.isFahrenheit) return Fahrenheit(value);
throw StateError('Unsupported type $runtimeType');
Expand Down
5 changes: 2 additions & 3 deletions data/lib/datasource/model_mapper/temperature_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:data/data.dart';
import 'package:domain/domain.dart';
part of 'model_mapper.dart';

extension TemperatureMapper on Temperature {
TemperatureData toTemperature() {
TemperatureData toTemperatureData() {
TemperatureUnit? unit;
if (this is Fahrenheit) unit = TemperatureUnit.fahrenheit;
if (this is Celsius) unit = TemperatureUnit.celsius;
Expand Down
Loading

0 comments on commit 0ca6608

Please sign in to comment.