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
Last synced: 10 months ago · JSON representation ·

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
Created over 3 years ago · Last pushed about 1 year ago
Metadata Files
Readme Code of conduct Citation

README.md

Weather forecaster app with Ruby on Rails

Scope

  1. Use Ruby On Rails.

  2. Accept an address as input.

  3. Retrieve forecast data for the given address. This should include, at minimum, the current temperature. Bonus points: retrieve high/low and/or extended forecast.

  4. Display the requested forecast details to the user.

  5. 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?" %>
<%= text
fieldtag(: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 %>
<% end %> ```

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

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
Top Authors
Issue Authors
Pull Request Authors
Top Labels
Issue Labels
Pull Request Labels