Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Geographical Data Access with State and City Information #28

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ Here, you can choose the language you want. check out the the [`TranslationLangu
List<string> result = RestCountriesService.GetAllCountriesNames(TranslationLanguage.French).ToList();
```

- Get all states, identified by its s ISO2 code of the country


```csharp
State states = RestCountriesService.GetStatesByCountryCode("US").ToList();
```

- Get all cities within a specified state, identified by its state code and ISO2 code of the country

```csharp
City cities = RestCountriesService.GetCitiesInState("CA", "US").ToList();
```


## Country class

```csharp
Expand Down
47 changes: 47 additions & 0 deletions src/RESTCountries.NET/Models/State.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace RESTCountries.NET.Models
{
/// <summary>
/// Represents a state or region within a country, including its cities.
/// </summary>
public class State
{
/// <summary>
/// Gets or sets the ISO country code.
/// </summary>
[JsonPropertyName("countryCode")]
public string CountryCode { get; set; }

/// <summary>
/// Gets or sets the name of the state or region.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }

/// <summary>
/// Gets or sets the state or region code.
/// </summary>
[JsonPropertyName("code")]
public string Code { get; set; }

/// <summary>
/// Gets or sets the list of cities within the state or region.
/// </summary>
[JsonPropertyName("cities")]
public List<City> Cities { get; set; }
}

/// <summary>
/// Represents a city within a state or region.
/// </summary>
public class City
{
/// <summary>
/// Gets or sets the name of the city.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
4 changes: 4 additions & 0 deletions src/RESTCountries.NET/RESTCountries.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@

<ItemGroup>
<None Remove="Services\data.json" />
<None Remove="Services\states.json.zip"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Services\data.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Services\states.json.zip">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
Expand Down
25 changes: 25 additions & 0 deletions src/RESTCountries.NET/Services/RestCountriesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,30 @@ public static IEnumerable<string> GetAllCountriesNames(string translationLanguag
: null
).Where(c => c != null).OrderBy(c => c);
}

/// <summary>
/// Retrieves all states within a given country, identified by its ISO2 country code.
/// This method provides an easy way to access state information for a specific country,
/// leveraging the detailed geographical data managed by RestStateService.
/// </summary>
/// <param name="countryCode">The ISO2 country code of the country. Eg. US </param>
/// <returns>A collection of State objects within the specified country. Returns an empty collection if no states are found.</returns>
public static IEnumerable<State> GetStatesByCountryCode(string countryCode)
{
return RestStateService.GetStatesInCountry(countryCode);
}

/// <summary>
/// Retrieves all cities within a specified state, identified by its state code and ISO2 code of the country.
/// This method simplifies the process of finding cities within a state, using state-specific data
/// provided by RestStateService.
/// </summary>
/// <param name="stateCode">The state code. Eg. CA</param>
/// <param name="countryCode">The ISO2 code of the country. Eg. US </param>
/// <returns>A collection of City objects within the specified state. Returns an empty collection if no cities are found or the state does not exist.</returns>
public static IEnumerable<City> GetCitiesInState(string stateCode, string countryCode)
{
return RestStateService.GetCitiesInState(stateCode, countryCode);
}
}
}
124 changes: 124 additions & 0 deletions src/RESTCountries.NET/Services/RestStateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using RESTCountries.NET.Models;

namespace RESTCountries.NET.Services
{
/// <summary>
/// Serves as a static repository for geographical data, specifically states and cities, within applications. This class provides efficient, read-only access to structured geographical information, making it ideal for applications that require reliable and quick access to such data without the overhead of dynamic data loading or external database dependencies.
///
/// Functionality:
/// - Retrieves lists of states within a specified country, identified by ISO2 country codes.
/// - Retrieves lists of cities within a specified state, supporting both state-only and state-country queries.
///
/// Designed to be used as part of a larger library, RestStateService abstracts away the complexities of geographical data management, offering straightforward methods for accessing the data. This approach allows developers to incorporate geographical information into their applications seamlessly, with minimal setup or configuration.
///
/// Example Usage:
/// var statesInUS = RestStateService.GetStatesInCountry("US");
/// var citiesInCalifornia = RestStateService.GetCitiesInState("CA");
///
/// Note:
/// The geographical data is preloaded from an embedded JSON resource, ensuring fast access times and consistency across application instances. As such, no external data loading or initialization is required from the consumer's side.
/// </summary>
internal static class RestStateService
{
private static readonly Dictionary<string, IEnumerable<State>> statesByCountry;
private static readonly Dictionary<string, State> statesByCountryAndStateCode;

/// <summary>
/// Instantiates the RestStateService by loading state and city location data embedded within the assembly.
/// This constructor attempts to locate and deserialize the JSON data from an embedded resource, organizing it into
/// useful structures for quick access. It ensures the service is ready to use immediately after instantiation by
/// pre-loading all necessary data.
/// The data is organized into two primary dictionaries for efficient retrieval: one mapping country codes to their
/// corresponding states, and another mapping state codes to their detailed state information. This organization
/// facilitates rapid lookup operations for states and cities by their respective codes.
/// Exception Handling:
/// If the JSON data file cannot be found or an error occurs during deserialization, the constructor throws an
/// exception,
/// preventing the instantiation of the class with invalid or incomplete data. This approach ensures that any instance
/// of RestStateService is fully operational and contains all necessary location data upon creation.
/// Usage Note:
/// Ensure the JSON data file is correctly embedded within the assembly and accessible via the specified resource path.
/// Incorrect file paths or missing resources will result in exceptions, indicating initialization failures.
/// </summary>
static RestStateService()
{
IEnumerable<State> allStates;
try
{
var assembly = Assembly.GetExecutingAssembly();
string zipResourcePath = "RESTCountries.NET.Services.states.json.zip";

using var zipStream = assembly.GetManifestResourceStream(zipResourcePath);
if (zipStream == null) throw new Exception("Unable to load ZIP resource.");

using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
ZipArchiveEntry entry = archive.GetEntry("states.json");

if (entry == null) throw new Exception("JSON file not found inside ZIP.");

using var entryStream = entry.Open();
using var streamReader = new StreamReader(entryStream);

allStates = streamReader != null
? JsonSerializer.Deserialize<IEnumerable<State>>(streamReader.ReadToEnd())
: throw new Exception("Unable to load data source.");


// Organizes states by country code for efficient retrieval.
if (allStates != null)
{
statesByCountry = allStates.GroupBy(s => s.CountryCode)
.ToDictionary(g => g.Key, g => g.AsEnumerable());

// Organizes states by concatenating country code and state code for unique access.
statesByCountryAndStateCode = allStates
.ToDictionary(s => $"{s.CountryCode}:{s.Code}", s => s);
}
}
catch (Exception ex)
{
statesByCountry = new Dictionary<string, IEnumerable<State>>();
statesByCountryAndStateCode = new Dictionary<string, State>();
allStates = new List<State>();
throw new Exception($"An error occurred while initializing state location data: {ex.Message}");
}
}

/// <summary>
/// Retrieves all states within a given country, identified by its ISO2 country code.
/// </summary>
/// <param name="countryCode">The ISO2 country code of the country.</param>
/// <returns>A list of State objects within the specified country. Returns an empty list if no states are found.</returns>
public static IEnumerable<State> GetStatesInCountry(string countryCode)
{
return statesByCountry.TryGetValue(countryCode, out var states) ? states : new List<State>();
}

/// <summary>
/// Retrieves all cities within a given state, identified by its state code and country code.
/// </summary>
/// <param name="stateCode">The code of the state.</param>
/// <param name="countryCode">The ISO2 code of the country.</param>
/// <returns>
/// A list of City objects within the specified state. Returns an empty list if no cities are found or the state
/// does not exist.
/// </returns>
public static IEnumerable<City> GetCitiesInState(string stateCode, string countryCode)
{
var key = $"{countryCode}:{stateCode}";
if (statesByCountryAndStateCode.TryGetValue(key, out var state))
{
return state.Cities;
}

return new List<City>();
}
}
}
Binary file added src/RESTCountries.NET/Services/states.json.zip
Binary file not shown.
42 changes: 42 additions & 0 deletions tests/RESTCountries.NET.Tests/RestCountriesServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,47 @@ public void GetAllCountriesNames_Should_Return_Right_Result()
result = RestCountriesService.GetAllCountriesNames("unknow language").ToList();
result.ShouldBeEmpty();
}

[Fact]
public void GetStatesByCountryCode_Should_Return_States_For_Valid_CountryCode()
{
// Given "US" is a valid country code with known states
var result = RestCountriesService.GetStatesByCountryCode("US").ToList();

result.ShouldNotBeEmpty();
result.Count.ShouldBeGreaterThan(1); // Assuming there are multiple states for "US"
result.ShouldContain(s => s.Name == "California"); // Assuming California is a state in "US"
}

[Fact]
public void GetStatesByCountryCode_Should_Return_Empty_For_Invalid_CountryCode()
{
// Given "XX" is an invalid country code
var result = RestCountriesService.GetStatesByCountryCode("XX").ToList();

result.ShouldBeEmpty();
}

[Fact]
public void GetCitiesInState_Should_Return_Cities_For_Valid_StateCode()
{
// Given "CA" is a valid state code with known cities and "US" is a valid ISO2 country code in test data
var result = RestCountriesService.GetCitiesInState("CA", "US").ToList();

result.ShouldNotBeEmpty();
result.Count.ShouldBeGreaterThan(1); // Assuming there are multiple cities in "CA"
result.ShouldContain(c => c.Name == "Los Angeles"); // Assuming Los Angeles is a city in "CA"
}

[Fact]
public void GetCitiesInState_Should_Return_Empty_For_Invalid_StateCode()
{
// Given "XX" is an invalid state code and "YY" is an invalid ISO2 country code
var result = RestCountriesService.GetCitiesInState("XX", "YY").ToList();

result.ShouldBeEmpty();
}


}
}