Skip to content

Commit c8489a6

Browse files
committed
feat(ht_api): implement core API endpoints for countries
- Add GET, POST, PUT, DELETE endpoints for country data - Implement error handling middleware - Create countries client provider middleware - Set up basic API structure with versioning - Add initial test data and mock client implementation
0 parents  commit c8489a6

File tree

11 files changed

+593
-0
lines changed

11 files changed

+593
-0
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# See https://www.dartlang.org/guides/libraries/private-files
2+
3+
# Files and directories created by the Operating System
4+
.DS_Store
5+
6+
# Files and directories created by pub
7+
.dart_tool/
8+
.packages
9+
pubspec.lock
10+
11+
# Files and directories created by dart_frog
12+
build/
13+
.dart_frog
14+
15+
# Test related files
16+
coverage/

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["VeryGoodVentures.dart-frog"]
3+
}

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ht_api
2+
3+
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
4+
[![License: MIT][license_badge]][license_link]
5+
[![Powered by Dart Frog](https://img.shields.io/endpoint?url=https://tinyurl.com/dartfrog-badge)](https://dartfrog.vgv.dev)
6+
7+
An example application built with dart_frog
8+
9+
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
10+
[license_link]: https://opensource.org/licenses/MIT
11+
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
12+
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

analysis_options.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
include: package:very_good_analysis/analysis_options.7.0.0.yaml
2+
analyzer:
3+
errors:
4+
avoid_redundant_argument_values: ignore
5+
avoid_print: ignore
6+
exclude:
7+
- build/**
8+
linter:
9+
rules:
10+
file_names: false

lib/src/middleware/error_handler.dart

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:ht_countries_client/ht_countries_client.dart'; // Import client exceptions
5+
6+
/// Creates a JSON response for errors.
7+
Response _errorResponse(int statusCode, String message) {
8+
return Response.json(
9+
statusCode: statusCode,
10+
body: {'error': message},
11+
);
12+
}
13+
14+
/// Middleware to handle common exceptions and return standardized error
15+
/// responses.
16+
///
17+
/// This middleware should be placed early in the middleware chain, typically
18+
/// after logging but before route-specific logic, to catch errors effectively.
19+
Middleware errorHandler() {
20+
return (handler) {
21+
return (context) async {
22+
try {
23+
// Attempt to execute the rest of the handler chain.
24+
final response = await handler(context);
25+
return response;
26+
} on FormatException catch (e, stackTrace) {
27+
// Handle errors related to invalid request data format
28+
// (e.g., bad JSON).
29+
print('FormatException caught: $e\n$stackTrace'); // Log for debugging
30+
return _errorResponse(
31+
HttpStatus.badRequest, // 400
32+
'Invalid request format: ${e.message}',
33+
);
34+
} on CountryNotFound catch (e, stackTrace) {
35+
// Handle cases where a requested country resource doesn't exist.
36+
print('CountryNotFound caught: $e\n$stackTrace'); // Log for debugging
37+
return _errorResponse(
38+
HttpStatus.notFound, // 404
39+
'Country not found: ${e.error}', // Use the underlying error message
40+
);
41+
} on CountryFetchFailure catch (e, stackTrace) {
42+
// Handle generic failures during country data fetching.
43+
print(
44+
'CountryFetchFailure caught: $e\n$stackTrace',
45+
); // Log for debugging
46+
return _errorResponse(
47+
HttpStatus.internalServerError, // 500
48+
'Failed to fetch country data: ${e.error}',
49+
);
50+
} on CountryCreateFailure catch (e, stackTrace) {
51+
// Handle failures during country creation.
52+
print(
53+
'CountryCreateFailure caught: $e\n$stackTrace',
54+
); // Log for debugging
55+
// Could potentially be a 409 Conflict if it's a duplicate,
56+
// but 500 is safer default if the cause is unknown.
57+
return _errorResponse(
58+
HttpStatus.internalServerError, // 500
59+
'Failed to create country: ${e.error}',
60+
);
61+
} on CountryUpdateFailure catch (e, stackTrace) {
62+
// Handle failures during country update.
63+
print(
64+
'CountryUpdateFailure caught: $e\n$stackTrace',
65+
); // Log for debugging
66+
return _errorResponse(
67+
HttpStatus.internalServerError, // 500
68+
'Failed to update country: ${e.error}',
69+
);
70+
} on CountryDeleteFailure catch (e, stackTrace) {
71+
// Handle failures during country deletion.
72+
print(
73+
'CountryDeleteFailure caught: $e\n$stackTrace',
74+
); // Log for debugging
75+
return _errorResponse(
76+
HttpStatus.internalServerError, // 500
77+
'Failed to delete country: ${e.error}',
78+
);
79+
} catch (e, stackTrace) {
80+
// Catch any other unexpected errors.
81+
print(
82+
'Unhandled exception caught: $e\n$stackTrace',
83+
); // Log for debugging
84+
return _errorResponse(
85+
HttpStatus.internalServerError, // 500
86+
'An unexpected server error occurred.',
87+
);
88+
}
89+
};
90+
};
91+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import 'package:dart_frog/dart_frog.dart';
2+
import 'package:ht_countries_client/ht_countries_client.dart';
3+
import 'package:meta/meta.dart'; // For @visibleForTesting
4+
5+
/// Provides an instance of [HtCountriesClient] to the request context.
6+
///
7+
/// This middleware is responsible for creating or obtaining the client instance
8+
/// (e.g., based on environment variables or configuration) and making it
9+
/// available via `context.read<HtCountriesClient>()` in subsequent handlers
10+
/// and middleware.
11+
Middleware countriesClientProvider() {
12+
return provider<HtCountriesClient>(
13+
(context) {
14+
// TODO(fulleni): Replace this with your actual HtCountriesClient
15+
// implementation.
16+
//
17+
// This could involve reading configuration, setting up database
18+
// connections, or initializing an HTTP client depending on your
19+
// chosen backend.
20+
//
21+
// Example:
22+
// final firestore = Firestore.instance; // If using Firestore
23+
// return FirestoreHtCountriesClient(firestore);
24+
//
25+
// final dio = Dio(); // If using an HTTP API
26+
// return ApiHtCountriesClient(dio);
27+
28+
// For development/testing purposes, we provide an in-memory mock.
29+
// DO NOT use this in production.
30+
print(
31+
'WARNING: Using InMemoryHtCountriesClient. Replace for production!',
32+
);
33+
return InMemoryHtCountriesClient();
34+
},
35+
);
36+
}
37+
38+
/// {@template in_memory_ht_countries_client}
39+
/// A simple in-memory implementation of [HtCountriesClient] for testing
40+
/// and development purposes.
41+
///
42+
/// **Do not use this in production.**
43+
/// {@endtemplate}
44+
@visibleForTesting
45+
class InMemoryHtCountriesClient implements HtCountriesClient {
46+
/// {@macro in_memory_ht_countries_client}
47+
InMemoryHtCountriesClient() {
48+
// Initialize with some sample data
49+
_countries = {
50+
'US': Country(
51+
isoCode: 'US',
52+
name: 'United States',
53+
flagUrl: 'https://example.com/flags/us.png',
54+
),
55+
'CA': Country(
56+
isoCode: 'CA',
57+
name: 'Canada',
58+
flagUrl: 'https://example.com/flags/ca.png',
59+
),
60+
'GB': Country(
61+
isoCode: 'GB',
62+
name: 'United Kingdom',
63+
flagUrl: 'https://example.com/flags/gb.png',
64+
),
65+
'DZ': Country(
66+
isoCode: 'DZ',
67+
name: 'Algeria',
68+
flagUrl: 'https://example.com/flags/dz.png',
69+
),
70+
};
71+
}
72+
73+
/// Internal storage for countries, mapping ISO code to Country object.
74+
late final Map<String, Country> _countries;
75+
76+
@override
77+
Future<List<Country>> fetchCountries({
78+
required int limit,
79+
String? startAfterId,
80+
}) async {
81+
await _simulateDelay();
82+
final countryList = _countries.values.toList()
83+
..sort((a, b) => a.isoCode.compareTo(b.isoCode)); // Consistent order
84+
85+
var startIndex = 0;
86+
if (startAfterId != null) {
87+
final startAfterCountry = _countries.values.firstWhere(
88+
(c) => c.id == startAfterId,
89+
orElse: () => throw const CountryNotFound('StartAfterId not found'),
90+
);
91+
final index = countryList.indexWhere((c) => c.id == startAfterCountry.id);
92+
if (index != -1) {
93+
startIndex = index + 1;
94+
} else {
95+
// Should not happen if startAfterId was valid, but handle defensively
96+
throw const CountryNotFound('StartAfterId inconsistency');
97+
}
98+
}
99+
100+
if (startIndex >= countryList.length) {
101+
return []; // No more items after the specified ID
102+
}
103+
104+
final endIndex = (startIndex + limit).clamp(0, countryList.length);
105+
return countryList.sublist(startIndex, endIndex);
106+
}
107+
108+
@override
109+
Future<Country> fetchCountry(String isoCode) async {
110+
await _simulateDelay();
111+
final country = _countries[isoCode.toUpperCase()];
112+
if (country == null) {
113+
throw CountryNotFound('Country with ISO code $isoCode not found.');
114+
}
115+
return country;
116+
}
117+
118+
@override
119+
Future<void> createCountry(Country country) async {
120+
await _simulateDelay();
121+
final upperIsoCode = country.isoCode.toUpperCase();
122+
if (_countries.containsKey(upperIsoCode)) {
123+
throw CountryCreateFailure(
124+
'Country with ISO code $upperIsoCode already exists.',
125+
);
126+
}
127+
// Ensure ID is generated if not provided (though constructor handles this)
128+
final countryWithId = Country(
129+
id: country.id, // Use existing or generate new
130+
isoCode: upperIsoCode,
131+
name: country.name,
132+
flagUrl: country.flagUrl,
133+
);
134+
_countries[upperIsoCode] = countryWithId;
135+
}
136+
137+
@override
138+
Future<void> updateCountry(Country country) async {
139+
await _simulateDelay();
140+
final upperIsoCode = country.isoCode.toUpperCase();
141+
if (!_countries.containsKey(upperIsoCode)) {
142+
throw CountryNotFound('Country with ISO code $upperIsoCode not found.');
143+
}
144+
// Update using the provided country data, keeping the original ID if needed
145+
// or ensuring the provided one is used consistently.
146+
_countries[upperIsoCode] = country;
147+
}
148+
149+
@override
150+
Future<void> deleteCountry(String isoCode) async {
151+
await _simulateDelay();
152+
final upperIsoCode = isoCode.toUpperCase();
153+
if (!_countries.containsKey(upperIsoCode)) {
154+
throw CountryNotFound('Country with ISO code $upperIsoCode not found.');
155+
}
156+
_countries.remove(upperIsoCode);
157+
}
158+
159+
/// Helper to simulate network latency.
160+
Future<void> _simulateDelay() async {
161+
await Future<void>.delayed(const Duration(milliseconds: 50));
162+
}
163+
}

pubspec.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: ht_api
2+
description: A new Dart Frog application
3+
version: 1.0.0+1
4+
publish_to: none
5+
6+
environment:
7+
sdk: ">=3.0.0 <4.0.0"
8+
9+
dependencies:
10+
dart_frog: ^1.1.0
11+
ht_countries_client:
12+
git:
13+
url: https://github.com/headlines-toolkit/ht-countries-client.git
14+
meta: ^1.16.0
15+
uuid: ^4.5.1
16+
17+
dev_dependencies:
18+
mocktail: ^1.0.3
19+
test: ^1.25.5
20+
very_good_analysis: ^7.0.0

routes/_middleware.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:dart_frog/dart_frog.dart';
2+
import 'package:ht_api/src/middleware/error_handler.dart'; // Import error handler
3+
import 'package:ht_api/src/providers/countries_client_provider.dart';
4+
import 'package:ht_countries_client/ht_countries_client.dart'
5+
show HtCountriesClient; // Import client provider
6+
7+
/// Applies global middleware to the entire application.
8+
///
9+
/// This middleware chain:
10+
/// 1. Provides the [HtCountriesClient] instance to the context.
11+
/// 2. Handles errors using the centralized [errorHandler].
12+
///
13+
/// The order is important: the error handler needs to wrap the provider
14+
/// and subsequent route handlers to catch exceptions from them.
15+
Handler middleware(Handler handler) {
16+
return handler
17+
// Inject the HtCountriesClient instance into the request context.
18+
.use(countriesClientProvider())
19+
// Apply the centralized error handling middleware.
20+
// This should generally be one of the outermost middlewares
21+
// to catch errors from providers and route handlers.
22+
.use(errorHandler());
23+
24+
// You could add other global middleware here, like logging:
25+
// .use(requestLogger())
26+
}

0 commit comments

Comments
 (0)