weather-forecaster-with-rails
Weather forecaster demonstration app implemented with Ruby on Rails
https://github.com/joelparkerhenderson/weather-forecaster-with-rails
Science Score: 44.0%
This score indicates how likely this project is to be science-related based on various indicators:
-
✓CITATION.cff file
Found CITATION.cff file -
✓codemeta.json file
Found codemeta.json file -
✓.zenodo.json file
Found .zenodo.json file -
○DOI references
-
○Academic publication links
-
○Academic email domains
-
○Institutional organization owner
-
○JOSS paper metadata
-
○Scientific vocabulary similarity
Low similarity (12.0%) to scientific vocabulary
Repository
Weather forecaster demonstration app implemented with Ruby on Rails
Basic Info
- Host: GitHub
- Owner: joelparkerhenderson
- Language: Ruby
- Default Branch: main
- Size: 55.7 KB
Statistics
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 0
- Releases: 0
Metadata Files
README.md
Weather forecaster app with Ruby on Rails
Scope
Use Ruby On Rails.
Accept an address as input.
Retrieve forecast data for the given address. This should include, at minimum, the current temperature. Bonus points: retrieve high/low and/or extended forecast.
Display the requested forecast details to the user.
Cache the forecast details for 30 minutes for all subsequent requests by zip codes. Display indicator in result is pulled from cache.
Set up Rails
This app is developed on a MacBook Pro M1 with macOS Ventura.
Install asdf
I like to use the asdf version manager to install software such as programming languages, because asdf makes it easier for me to manage multiple versions, environment paths, and dependencies.
Install asdf version manager via brew:
sh
% brew install asdf
% echo -e "\n. $(brew --prefix asdf)/asdf.sh" >> ~/.zshrc
% echo -e "\n. $(brew --prefix asdf)/etc/bash_completion.d/asdf.bash" >> ~/.zshrc
% source ~/.zshrc
Install Ruby
I like to install ruby using the latest version, and via brew and asdf.
To do this on the MacBook Pro M1 with macOS Ventura, the installer requires the capstone package library files and include files.
Set up capstone:
sh
% brew install capstone
% export LDFLAGS="-L"$(brew --prefix capstone)"/lib"
% export CPPFLAGS="-I"$(brew --prefix capstone)"/include"
Add the asdf plugin:
sh
% asdf plugin add ruby
% asdf plugin-update ruby
Install Ruby and use it:
sh
% asdf install ruby latest
% asdf global ruby latest
Install Rails
Install Ruby on Rails:
sh
% gem install rails
Install Google Chrome
Install Google Chrome for Ruby on Rails system tests:
sh
% brew install google-chrome
Set up the app
Create a new app
Create a new Ruby on Rails app and test it:
sh
% rails new forecaster --skip-activerecord
% cd forecaster
% bin/rails test
% bin/rails test:system
% bin/rails server -d
% curl http://127.0.0.1:3000
% lsof -ti:3000 | xargs kill -9
Add flash
I like to use Rails flash messages to show the user notices, alerts, and the like. I use some simple CSS to make the styling easy.
Add flash messages that are rendered via a view partial:
sh
% mkdir app/views/shared
Create app/views/shared/_flash.html.erb:
ruby
<% flash.each do |type, message| %>
<div class="flash flash-<%= type %>">
<%= message %>
</div>
<% end %>
Accept an address as input
We want a controller can accept an address as an input parameter.
A simple way to test this is by saving the address in the session.
Add faker gem
To create test data, we can use the faker gem, which can create fake addresses.
Edit Gemfile and its test section to add the faker gem:
ruby
gem "faker"
Run:
sh
bundle
Generate forecasts controller
Generate a forecasts controller and its tests:
sh
% bin/rails generate controller forecasts show
Write a test in test/controllers/forecasts_controller_test.rb:
```ruby require "test_helper"
class ForecastControllerTest < ActionDispatch::IntegrationTest
test "show with an input address" do address = Faker::Address.fulladdress get forecastsshowurl, params: { address: address } assertresponse :success assert_equal address, session[:address] end
end ```
Generate a system test that will launch the web page, and provide the correct placeholder for certain future work:
% bin/rails generate system_test forecasts
Write a test in test/system/forecasts_test.rb:
```ruby require "applicationsystemtest_case"
class ForecastsTest < ApplicationSystemTestCase
test "show" do address = Faker::Address.fulladdress visit urlfor \ controller: "forecasts", action: "show", params: { address: address } assert_selector "h1", text: "Forecasts#show" end
end ```
TDD should fail:
sh
% bin/rails test:all
Implement in app/controllers/forecasts_controller.rb:
```ruby class ForecastsController < ApplicationController
def show session[:address] = params[:address] end
end ```
TDD should succeed:
sh
% bin/rails test:all
Set the root path route
Edit config/routes.rb:
```ruby
Defines the root path route ("/")
root "forecasts#show" ```
Get forecast data for the given address
There are many ways we could get forecast data.
We choose to convert the address to a latitude and longitude, by using the geocoder gem and the ESRI ArcGIS API available here
We choose to send the latitude and longitude to the OpenWeatherMap API available here
We choose to implement each API as an application service, by creating a plain old Ruby object (PORO) in the directory
app/services
Run:
sh
% mkdir -p {app,test}/services
% touch {app,test}/services/.keep
Set ArcGIS API credentials
Edit Rails credentials:
sh
EDITOR="code --wait" bin/rails credentials:edit
Add your ArcGIS credentials by replacing these fake credentials with your real credentials:
ruby
arcgis_api_user_id: alice
arcgis_api_secret_key: 6d9ecd1c-2b00-4a0e-89d7-8f250418a9c4
Add Geocoder gem
Ruby has an excellent way to access the ArcGIS API, by using the Geocoder gem, and configuring it for the ArcGIS API.
Edit Gemfile to add:
```ruby
Look up a map address and convert it to latitude, longitude, etc.
gem "geocoder" ```
Run:
sh
bundle
Configure Geocoder
Create config/initializers/geocoder.rb:
ruby
Geocoder.configure(
esri: {
api_key: [
Rails.application.credentials.arcgis_api_user_id,
Rails.application.credentials.arcgis_api_secret_key,
],
for_storage: true
}
)
Create GeocodeService
We want to create a geocode service that converts from an address string into a latitude, longitude, country code, and postal code.
Create test/services/geocode_service_test:
```ruby require 'test_helper'
class GeocodeServiceTest < ActiveSupport::TestCase
test "call with known address" do address = "1 Infinite Loop, Cupertino, California" geocode = GeocodeService.call(address) assertindelta 37.33, geocode.latitude, 0.1 assertindelta -122.03, geocode.longitude, 0.1 assertequal "us", geocode.countrycode assertequal "95014", geocode.postalcode end
end ```
Create app/services/geocode_service:
```ruby class GeocodeService
def self.call(address) response = Geocoder.search(address) response or raise IOError.new "Geocoder error" response.length > 0 or raise IOError.new "Geocoder is empty: #{response}" data = response.first.data data or raise IOError.new "Geocoder data error" data["lat"] or raise IOError.new "Geocoder latitude is missing" data["lon"] or raise IOError.new "Geocoder longitude is missing" data["address"] or raise IOError.new "Geocoder address is missing" data["address"]["countrycode"] or raise IOError.new "Geocoder country code is missing" data["address"]["postcode"] or raise IOError.new "Geocoder postal code is missing" geocode = OpenStruct.new geocode.latitude = data["lat"].tof geocode.longitude = data["lon"].tof geocode.countrycode = data["address"]["countrycode"] geocode.postalcode = data["address"]["postcode"] geocode end
end ```
Join OpenWeather API
Sign up at https://openweathermap.org
- The process creates your API key.
Example:
- OpenWeather API key: 70a6c8131f03fe7a745b6b713ed9ebfd
Set OpenWeather API credentials
Edit Rails credentials:
sh
EDITOR="code --wait" bin/rails credentials:edit
Add your OpenWeather credentials by replacing these fake credentials with your real credentials:
ruby
openweather_api_key: 70a6c8131f03fe7a745b6b713ed9ebfd
Add Faraday gems
Ruby has many excellent ways to do HTTP API requests. I prefer the Faraday gem because it tends to provide the most power and the most capabilities, such as for asynchronous programming.
Edit Gemfile and add:
```ruby
Simple flexible HTTP client library, with support for multiple backends.
gem "faraday" gem "faraday_middleware" ```
Run:
sh
bundle
Create WeatherService
Create test/services/weather_service_test.rb:
```ruby require 'test_helper'
class WeatherServiceTest < ActiveSupport::TestCase
test "call with known parameters" do # Example address is 1 Infinite Loop, Cupertino, California latitude = 37.331669 longitude = -122.030098 weather = WeatherService.call(latitude, longitude) assertincludes -4..44, weather.temperature assertincludes -4..44, weather.temperaturemin assertincludes -4..44, weather.temperaturemax assertincludes 0..100, weather.humidity assertincludes 900..1100, weather.pressure refuteempty weather.description end
end ```
Create app/services/weather_service.rb:
```ruby class WeatherService
def self.call(latitude, longitude)
conn = Faraday.new("https://api.openweathermap.org") do |f|
f.request :json # encode req bodies as JSON and automatically set the Content-Type header
f.request :retry # retry transient failures
f.response :json # decode response bodies as JSON
end
response = conn.get('/data/2.5/weather', {
appid: Rails.application.credentials.openweatherapikey,
lat: latitude,
lon: longitude,
units: "metric",
})
body = response.body
body or raise IOError.new "OpenWeather response body failed"
body["main"] or raise IOError.new "OpenWeather main section is missing"
body["main"]["temp"] or raise IOError.new "OpenWeather temperature is missing"
body["main"]["tempmin"] or raise IOError.new "OpenWeather temperature minimum is missing"
body["main"]["temp_max"] or raise IOError.new "OpenWeather temperature maximum is missing"
body["weather"] or raise IOError.new "OpenWeather weather section is missing"
body["weather"].length > 0 or raise IOError.new "OpenWeather weather section is empty"
body["weather"][0]["description"] or raise IOError.new "OpenWeather weather description is missing"
weather = OpenStruct.new
weather.temperature = body["main"]["temp"]
weather.temperaturemin = body["main"]["tempmin"]
weather.temperaturemax = body["main"]["temp_max"]
weather.humidity = body["main"]["humidity"]
weather.pressure = body["main"]["pressure"]
weather.description = body["weather"][0]["description"]
weather
end
end ```
Complete the app
In the interest of time, I'll complete the app by doing the forecasts controller and view. Use of TDD and/or step-by-step additions are shown above, so are elided below.
Complete the forecasts controller
Complete app/controllers/forecasts_controller:
```ruby class ForecastsController < ApplicationController
def show
@addressdefault = "1 Infinite Loop, Cupertino, California"
session[:address] = params[:address]
if params[:address]
begin
@address = params[:address]
@geocode = GeocodeService.call(@address)
@weathercachekey = "#{@geocode.countrycode}/#{@geocode.postalcode}"
@weathercacheexist = Rails.cache.exist?(@weathercachekey)
@weather = Rails.cache.fetch(@weathercachekey, expiresin: 30.minutes) do
WeatherService.call(@geocode.latitude, @geocode.longitude)
end
rescue => e
flash.alert = e.message
end
end
end
end ```
Complete the forecasts view
Complete app/views/forecasts/show.html.erb:
```erb <%= render "shared/flash" %>
Forecast
<%= formwith(method: 'get', local: true) do %>
<%= label :address, "What is your address?" %>
<%= textfieldtag(:address, @address || @addressdefault, size: 70) %>
<%= submit_tag("Lookup") %>
<% end %>
<% if defined?(@weather) %>
- Temperature: <%= @weather.temperature %> ℃
- Temperature Minimum: <%= @weather.temperaturemin %> ℃
- Temperature Maximum: <%= @weather.temperaturemax %> ℃
- Humidity: <%= @weather.humidity %>%
- Pressure: <%= @weather.pressure %> millibars
- Description: <%= @weather.description %>
- Is this result from the cache? <%= @weathercacheexist %>
Update test/system/forecasts_test.rb:
ruby
assert_selector "h1", text: "Forecast"
Enable the cache
Enable the Rails development cache, so a developer can see that the forecasts are cached as expected.
sh
bin/rails dev:cache
Conclusion
The app now works successfully:
sh
% bin/rails test
% bin/rails test:system
% bin/rails server -d
Browse to http://127.0.0.1:3000
I hope you've enjoyed seeing this app. It's a quick demonstration of Ruby on Rails, Test Driven Development, API access using a specialized gem (i.e. geocoder) and a generalized gem (i.e. faraday).
Future ideas
Add functional tests, such as for flash messages, error messages, the search form, the API reachability, etc.
Add internationalization/localization for messages and temperature units.
Add deployment capabilities, such as to Heroku, or AWS, or GCP.
Add continuous integration and continuous delivery, such as via GitHub actions.
Add styles, such as for fonts, colors, a page header, a page footer, etc.
Add circuit breakers that can detect when the app load is too high to be viable for the API calls.
Add API capabilities, such as returning JSON, or offering gRPC, etc.
Appendix
Unicode
Unicode characters that can help the app display the temperature units:
```txt U+2103 ℃ DEGREE CELSIUS
U+2109 ℉ DEGREE FAHRENHEIT ```
OpenAI ChatGPT comparison
I'm currently experimenting with AI/ML for programming, such as with GitHub Copilot and OpenAI ChatGPT.
The OpenAI ChatGPT is notable, in my opinion, because it gets in the right ballpark of what Ruby on Rails code could look like, but it doesn't actually run, and doesn't create services, or API credentials, etc.
I believe AI/ML is a promising area to explore, so I'm including this appendix section that shows the OpenAI ChatGPT reply to the original requirements documentation.
ChatGPT says:
In this implementation, the ForecastController's show action accepts an address as input and retrieves the forecast data for that address using the Forecast.for_address class method.
This method uses Rails' built-in caching mechanism to store the forecast data in memory for 30 minutes, so that subsequent requests for the same address will not have to hit the weather API again and can instead return the cached data.
The fromcache attribute of the Forecast object is set to true if the data was retrieved from the cache. The controller then renders the forecast data as a JSON object, including the current temperature, high and low temperatures, extended forecast, and a fromcache flag indicating whether the data was retrieved from the cache or not.
```ruby
app/controllers/forecast_controller.rb
class ForecastController < ApplicationController def show # Accept an address as input address = params[:address]
# Retrieve forecast data for the given address
forecast = Forecast.for_address(address)
# Display the requested forecast details to the user
render json: {
current_temperature: forecast.current_temperature,
high: forecast.high,
low: forecast.low,
extended_forecast: forecast.extended_forecast,
from_cache: forecast.from_cache
}
end end ```
```ruby
app/models/forecast.rb
class Forecast CACHE_EXPIRATION = 30.minutes
attrreader :currenttemperature, :high, :low, :extendedforecast, :fromcache
def initialize(attributes) @currenttemperature = attributes[:currenttemperature] @high = attributes[:high] @low = attributes[:low] @extendedforecast = attributes[:extendedforecast] @fromcache = attributes[:fromcache] end
def self.foraddress(address) Rails.cache.fetch("forecast-#{address}", expiresin: CACHEEXPIRATION) do # Retrieve the forecast data from a weather API or other source forecastdata = retrieveforecastdata(address)
# Create a Forecast object with the retrieved data
new(
current_temperature: forecast_data[:current_temperature],
high: forecast_data[:high],
low: forecast_data[:low],
extended_forecast: forecast_data[:extended_forecast],
from_cache: false
)
end
end
private
def self.retrieveforecastdata(address) # Code to retrieve forecast data from a weather API or other source end end ```
Owner
- Name: Joel Parker Henderson
- Login: joelparkerhenderson
- Kind: user
- Location: California
- Website: http://www.joelparkerhenderson.com
- Repositories: 319
- Profile: https://github.com/joelparkerhenderson
Software developer. Technology consultant. Creator of GitAlias.com, NumCommand.com, SixArm.com, and many open source projects.
Citation (CITATION.cff)
cff-version: 1.2.0
title: Weather forecaster app with Ruby on Rails
message: >-
If you use this work and you want to cite it,
then you can use the metadata from this file.
type: software
authors:
- given-names: Joel Parker
family-names: Henderson
email: joel@joelparkerhenderson.com
affiliation: joelparkerhenderson.com
orcid: 'https://orcid.org/0009-0000-4681-282X'
identifiers:
- type: url
value: 'https://github.com/joelparkerhenderson/weather-forecaster-with-rails/'
description: Weather forecaster app with Ruby on Rails
repository-code: 'https://github.com/joelparkerhenderson/weather-forecaster-with-rails/'
abstract: >-
Weather forecaster app with Ruby on Rails
license: See license file
GitHub Events
Total
- Push event: 2
Last Year
- Push event: 2
Issues and Pull Requests
Last synced: about 1 year ago
All Time
- Total issues: 0
- Total pull requests: 0
- Average time to close issues: N/A
- Average time to close pull requests: N/A
- Total issue authors: 0
- Total pull request authors: 0
- Average comments per issue: 0
- Average comments per pull request: 0
- Merged pull requests: 0
- Bot issues: 0
- Bot pull requests: 0
Past Year
- Issues: 0
- Pull requests: 0
- Average time to close issues: N/A
- Average time to close pull requests: N/A
- Issue authors: 0
- Pull request authors: 0
- Average comments per issue: 0
- Average comments per pull request: 0
- Merged pull requests: 0
- Bot issues: 0
- Bot pull requests: 0