├── assets └── images │ └── country_logo.png ├── .metadata ├── pubspec.yaml ├── analysis_options.yaml ├── install-hooks.sh ├── test └── src │ ├── domain │ └── country_model_test.dart │ ├── repository │ ├── countries_repository_impl_test.dart │ └── mock_data.json │ └── data │ └── api_helper_test.dart ├── hooks └── pre-commit ├── .gitignore ├── .github └── workflows │ └── dart.yml ├── CHANGELOG.md ├── lib ├── src │ ├── data │ │ ├── api_helper.dart │ │ └── countries_api.dart │ ├── repository │ │ ├── countries_repository.dart │ │ └── countries_repository_impl.dart │ └── domain │ │ ├── enums │ │ └── country_fields.dart │ │ └── country_model.dart └── rest_countries_data.dart ├── LICENSE ├── example └── main.dart └── README.md /assets/images/country_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frankdroid7/rest_countries_package/HEAD/assets/images/country_logo.png -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "fcf2c11572af6f390246c056bc905eca609533a0" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rest_countries_data 2 | description: "A Dart package that acts as a wrapper for the REST Countries API, providing easy access to countries data." 3 | version: 1.0.2 4 | repository: https://github.com/Frankdroid7/rest_countries_package 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | http: ^1.4.0 11 | 12 | dev_dependencies: 13 | mocktail: ^1.0.4 14 | lints: ^5.0.0 15 | test: ^1.25.15 16 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | avoid_print: true 6 | prefer_const_constructors: true 7 | prefer_final_fields: true 8 | avoid_unused_constructor_parameters: true 9 | unnecessary_null_in_if_null_operators: true 10 | no_duplicate_case_values: true 11 | prefer_is_empty: true 12 | always_specify_types: true 13 | avoid_single_cascade_in_expression_statements: true -------------------------------------------------------------------------------- /install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script to install git hooks 4 | 5 | echo "Installing git hooks..." 6 | 7 | # Copy pre-commit hook 8 | cp hooks/pre-commit .git/hooks/pre-commit 9 | 10 | # Make it executable 11 | chmod +x .git/hooks/pre-commit 12 | 13 | echo " Git hooks installed successfully!" 14 | echo "The pre-commit hook will now:" 15 | echo " - Format all Dart files automatically" 16 | echo " - Run dart analyze to check for issues" 17 | echo " - Prevent commits if there are analysis errors" -------------------------------------------------------------------------------- /test/src/domain/country_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:rest_countries_data/rest_countries_data.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test(''' GIVEN CountryModel is returned, 6 | WHEN getCountryPhoneNumberCode is called 7 | THEN return the accurate code''', () async { 8 | final CountryModel countryModel = CountryModel( 9 | idd: Idd( 10 | root: '+2', 11 | suffixes: ['34'], 12 | ), 13 | ); 14 | 15 | expect(countryModel.getCountryPhoneNumberCode, '+234'); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Pre-commit hook for Dart formatting and linting 4 | 5 | echo "Running Dart formatter..." 6 | 7 | # Check if any files need formatting 8 | if ! dart format --set-exit-if-changed . > /dev/null 2>&1; then 9 | echo "Some files were not formatted. Auto-formatting now..." 10 | dart format . 11 | git add . 12 | fi 13 | 14 | echo "Running Dart analyzer..." 15 | 16 | # Analyze with fatal-infos (same as CI) 17 | if ! dart analyze --fatal-infos; then 18 | echo "Dart analyzer found issues. Fix them before committing." 19 | exit 1 20 | fi 21 | 22 | echo "Pre-commit checks passed!" 23 | exit 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | build/ 32 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Dart 7 | 8 | on: 9 | push: 10 | branches: [ "main" ] 11 | pull_request: 12 | branches: [ "main" ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | # Note: This workflow uses the latest stable version of the Dart SDK. 22 | # You can specify other versions if desired, see documentation here: 23 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 24 | # - uses: dart-lang/setup-dart@v1 25 | - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 26 | 27 | - name: Install dependencies 28 | run: dart pub get 29 | 30 | # Checks if the Dart code is properly formatted. 31 | # Fails the workflow if any files need formatting. 32 | - name: Check Dart formatting 33 | run: | 34 | dart format --set-exit-if-changed . 35 | 36 | - name: Analyze project source 37 | run: dart analyze --fatal-infos 38 | 39 | - name: Run tests 40 | run: dart test 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.0.0] - 2025-07-16 6 | 7 | - 🎉 Initial release of `rest_countries_data` Dart package. 8 | - Provides a wrapper around the [restcountries.com](https://restcountries.com) API. 9 | - Features include: 10 | - `getAllCountries` with field filtering (max 10) 11 | - `getCountryByCode`, `getCountriesByCodes` 12 | - `getCountriesByCurrency`, `getCountriesByLanguage` 13 | - `getCountriesByCapital`, `getCountriesByRegion`, `getCountriesBySubRegion` 14 | - `getCountryByName`, `getCountryByFullName` 15 | - `getCountriesByTranslation` 16 | - `getCountriesByDemonym` 17 | - `getCountriesByIndependentStatus` 18 | - Includes example usage in `/example` folder. 19 | 20 | ## [1.0.1] - 2025-07-23 21 | ### Changed 22 | - Moved conditional logic from the service layer to the repository layer. 23 | - Updated tests to reflect logic change. 24 | 25 | ### Added 26 | - Image added to README. 27 | 28 | ## [1.0.2] - 2025-07-25 29 | ### Added 30 | - `toTrim()` method for all parameters to sanitize input data. 31 | - `toString()` override method for `CountryModel` to improve debug readability. 32 | - `getCountryPhoneNumberCode` getter to the CountryModel to return country phone dialing code. 33 | - Unit test to verify `CountryModel.getCountryPhoneNumberCode`. 34 | 35 | ### Changed 36 | - Renamed `getCountriesByCapital` to `getCountryByCapital` for consistency. -------------------------------------------------------------------------------- /lib/src/data/api_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:http/http.dart' as http; 6 | 7 | class ApiHelper { 8 | static const String baseUrl = 'https://restcountries.com/v3.1'; 9 | Future>> callAPI({ 10 | required String apiUrl, 11 | }) async { 12 | try { 13 | http.Response response = 14 | await http.get(Uri.parse('$baseUrl${apiUrl.trim()}')); 15 | if (response.statusCode == 200) { 16 | List> rawData = 17 | List>.from(jsonDecode(response.body)); 18 | if (rawData.isEmpty) { 19 | throw Exception('No country found. Specify a valid field'); 20 | } 21 | return rawData; 22 | } else if (response.statusCode == 400) { 23 | throw Exception( 24 | 'Bad Request: You may have specified an unsupported field or invalid country data.'); 25 | } else if (response.statusCode == 404) { 26 | throw Exception('Country not found'); 27 | } else if (response.statusCode >= 500) { 28 | throw Exception('Server error: ${response.statusCode}'); 29 | } else { 30 | throw Exception('API error: ${response.statusCode} - ${response.body}'); 31 | } 32 | } on SocketException { 33 | throw Exception('No internet connection'); 34 | } on FormatException { 35 | throw Exception('Invalid response format'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/repository/countries_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:rest_countries_data/src/domain/country_model.dart'; 2 | import 'package:rest_countries_data/src/domain/enums/country_fields.dart'; 3 | 4 | abstract class CountryRepository { 5 | Future> getAllCountries({ 6 | required List fields, 7 | }); 8 | 9 | Future> getCountriesByName({required String name}); 10 | 11 | Future getCountryByFullName({required String fullName}); 12 | 13 | Future getCountryByCode({required String code}); 14 | 15 | Future> getCountriesByCodes({required List codes}); 16 | 17 | Future> getCountriesByCurrency({required String currency}); 18 | 19 | Future> getCountriesByDemonym({required String demonym}); 20 | 21 | Future> getCountriesByLanguage({required String language}); 22 | 23 | Future getCountryByCapital({required String capital}); 24 | 25 | Future> getCountriesByRegion({required String region}); 26 | 27 | Future> getCountriesBySubRegion({ 28 | required String subRegion, 29 | }); 30 | 31 | Future> getCountriesByTranslation({ 32 | required String translation, 33 | }); 34 | 35 | Future> getCountriesByIndependentStatus({ 36 | bool independent = true, 37 | List fields = const [], 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Franklin Oladipo 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:rest_countries_data/rest_countries_data.dart'; 2 | 3 | void main() async { 4 | await getCountriesByRegion(); 5 | await getCountryByCode(); 6 | await getCountriesByCurrency(); 7 | await getAllCountries(); 8 | await getCountriesByIndependentStatus(); 9 | } 10 | 11 | void log(String message) { 12 | //ignore: avoid_print 13 | print(message); 14 | } 15 | 16 | Future getCountriesByRegion() async { 17 | try { 18 | final List countries = 19 | await RestCountries.getCountriesByRegion(region: 'Africa'); 20 | log('\nCountries in Africa:'); 21 | for (final CountryModel country in countries) { 22 | log('- ${country.name?.common}'); 23 | } 24 | } catch (e) { 25 | log('$e'); 26 | } 27 | } 28 | 29 | Future getCountryByCode() async { 30 | try { 31 | final CountryModel country = 32 | await RestCountries.getCountryByCode(code: 'NG'); 33 | log('\nCountry with code NG: ${country.name?.official}'); 34 | } catch (e) { 35 | log('$e'); 36 | } 37 | } 38 | 39 | Future getCountriesByCurrency() async { 40 | try { 41 | final List countries = 42 | await RestCountries.getCountriesByCurrency(currency: 'USD'); 43 | log('\nCountries using USD:'); 44 | for (final CountryModel country in countries) { 45 | log('- ${country.name?.common}'); 46 | } 47 | } catch (e) { 48 | log('$e'); 49 | } 50 | } 51 | 52 | Future getAllCountries() async { 53 | try { 54 | final List countries = await RestCountries.getAllCountries( 55 | fields: [CountryFields.name]); 56 | log('\nAll countries (limited fields):'); 57 | for (final CountryModel country in countries.take(5)) { 58 | log('- ${country.name?.common}'); 59 | } 60 | } catch (e) { 61 | log('$e'); 62 | } 63 | } 64 | 65 | Future getCountriesByIndependentStatus() async { 66 | try { 67 | final List countries = 68 | await RestCountries.getCountriesByIndependentStatus(independent: true); 69 | log('\nIndependent countries:'); 70 | for (final CountryModel country in countries.take(5)) { 71 | log('- ${country.name?.common}'); 72 | } 73 | } catch (e) { 74 | log('$e'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/rest_countries_data.dart: -------------------------------------------------------------------------------- 1 | export 'src/domain/country_model.dart'; 2 | export 'src/domain/enums/country_fields.dart'; 3 | 4 | import 'package:rest_countries_data/src/data/countries_api.dart'; 5 | import 'package:rest_countries_data/src/domain/country_model.dart'; 6 | import 'package:rest_countries_data/src/domain/enums/country_fields.dart'; 7 | import 'package:rest_countries_data/src/repository/countries_repository_impl.dart'; 8 | 9 | class RestCountries { 10 | static final CountriesRepositoryImpl _repo = 11 | CountriesRepositoryImpl(CountriesApi()); 12 | 13 | static Future> getAllCountries({ 14 | required List fields, 15 | }) { 16 | return _repo.getAllCountries(fields: fields); 17 | } 18 | 19 | static Future getCountryByCapital({ 20 | required String capital, 21 | }) { 22 | return _repo.getCountryByCapital(capital: capital); 23 | } 24 | 25 | static Future getCountryByCode({ 26 | required String code, 27 | }) { 28 | return _repo.getCountryByCode(code: code); 29 | } 30 | 31 | static Future> getCountriesByCodes({ 32 | required List codes, 33 | }) { 34 | return _repo.getCountriesByCodes(codes: codes); 35 | } 36 | 37 | static Future> getCountriesByCurrency({ 38 | required String currency, 39 | }) { 40 | return _repo.getCountriesByCurrency(currency: currency); 41 | } 42 | 43 | static Future> getCountriesByDemonym({ 44 | required String demonym, 45 | }) { 46 | return _repo.getCountriesByDemonym(demonym: demonym); 47 | } 48 | 49 | static Future> getCountriesByLanguage({ 50 | required String language, 51 | }) { 52 | return _repo.getCountriesByLanguage(language: language); 53 | } 54 | 55 | static Future> getCountriesByRegion({ 56 | required String region, 57 | }) { 58 | return _repo.getCountriesByRegion(region: region); 59 | } 60 | 61 | static Future> getCountriesBySubRegion({ 62 | required String subRegion, 63 | }) { 64 | return _repo.getCountriesBySubRegion(subRegion: subRegion); 65 | } 66 | 67 | static Future> getCountriesByTranslation({ 68 | required String translation, 69 | }) { 70 | return _repo.getCountriesByTranslation(translation: translation); 71 | } 72 | 73 | static Future getCountryByFullName({ 74 | required String fullName, 75 | }) { 76 | return _repo.getCountryByFullName(fullName: fullName); 77 | } 78 | 79 | static Future> getCountriesByName({ 80 | required String name, 81 | }) { 82 | return _repo.getCountriesByName(name: name); 83 | } 84 | 85 | static Future> getCountriesByIndependentStatus({ 86 | bool independent = true, 87 | List fields = const [], 88 | }) { 89 | return _repo.getCountriesByIndependentStatus( 90 | independent: independent, 91 | fields: fields, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/data/countries_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:rest_countries_data/src/data/api_helper.dart'; 2 | import 'package:rest_countries_data/src/domain/enums/country_fields.dart'; 3 | 4 | class CountriesApi { 5 | final ApiHelper apiHelper = ApiHelper(); 6 | 7 | Future>> getAllCountries({ 8 | required List fields, 9 | }) async { 10 | final String apiQueryFields = buildCountryQueryFields( 11 | countryFields: fields, 12 | ); 13 | 14 | return await apiHelper.callAPI( 15 | apiUrl: '/all?fields=$apiQueryFields', 16 | ); 17 | } 18 | 19 | Future>> getCountryByCapital({ 20 | required String capital, 21 | }) async { 22 | return await apiHelper.callAPI(apiUrl: '/capital/$capital'); 23 | } 24 | 25 | Future>> getCountryByCode({ 26 | required String code, 27 | }) async { 28 | return await apiHelper.callAPI(apiUrl: '/alpha/$code'); 29 | } 30 | 31 | Future>> getCountryByCodes({ 32 | required List codes, 33 | }) async { 34 | String formattedCodes = codes.map((String code) => code).toList().join(','); 35 | return await apiHelper.callAPI( 36 | apiUrl: '/alpha?codes=$formattedCodes', 37 | ); 38 | } 39 | 40 | Future>> getCountryByCurrency({ 41 | required String currency, 42 | }) async { 43 | return await apiHelper.callAPI(apiUrl: '/currency/$currency'); 44 | } 45 | 46 | Future>> getCountryByDemonym({ 47 | required String demonym, 48 | }) async { 49 | return await apiHelper.callAPI(apiUrl: '/demonym/$demonym'); 50 | } 51 | 52 | Future>> getCountryByLanguage({ 53 | required String language, 54 | }) async { 55 | return await apiHelper.callAPI(apiUrl: '/lang/$language'); 56 | } 57 | 58 | Future>> getCountryByRegion({ 59 | required String region, 60 | }) async { 61 | return await apiHelper.callAPI(apiUrl: '/region/$region'); 62 | } 63 | 64 | Future>> getCountryBySubRegion({ 65 | required String subRegion, 66 | }) async { 67 | return await apiHelper.callAPI(apiUrl: '/subregion/$subRegion'); 68 | } 69 | 70 | Future>> getCountryByTranslation({ 71 | required String translation, 72 | }) async { 73 | return await apiHelper.callAPI(apiUrl: '/translation/$translation'); 74 | } 75 | 76 | Future>> getCountryByFullName({ 77 | required String fullName, 78 | }) async { 79 | return await apiHelper.callAPI( 80 | apiUrl: '/name/$fullName?fullText=true', 81 | ); 82 | } 83 | 84 | Future>> getCountriesByName({ 85 | required String name, 86 | }) async { 87 | return await apiHelper.callAPI(apiUrl: '/name/$name'); 88 | } 89 | 90 | Future>> getCountriesByIndependentStatus({ 91 | bool independent = true, 92 | List fields = const [], 93 | }) async { 94 | final String apiQueryFields = buildCountryQueryFields( 95 | countryFields: fields, 96 | ); 97 | 98 | return await apiHelper.callAPI( 99 | apiUrl: '/independent?status=$independent&fields=$apiQueryFields', 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/domain/enums/country_fields.dart: -------------------------------------------------------------------------------- 1 | enum CountryFields { 2 | name, 3 | cca2, 4 | cca3, 5 | ccn3, 6 | cioc, 7 | independent, 8 | status, 9 | flag, 10 | unMember, 11 | capital, 12 | region, 13 | subregion, 14 | continents, 15 | population, 16 | area, 17 | gini, 18 | timezones, 19 | topLevelDomain, 20 | latlng, 21 | demonym, 22 | borders, 23 | currencies, 24 | idd, 25 | languages, 26 | translations, 27 | flags, 28 | regionalBlocs, 29 | altSpellings, 30 | capitalInfo, 31 | car, 32 | coatOfArms, 33 | demonyms, 34 | fifa, 35 | maps, 36 | startOfWeek, 37 | landlocked, 38 | } 39 | 40 | extension CountryFieldsExtension on CountryFields { 41 | String get apiValue { 42 | switch (this) { 43 | case CountryFields.ccn3: 44 | return "ccn3"; 45 | case CountryFields.cioc: 46 | return "cioc"; 47 | case CountryFields.independent: 48 | return "independent"; 49 | case CountryFields.status: 50 | return "status"; 51 | case CountryFields.flag: 52 | return "flag"; 53 | case CountryFields.unMember: 54 | return "unMember"; 55 | case CountryFields.capital: 56 | return "capital"; 57 | case CountryFields.region: 58 | return "region"; 59 | case CountryFields.subregion: 60 | return "subregion"; 61 | case CountryFields.continents: 62 | return "continents"; 63 | case CountryFields.population: 64 | return "population"; 65 | case CountryFields.area: 66 | return "area"; 67 | case CountryFields.gini: 68 | return "gini"; 69 | case CountryFields.timezones: 70 | return "timezones"; 71 | case CountryFields.topLevelDomain: 72 | return "tld"; 73 | case CountryFields.latlng: 74 | return "latlng"; 75 | case CountryFields.demonym: 76 | return "demonym"; 77 | case CountryFields.borders: 78 | return "borders"; 79 | case CountryFields.currencies: 80 | return "currencies"; 81 | case CountryFields.idd: 82 | return "idd"; 83 | case CountryFields.languages: 84 | return "languages"; 85 | case CountryFields.translations: 86 | return "translations"; 87 | case CountryFields.flags: 88 | return "flags"; 89 | case CountryFields.regionalBlocs: 90 | return "regionalBlocs"; 91 | case CountryFields.altSpellings: 92 | return "altSpellings"; 93 | case CountryFields.capitalInfo: 94 | return "capitalInfo"; 95 | case CountryFields.car: 96 | return "car"; 97 | case CountryFields.coatOfArms: 98 | return "coatOfArms"; 99 | case CountryFields.demonyms: 100 | return "demonyms"; 101 | case CountryFields.fifa: 102 | return "fifa"; 103 | case CountryFields.maps: 104 | return "maps"; 105 | case CountryFields.startOfWeek: 106 | return "startOfWeek"; 107 | case CountryFields.landlocked: 108 | return "landlocked"; 109 | case CountryFields.name: 110 | return "name"; 111 | case CountryFields.cca2: 112 | return "cca2"; 113 | case CountryFields.cca3: 114 | return "cca3"; 115 | } 116 | } 117 | } 118 | 119 | String buildCountryQueryFields({required List countryFields}) { 120 | return countryFields 121 | .map((CountryFields field) => field.apiValue) 122 | .toList() 123 | .join(','); 124 | } 125 | -------------------------------------------------------------------------------- /test/src/repository/countries_repository_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:test/test.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | import 'package:rest_countries_data/rest_countries_data.dart'; 6 | import 'package:rest_countries_data/src/data/countries_api.dart'; 7 | import 'package:rest_countries_data/src/repository/countries_repository_impl.dart'; 8 | 9 | class MockCountriesApi extends Mock implements CountriesApi {} 10 | 11 | void main() { 12 | group('Test the methods in CountriesRepositoryImpl', () { 13 | late final List> jsonData; 14 | late MockCountriesApi mockCountriesApi; 15 | late CountriesRepositoryImpl countriesRepositoryImpl; 16 | 17 | setUp(() { 18 | mockCountriesApi = MockCountriesApi(); 19 | countriesRepositoryImpl = CountriesRepositoryImpl(mockCountriesApi); 20 | }); 21 | 22 | setUpAll(() async { 23 | final File file = File('test/src/repository/mock_data.json'); 24 | final String jsonStr = await file.readAsString(); 25 | jsonData = List>.from(jsonDecode(jsonStr)); 26 | }); 27 | 28 | test( 29 | 'GIVEN getAllCountries is called, WHEN fields list is more than 10, THEN throw an exception ', 30 | () async { 31 | expect( 32 | () async => await countriesRepositoryImpl.getAllCountries( 33 | fields: List.filled(11, CountryFields.name), 34 | ), 35 | throwsA( 36 | predicate( 37 | (Object? e) => 38 | e is Exception && 39 | e.toString().contains( 40 | 'CountryFields cannot be more than 10', 41 | ), 42 | ), 43 | ), 44 | ); 45 | }, 46 | ); 47 | 48 | test( 49 | "GIVEN valid fields, WHEN getAllCountries is called, THEN returns a List", 50 | () async { 51 | when( 52 | () => mockCountriesApi 53 | .getAllCountries(fields: [CountryFields.area]), 54 | ).thenAnswer( 55 | (_) => 56 | Future>>.value(>[ 57 | { 58 | "name": { 59 | "common": "Mayotte", 60 | "official": "Department of Mayotte", 61 | "nativeName": >{ 62 | "fra": { 63 | "official": "Department of Mayotte", 64 | "common": "Mayotte", 65 | }, 66 | }, 67 | }, 68 | }, 69 | ]), 70 | ); 71 | 72 | final List getAllCountries = 73 | await countriesRepositoryImpl.getAllCountries( 74 | fields: [CountryFields.area], 75 | ); 76 | 77 | expect(getAllCountries, isA>()); 78 | expect(getAllCountries.first.name?.common, 'Mayotte'); 79 | }, 80 | ); 81 | 82 | test( 83 | "GIVEN valid field, WHEN getCountryByCapital is called, THEN verify that countriesApi.getCountryByCapital is called", 84 | () async { 85 | when( 86 | () => mockCountriesApi.getCountryByCapital(capital: 'Abuja'), 87 | ).thenAnswer( 88 | (_) => Future>>.value( 89 | >[{}], 90 | ), 91 | ); 92 | await countriesRepositoryImpl.getCountryByCapital(capital: 'Abuja'); 93 | 94 | verify( 95 | () => mockCountriesApi.getCountryByCapital(capital: 'Abuja'), 96 | ).called(1); 97 | }, 98 | ); 99 | 100 | test( 101 | 'GIVEN valid fields, WHEN getCountryByCode is called, THEN verify CountryModel is returned', 102 | () async { 103 | when( 104 | () => mockCountriesApi.getCountryByCode(code: '170'), 105 | ).thenAnswer((_) => Future>>.value(jsonData)); 106 | 107 | final CountryModel getCountriesByCode = 108 | await countriesRepositoryImpl.getCountryByCode(code: '170'); 109 | 110 | expect(getCountriesByCode, isA()); 111 | }, 112 | ); 113 | test( 114 | 'GIVEN valid fields, WHEN getCountryByCode is called, THEN verify List is returned', 115 | () async { 116 | when( 117 | () => mockCountriesApi.getCountryByCodes(codes: ['170']), 118 | ).thenAnswer((_) => Future>>.value(jsonData)); 119 | 120 | final List getCountriesByCode = 121 | await countriesRepositoryImpl 122 | .getCountriesByCodes(codes: ['170']); 123 | 124 | expect(getCountriesByCode, isA>()); 125 | }, 126 | ); 127 | test( 128 | 'GIVEN valid fields, WHEN getCountriesByCurrency is called and response is an empty list, THEN return an empty list', 129 | () async { 130 | when( 131 | () => mockCountriesApi.getCountryByCurrency(currency: 'NGN'), 132 | ).thenAnswer((_) => 133 | Future>>.value(>[])); 134 | 135 | final List getCountryByCurrency = 136 | await countriesRepositoryImpl.getCountriesByCurrency( 137 | currency: 'NGN'); 138 | 139 | expect(getCountryByCurrency, isEmpty); 140 | }, 141 | ); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /test/src/repository/mock_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": { 4 | "common": "Colombia", 5 | "official": "Republic of Colombia", 6 | "nativeName": { 7 | "spa": { 8 | "official": "República de Colombia", 9 | "common": "Colombia" 10 | } 11 | } 12 | }, 13 | "tld": [ 14 | ".co" 15 | ], 16 | "cca2": "CO", 17 | "ccn3": "170", 18 | 19 | "cioc": "COL", 20 | "independent": true, 21 | "status": "officially-assigned", 22 | "unMember": true, 23 | "currencies": { 24 | "COP": { 25 | "symbol": "$", 26 | "name": "Colombian peso" 27 | } 28 | }, 29 | "idd": { 30 | "root": "+5", 31 | "suffixes": [ 32 | "7" 33 | ] 34 | }, 35 | "capital": [ 36 | "Bogotá" 37 | ], 38 | "altSpellings": [ 39 | "CO", 40 | "Republic of Colombia", 41 | "República de Colombia" 42 | ], 43 | "region": "Americas", 44 | "subregion": "South America", 45 | "languages": { 46 | "spa": "Spanish" 47 | }, 48 | "latlng": [ 49 | 4, 50 | -72 51 | ], 52 | "landlocked": false, 53 | "borders": [ 54 | "BRA", 55 | "ECU", 56 | "PAN", 57 | "PER", 58 | "VEN" 59 | ], 60 | "area": 1141748, 61 | "demonyms": { 62 | "eng": { 63 | "f": "Colombian", 64 | "m": "Colombian" 65 | }, 66 | "fra": { 67 | "f": "Colombienne", 68 | "m": "Colombien" 69 | } 70 | }, 71 | "cca3": "COL", 72 | "translations": { 73 | "ara": { 74 | "official": "جمهورية كولومبيا", 75 | "common": "كولومبيا" 76 | }, 77 | "bre": { 78 | "official": "Republik Kolombia", 79 | "common": "Kolombia" 80 | }, 81 | "ces": { 82 | "official": "Kolumbijská republika", 83 | "common": "Kolumbie" 84 | }, 85 | "cym": { 86 | "official": "Gweriniaeth Colombia", 87 | "common": "Colombia" 88 | }, 89 | "deu": { 90 | "official": "Republik Kolumbien", 91 | "common": "Kolumbien" 92 | }, 93 | "est": { 94 | "official": "Colombia Vabariik", 95 | "common": "Colombia" 96 | }, 97 | "fin": { 98 | "official": "Kolumbian tasavalta", 99 | "common": "Kolumbia" 100 | }, 101 | "fra": { 102 | "official": "République de Colombie", 103 | "common": "Colombie" 104 | }, 105 | "hrv": { 106 | "official": "Republika Kolumbija", 107 | "common": "Kolumbija" 108 | }, 109 | "hun": { 110 | "official": "Kolumbiai Köztársaság", 111 | "common": "Kolumbia" 112 | }, 113 | "ind": { 114 | "official": "Republik Kolombia", 115 | "common": "Kolombia" 116 | }, 117 | "ita": { 118 | "official": "Repubblica di Colombia", 119 | "common": "Colombia" 120 | }, 121 | "jpn": { 122 | "official": "コロンビア共和国", 123 | "common": "コロンビア" 124 | }, 125 | "kor": { 126 | "official": "콜롬비아 공화국", 127 | "common": "콜롬비아" 128 | }, 129 | "nld": { 130 | "official": "Republiek Colombia", 131 | "common": "Colombia" 132 | }, 133 | "per": { 134 | "official": "جمهوری کلمبیا", 135 | "common": "کلمبیا" 136 | }, 137 | "pol": { 138 | "official": "Republika Kolumbii", 139 | "common": "Kolumbia" 140 | }, 141 | "por": { 142 | "official": "República da Colômbia", 143 | "common": "Colômbia" 144 | }, 145 | "rus": { 146 | "official": "Республика Колумбия", 147 | "common": "Колумбия" 148 | }, 149 | "slk": { 150 | "official": "Kolumbijská republika", 151 | "common": "Kolumbia" 152 | }, 153 | "spa": { 154 | "official": "República de Colombia", 155 | "common": "Colombia" 156 | }, 157 | "srp": { 158 | "official": "Република Колумбија", 159 | "common": "Колумбија" 160 | }, 161 | "swe": { 162 | "official": "Republiken Colombia", 163 | "common": "Colombia" 164 | }, 165 | "tur": { 166 | "official": "Kolombiya Cumhuriyeti", 167 | "common": "Kolombiya" 168 | }, 169 | "urd": { 170 | "official": "جمہوریہ کولمبیا", 171 | "common": "کولمبیا" 172 | }, 173 | "zho": { 174 | "official": "哥伦比亚共和国", 175 | "common": "哥伦比亚" 176 | } 177 | }, 178 | "flag": "🇨🇴", 179 | "maps": { 180 | "googleMaps": "https://goo.gl/maps/zix9qNFX69E9yZ2M6", 181 | "openStreetMaps": "https://www.openstreetmap.org/relation/120027" 182 | }, 183 | "population": 50882884, 184 | "gini": { 185 | "2019": 51.3 186 | }, 187 | "fifa": "COL", 188 | "car": { 189 | "signs": [ 190 | "CO" 191 | ], 192 | "side": "right" 193 | }, 194 | "timezones": [ 195 | "UTC-05:00" 196 | ], 197 | "continents": [ 198 | "South America" 199 | ], 200 | "flags": { 201 | "png": "https://flagcdn.com/w320/co.png", 202 | "svg": "https://flagcdn.com/co.svg", 203 | "alt": "The flag of Colombia is composed of three horizontal bands of yellow, blue and red, with the yellow band twice the height of the other two bands." 204 | }, 205 | "coatOfArms": { 206 | "png": "https://mainfacts.com/media/images/coats_of_arms/co.png", 207 | "svg": "https://mainfacts.com/media/images/coats_of_arms/co.svg" 208 | }, 209 | "startOfWeek": "monday", 210 | "capitalInfo": { 211 | "latlng": [ 212 | 4.71, 213 | -74.07 214 | ] 215 | }, 216 | "postalCode": { 217 | "format": null, 218 | "regex": null 219 | } 220 | } 221 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rest_countries_data 2 | 3 |

