diff --git a/.github/.git_commit_msg.txt b/.github/.git_commit_msg.txt new file mode 100644 index 0000000..4a6d579 --- /dev/null +++ b/.github/.git_commit_msg.txt @@ -0,0 +1,33 @@ +# : (If applied, this commit will...) (Max 50 char) +# |<---- Using a Maximum Of 50 Characters ---->| Hard limit to 72 -->| + + +# Explain why this change is being made +# |<---- Try To Limit Each Line to a Maximum Of 72 Characters ---->| + +# Provide links to any relevant issues, articles, commits, or other +# pull requests +# Example: See #23, fixes #58 + +# --- COMMIT END --- +# can be +# feat (new feature) +# fix (bug fix) +# refactor (refactoring production code) +# style (formatting, missing semi colons, etc; no code change) +# test (adding or refactoring tests; no production code change) +# chore (updating npm scripts etc; no production code change) +# -------------------- +# Remember to +# Capitalize the subject line +# Use the imperative mood in the subject line +# Do not end the subject line with a period +# Separate subject from body with a blank line (comments don't count) +# Use the body to explain what and why vs. how +# Can use multiple lines with "-" for bullet points in body +# +# If you can't summarize your changes in a single line, they should +# probably be split into multiple commits +# -------------------- +# For more information about this template, check out +# https://gist.github.com/adeekshith/cd4c95a064977cdc6c50 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 330b7d1..98b6810 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.gem -.vscode/ \ No newline at end of file +.vscode/ +.idea \ No newline at end of file diff --git a/README.md b/README.md index d1fe9c4..a6121d4 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ License - RubyDoc +## Why would I use this library ? + +Before writing this OpenWeatherMap implementation, I checked for existing ones on rubygems.org. There's only small libraries, that has at least one or two good thing but that's all. Consequently, I decided to make my own one, combining all their advantages : + +- **Centralized :** all the options and fetch methods are stored in one class, that is initialized only once in all the program. Parameters are the same across all requests. +- **Fast :** the only thing that can slow the library is your Internet connection : indeed, no heavy operations are made in the background. As soon as it receives weather conditions, the only step for it is organizing them. +- **Simple :** the library only contains essential operations to keep the number of methods low. Moreover, all the information is perfectly human-readable. +- **Documented :** every method and class attribute is explained and every exception thrown is explicit, therefore learning or debugging the library remains easy. + +This work resulted in a powerful implementation that responds to primary needs while staying abordable. + - [📌 Requirements](#-requirements) - [🔧 Setup](#-setup) - [Quick installation](#quick-installation) @@ -90,7 +101,7 @@ The constructor takes three parameters : - The first is an API key, that can be generated on the [OpenWeatherMap website](https://openweathermap.org/appid) - The second is the language of the data. It can be one of these : Arabic - ar, Bulgarian - bg, Catalan - ca, Czech - cz, German - de, Greek - el, English - en, Persian (Farsi) - fa, Finnish - fi, French - fr, Galician - gl, Croatian - hr, Hungarian - hu, Italian - it, Japanese - ja, Korean - kr, Latvian - la, Lithuanian - lt, Macedonian - mk, Dutch - nl, Polish - pl, Portuguese - pt, Romanian - ro, Russian - ru, Swedish - se, Slovak - sk, Slovenian - sl, Spanish - es, Turkish - tr, Ukrainian - ua, Vietnamese - vi, Chinese Simplified - zh_cn, Chinese Traditional - zh_tw. - The third is the unit system. It can be one of these : - - none (temperatures in Kelvin) + - default (temperatures in Kelvin) - metric (temperatures in Celsius) - imperial (temperatures in Fahrenheit) @@ -122,7 +133,7 @@ Its parameter is the same as the `current` method. It will return a `OpenWeather ### Possible exceptions -Your requests may return exceptions that are in the `OpenWeatherMap::Exceptions` module : +Your requests may return exceptions that are in the `OpenWeatherMap::Exceptions` module. All are based on the `OpenWeatherMap::Exception` class. - An `Unauthorized` exception, caused when your API key is wrong - An `UnknownLocation` exception, caused if the location you wrote is wrong diff --git a/lib/openweathermap.rb b/lib/openweathermap.rb index 5dc6a0f..4001ef8 100644 --- a/lib/openweathermap.rb +++ b/lib/openweathermap.rb @@ -1,9 +1,99 @@ +# frozen_string_literal: true + require 'net/http' require 'json' -require 'openweathermap/data/constants' -require 'openweathermap/data/exceptions' require 'openweathermap/classes' require 'openweathermap/current-weather' require 'openweathermap/forecast' require 'openweathermap/api' + +module OpenWeatherMap + + ## + # All the constants needed for the library + + module Constants + + ## + # URL of the OpenWeatherMap API + + API_URL = 'https://api.openweathermap.org' + + ## + # Accepted types of unit + + UNITS = %w(default metric imperial) + + ## + # Accepted locales + + LANGS = %w(ar bg ca cz de el fa fi fr gl hr hu it ja kr la lt mk nl pl pt ro ru se sk sl es tr ua vi zh_cn zh_tw en) + + ## + # The different URLs + + URLS = { + current: '/data/2.5/weather', + forecast: '/data/2.5/forecast' + } + + ## + # All condition codes associated with emojis + + CONDITION_CODE = { + '01d' => '☀', + '02d' => '⛅', + '03d' => '☁', + '04d' => '☁☁', + '09d' => '🌧', + '10d' => '🌦', + '11d' => '🌩', + '13d' => '🌨', + '50d' => '🌫', + } + end + + ## + # Base exception for the OpenWeatherMap library + + class Exception < StandardError + end + + ## + # Exceptions that can be thrown by the library + + module Exceptions + + ## + # Exception to handle unknown lang + + class UnknownLang < OpenWeatherMap::Exception + end + + ## + # Exception to handle unknown units + + class UnknownUnits < OpenWeatherMap::Exception + end + + ## + # Exception to handle unknown location + + class UnknownLocation < OpenWeatherMap::Exception + end + + ## + # Exception to tell that the API key isn't authorized + + class Unauthorized < OpenWeatherMap::Exception + end + + ## + # Exception to handle data error + + class DataError < OpenWeatherMap::Exception + end + + end +end diff --git a/lib/openweathermap/api.rb b/lib/openweathermap/api.rb index 723011b..2b5b082 100644 --- a/lib/openweathermap/api.rb +++ b/lib/openweathermap/api.rb @@ -1,12 +1,23 @@ +# frozen_string_literal: true + module OpenWeatherMap + ## # The main API class. + class API - # @return [String] Default lang to use - attr_accessor :lang - - # @return [String] Default units to use - attr_accessor :units + ## + # The default lang to use across the program + # @return [String] + + attr_reader :lang + ## + # The default unit system to use across the program + # @return [String] + + attr_reader :units + + ## # Initialize the API object # # @param api_key [String] your OpenWeatherMap's API key @@ -20,16 +31,18 @@ class API # - imperial (temperatures in Fahrenheit) # @raise [OpenWeatherMap::Exceptions::UnknownLang] if the selected lang is not unknown # @raise [OpenWeatherMap::Exceptions::UnknownUnits] if the selected units is not unknown - def initialize(api_key, lang = 'en', units = nil) + + def initialize(api_key, lang = 'en', units = 'default') @api_key = api_key - raise OpenWeatherMap::Exceptions::UnknownLang, "[owm-ruby] error : unknown lang #{lang}" unless OpenWeatherMap::Constants::LANGS.include? lang + raise OpenWeatherMap::Exceptions::UnknownLang, "unknown lang #{lang}" unless OpenWeatherMap::Constants::LANGS.include? lang @lang = lang - raise OpenWeatherMap::Exceptions::UnknownUnits, "[owm-ruby] error : unknown units #{units}" unless OpenWeatherMap::Constants::UNITS.include? units + raise OpenWeatherMap::Exceptions::UnknownUnits, "unknown units #{units}" unless OpenWeatherMap::Constants::UNITS.include? units @units = units end + ## # Get current weather at a specific location. # # @param location [String, Integer, Array] the location @@ -38,11 +51,13 @@ def initialize(api_key, lang = 'en', units = nil) # - Integer : search by city ID (refer to http://bulk.openweathermap.org/sample/city.list.json.gz) # - Array : search by coordinates (format : [lon, lat]) # @return [OpenWeatherMap::CurrentWeather] requested data + def current(location) data = make_request(OpenWeatherMap::Constants::URLS[:current], location) OpenWeatherMap::CurrentWeather.new(data) end + ## # Get weather forecast for a specific location. # # @param location [String, Integer, Array] the location @@ -51,6 +66,7 @@ def current(location) # - Integer : search by city ID (refer to bulk.openweathermap.org/sample/city.list.json.gz) # - Array : search by coordinates (format : [lon, lat]) # @return [OpenWeatherMap::Forecast] requested data + def forecast(location) data = make_request(OpenWeatherMap::Constants::URLS[:forecast], location) OpenWeatherMap::Forecast.new(data) @@ -58,11 +74,13 @@ def forecast(location) private + ## # Make a request to the OpenWeatherMap API. # # @param url [String] The endpoint to reach # @param options [Hash] mixed options # @return [String] request's body + def make_request(url, location) options = {} options[:q] = location if location.is_a? String @@ -79,16 +97,12 @@ def make_request(url, location) } params.merge! options - url = "#{OpenWeatherMap::Constants::API_URL}/#{url}?" - - params.each do |key, value| - url += "#{key}=#{value}&" - end + url = "#{OpenWeatherMap::Constants::API_URL}/#{url}?#{URI.encode_www_form(params)}" response = Net::HTTP.get_response(URI(url)) case response.code.to_i - when 401 then raise OpenWeatherMap::Exceptions::Unauthorized, "[openweathermap] error : unauthorized key. API message : #{response.message}" - when 404 then raise OpenWeatherMap::Exceptions::UnknownLocation, "[openweathermap] error : unknown location. API message : #{location}" + when 401 then raise OpenWeatherMap::Exceptions::Unauthorized, "unauthorized key. API message : #{response.message}" + when 404 then raise OpenWeatherMap::Exceptions::UnknownLocation, "unknown location. API message : #{location}" else response.body end end diff --git a/lib/openweathermap/classes.rb b/lib/openweathermap/classes.rb index cd55a01..487143d 100644 --- a/lib/openweathermap/classes.rb +++ b/lib/openweathermap/classes.rb @@ -1,21 +1,35 @@ +# frozen_string_literal: true + module OpenWeatherMap - # Represents a city + + ## + # Class representing a city + class City + + ## # @return [String] City's name + attr_reader :name + ## # @return [Coordinates] City's coordinates + attr_reader :coordinates + ## # @return [String] Country in which the city is + attr_reader :country + ## # Create a new City object # # @param name [String] City's name # @param lon [Float] Longitude of the city # @param lat [Float] Latitude of the city # @param country [String] Country in which the city is + def initialize(name, lon, lat, country) @name = name @coordinates = OpenWeatherMap::Coordinates.new(lon, lat) @@ -23,85 +37,127 @@ def initialize(name, lon, lat, country) end end + ## # Represents a location's coordinates + class Coordinates + + ## # @return [Float] Longitude of the location + attr_reader :lon - + + ## # @return [Float] Latitude of the location + attr_reader :lat + ## # Create a new Coordinates object # # @param lon [Float] Longitude of the location # @param lat [Float] Latitude of the location + def initialize(lon, lat) @lon = lon @lat = lat end end + ## # Represents the weather conditions + class WeatherConditions + + ## # @return [Time] time of the condition + attr_reader :time - # @return [String] Main weather contitions at the moment + ## + # @return [String] Main weather conditions at the moment + attr_reader :main + ## # @return [String] Details of weather conditions + attr_reader :description + ## # @return [String] URL to conditions icon illustration + attr_reader :icon + ## # @return [String] Conditions illustrated by an emoji + attr_reader :emoji + ## # @return [Float] Temperature + attr_reader :temperature + ## # @return [Float] Minimum temperature at the moment (for large areas) + attr_reader :temp_min + ## # @return [Float] Maximum temperature at the moment (for large areas) + attr_reader :temp_max + ## # @return [Float] Atmospheric pressure in hPa + attr_reader :pressure + ## # @return [Float] Humidity percentage + attr_reader :humidity + ## # @return [Hash] Wind data. Keys : (symbols) # - speed : Wind speed (m/s or miles/hour) # - direction : Wind direction (meteorological degrees) + attr_reader :wind + ## # @return [Float] Clouds percentage + attr_reader :clouds + ## # @return [Hash, nil] Rain volume. Keys : (symbols) # - one_hour : Rain volume for the last 1 hour (mm) # - three_hours : Rain volume for the last 3 hours (mm) # Can be nil if there is no rain + attr_reader :rain + ## # @return [Hash, nil] Snow volume. Keys : (symbols) # - one_hour : Snow volume for the last 1 hour (mm) # - three_hours : Snow volume for the last 3 hours (mm) # Can be nil if there is no snow + attr_reader :snow + ## # Create a new WeatherConditions object. # # @param data [Hash] all the received data + def initialize(data) @time = Time.at(data['dt']) @main = data['weather'][0]['main'] @description = data['weather'][0]['description'] @icon = "https://openweathermap.org/img/w/#{data['weather'][0]['icon']}.png" - @emoji = OpenWeatherMap::Constants::CONDITION_CODE[data['weather'][0]['icon'].sub('n', 'd')] + @emoji = OpenWeatherMap::Constants::CONDITION_CODE[data['weather'][0]['icon'].tr('n', 'd')] @temperature = data['main']['temp'] @temp_min = data['main']['temp_min'].to_f @temp_max = data['main']['temp_max'].to_f diff --git a/lib/openweathermap/current-weather.rb b/lib/openweathermap/current-weather.rb index 5c4c340..968556d 100644 --- a/lib/openweathermap/current-weather.rb +++ b/lib/openweathermap/current-weather.rb @@ -1,17 +1,33 @@ +# frozen_string_literal: true + module OpenWeatherMap + + ## # Represents the current weather at a location + class CurrentWeather + + ## # @return [OpenWeatherMap::WeatherConditions] Conditions at the moment + attr_reader :weather_conditions + ## # @return [OpenWeatherMap::City] Requested city's data + attr_reader :city - + + ## # Create a new CurrentWeather object # # @param data [Hash] mixed data from the request + def initialize(data) - data = JSON.parse(data) + begin + data = JSON.parse(data) + rescue JSON::JSONError => e + raise OpenWeatherMap::Exceptions::DataError, "error while parsing data : #{e}" + end @city = OpenWeatherMap::City.new(data['name'], data['coord']['lon'], data['coord']['lat'], data['sys']['country']) @weather_conditions = OpenWeatherMap::WeatherConditions.new(data) end diff --git a/lib/openweathermap/data/constants.rb b/lib/openweathermap/data/constants.rb deleted file mode 100644 index 11df7d7..0000000 --- a/lib/openweathermap/data/constants.rb +++ /dev/null @@ -1,31 +0,0 @@ -module OpenWeatherMap - module Constants - # URL of the OpenWeatherMap API - API_URL = 'https://api.openweathermap.org' - - # Accepted types of unit - UNITS = [nil, 'metric', 'imperial'] - - # Accepted locales - LANGS = %w(ar bg ca cz de el fa fi fr gl hr hu it ja kr la lt mk nl pl pt ro ru se sk sl es tr ua vi zh_cn zh_tw en) - - # The different URLs - URLS = { - current: '/data/2.5/weather', - forecast: '/data/2.5/forecast' - } - - # All condition codes associated with emojis - CONDITION_CODE = { - '01d' => '☀', - '02d' => '⛅', - '03d' => '☁', - '04d' => '☁☁', - '09d' => '🌧', - '10d' => '🌦', - '11d' => '🌩', - '13d' => '🌨', - '50d' => '🌫', - } - end -end diff --git a/lib/openweathermap/data/exceptions.rb b/lib/openweathermap/data/exceptions.rb deleted file mode 100644 index 363e1f5..0000000 --- a/lib/openweathermap/data/exceptions.rb +++ /dev/null @@ -1,19 +0,0 @@ -module OpenWeatherMap - module Exceptions - # Exception to handle unknown lang - class UnknownLang < StandardError - end - - # Exception to handle unknown units - class UnknownUnits < StandardError - end - - # Exception to handle unknown location - class UnknownLocation < StandardError - end - - # Exception to tell that the API key isn't authorized - class Unauthorized < StandardError - end - end -end \ No newline at end of file diff --git a/lib/openweathermap/forecast.rb b/lib/openweathermap/forecast.rb index a9527f2..d718b02 100644 --- a/lib/openweathermap/forecast.rb +++ b/lib/openweathermap/forecast.rb @@ -1,17 +1,33 @@ +# frozen_string_literal: true + module OpenWeatherMap + + ## # Represents the forecast for a specific location + class Forecast + + ## # @return [OpenWeatherMap::City] Requested city's data + attr_reader :city + ## # @return [Array] Forecast for many days and hours + attr_reader :forecast + ## # Create a new Forecast object # # @param data [Hash] mixed data from the request + def initialize(data) - data = JSON.parse(data) + begin + data = JSON.parse(data) + rescue JSON::JSONError => e + raise OpenWeatherMap::Exceptions::DataError, "error while parsing data : #{e}" + end @city = OpenWeatherMap::City.new(data['city']['name'], data['city']['coord']['lon'], data['city']['coord']['lat'], data['city']['country']) @forecast = [] data['list'].each do |element| diff --git a/openweathermap.gemspec b/openweathermap.gemspec index cb2f7ba..a10ae1d 100644 --- a/openweathermap.gemspec +++ b/openweathermap.gemspec @@ -1,12 +1,12 @@ Gem::Specification.new do |s| s.name = 'openweathermap' - s.version = '0.2.2' + s.version = '0.2.3' s.date = '2019-03-16' s.summary = "🌐 Implementation of OpenWeatherMap API." s.description = "An implementation to easily fetch the OpenWeatherMap API." s.authors = ["Exybore"] s.email = 'exybore@becauseofprog.fr' - s.files = ["lib/openweathermap.rb", "lib/openweathermap/current-weather.rb", "lib/openweathermap/forecast.rb", "lib/openweathermap/classes.rb", "lib/openweathermap/api.rb", "lib/openweathermap/data/constants.rb", "lib/openweathermap/data/exceptions.rb", ] + s.files = ["lib/openweathermap.rb", "lib/openweathermap/current-weather.rb", "lib/openweathermap/forecast.rb", "lib/openweathermap/classes.rb", "lib/openweathermap/api.rb"] s.homepage = 'https://github.com/becauseofprog/openweathermap-ruby' s.license = 'MIT' end