Skip to content

Commit

Permalink
update and refactor code, language versions, dependencies, add observ…
Browse files Browse the repository at this point in the history
…er and dialyzer
  • Loading branch information
Kociamber committed Jul 10, 2024
1 parent 0c96148 commit eb25df7
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 119 deletions.
21 changes: 15 additions & 6 deletions lib/ex_owm.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
defmodule ExOwm do
alias ExOwm.{
Weather,
CurrentWeather,
FiveDayForecast,
HourlyForecast,
SixteenDayForecast,
HistoricalWeather
}

require Logger

@moduledoc """
Expand Down Expand Up @@ -47,7 +56,7 @@ defmodule ExOwm do
def get_current_weather(loc, opts \\ [])

def get_current_weather(locations, opts) when is_list(locations),
do: ExOwm.CurrentWeather.Coordinator.get_weather(locations, opts)
do: CurrentWeather.Coordinator.get_weather(locations, opts)

def get_current_weather(location, opts) when is_map(location),
do: get_current_weather([location], opts)
Expand All @@ -64,7 +73,7 @@ defmodule ExOwm do
def get_weather(loc, opts \\ [])

def get_weather(locations, opts) when is_list(locations),
do: ExOwm.Weather.Coordinator.get_weather(locations, opts)
do: Weather.Coordinator.get_weather(locations, opts)

def get_weather(location, opts) when is_map(location),
do: get_weather([location], opts)
Expand All @@ -81,7 +90,7 @@ defmodule ExOwm do
def get_five_day_forecast(locations, opts \\ [])

def get_five_day_forecast(locations, opts) when is_list(locations),
do: ExOwm.FiveDayForecast.Coordinator.get_weather(locations, opts)
do: FiveDayForecast.Coordinator.get_weather(locations, opts)

def get_five_day_forecast(location, opts) when is_map(location),
do: get_five_day_forecast([location], opts)
Expand All @@ -97,7 +106,7 @@ defmodule ExOwm do
def get_hourly_forecast(locations, opts \\ [])

def get_hourly_forecast(locations, opts) when is_list(locations),
do: ExOwm.HourlyForecast.Coordinator.get_weather(locations, opts)
do: HourlyForecast.Coordinator.get_weather(locations, opts)

def get_hourly_forecast(location, opts) when is_map(location),
do: get_hourly_forecast([location], opts)
Expand All @@ -114,7 +123,7 @@ defmodule ExOwm do
def get_sixteen_day_forecast(locations, opts \\ [])

def get_sixteen_day_forecast(locations, opts) when is_list(locations),
do: ExOwm.SixteenDayForecast.Coordinator.get_weather(locations, opts)
do: SixteenDayForecast.Coordinator.get_weather(locations, opts)

def get_sixteen_day_forecast(location, opts) when is_map(location),
do: get_sixteen_day_forecast([location], opts)
Expand All @@ -132,7 +141,7 @@ defmodule ExOwm do
def get_historical_weather(loc, opts \\ [])

def get_historical_weather(locations, opts) when is_list(locations),
do: ExOwm.HistoricalWeather.Coordinator.get_weather(locations, opts)
do: HistoricalWeather.Coordinator.get_weather(locations, opts)

def get_historical_weather(location, opts) when is_map(location),
do: get_historical_weather([location], opts)
Expand Down
52 changes: 35 additions & 17 deletions lib/ex_owm/api.ex
Original file line number Diff line number Diff line change
@@ -1,47 +1,65 @@
defmodule ExOwm.Api do
@moduledoc """
This module contains OpenWeatherMap API related functions.
This module contains functions for interacting with the OpenWeatherMap API.
It prepares request strings, makes API calls, and parses the responses.
"""
alias ExOwm.RequestString
alias HTTPoison.{Error, Response}

@doc """
Prepares request string basing on given params, calls OWM API, parses and
decodes the answers.
Prepares a request string based on the given parameters, calls the OWM API,
and parses the JSON response.
## Parameters
- `api_call_type` (atom): The type of API call (e.g., `:get_weather`, `:get_current_weather`).
- `location` (map): The location parameters (e.g., city, coordinates, zip code).
- `opts` (term): Optional parameters for the API call (e.g., type, mode, units, cnt, lang).
## Returns
- (map): The parsed JSON response.
- `{:error, term, term}`: An error tuple containing the error type and the response.
"""
@spec send_and_parse_request(atom, map, term) :: map | {:error, term, term}
def send_and_parse_request(api_call_type, location, opts) do
RequestString.build(api_call_type, location, opts)
api_call_type
|> RequestString.build(location, opts)
|> call_api()
|> parse_json()
|> parse_response()
end

defp call_api(string) do
case HTTPoison.get(string) do
{:ok, %HTTPoison.Response{status_code: 200, body: json_body}} ->
@spec call_api(String.t()) :: {:ok, String.t()} | {:error, atom, term} | {:error, term}
defp call_api(url) do
case HTTPoison.get(url) do
{:ok, %Response{status_code: 200, body: json_body}} ->
{:ok, json_body}

{:ok, %HTTPoison.Response{status_code: 404, body: json_body}} ->
{:ok, %Response{status_code: 404, body: json_body}} ->
{:error, :not_found, json_body}

{:ok, %HTTPoison.Response{status_code: 400, body: json_body}} ->
{:error, :not_found, json_body}
{:ok, %Response{status_code: 400, body: json_body}} ->
{:error, :bad_request, json_body}

{:ok, %HTTPoison.Response{status_code: 401, body: json_body}} ->
{:ok, %Response{status_code: 401, body: json_body}} ->
{:error, :api_key_invalid, json_body}

{:ok, response} ->
{:error, :unknown_api_response, response}

{:error, reason} ->
{:error, %Error{} = reason} ->
{:error, reason}
end
end

defp parse_json({:ok, json}), do: Jason.decode(json)
@spec parse_response({:ok, String.t()} | {:error, atom, String.t()} | {:error, term}) ::
map | {:error, term, term}
defp parse_response({:ok, json}), do: Jason.decode(json)

defp parse_json({:error, :unknown_api_response, response}),
defp parse_response({:error, :unknown_api_response, response}),
do: {:error, :unknown_api_response, response}

defp parse_json({:error, reason, json_body}), do: {:error, reason, Jason.decode!(json_body)}
defp parse_json({:error, %HTTPoison.Error{} = reason}), do: {:error, reason}
defp parse_response({:error, reason, json_body}), do: {:error, reason, Jason.decode(json_body)}

defp parse_response({:error, %Error{} = reason}), do: {:error, reason}
end
155 changes: 65 additions & 90 deletions lib/ex_owm/request_string.ex
Original file line number Diff line number Diff line change
@@ -1,129 +1,104 @@
defmodule ExOwm.RequestString do
@moduledoc """
Request string creation.
Module responsible for creating request strings for OpenWeatherMap API calls.
It supports various API endpoints and allows for dynamic construction
of query strings based on provided parameters.
"""

# 5 day forecast is available at any location or city.
# It includes weather data every 3 hours. Forecast is available in JSON or XML format.
@api_endpoints %{
get_weather: "api.openweathermap.org/data/2.5/onecall",
get_current_weather: "api.openweathermap.org/data/2.5/weather",
get_five_day_forecast: "api.openweathermap.org/data/2.5/forecast",
get_hourly_forecast: "pro.openweathermap.org/data/2.5/forecast/hourly",
get_sixteen_day_forecast: "api.openweathermap.org/data/2.5/forecast/daily",
get_historical_weather: "api.openweathermap.org/data/2.5/onecall/timemachine"
}

@doc """
Builds request string basing on provided params.
Builds request string based on provided params.
## Parameters
- `api_call_type` (atom): The type of API call (e.g., `:get_weather`, `:get_current_weather`).
- `location` (map): The location parameters (e.g., city, coordinates, zip code).
- `opts` (keyword list): Optional parameters (e.g., type, mode, units, cnt, lang).
## Returns
- (String.t()): The constructed request string.
"""
@spec build(atom, map, key: :atom) :: String.t()
@spec build(atom, map, keyword) :: String.t()
def build(api_call_type, location, opts) do
{api_call_type, location, opts}
api_call_type
|> add_prefix_substring()
|> add_location_substring()
|> add_search_accuracy_substring()
|> add_format_substring()
|> add_units_substring()
|> add_day_count()
|> add_language_substring()
|> add_location_substring(location)
|> add_search_accuracy_substring(opts)
|> add_format_substring(opts)
|> add_units_substring(opts)
|> add_day_count(opts)
|> add_language_substring(opts)
|> add_api_key_substring()
end

# One call weather call.
defp add_prefix_substring({:get_weather, location, opts}),
do: {"api.openweathermap.org/data/2.5/onecall", location, opts}

# Current weather call.
defp add_prefix_substring({:get_current_weather, location, opts}),
do: {"api.openweathermap.org/data/2.5/weather", location, opts}

# Five day forecast call.
defp add_prefix_substring({:get_five_day_forecast, location, opts}),
do: {"api.openweathermap.org/data/2.5/forecast", location, opts}

# Four day hourly forecast call. https://openweathermap.org/api/hourly-forecast
defp add_prefix_substring({:get_hourly_forecast, location, opts}),
do: {"pro.openweathermap.org/data/2.5/forecast/hourly", location, opts}

# Sixteen day forecast call.
defp add_prefix_substring({:get_sixteen_day_forecast, location, opts}),
do: {"api.openweathermap.org/data/2.5/forecast/daily", location, opts}

# History call.
defp add_prefix_substring({:get_historical_weather, location, opts}),
do: {"api.openweathermap.org/data/2.5/onecall/timemachine", location, opts}
defp add_prefix_substring(api_call_type) do
@api_endpoints[api_call_type]
end

# Call by city name and ISO 3166 country code.
defp add_location_substring({string, %{city: city, country_code: country_code}, opts}),
do: {string <> "?q=#{city},#{country_code}", opts}
defp add_location_substring(base_url, %{city: city, country_code: country_code}),
do: "#{base_url}?q=#{city},#{country_code}"

# Call by city name.
defp add_location_substring({string, %{city: city}, opts}),
do: {string <> "?q=#{city}", opts}
defp add_location_substring(base_url, %{city: city}),
do: "#{base_url}?q=#{city}"

# Call by city id.
defp add_location_substring({string, %{id: id}, opts}),
do: {string <> "?id=#{id}", opts}
defp add_location_substring(base_url, %{id: id}),
do: "#{base_url}?id=#{id}"

# Call by geo coordinates with datetime string
defp add_location_substring({string, %{lat: lat, lon: lon, dt: dt}, opts}),
do: {string <> "?lat=#{lat}&lon=#{lon}&dt=#{dt}", opts}
defp add_location_substring(base_url, %{lat: lat, lon: lon, dt: dt}),
do: "#{base_url}?lat=#{lat}&lon=#{lon}&dt=#{dt}"

# Call by geo coordinates
defp add_location_substring({string, %{lat: lat, lon: lon}, opts}),
do: {string <> "?lat=#{lat}&lon=#{lon}", opts}
defp add_location_substring(base_url, %{lat: lat, lon: lon}),
do: "#{base_url}?lat=#{lat}&lon=#{lon}"

# Call by zip and ISO 3166 country code.
defp add_location_substring({string, %{zip: zip, country_code: country_code}, opts}),
do: {string <> "?zip=#{zip},#{country_code}", opts}
defp add_location_substring(base_url, %{zip: zip, country_code: country_code}),
do: "#{base_url}?zip=#{zip},#{country_code}"

defp add_search_accuracy_substring({string, opts}) do
defp add_search_accuracy_substring(url, opts) do
case Keyword.get(opts, :type) do
:like -> {string <> "&type=like", opts}
:accurate -> {string <> "&type=accurate", opts}
_ -> {string, opts}
:like -> "#{url}&type=like"
:accurate -> "#{url}&type=accurate"
_ -> url
end
end

# Add format type to a query string. Lack of this part make api to return default
# JSON format answer.
defp add_format_substring({string, opts}) do
defp add_format_substring(url, opts) do
case Keyword.get(opts, :mode) do
:xml -> {string <> "&mode=xml", opts}
_ -> {string, opts}
:xml -> "#{url}&mode=xml"
_ -> url
end
end

# Add temperature type to a query string. Lack of this part make api to return default
# temperature in Kelvins.
defp add_units_substring({string, opts}) do
defp add_units_substring(url, opts) do
case Keyword.get(opts, :units) do
:metric -> {string <> "&units=metric", opts}
:imperial -> {string <> "&units=imperial", opts}
_ -> {string, opts}
:metric -> "#{url}&units=metric"
:imperial -> "#{url}&units=imperial"
_ -> url
end
end

# Add day count for 1 to 16 day forecast.
defp add_day_count({string, opts}) do
defp add_day_count(url, opts) do
case Keyword.get(opts, :cnt) do
nil -> {string, opts}
cnt -> {string <> "&cnt=#{cnt}", opts}
nil -> url
cnt -> "#{url}&cnt=#{cnt}"
end
end

# Add language code parameter. API call will return data with description field
# in selected language. List of available languages can be found here:
# http://openweathermap.org/forecast16#multi
defp add_language_substring({string, opts}) do
case Keyword.has_key?(opts, :lang) do
true ->
lang =
opts
|> Keyword.get(:lang)
|> Atom.to_string()

string <> "&lang=#{lang}"

false ->
string
defp add_language_substring(url, opts) do
case Keyword.get(opts, :lang) do
nil -> url
lang -> "#{url}&lang=#{Atom.to_string(lang)}"
end
end

# Add API key.
defp add_api_key_substring(string),
do: string <> "&APPID=#{Application.get_env(:ex_owm, :api_key)}"
defp add_api_key_substring(url),
do: "#{url}&APPID=#{Application.get_env(:ex_owm, :api_key)}"
end
7 changes: 3 additions & 4 deletions lib/ex_owm/supervisor.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
defmodule ExOwm.Supervisor do
@moduledoc """
Standard Supervisor implementation. The only child is Coordinator GenSever
used for concurrent OWM API calls handling.
Standard Supervisor implementation. This supervisor oversees various Coordinator GenServers used for handling concurrent OpenWeatherMap API calls.
"""
use Supervisor

## Client API
@spec start_link(keyword) :: Supervisor.on_start()
def start_link(options \\ []) do
Supervisor.start_link(__MODULE__, [], options ++ [name: __MODULE__])
end

## Server implementation
@spec init(any) :: {:ok, tuple}
def init(_) do
children = [
ExOwm.CurrentWeather.Coordinator,
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule ExOwm.Mixfile do
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
extra_applications: [:logger, :wx, :observer, :runtime_tools],
mod: {ExOwm.Application, []}
]
end
Expand Down
Binary file modified priv/plts/project.plt
Binary file not shown.
2 changes: 1 addition & 1 deletion priv/plts/project.plt.hash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
|V��'���f긤����X`
��U�,�A�0�O�S˜

0 comments on commit eb25df7

Please sign in to comment.