4 | rest_countries_data Logo 5 |

6 | 7 | --- 8 | 9 | A Dart wrapper around the [REST Countries v3.1 API](https://restcountries.com) that provides a fully-typed, developer-friendly interface for accessing detailed country data. 10 | 11 | --- 12 | 13 | ## ✨ Features 14 | 15 | - Get all countries with selected fields (limited to 10 fields) 16 | - Query countries by: 17 | - Capital 18 | - Full name or partial name 19 | - ISO 3166-1 code or list of codes 20 | - Region / Subregion 21 | - Language / Currency / Demonym 22 | - Translation 23 | - Independence status 24 | - Built-in `CountryModel` to map the API response 25 | - Enum-based `CountryFields` selector for fine-grained queries 26 | - Clean separation of concerns via Repository, API and Domain layers 27 | 28 | --- 29 | 30 | ## 🚀 Getting Started 31 | 32 | ### 1. Install the Package 33 | 34 | Add to your `pubspec.yaml`: 35 | 36 | ```yaml 37 | dependencies: 38 | rest_countries_data: ^1.0.0 39 | ``` 40 | 41 | Then run: 42 | 43 | ```bash 44 | # For Dart users 45 | dart pub get 46 | 47 | # For Flutter users 48 | flutter pub get 49 | ``` 50 | 51 | ### 2. Import It 52 | 53 | ```dart 54 | import 'package:rest_countries_data/rest_countries_data.dart'; 55 | ``` 56 | 57 | --- 58 | 59 | ## 📦 Usage 60 | 61 | ### Fetch All Countries with Selected Fields 62 | 63 | ```dart 64 | final countries = await RestCountries.getAllCountries( 65 | fields: [CountryFields.name, CountryFields.capital, CountryFields.region], 66 | ); 67 | ``` 68 | ## Field Limitation ⚠️ 69 | When using the `fields` parameter in methods like `getAllCountries`, the maximum number of allowed fields is **10**. This limit is enforced by the [REST Countries API](https://restcountries.com/). 70 | 71 | Also note: Whenever you specify the fields property, the properties that will be available in the returned `CountryModel` depend entirely on which `CountryFields` you request. If you omit a field, the corresponding property in `CountryModel` may be `null`. 72 | 73 | 74 | 75 | ### Get Countries by Capital 76 | 77 | ```dart 78 | final country = await RestCountries.getCountryByCapital(capital: 'Paris'); 79 | ``` 80 | 81 | ### Get a Country by ISO Code 82 | 83 | ```dart 84 | final country = await RestCountries.getCountryByCode(code: 'NG'); 85 | ``` 86 | 87 | ### Get Countries by Currency 88 | 89 | ```dart 90 | final countries = await RestCountries.getCountriesByCurrency(currency: 'USD'); 91 | ``` 92 | 93 | ### Get Countries by Region 94 | 95 | ```dart 96 | final countries = await RestCountries.getCountriesByRegion(region: 'Europe'); 97 | ``` 98 | 99 | ### Get Countries by SubRegion 100 | 101 | ```dart 102 | final countries = await RestCountries.getCountriesBySubRegion(subRegion: 'Eastern Africa'); 103 | ``` 104 | 105 | ### Get Country by Full Name 106 | 107 | ```dart 108 | final country = await RestCountries.getCountryByFullName(fullName: 'Federal Republic of Nigeria'); 109 | ``` 110 | 111 | ### Get Countries by Language, Demonym, or Translation 112 | 113 | ```dart 114 | final countries = await RestCountries.getCountriesByLanguage(language: 'en'); 115 | final countries = await RestCountries.getCountriesByDemonym(demonym: 'Nigerian'); 116 | final countries = await RestCountries.getCountriesByTranslation(translation: 'Niger'); 117 | ``` 118 | 119 | ### Get Countries by Independence Status 120 | 121 | ```dart 122 | final countries = await RestCountries.getCountriesByIndependentStatus( 123 | independent: true, 124 | fields: [CountryFields.name, CountryFields.flag], 125 | ); 126 | ``` 127 | 128 | --- 129 | 130 | ## 📚 Data Model 131 | 132 | ### CountryModel (Simplified) 133 | 134 | ```dart 135 | class CountryModel { 136 | final Name? name; 137 | final List? capital; 138 | final String? region; 139 | final String? subregion; 140 | final Map? flags; 141 | // ... 142 | } 143 | 144 | class Name { 145 | final String common; 146 | final String official; 147 | final Map? nativeName; 148 | } 149 | ``` 150 | 151 | ### CountryFields Enum 152 | 153 | Use the `CountryFields` enum to limit which fields are returned from the API: 154 | 155 | ```dart 156 | CountryFields.name 157 | CountryFields.capital 158 | CountryFields.region 159 | CountryFields.area 160 | CountryFields.languages 161 | // etc. 162 | ``` 163 | 164 | **Note**: Maximum 10 fields per request for `getAllCountries()` 165 | 166 | ### 🧩 Utility Getter 167 | 168 | #### `getCountryPhoneNumberCode` 169 | 170 | This convenience getter is available on the `CountryModel` and returns the full numeric dialing code (e.g., `+234` for the country Nigeria). It is constructed by combining `idd.root` and `idd.suffixes` in the `CountryModel`. 171 | 172 | --- 173 | 174 | ## 🔍 API Overview 175 | 176 | | Method | Description | 177 | | ---------------------------------------- | ----------------------------------------------------- | 178 | | `getAllCountries({required fields})` | Get all countries with selected fields | 179 | | `getCountryByCapital({required capital})` | Filter by capital city | 180 | | `getCountryByCode({required code})` | Get a single country by ISO code | 181 | | `getCountriesByCodes({required codes})` | Get multiple countries by ISO codes | 182 | | `getCountriesByCurrency({required currency})` | Filter by currency | 183 | | `getCountriesByDemonym({required demonym})` | Filter by demonym | 184 | | `getCountriesByLanguage({required language})` | Filter by spoken language | 185 | | `getCountriesByRegion({required region})` | Filter by region | 186 | | `getCountriesBySubRegion({required subRegion})` | Filter by subregion | 187 | | `getCountriesByTranslation({required translation})` | Filter by translated country name | 188 | | `getCountryByFullName({required fullName})` | Get a country by its full name | 189 | | `getCountriesByName({required name})` | Filter by partial or full name | 190 | | `getCountriesByIndependentStatus(...)` | Filter by independence and optionally fields | 191 | | `CountryModel.getCountryPhoneNumberCode` | Get the phone dialing code of a country (e.g. `+234`) | 192 | 193 | 194 | ## 📊 Example (Run it) 195 | 196 | ```dart 197 | import 'package:rest_countries_data/rest_countries_data.dart'; 198 | 199 | void main() async { 200 | final countries = await RestCountries.getCountriesByRegion(region: 'Africa'); 201 | 202 | for (var country in countries) { 203 | print(country.name?.common); 204 | } 205 | } 206 | ``` 207 | 208 | --- 209 | 210 | ## 🚪 Contributing 211 | 212 | Pull requests are welcome! If you have ideas or find bugs, feel free to open an issue. 213 | 214 | --- 215 | 216 | ### Development Setup 217 | 218 | After cloning the repository, install the git hooks for automatic formatting and linting: 219 | 220 | ```bash 221 | ./install-hooks.sh 222 | ``` 223 | 224 | This will set up a pre-commit hook that: 225 | - Automatically formats all Dart files 226 | - Runs dart analyze to check for issues 227 | - Prevents commits if there are analysis errors 228 | 229 | 230 | ## 👋 Author 231 | 232 | Maintained by [Franklin Oladipo](https://github.com/frankdroid7) 233 | 234 | --- 235 | 236 | ## 📄 License 237 | 238 | [BSD 3-Clause](LICENSE) 239 | -------------------------------------------------------------------------------- /lib/src/repository/countries_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:rest_countries_data/src/data/countries_api.dart'; 2 | import 'package:rest_countries_data/src/domain/country_model.dart'; 3 | import 'package:rest_countries_data/src/domain/enums/country_fields.dart'; 4 | import 'package:rest_countries_data/src/repository/countries_repository.dart'; 5 | 6 | /// Repository implementation for accessing country data via [CountriesApi]. 7 | /// 8 | /// Provides methods to fetch countries by various criteria such as capital, 9 | /// region, language, currency, and more. 10 | class CountriesRepositoryImpl implements CountryRepository { 11 | final CountriesApi countriesApi; 12 | 13 | /// Creates a new [CountriesRepositoryImpl] with the given [countriesApi]. 14 | CountriesRepositoryImpl(this.countriesApi); 15 | 16 | /// Retrieves all countries with the specified [fields]. 17 | /// 18 | /// Returns a list of [CountryModel]. 19 | @override 20 | Future> getAllCountries({ 21 | required List fields, 22 | }) async { 23 | if (fields.length > 10) { 24 | throw Exception('CountryFields cannot be more than 10'); 25 | } 26 | 27 | if (fields.isEmpty) { 28 | throw Exception('CountryFields cannot be empty'); 29 | } 30 | 31 | List> response = 32 | await countriesApi.getAllCountries(fields: fields); 33 | 34 | List countryModelList = response 35 | .map((Map country) => CountryModel.fromJson(country)) 36 | .toList(); 37 | 38 | return countryModelList; 39 | } 40 | 41 | /// Retrieves countries whose capital city matches [capital]. 42 | /// 43 | /// Returns a list of [CountryModel]. 44 | @override 45 | Future getCountryByCapital({ 46 | required String capital, 47 | }) async { 48 | List> response = 49 | await countriesApi.getCountryByCapital(capital: capital); 50 | 51 | return CountryModel.fromJson(response.first); 52 | } 53 | 54 | /// Retrieves a country by its [code]. 55 | /// 56 | /// Returns a single [CountryModel]. 57 | @override 58 | Future getCountryByCode({required String code}) async { 59 | List> response = 60 | await countriesApi.getCountryByCode(code: code); 61 | 62 | return CountryModel.fromJson(response.first); 63 | } 64 | 65 | /// Retrieves countries by a list of country [codes]. 66 | /// 67 | /// Returns a list of [CountryModel]. 68 | @override 69 | Future> getCountriesByCodes({ 70 | required List codes, 71 | }) async { 72 | List> response = 73 | await countriesApi.getCountryByCodes(codes: codes); 74 | 75 | List countryModelList = response 76 | .map((Map country) => CountryModel.fromJson(country)) 77 | .toList(); 78 | 79 | return countryModelList; 80 | } 81 | 82 | /// Retrieves countries that use the specified [currency]. 83 | /// 84 | /// Returns a list of [CountryModel]. 85 | @override 86 | Future> getCountriesByCurrency({ 87 | required String currency, 88 | }) async { 89 | List> response = 90 | await countriesApi.getCountryByCurrency(currency: currency); 91 | 92 | List countryModelList = response 93 | .map((Map country) => CountryModel.fromJson(country)) 94 | .toList(); 95 | 96 | return countryModelList; 97 | } 98 | 99 | /// Retrieves countries that have the specified [demonym]. 100 | /// 101 | /// Returns a list of [CountryModel]. 102 | @override 103 | Future> getCountriesByDemonym({ 104 | required String demonym, 105 | }) async { 106 | List> response = 107 | await countriesApi.getCountryByDemonym(demonym: demonym); 108 | 109 | List countryModelList = response 110 | .map((Map country) => CountryModel.fromJson(country)) 111 | .toList(); 112 | 113 | return countryModelList; 114 | } 115 | 116 | /// Retrieves countries that speak the specified [language]. 117 | /// 118 | /// Returns a list of [CountryModel]. 119 | @override 120 | Future> getCountriesByLanguage({ 121 | required String language, 122 | }) async { 123 | List> response = 124 | await countriesApi.getCountryByLanguage(language: language); 125 | 126 | List countryModelList = response 127 | .map((Map country) => CountryModel.fromJson(country)) 128 | .toList(); 129 | 130 | return countryModelList; 131 | } 132 | 133 | /// Retrieves countries in the specified [region]. 134 | /// 135 | /// Returns a list of [CountryModel]. 136 | @override 137 | Future> getCountriesByRegion({ 138 | required String region, 139 | }) async { 140 | List> response = 141 | await countriesApi.getCountryByRegion(region: region); 142 | 143 | List countryModelList = response 144 | .map((Map country) => CountryModel.fromJson(country)) 145 | .toList(); 146 | 147 | return countryModelList; 148 | } 149 | 150 | /// Retrieves countries in the specified [subRegion]. 151 | /// 152 | /// Returns a list of [CountryModel]. 153 | @override 154 | Future> getCountriesBySubRegion({ 155 | required String subRegion, 156 | }) async { 157 | List> response = 158 | await countriesApi.getCountryBySubRegion( 159 | subRegion: subRegion, 160 | ); 161 | 162 | List countryModelList = response 163 | .map((Map country) => CountryModel.fromJson(country)) 164 | .toList(); 165 | 166 | return countryModelList; 167 | } 168 | 169 | /// Retrieves countries that have the specified [translation]. 170 | /// 171 | /// Returns a list of [CountryModel]. 172 | @override 173 | Future> getCountriesByTranslation({ 174 | required String translation, 175 | }) async { 176 | List> response = 177 | await countriesApi.getCountryByTranslation( 178 | translation: translation, 179 | ); 180 | 181 | List countryModelList = response 182 | .map((Map country) => CountryModel.fromJson(country)) 183 | .toList(); 184 | 185 | return countryModelList; 186 | } 187 | 188 | /// Retrieves a country by its full name [fullName]. 189 | /// 190 | /// Returns a single [CountryModel]. 191 | @override 192 | Future getCountryByFullName({required String fullName}) async { 193 | List> response = 194 | await countriesApi.getCountryByFullName(fullName: fullName); 195 | 196 | return CountryModel.fromJson(response.first); 197 | } 198 | 199 | /// Retrieves countries whose name matches [name]. 200 | /// 201 | /// Returns a list of [CountryModel]. 202 | @override 203 | Future> getCountriesByName({required String name}) async { 204 | List> response = 205 | await countriesApi.getCountriesByName(name: name); 206 | 207 | List countryModelList = response 208 | .map((Map country) => CountryModel.fromJson(country)) 209 | .toList(); 210 | 211 | return countryModelList; 212 | } 213 | 214 | /// Retrieves countries by their independence status. 215 | /// 216 | /// [independent] indicates whether to filter for independent countries (default: true). 217 | /// [fields] allows specifying which country fields to include. 218 | /// 219 | /// Returns a list of [CountryModel]. 220 | @override 221 | Future> getCountriesByIndependentStatus({ 222 | bool independent = true, 223 | List fields = const [], 224 | }) async { 225 | List> response = 226 | await countriesApi.getCountriesByIndependentStatus( 227 | independent: independent, 228 | fields: fields, 229 | ); 230 | 231 | List countryModelList = response 232 | .map((Map country) => CountryModel.fromJson(country)) 233 | .toList(); 234 | 235 | return countryModelList; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/src/domain/country_model.dart: -------------------------------------------------------------------------------- 1 | //ignore_for_file: always_specify_types 2 | class CountryModel { 3 | final Name? name; 4 | final String? cca2; 5 | final String? cca3; 6 | final String? ccn3; 7 | final String? cioc; 8 | final bool? independent; 9 | final String? status; 10 | final String? flag; 11 | final bool? unMember; 12 | final List? capital; 13 | final String? region; 14 | final String? subregion; 15 | final List? continents; 16 | final int? population; 17 | final double? area; 18 | final Map? gini; 19 | final List? timezones; 20 | final List? topLevelDomain; 21 | final List? latlng; 22 | final String? demonym; 23 | final List? borders; 24 | final Map? currencies; 25 | final Idd? idd; 26 | final Map? languages; 27 | final Map? translations; 28 | final Flags? flags; 29 | final List? regionalBlocs; 30 | final List? altSpellings; 31 | final CapitalInfo? capitalInfo; 32 | final Car? car; 33 | final CoatOfArms? coatOfArms; 34 | final Map? demonyms; 35 | final String? fifa; 36 | final Maps? maps; 37 | final String? startOfWeek; 38 | final bool? landlocked; 39 | 40 | CountryModel({ 41 | this.name, 42 | this.cca2, 43 | this.cca3, 44 | this.flag, 45 | this.ccn3, 46 | this.landlocked, 47 | this.maps, 48 | this.cioc, 49 | this.car, 50 | this.independent, 51 | this.status, 52 | this.unMember, 53 | this.capital, 54 | this.region, 55 | this.subregion, 56 | this.continents, 57 | this.fifa, 58 | this.population, 59 | this.area, 60 | this.gini, 61 | this.timezones, 62 | this.topLevelDomain, 63 | this.latlng, 64 | this.demonym, 65 | this.borders, 66 | this.currencies, 67 | this.idd, 68 | this.languages, 69 | this.translations, 70 | this.flags, 71 | this.regionalBlocs, 72 | this.demonyms, 73 | this.altSpellings, 74 | this.capitalInfo, 75 | this.startOfWeek, 76 | this.coatOfArms, 77 | }); 78 | 79 | String get getCountryPhoneNumberCode => 80 | '${idd?.root}${idd?.suffixes?.join()}'; 81 | 82 | factory CountryModel.fromJson(Map json) { 83 | return CountryModel( 84 | name: json['name'] != null ? Name.fromJson(json['name']) : null, 85 | cca2: json['cca2'], 86 | cca3: json['cca3'], 87 | ccn3: json['ccn3'], 88 | cioc: json['cioc'], 89 | fifa: json['fifa'], 90 | landlocked: json['landlocked'], 91 | independent: json['independent'], 92 | status: json['status'], 93 | unMember: json['unMember'], 94 | capital: 95 | (json['capital'] as List?)?.map((e) => e as String).toList(), 96 | region: json['region'], 97 | subregion: json['subregion'], 98 | continents: (json['continents'] as List?) 99 | ?.map((e) => e as String) 100 | .toList(), 101 | population: json['population'], 102 | area: (json['area'] is num) ? (json['area'] as num).toDouble() : null, 103 | gini: (json['gini'] as Map?)?.map( 104 | (String k, v) => MapEntry(k, (v as num).toDouble()), 105 | ), 106 | timezones: (json['timezones'] as List?)?.cast(), 107 | topLevelDomain: (json['tld'] as List?)?.cast(), 108 | latlng: (json['latlng'] as List?) 109 | ?.map((e) => (e as num).toDouble()) 110 | .toList(), 111 | demonym: json['demonym'], 112 | borders: (json['borders'] as List?)?.cast(), 113 | currencies: (json['currencies'] as Map?)?.map( 114 | (String k, v) => MapEntry(k, Currency.fromJson(v)), 115 | ), 116 | idd: json['idd'] != null ? Idd.fromJson(json['idd']) : null, 117 | languages: (json['languages'] as Map?)?.map( 118 | (String k, v) => MapEntry(k, v as String), 119 | ), 120 | translations: (json['translations'] as Map?)?.map( 121 | (String k, dynamic v) => MapEntry(k, Translation.fromJson(v)), 122 | ), 123 | flags: json['flags'] != null ? Flags.fromJson(json['flags']) : null, 124 | regionalBlocs: (json['regionalBlocs'] as List?) 125 | ?.map((e) => RegionalBloc.fromJson(e)) 126 | .toList(), 127 | altSpellings: (json['altSpellings'] as List?) 128 | ?.map((e) => e as String) 129 | .toList(), 130 | capitalInfo: json['capitalInfo'] != null 131 | ? CapitalInfo.fromJson(json['capitalInfo']) 132 | : null, 133 | car: json['car'] != null ? Car.fromJson(json['car']) : null, 134 | coatOfArms: json['coatOfArms'] != null 135 | ? CoatOfArms.fromJson(json['coatOfArms']) 136 | : null, 137 | maps: json['maps'] != null ? Maps.fromJson(json['maps']) : null, 138 | flag: json['flag'], 139 | startOfWeek: json['startOfWeek'], 140 | demonyms: (json['demonyms'] as Map?)?.map( 141 | (String key, value) => MapEntry(key, GenderedDemonym.fromJson(value)), 142 | ), 143 | ); 144 | } 145 | 146 | @override 147 | String toString() { 148 | return 'CountryModel(\n' 149 | ' name: $name,\n' 150 | ' cca2: $cca2,\n' 151 | ' cca3: $cca3,\n' 152 | ' ccn3: $ccn3,\n' 153 | ' cioc: $cioc,\n' 154 | ' independent: $independent,\n' 155 | ' status: $status,\n' 156 | ' flag: $flag,\n' 157 | ' unMember: $unMember,\n' 158 | ' capital: $capital,\n' 159 | ' region: $region,\n' 160 | ' subregion: $subregion,\n' 161 | ' continents: $continents,\n' 162 | ' population: $population,\n' 163 | ' area: $area,\n' 164 | ' gini: $gini,\n' 165 | ' timezones: $timezones,\n' 166 | ' topLevelDomain: $topLevelDomain,\n' 167 | ' latlng: $latlng,\n' 168 | ' demonym: $demonym,\n' 169 | ' borders: $borders,\n' 170 | ' currencies: $currencies,\n' 171 | ' idd: $idd,\n' 172 | ' languages: $languages,\n' 173 | ' translations: $translations,\n' 174 | ' flags: $flags,\n' 175 | ' regionalBlocs: $regionalBlocs,\n' 176 | ' altSpellings: $altSpellings,\n' 177 | ' capitalInfo: $capitalInfo,\n' 178 | ' car: $car,\n' 179 | ' coatOfArms: $coatOfArms,\n' 180 | ' demonyms: $demonyms,\n' 181 | ' fifa: $fifa,\n' 182 | ' maps: $maps,\n' 183 | ' startOfWeek: $startOfWeek,\n' 184 | ' landlocked: $landlocked\n' 185 | ')'; 186 | } 187 | } 188 | 189 | class Name { 190 | final String? common; 191 | final String? official; 192 | final Map? nativeName; 193 | 194 | Name({this.common, this.official, this.nativeName}); 195 | 196 | factory Name.fromJson(Map json) { 197 | final Map? native = 198 | (json['nativeName'] as Map?)?.map( 199 | (String k, v) => MapEntry(k, Translation.fromJson(v)), 200 | ); 201 | return Name( 202 | common: json['common'], 203 | official: json['official'], 204 | nativeName: native, 205 | ); 206 | } 207 | 208 | @override 209 | String toString() { 210 | return 'Name(\n' 211 | ' common: $common,\n' 212 | ' official: $official,\n' 213 | ' nativeName: $nativeName\n' 214 | ')'; 215 | } 216 | } 217 | 218 | class Currency { 219 | final String? name; 220 | final String? symbol; 221 | 222 | Currency({this.name, this.symbol}); 223 | 224 | factory Currency.fromJson(Map json) => 225 | Currency(name: json['name'], symbol: json['symbol']); 226 | 227 | @override 228 | String toString() { 229 | return 'Currency(\n' 230 | ' name: $name,\n' 231 | ' symbol: $symbol\n' 232 | ')'; 233 | } 234 | } 235 | 236 | class Idd { 237 | final String? root; 238 | final List? suffixes; 239 | 240 | Idd({this.root, this.suffixes}); 241 | 242 | factory Idd.fromJson(Map json) => Idd( 243 | root: json['root'], 244 | suffixes: (json['suffixes'] as List?)?.cast(), 245 | ); 246 | 247 | @override 248 | String toString() { 249 | return 'Idd(\n' 250 | ' root: $root,\n' 251 | ' suffixes: $suffixes\n' 252 | ')'; 253 | } 254 | } 255 | 256 | class Translation { 257 | final String? official; 258 | final String? common; 259 | 260 | Translation({this.official, this.common}); 261 | 262 | factory Translation.fromJson(Map json) => 263 | Translation(official: json['official'], common: json['common']); 264 | 265 | @override 266 | String toString() { 267 | return 'Translation(\n' 268 | ' official: $official,\n' 269 | ' common: $common\n' 270 | ')'; 271 | } 272 | } 273 | 274 | class Flags { 275 | final String? svg; 276 | final String? png; 277 | 278 | Flags({this.svg, this.png}); 279 | 280 | factory Flags.fromJson(Map json) => 281 | Flags(svg: json['svg'], png: json['png']); 282 | 283 | @override 284 | String toString() { 285 | return 'Flags(\n' 286 | ' svg: $svg,\n' 287 | ' png: $png\n' 288 | ')'; 289 | } 290 | } 291 | 292 | class RegionalBloc { 293 | final String? acronym; 294 | final String? name; 295 | final List? otherAcronyms; 296 | final List? otherNames; 297 | 298 | RegionalBloc({this.acronym, this.name, this.otherAcronyms, this.otherNames}); 299 | 300 | factory RegionalBloc.fromJson(Map json) => RegionalBloc( 301 | acronym: json['acronym'], 302 | name: json['name'], 303 | otherAcronyms: 304 | (json['otherAcronyms'] as List?)?.cast(), 305 | otherNames: (json['otherNames'] as List?)?.cast(), 306 | ); 307 | 308 | @override 309 | String toString() { 310 | return 'RegionalBloc(\n' 311 | ' acronym: $acronym,\n' 312 | ' name: $name,\n' 313 | ' otherAcronyms: $otherAcronyms,\n' 314 | ' otherNames: $otherNames\n' 315 | ')'; 316 | } 317 | } 318 | 319 | class CapitalInfo { 320 | final List? latlng; 321 | 322 | CapitalInfo({this.latlng}); 323 | 324 | factory CapitalInfo.fromJson(Map json) { 325 | return CapitalInfo( 326 | latlng: (json['latlng'] as List?) 327 | ?.map((e) => (e as num).toDouble()) 328 | .toList(), 329 | ); 330 | } 331 | 332 | @override 333 | String toString() { 334 | return 'CapitalInfo(\n' 335 | ' latlng: $latlng\n' 336 | ')'; 337 | } 338 | } 339 | 340 | class Car { 341 | final List? signs; 342 | final String? side; 343 | 344 | Car({this.signs, this.side}); 345 | 346 | factory Car.fromJson(Map json) { 347 | return Car( 348 | signs: 349 | (json['signs'] as List?)?.map((e) => e as String).toList(), 350 | side: json['side'], 351 | ); 352 | } 353 | 354 | @override 355 | String toString() { 356 | return 'Car(\n' 357 | ' signs: $signs,\n' 358 | ' side: $side\n' 359 | ')'; 360 | } 361 | } 362 | 363 | class CoatOfArms { 364 | final String? png; 365 | final String? svg; 366 | 367 | CoatOfArms({this.png, this.svg}); 368 | 369 | factory CoatOfArms.fromJson(Map json) { 370 | return CoatOfArms(png: json['png'], svg: json['svg']); 371 | } 372 | @override 373 | String toString() { 374 | return 'CoatOfArms(\n' 375 | ' png: $png,\n' 376 | ' svg: $svg\n' 377 | ')'; 378 | } 379 | } 380 | 381 | class GenderedDemonym { 382 | final String? f; 383 | final String? m; 384 | 385 | GenderedDemonym({this.f, this.m}); 386 | 387 | factory GenderedDemonym.fromJson(Map json) { 388 | return GenderedDemonym(f: json['f'], m: json['m']); 389 | } 390 | @override 391 | String toString() { 392 | return 'GenderedDemonym(\n' 393 | ' f: $f,\n' 394 | ' m: $m\n' 395 | ')'; 396 | } 397 | } 398 | 399 | class Maps { 400 | final String? googleMaps; 401 | final String? openStreetMaps; 402 | 403 | Maps({this.googleMaps, this.openStreetMaps}); 404 | 405 | factory Maps.fromJson(Map json) { 406 | return Maps( 407 | googleMaps: json['googleMaps'], 408 | openStreetMaps: json['openStreetMaps'], 409 | ); 410 | } 411 | 412 | @override 413 | String toString() { 414 | return 'Maps(\n' 415 | ' googleMaps: $googleMaps,\n' 416 | ' openStreetMaps: $openStreetMaps\n' 417 | ')'; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /test/src/data/api_helper_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | import 'package:rest_countries_data/src/data/api_helper.dart'; 6 | 7 | class MockApiHelper extends Mock implements ApiHelper {} 8 | 9 | void main() { 10 | late MockApiHelper mockApiHelper; 11 | 12 | setUpAll(() { 13 | mockApiHelper = MockApiHelper(); 14 | }); 15 | group('API Helper', () { 16 | test( 17 | 'GIVEN api url is correct, WHEN callAPI is called, THEN return a List>', 18 | () async { 19 | final String url = 'https://restcountries.com/v3.1/all?fields=name'; 20 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 21 | Future>>.value( 22 | List>.from(jsonDecode(mockData)))); 23 | 24 | List> rest = 25 | await mockApiHelper.callAPI(apiUrl: url); 26 | 27 | expect(rest, isA>>()); 28 | }, 29 | ); 30 | 31 | test( 32 | 'GIVEN api url has an unknown country, WHEN callAPI is called, THEN throw a country not found exception', 33 | () async { 34 | final String url = 'https://restcountries.com/v3.1/capital/bongo'; 35 | when(() => mockApiHelper.callAPI(apiUrl: url)) 36 | .thenThrow(Exception('Country not found')); 37 | 38 | expect( 39 | () => mockApiHelper.callAPI(apiUrl: url), 40 | throwsA( 41 | predicate( 42 | (Object? e) => 43 | e is Exception && e.toString().contains('Country not found'), 44 | ), 45 | ), 46 | ); 47 | }, 48 | ); 49 | 50 | test( 51 | 'GIVEN api url, WHEN callAPI is called and server fails, THEN throw a server error exception', 52 | () async { 53 | final String url = 'https://restcountries.com/v3.1/capital/paris'; 54 | when(() => mockApiHelper.callAPI(apiUrl: url)) 55 | .thenThrow(Exception('Server error')); 56 | 57 | expect( 58 | () => mockApiHelper.callAPI(apiUrl: url), 59 | throwsA( 60 | predicate( 61 | (Object? e) => 62 | e is Exception && e.toString().contains('Server error'), 63 | ), 64 | ), 65 | ); 66 | }, 67 | ); 68 | 69 | test( 70 | 'GIVEN country name endpoint, WHEN callAPI is called with valid name, THEN return a List>', 71 | () async { 72 | final String url = 'https://restcountries.com/v3.1/name/nigeria'; 73 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 74 | Future>>.value( 75 | List>.from(jsonDecode(nigeriaData)))); 76 | 77 | List> result = 78 | await mockApiHelper.callAPI(apiUrl: url); 79 | 80 | expect(result, isA>>()); 81 | }, 82 | ); 83 | 84 | test( 85 | 'GIVEN country code endpoint, WHEN callAPI is called with valid code, THEN return a List>', 86 | () async { 87 | final String url = 'https://restcountries.com/v3.1/alpha/ng'; 88 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 89 | Future>>.value( 90 | List>.from(jsonDecode(nigeriaData)))); 91 | 92 | List> result = 93 | await mockApiHelper.callAPI(apiUrl: url); 94 | 95 | expect(result, isA>>()); 96 | }, 97 | ); 98 | 99 | test( 100 | 'GIVEN currency endpoint, WHEN callAPI is called with valid currency, THEN return a List>', 101 | () async { 102 | final String url = 'https://restcountries.com/v3.1/currency/usd'; 103 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 104 | Future>>.value( 105 | List>.from(jsonDecode(mockData)))); 106 | 107 | List> result = 108 | await mockApiHelper.callAPI(apiUrl: url); 109 | 110 | expect(result, isA>>()); 111 | }, 112 | ); 113 | 114 | test( 115 | 'GIVEN language endpoint, WHEN callAPI is called with valid language, THEN return a List>', 116 | () async { 117 | final String url = 'https://restcountries.com/v3.1/lang/french'; 118 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 119 | Future>>.value( 120 | List>.from(jsonDecode(mockData)))); 121 | 122 | List> result = 123 | await mockApiHelper.callAPI(apiUrl: url); 124 | 125 | expect(result, isA>>()); 126 | }, 127 | ); 128 | 129 | test( 130 | 'GIVEN capital endpoint, WHEN callAPI is called with valid capital, THEN return a List>', 131 | () async { 132 | final String url = 'https://restcountries.com/v3.1/capital/london'; 133 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 134 | Future>>.value( 135 | List>.from(jsonDecode(mockData)))); 136 | 137 | List> result = 138 | await mockApiHelper.callAPI(apiUrl: url); 139 | 140 | expect(result, isA>>()); 141 | }, 142 | ); 143 | 144 | test( 145 | 'GIVEN region endpoint, WHEN callAPI is called with valid region, THEN return a List>', 146 | () async { 147 | final String url = 'https://restcountries.com/v3.1/region/africa'; 148 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 149 | Future>>.value( 150 | List>.from(jsonDecode(mockData)))); 151 | 152 | List> result = 153 | await mockApiHelper.callAPI(apiUrl: url); 154 | 155 | expect(result, isA>>()); 156 | }, 157 | ); 158 | 159 | test( 160 | 'GIVEN subregion endpoint, WHEN callAPI is called with valid subregion, THEN return a List>', 161 | () async { 162 | final String url = 163 | 'https://restcountries.com/v3.1/subregion/western%20africa'; 164 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 165 | Future>>.value( 166 | List>.from(jsonDecode(mockData)))); 167 | 168 | List> result = 169 | await mockApiHelper.callAPI(apiUrl: url); 170 | 171 | expect(result, isA>>()); 172 | }, 173 | ); 174 | 175 | test( 176 | 'GIVEN translation endpoint, WHEN callAPI is called with valid translation, THEN return a List>', 177 | () async { 178 | final String url = 'https://restcountries.com/v3.1/translation/france'; 179 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 180 | Future>>.value( 181 | List>.from(jsonDecode(mockData)))); 182 | 183 | List> result = 184 | await mockApiHelper.callAPI(apiUrl: url); 185 | 186 | expect(result, isA>>()); 187 | }, 188 | ); 189 | 190 | test( 191 | 'GIVEN demonym endpoint, WHEN callAPI is called with valid demonym, THEN return a List>', 192 | () async { 193 | final String url = 'https://restcountries.com/v3.1/demonym/american'; 194 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 195 | Future>>.value( 196 | List>.from(jsonDecode(mockData)))); 197 | 198 | List> result = 199 | await mockApiHelper.callAPI(apiUrl: url); 200 | 201 | expect(result, isA>>()); 202 | }, 203 | ); 204 | 205 | test( 206 | 'GIVEN invalid country name, WHEN callAPI is called, THEN throw a country not found exception', 207 | () async { 208 | final String url = 'https://restcountries.com/v3.1/name/invalidcountry'; 209 | when(() => mockApiHelper.callAPI(apiUrl: url)) 210 | .thenThrow(Exception('Country not found')); 211 | 212 | expect( 213 | () => mockApiHelper.callAPI(apiUrl: url), 214 | throwsA( 215 | predicate( 216 | (Object? e) => 217 | e is Exception && e.toString().contains('Country not found'), 218 | ), 219 | ), 220 | ); 221 | }, 222 | ); 223 | 224 | test( 225 | 'GIVEN independent status endpoint, WHEN callAPI is called with independent status, THEN return a List>', 226 | () async { 227 | final String url = 228 | 'https://restcountries.com/v3.1/independent?status=true'; 229 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 230 | Future>>.value( 231 | List>.from(jsonDecode(mockData)))); 232 | 233 | List> result = 234 | await mockApiHelper.callAPI(apiUrl: url); 235 | 236 | expect(result, isA>>()); 237 | }, 238 | ); 239 | 240 | test( 241 | 'GIVEN calling code endpoint, WHEN callAPI is called with valid calling code, THEN return a List>', 242 | () async { 243 | final String url = 'https://restcountries.com/v3.1/callingcode/234'; 244 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 245 | Future>>.value( 246 | List>.from(jsonDecode(nigeriaData)))); 247 | 248 | List> result = 249 | await mockApiHelper.callAPI(apiUrl: url); 250 | 251 | expect(result, isA>>()); 252 | }, 253 | ); 254 | 255 | test( 256 | 'GIVEN full name endpoint, WHEN callAPI is called with full country name, THEN return a List>', 257 | () async { 258 | final String url = 259 | 'https://restcountries.com/v3.1/name/federal%20republic%20of%20nigeria?fullText=true'; 260 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 261 | Future>>.value( 262 | List>.from(jsonDecode(nigeriaData)))); 263 | 264 | List> result = 265 | await mockApiHelper.callAPI(apiUrl: url); 266 | 267 | expect(result, isA>>()); 268 | }, 269 | ); 270 | 271 | test( 272 | 'GIVEN multiple alpha codes endpoint, WHEN callAPI is called with multiple codes, THEN return a List>', 273 | () async { 274 | final String url = 275 | 'https://restcountries.com/v3.1/alpha?codes=ng,gh,bj'; 276 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 277 | Future>>.value( 278 | List>.from(jsonDecode(mockData)))); 279 | 280 | List> result = 281 | await mockApiHelper.callAPI(apiUrl: url); 282 | 283 | expect(result, isA>>()); 284 | }, 285 | ); 286 | 287 | test( 288 | 'GIVEN fields filter endpoint, WHEN callAPI is called with specific fields, THEN return a List>', 289 | () async { 290 | final String url = 291 | 'https://restcountries.com/v3.1/all?fields=name,capital,population'; 292 | when(() => mockApiHelper.callAPI(apiUrl: url)).thenAnswer((_) => 293 | Future>>.value( 294 | List>.from(jsonDecode(mockData)))); 295 | 296 | List> result = 297 | await mockApiHelper.callAPI(apiUrl: url); 298 | 299 | expect(result, isA>>()); 300 | }, 301 | ); 302 | 303 | test( 304 | 'GIVEN invalid currency code, WHEN callAPI is called, THEN throw an exception', 305 | () async { 306 | final String url = 307 | 'https://restcountries.com/v3.1/currency/invalidcurrency'; 308 | when(() => mockApiHelper.callAPI(apiUrl: url)) 309 | .thenThrow(Exception('Currency not found')); 310 | 311 | expect( 312 | () => mockApiHelper.callAPI(apiUrl: url), 313 | throwsA( 314 | predicate( 315 | (Object? e) => 316 | e is Exception && e.toString().contains('Currency not found'), 317 | ), 318 | ), 319 | ); 320 | }, 321 | ); 322 | 323 | test( 324 | 'GIVEN invalid alpha code, WHEN callAPI is called, THEN throw an exception', 325 | () async { 326 | final String url = 'https://restcountries.com/v3.1/alpha/xyz'; 327 | when(() => mockApiHelper.callAPI(apiUrl: url)) 328 | .thenThrow(Exception('Invalid country code')); 329 | 330 | expect( 331 | () => mockApiHelper.callAPI(apiUrl: url), 332 | throwsA( 333 | predicate( 334 | (Object? e) => 335 | e is Exception && 336 | e.toString().contains('Invalid country code'), 337 | ), 338 | ), 339 | ); 340 | }, 341 | ); 342 | }); 343 | } 344 | 345 | String nigeriaData = """[ 346 | { 347 | "name": { 348 | "common": "Nigeria", 349 | "official": "Federal Republic of Nigeria", 350 | "nativeName": { 351 | "eng": {"official": "Federal Republic of Nigeria", "common": "Nigeria"} 352 | } 353 | } 354 | } 355 | ]"""; 356 | 357 | String mockData = """[ 358 | { 359 | "name": { 360 | "common": "Togo", 361 | "official": "Togolese Republic", 362 | "nativeName": { 363 | "fra": {"official": "Togolese Republic", "common": "Togo"} 364 | } 365 | } 366 | } 367 | ]"""; 368 | --------------------------------------------------------------------------------