From 660dc30180b55507be7069f704f690a56dd1ed22 Mon Sep 17 00:00:00 2001 From: Vladimir Drobyshevskiy Date: Fri, 10 Dec 2021 15:59:10 +0000 Subject: [PATCH] - Runtime support for base_url change (so it would work even without recompilation of dependency), - An option to change JSON decoder library to reduce dependency burden, - README updates, - formatting, - minor refactoring --- README.md | 23 ++++++++++++++---- config/config.exs | 2 +- lib/geonames.ex | 47 ++++++++++++++++++------------------ lib/geonames/helpers.ex | 43 ++++++++++++++++----------------- lib/geonames/json_decoder.ex | 10 ++++++++ mix.exs | 33 +++++++++++++------------ mix.lock | 29 ++++++++++++++-------- 7 files changed, 110 insertions(+), 77 deletions(-) create mode 100644 lib/geonames/json_decoder.ex diff --git a/README.md b/README.md index c6e4c35..8f7a2ad 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,21 @@ simple access to all available API endpoints that return a JSON result. To install GeoNames-Elixir, follow these instructions: -- Add geonames-elixir to your list of dependencies in `mix.exs`: +- Add to your list of dependencies in `mix.exs` geonames-elixir and a JSON decoder library (Jason, Poison or another supported `decode!/1` method): ```elixir def deps do - [{ :geonames, "~> 1.0.2" }] + [ + { :geonames, "~> 1.0.2" }, + { :poison, "> 3.0.0" } + ] end ``` +- (optional) Add to your config a name of the chosen library; if none is set then Poison will be used: + + ```elixir + config :geonames, json_library: Jason + ``` - Ensure geonames is started before your application: @@ -44,13 +52,15 @@ documentation. ## Configuration Before you can use GeoNames-Elixir, you must configure it with your -username, and preferred language. This can be done by simply adding -the following to your application configuration +username, base URL (for example, to use with premium plans or your own service) +and preferred language. This can be done by simply adding the following +to your application configuration ```elixir config :geonames, username: "demo", - language: "en" + language: "en", + base_url: "https://secure.geonames.net" ``` Alternatively, you can set these with the environment variables @@ -58,6 +68,7 @@ Alternatively, you can set these with the environment variables ``` GEONAMES_USERNAME=demo GEONAMES_LANGUAGE=en + GEONAMES_BASE_URL=https://secure.geonames.net ``` Note that the language is not required, and will default to `en` @@ -68,6 +79,8 @@ It will be supplied for all, however the geonames.org documentation does not specify its usage in every endpoint, so there are no guarantees that your preferred language will be effective. +If `:base_url` is not specified then `"api.geonames.org"` will be used by default. + ## Available functions diff --git a/config/config.exs b/config/config.exs index e735af0..5bb1538 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config config :geonames, username: "testuser", diff --git a/lib/geonames.ex b/lib/geonames.ex index b511f7b..358606f 100644 --- a/lib/geonames.ex +++ b/lib/geonames.ex @@ -83,34 +83,35 @@ defmodule Geonames do Makes a request to the GeoNames endpoint `/#{endpoint.endpoint}` The arguments map may contain the following keys: - #{Enum.map(endpoint.available_url_parameters, fn(e) -> "- #{e}\n" end)} + #{Enum.map(endpoint.available_url_parameters, fn e -> "- #{e}\n" end)} Each request parameter should be supplied in a map. For example, Geonames.#{endpoint.function_name}(%{ - #{Enum.join(Enum.map(endpoint.available_url_parameters, fn(e) -> "#{to_string(e)}: \"val\"" end), ",\n ")} + #{Enum.join(Enum.map(endpoint.available_url_parameters, fn e -> "#{to_string(e)}: \"val\"" end), ",\n ")} }) """ - @spec unquote(endpoint.function_name)(map) :: { Atom.t, map } + @spec unquote(endpoint.function_name)(map() | keyword()) :: map() def unquote(endpoint.function_name)(args \\ %{}) do - url_params = unquote(endpoint).url_arguments(args) - case Helpers.required_parameters_provided?(unquote(endpoint).required_url_parameters, url_params) do - true -> - url = Helpers.build_url_string(unquote(endpoint).endpoint, url_params) - case perform_geonames_request(url) do - { :ok, json_response } -> - json_response - { :error, error_message } -> - raise RuntimeError, message: error_message - end - false -> - raise ArgumentError, message: "Not all required parameters were supplied" + url_params = unquote(endpoint).url_arguments(Enum.into(args, %{})) + + unless Helpers.required_parameters_provided?( + unquote(endpoint).required_url_parameters, + url_params + ), + do: raise(ArgumentError, message: "Not all required parameters were supplied") + + unquote(endpoint).endpoint + |> Helpers.build_url_string(url_params) + |> perform_geonames_request() + |> case do + {:ok, json_response} -> json_response + {:error, error_message} -> raise RuntimeError, message: error_message end end end - @doc """ Performs a simple get request to the specified URL. This is not specific to GeoNames and could be used @@ -127,17 +128,17 @@ defmodule Geonames do be returned with a string describing the error. """ + @spec perform_geonames_request(String.t()) :: {:ok, map()} | {:error, String.t()} def perform_geonames_request(url) do case HTTPoison.get(url) do - { :ok, %HTTPoison.Response{ status_code: 200, body: body }} -> - { :ok, Poison.decode!(body) } + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, Geonames.JsonDecoder.decode!(body)} - { :ok, %HTTPoison.Response{ status_code: status_code, body: body }} -> - { :error, "An unexpected #{status_code} response was received"} + {:ok, %HTTPoison.Response{status_code: status_code, body: _body}} -> + {:error, "An unexpected #{status_code} response was received"} - { :error, %HTTPoison.Error{ reason: reason }} -> - { :error, "Request failed with the following error: #{to_string(reason)}" } + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, "Request failed with the following error: #{to_string(reason)}"} end end - end diff --git a/lib/geonames/helpers.ex b/lib/geonames/helpers.ex index efe7603..60cf408 100644 --- a/lib/geonames/helpers.ex +++ b/lib/geonames/helpers.ex @@ -5,20 +5,14 @@ defmodule Geonames.Helpers do any of the functions defined here directly. """ - @base_url Application.get_env(:geonames, :base_url, "api.geonames.org/") + @default_base_url "api.geonames.org" @doc """ Determines whether or not all of the required request parameters have been set """ - def required_parameters_provided?(required, opts) do - invalid = - required - |> Enum.map(fn(k) -> opts[k] end) - |> Enum.member?(nil) - - !invalid - end + def required_parameters_provided?(required, opts), + do: Enum.all?(required, &Map.get(opts, &1)) @doc """ For each request to the GeoNames API, the @@ -29,30 +23,35 @@ defmodule Geonames.Helpers do """ def build_url_string(endpoint, parameters) do encoded_params = - user_configuration + user_configuration() |> Map.merge(parameters) |> Enum.map(fn - {_,nil} -> nil - {k,v} when is_binary(v) -> "#{k}=#{v}" - {k,v} when is_float(v) -> "#{k}=#{v}" - {k,v} when is_boolean(v) -> "#{k}=#{v}" - {k,v} when is_integer(v) -> "#{k}=#{v}" - {k,arr} when is_list(arr) -> Enum.map(arr, &"#{k}=#{&1}") + {_, nil} -> nil + {k, arr} when is_list(arr) -> Enum.map(arr, &"#{k}=#{&1}") + {k, v} -> "#{k}=#{v}" end) - |> List.flatten - |> Enum.filter(fn(k) -> !is_nil(k) end) + |> List.flatten() + |> Enum.reject(fn k -> is_nil(k) end) |> Enum.join("&") - |> URI.encode + |> URI.encode() - @base_url <> endpoint <> "?" <> encoded_params + [base_url(), "/", endpoint, "?", encoded_params] |> Enum.join() end - defp user_configuration do %{ username: Application.get_env(:geonames, :username) || System.get_env("GEONAMES_USERNAME"), - language: Application.get_env(:geonames, :language, System.get_env("GEONAMES_LANGUAGE") || "en") + language: + Application.get_env(:geonames, :language) || System.get_env("GEONAMES_LANGUAGE") || "en" } end + defp base_url do + url = + Application.get_env(:geonames, :base_url) || + System.get_env("GEONAMES_BASE_URL") || + @default_base_url + + String.trim_trailing(url, "/") + end end diff --git a/lib/geonames/json_decoder.ex b/lib/geonames/json_decoder.ex new file mode 100644 index 0000000..a172a80 --- /dev/null +++ b/lib/geonames/json_decoder.ex @@ -0,0 +1,10 @@ +defmodule Geonames.JsonDecoder do + @moduledoc """ + An interface module to the actual JSON decode library + """ + + @spec decode!(binary()) :: map() + def decode!(json_string), do: json_library().decode!(json_string) + + defp json_library, do: Application.get_env(:geonames, :json_library, Poison) +end diff --git a/mix.exs b/mix.exs index 4c274c4..b0dfa1e 100644 --- a/mix.exs +++ b/mix.exs @@ -2,27 +2,29 @@ defmodule Geonames.Mixfile do use Mix.Project def project do - [app: :geonames, - version: "1.0.3", - elixir: "~> 1.2", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps, - - name: "GeoNames-Elixir", - description: description, - package: package] + [ + app: :geonames, + version: "1.0.3", + elixir: "~> 1.4", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + name: "GeoNames-Elixir", + description: description(), + package: package() + ] end def application do - [applications: [:logger, :poison, :httpoison]] + [extra_applications: [:httpoison, :jason, :httpoison]] end defp deps do [ - { :poison, "~> 3.0" }, - { :httpoison, "~> 0.11.0" }, - { :ex_doc, "~> 0.12", only: :dev } + {:jason, "> 1.0.0", optional: true}, + {:poison, "> 3.0.0", optional: true}, + {:httpoison, "~> 1.8"}, + {:ex_doc, "~> 0.24", only: :dev, runtime: false} ] end @@ -36,9 +38,8 @@ defmodule Geonames.Mixfile do [ maintainers: ["Adrian Hooper"], licenses: ["MIT"], - links: %{ "GitHub" => "https://github.com/pareeohnos/geonames-elixir"}, + links: %{"GitHub" => "https://github.com/pareeohnos/geonames-elixir"}, files: ~w(mix.exs README.md lib) ] end - end diff --git a/mix.lock b/mix.lock index 4c75a77..a72e4a5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,19 @@ -%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []}, - "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, - "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, - "httpoison": {:hex, :httpoison, "0.11.1", "d06c571274c0e77b6cc50e548db3fd7779f611fbed6681fd60a331f66c143a0b", [:mix], [{:hackney, "~> 1.7.0", [hex: :hackney, optional: false]}]}, - "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}} +%{ + "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"}, + "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, + "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, + "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, +}