rails application that retrieves weather information based on an address using SOLID principles and enterprise design patterns.
addresses/location are normalized using OpenStreetMaps and include a zip code.
weather information is retrieved from OpenWeather API
# pre-requisites: ruby 3.4.5
bundle install
OPEN_WEATHER_API_KEY= RAILS_MASTER_KEY= ./bin/rails server
# http://127.0.0.1:3000/
# docker alternatives
# pull latest image
docker pull ghcr.io/dyoun/weather_app:latest
# or build image locally
docker build -t weather_app .
# start the container
docker run -e OPEN_WEATHER_API_KEY= -e RAILS_MASTER_KEY= -p 3000:3000 "ghcr.io/dyoun/weather_app"
# http://127.0.0.1:3000//weather/api?address=space%20needle
CI is run using GitHub Actions and includes:
- linting with RuboCop
- security scanning with Brakeman
- testing with RSpec
- docker image build and push to GitHub Container Registry
docker image is stored in Github container registry where it can be easily pulled and deployed and composed for use in a production environment.
address and weather data is cached locally by request. a more optimal solution would be to pre-populate a shared cache such as redis by all zip codes to provide a fast response to all requests.
to optimize globally, CDNs and serving app instances in geographic locations will provide a better user experience by sending users to closest data centers in their locale, eg, europe, asia, etc.
open street maps is used to normalize address/locations, however, all locations do not return a zip code, eg, golden gate park.
to allow better location weather support, latitude/longitude could be used as a cache key for weather data.
weather data is cached for 30 minutes by zipcode
-
single responsibility principle: each class a single responsibility
WeatherService- weather APIGeocodingService- address normalizationWeatherRepository- coordinates data retrievalWeatherController- handles HTTP requests
-
open/closed principle: open for extension, closed for modification
- open for extension by implementing interfaces, private methods to prevent modifications
WeatherServiceInterfaceandGeocodingServiceInterfacedefine interfaces
- interface design allows for easy service swapping
- open for extension by implementing interfaces, private methods to prevent modifications
-
liskov substitution principle: implementations are substitutable by their interfaces
- weather and address services can be substituted in
WeatherRepository - any weather/address service implementing
WeatherServiceInterfaceorGeocodingServiceInterfacecan be used
- weather and address services can be substituted in
-
interface segregation principle: focused, specific interfaces
- separate interfaces for weather/address services
- no dependencies on unused methods
-
dependency inversion principle: high level modules don't depend on low level modules
- repository class
WeatherRepositoryabstracts data access - services are injected as dependencies
- repository class
- repository pattern
WeatherRepositoryisolates data access and separate concerns by keeping application logic separate
- command pattern
GetWeatherByAddressCommanddecouples sender of request from receiver providing more flexibility
- strategy pattern
WeatherRepositoryseparate concerns as services and make them interchangeable allowing flexibility
- factory pattern
GetWeatherByAddressCommandclient doesn't create objects directly
classDiagram
class WeatherController {
+index()
+show()
-render_weather_by_address()
-get_weather_by_address()
-render_missing_parameters()
-render_success_response()
}
class GetWeatherByAddressCommand {
-address: string
+execute() WeatherData
-repository() WeatherRepository
-weather_service() WeatherService
-address_service() AddressService
}
class WeatherRepository {
-weather_service: WeatherServiceInterface
-address_service: AddressServiceInterface
+initialize(weather_service, address_service)
+find_weather_by_address(address) WeatherData
}
class WeatherServiceInterface {
<<interface>>
+get_weather_by_zip(zip) WeatherData
}
class AddressServiceInterface {
<<interface>>
+address_lookup(address) Addresses
}
class OpenWeatherMapService {
-api_key: string
-http_client: Faraday
+get_weather_by_zip(zip) WeatherData
-validate(zip)
-fetch_weather_data(zip)
-parse_weather_response(response, zip)
}
class OpenStreetMapService {
-http_client: Faraday
+address_lookup(address) Addresses
-validate_address(address)
-fetch_address(address)
-parse_response(response, address)
}
class WeatherData {
+to_h() Hash
}
class Addresses {
+to_h() Hash
}
%% Relationships
WeatherController --> GetWeatherByAddressCommand : uses
GetWeatherByAddressCommand --> WeatherRepository : creates/uses
WeatherRepository --> WeatherServiceInterface : depends on
WeatherRepository --> AddressServiceInterface : depends on
OpenWeatherMapService --|> WeatherServiceInterface : implements
OpenStreetMapService --|> AddressServiceInterface : implements
WeatherRepository --> WeatherData : returns
WeatherRepository --> Addresses : uses
OpenWeatherMapService --> WeatherData : creates
OpenStreetMapService --> Addresses : creates
GetWeatherByAddressCommand --> OpenWeatherMapService : instantiates
GetWeatherByAddressCommand --> OpenStreetMapService : instantiates
%% Design Patterns
note for GetWeatherByAddressCommand "Command Pattern"
note for WeatherRepository "Repository Pattern"
note for WeatherServiceInterface "Strategy Pattern"
note for AddressServiceInterface "Strategy Pattern"