Skip to content

Commit 40d109d

Browse files
author
Freek van de Ven
committed
feat: add restful api implementation of the CatalogRepository
1 parent a64ff33 commit 40d109d

5 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: 2025 Iconica
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
include: package:flutter_iconica_analysis/components_options.yaml
6+
7+
analyzer:
8+
9+
linter:
10+
rules:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
///
2+
library flutter_catalog_rest_api;
3+
4+
export "src/rest_catalog_repository.dart";
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import "dart:convert";
2+
3+
import "package:dart_api_service/dart_api_service.dart";
4+
import "package:dart_api_service/dart_api_service.dart" as http;
5+
import "package:flutter_catalog_interface/flutter_catalog_interface.dart";
6+
import "package:flutter_catalog_rest_api/src/rest_converters.dart";
7+
8+
export "package:dart_api_service/dart_api_service.dart" show Client;
9+
10+
/// An implementation of [CatalogRepository] that uses a RESTful API.
11+
class RestCatalogRepository extends HttpApiService<List<CatalogItem>>
12+
implements CatalogRepository {
13+
/// Creates a [RestCatalogRepository].
14+
///
15+
/// [baseUrl] is the base URL for the catalog API.
16+
RestCatalogRepository({
17+
required super.baseUrl,
18+
super.apiResponseConverter,
19+
super.authenticationService,
20+
super.client,
21+
super.defaultHeaders,
22+
this.apiPrefix = "",
23+
});
24+
25+
/// The prefix for the API endpoints.
26+
final String apiPrefix;
27+
28+
Endpoint get _baseEndpoint => endpoint(apiPrefix);
29+
30+
@override
31+
Future<List<CatalogItem>> fetchCatalogItems({
32+
required String userId,
33+
LatLng? userLocation,
34+
Map<String, dynamic>? filters,
35+
int? limit,
36+
int? offset,
37+
}) async {
38+
var catalogEndpoint = _baseEndpoint.child("/catalog/catalog-items");
39+
40+
var queryParameters = <String, dynamic>{"userId": userId};
41+
if (userLocation != null) {
42+
queryParameters["latitude"] = userLocation.latitude.toString();
43+
queryParameters["longitude"] = userLocation.longitude.toString();
44+
}
45+
if (filters != null) {
46+
queryParameters["filters"] = jsonEncode(filters);
47+
}
48+
if (limit != null) {
49+
queryParameters["limit"] = limit.toString();
50+
}
51+
if (offset != null) {
52+
queryParameters["offset"] = offset.toString();
53+
}
54+
55+
try {
56+
var response = await catalogEndpoint.get(
57+
queryParameters: queryParameters,
58+
);
59+
60+
if (response.result != null) {
61+
return response.result!;
62+
} else {
63+
throw ApiException(
64+
inner: response.inner,
65+
error: "No catalog items found, but request succeeded.",
66+
);
67+
}
68+
} on ApiException {
69+
rethrow;
70+
} on Exception catch (e, s) {
71+
throw ApiException(
72+
inner: http.Response("Unexpected error: $e", 500),
73+
error: e,
74+
stackTrace: s,
75+
);
76+
}
77+
}
78+
79+
@override
80+
Future<CatalogItem?> fetchCatalogItemById(String id, String userId) async {
81+
var itemEndpoint = _baseEndpoint
82+
.child("/catalog/catalog-items/:id")
83+
.withVariables({"id": id}).withConverter(catalogItemConverter);
84+
85+
try {
86+
var response = await itemEndpoint.get();
87+
88+
if (response.statusCode == 200 && response.result != null) {
89+
return response.result!;
90+
} else if (response.statusCode == 404) {
91+
return null;
92+
} else {
93+
throw ApiException(inner: response.inner);
94+
}
95+
} on ApiException {
96+
rethrow;
97+
} on Exception catch (e, s) {
98+
throw ApiException(
99+
inner: http.Response("Unexpected error: $e", 500),
100+
error: e,
101+
stackTrace: s,
102+
);
103+
}
104+
}
105+
106+
@override
107+
Future<void> toggleFavorite(String itemId, String userId) async {
108+
var favoriteEndpoint = _baseEndpoint
109+
.child("/catalog/catalog-items/:itemId/favorite")
110+
.withVariables({"itemId": itemId}).withConverter(
111+
const NoOpConverter(),
112+
);
113+
114+
try {
115+
await favoriteEndpoint.post(
116+
requestModel: {"userId": userId},
117+
);
118+
} on ApiException {
119+
rethrow;
120+
} on Exception catch (e, s) {
121+
throw ApiException(
122+
inner: http.Response("Unexpected error: $e", 500),
123+
error: e,
124+
stackTrace: s,
125+
);
126+
}
127+
}
128+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// packages/flutter_catalog_rest_api/lib/src/rest_converters.dart
2+
import "dart:convert"; // Still needed for jsonEncode in _NoOpConverter
3+
4+
import "package:dart_api_service/dart_api_service.dart";
5+
import "package:flutter_catalog_interface/flutter_catalog_interface.dart";
6+
7+
/// A pre-configured [ApiConverter] for decoding/encoding a list of [CatalogItem]s.
8+
///
9+
/// It leverages [ModelListJsonResponseConverter] from `dart_api_service`
10+
/// to handle the JSON serialization and deserialization using
11+
/// [CatalogItem.fromJson] and [CatalogItem.toJson].
12+
ApiConverter<List<CatalogItem>, List<CatalogItem>> catalogItemsConverter =
13+
ModelListJsonResponseConverter(
14+
deserialize: CatalogItem.fromJson,
15+
serialize: (item) => item.toJson(),
16+
);
17+
18+
/// A pre-configured [ApiConverter] for decoding/encoding a single [CatalogItem].
19+
///
20+
/// It leverages [ModelJsonResponseConverter] from `dart_api_service`
21+
/// to handle the JSON serialization and deserialization using
22+
/// [CatalogItem.fromJson] and [CatalogItem.toJson].
23+
ApiConverter<CatalogItem, CatalogItem> catalogItemConverter =
24+
ModelJsonResponseConverter(
25+
deserialize: CatalogItem.fromJson,
26+
serialize: (item) => item.toJson(),
27+
);
28+
29+
/// A simple [ApiConverter] for requests that do not expect a specific
30+
/// response body, but can still encode a map for request bodies.
31+
class NoOpConverter implements ApiConverter<void, Map<String, dynamic>> {
32+
/// Creates a [NoOpConverter].
33+
const NoOpConverter();
34+
35+
@override
36+
void toRepresentation(Object? input) {}
37+
38+
@override
39+
Object fromRepresentation(Map<String, dynamic> representation) =>
40+
jsonEncode(representation);
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: flutter_catalog_rest_api
2+
description: A RESTful API implementation of the Flutter Feed Catalog repository.
3+
version: 0.1.0
4+
homepage: https://github.com/Iconica-Development/flutter_feed
5+
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
6+
7+
environment:
8+
sdk: ">=3.4.0 <4.0.0"
9+
10+
dependencies:
11+
dart_api_service:
12+
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
13+
version: ^1.1.2
14+
flutter_catalog_interface:
15+
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
16+
version: ^0.1.0
17+
18+
dev_dependencies:
19+
flutter_iconica_analysis:
20+
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
21+
version: ^7.0.0

0 commit comments

Comments
 (0)