Skip to content

Commit cc2b7d6

Browse files
authored
Merge pull request #22 from ipinfo/silvano/eng-505-add-plus-bundle-support-in-ipinfoerlang-library
Add support for Plus bundle
2 parents 00f702b + 2166d3e commit cc2b7d6

File tree

3 files changed

+334
-1
lines changed

3 files changed

+334
-1
lines changed

elvis.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
ruleset => erl_files,
88
rules => [
99
{elvis_style, function_naming_convention, #{
10-
ignore => [ipinfo, ipinfo_lite, ipinfo_core],
10+
ignore => [ipinfo, ipinfo_lite, ipinfo_core, ipinfo_plus],
1111
regex => "^([a-z][a-z0-9]*_?)*$"
1212
}}
1313
]

src/ipinfo_plus.erl

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
-module(ipinfo_plus).
2+
-include_lib("kernel/include/logger.hrl").
3+
4+
-export([
5+
'__struct__'/0,
6+
'__struct__'/1
7+
]).
8+
9+
-export([
10+
create/0,
11+
create/1,
12+
create/2,
13+
details/1,
14+
details/2
15+
]).
16+
17+
-define(DEFAULT_COUNTRY_FILE, "countries.json").
18+
-define(DEFAULT_EU_COUNTRY_FILE, "eu.json").
19+
-define(DEFAULT_COUNTRY_FLAG_FILE, "flags.json").
20+
-define(DEFAULT_COUNTRY_CURRENCY_FILE, "currency.json").
21+
-define(DEFAULT_CONTINENT_FILE, "continent.json").
22+
-define(DEFAULT_COUNTRY_FLAG_BASE_URL,
23+
<<"https://cdn.ipinfo.io/static/images/countries-flags/">>).
24+
-define(DEFAULT_BASE_URL, <<"https://api.ipinfo.io/lookup">>).
25+
-define(DEFAULT_TIMEOUT, timer:seconds(5)).
26+
-define(DEFAULT_CACHE_TTL_SECONDS, (24 * 60 * 60)).
27+
28+
-export_type([t/0]).
29+
30+
-type t() :: #{
31+
'__struct__' := ?MODULE,
32+
access_token := nil | binary(),
33+
base_url := nil | binary(),
34+
timeout := nil | timeout(),
35+
cache := nil | pid(),
36+
countries := map(),
37+
countries_flags := map(),
38+
country_flag_base_url := nil | binary(),
39+
countries_currencies := map(),
40+
continents := map(),
41+
eu_countries := list()
42+
}.
43+
44+
-spec new() -> t().
45+
%% @private
46+
new() ->
47+
#{
48+
'__struct__' => ?MODULE,
49+
access_token => nil,
50+
base_url => nil,
51+
timeout => nil,
52+
cache => nil,
53+
countries => #{},
54+
countries_currencies => #{},
55+
countries_flags => #{},
56+
country_flag_base_url => nil,
57+
continents => #{},
58+
eu_countries => []
59+
}.
60+
61+
-spec '__struct__'() -> t().
62+
%% @private
63+
'__struct__'() ->
64+
new().
65+
66+
-spec '__struct__'(From :: list() | map()) -> t().
67+
%% @private
68+
'__struct__'(From) ->
69+
new(From).
70+
71+
-spec new(From :: list() | map()) -> t().
72+
%% @private
73+
new(List) when is_list(List) ->
74+
new(maps:from_list(List));
75+
new(Map) when is_map(Map) ->
76+
maps:fold(fun maps:update/3, new(), Map).
77+
78+
create() ->
79+
create(application:get_env(ipinfo, access_token, nil)).
80+
81+
create(AccessToken) when is_list(AccessToken) ->
82+
create(list_to_binary(AccessToken));
83+
create(AccessToken) ->
84+
create(AccessToken, []).
85+
86+
-spec create(AccessToken, Settings) -> Result when
87+
AccessToken :: binary() | nil,
88+
Settings :: proplists:proplist(),
89+
Result :: {ok, t()} | {error, term()}.
90+
create(AccessToken, Settings) ->
91+
CountriesFile = get_config(countries, Settings,
92+
filename:join(code:priv_dir(ipinfo), ?DEFAULT_COUNTRY_FILE)),
93+
EuCountriesFile = get_config(eu_countries, Settings,
94+
filename:join(code:priv_dir(ipinfo), ?DEFAULT_EU_COUNTRY_FILE)),
95+
CountriesFlagsFile = get_config(countries_flags, Settings,
96+
filename:join(code:priv_dir(ipinfo), ?DEFAULT_COUNTRY_FLAG_FILE)),
97+
CountriesCurrenciesFile = get_config(countries_currencies, Settings,
98+
filename:join(code:priv_dir(ipinfo), ?DEFAULT_COUNTRY_CURRENCY_FILE)),
99+
ContinentsFile = get_config(continents, Settings,
100+
filename:join(code:priv_dir(ipinfo), ?DEFAULT_CONTINENT_FILE)),
101+
CountryFlagBaseUrl = get_config(country_flag_base_url, Settings,
102+
?DEFAULT_COUNTRY_FLAG_BASE_URL),
103+
BaseUrl = get_config(base_url, Settings, ?DEFAULT_BASE_URL),
104+
Timeout = get_config(timeout, Settings, ?DEFAULT_TIMEOUT),
105+
CacheTtl = get_config(cache_ttl, Settings, ?DEFAULT_CACHE_TTL_SECONDS),
106+
create_with_files(AccessToken, BaseUrl, Timeout, CacheTtl, CountriesFile,
107+
EuCountriesFile, CountriesFlagsFile, CountriesCurrenciesFile,
108+
ContinentsFile, CountryFlagBaseUrl).
109+
110+
create_with_files(AccessToken, BaseUrl, Timeout, CacheTtl, CountriesFile,
111+
EuCountriesFile, CountriesFlagsFile, CountriesCurrenciesFile,
112+
ContinentsFile, CountryFlagBaseUrl) ->
113+
Files = [
114+
CountriesFile,
115+
EuCountriesFile,
116+
CountriesFlagsFile,
117+
CountriesCurrenciesFile,
118+
ContinentsFile
119+
],
120+
case read_json_files(Files) of
121+
{ok, [Countries, EuCountries, CountriesFlags, CountriesCurrencies, Continents]} ->
122+
create_ipinfo_plus_struct(
123+
AccessToken,
124+
BaseUrl,
125+
Timeout,
126+
CacheTtl,
127+
Countries,
128+
EuCountries,
129+
CountriesFlags,
130+
CountriesCurrencies,
131+
Continents,
132+
CountryFlagBaseUrl
133+
);
134+
{error, Reason} ->
135+
{error, Reason}
136+
end.
137+
138+
read_json_files(Files) ->
139+
read_json_files(Files, []).
140+
141+
read_json_files([], Acc) ->
142+
{ok, lists:reverse(Acc)};
143+
read_json_files([File | Rest], Acc) ->
144+
case read_json(File) of
145+
{ok, Data} ->
146+
read_json_files(Rest, [Data | Acc]);
147+
{error, Reason} ->
148+
{error, Reason}
149+
end.
150+
151+
create_ipinfo_plus_struct(AccessToken, BaseUrl, Timeout, CacheTtl, Countries,
152+
EuCountries, CountriesFlags, CountriesCurrencies, Continents,
153+
CountryFlagBaseUrl) ->
154+
{ok, Cache} = ipinfo_cache:create(CacheTtl),
155+
{ok, new(#{
156+
access_token => AccessToken,
157+
base_url => BaseUrl,
158+
timeout => Timeout,
159+
cache => Cache,
160+
countries => Countries,
161+
eu_countries => EuCountries,
162+
countries_flags => CountriesFlags,
163+
country_flag_base_url => CountryFlagBaseUrl,
164+
countries_currencies => CountriesCurrencies,
165+
continents => Continents
166+
})}.
167+
168+
details(IpInfoPlus) ->
169+
details(IpInfoPlus, nil).
170+
171+
details(#{cache := Cache,
172+
countries := Countries,
173+
eu_countries := EuCountries,
174+
countries_flags := CountriesFlags,
175+
country_flag_base_url:= CountryFlagBaseUrl,
176+
countries_currencies := CountriesCurrencies,
177+
continents := Continents
178+
} = IpInfo, IpAddress) ->
179+
ActualIpAddress = case IpAddress of
180+
nil -> <<"me">>;
181+
_ -> IpAddress
182+
end,
183+
case get_details(Cache, IpInfo, ActualIpAddress) of
184+
{ok, Details} ->
185+
{ok, enrich_details(Details, Countries, EuCountries,
186+
CountriesFlags, CountryFlagBaseUrl, CountriesCurrencies,
187+
Continents)};
188+
{error, Reason} ->
189+
{error, Reason}
190+
end.
191+
192+
get_details(Cache, IpInfo, IpAddress) ->
193+
case ipinfo_cache:get(Cache, IpAddress) of
194+
{ok, Details} ->
195+
{ok, Details};
196+
error ->
197+
case ipinfo_http:request_details(IpInfo, IpAddress) of
198+
{ok, Details} ->
199+
ok = ipinfo_cache:add(Cache, IpAddress, Details),
200+
{ok, Details};
201+
{error, Reason} ->
202+
{error, Reason}
203+
end
204+
end.
205+
206+
enrich_details(Details, Countries, EuCountries, CountriesFlags,
207+
CountryFlagBaseUrl, CountriesCurrencies, Continents) ->
208+
Enrichers = [
209+
fun enrich_geo/1,
210+
fun(D) -> put_geo_enrichments(D, Countries, EuCountries, CountriesFlags,
211+
CountryFlagBaseUrl, CountriesCurrencies, Continents) end
212+
],
213+
lists:foldl(fun(F, Acc) -> F(Acc) end, Details, Enrichers).
214+
215+
enrich_geo(#{geo := Geo} = Details) when is_map(Geo) ->
216+
Details;
217+
enrich_geo(Details) ->
218+
Details.
219+
220+
put_geo_enrichments(#{geo := #{country_code := CountryCode} = Geo} = Details,
221+
Countries, EuCountries, CountriesFlags, CountryFlagBaseUrl,
222+
CountriesCurrencies, Continents) ->
223+
EnrichedGeo = Geo#{
224+
country_name => maps:get(CountryCode, Countries, CountryCode),
225+
is_eu => lists:member(CountryCode, EuCountries),
226+
country_flag => maps:get(CountryCode, CountriesFlags, #{}),
227+
country_currency => maps:get(CountryCode, CountriesCurrencies, #{}),
228+
continent => maps:get(CountryCode, Continents, #{}),
229+
country_flag_url => <<CountryFlagBaseUrl/binary, CountryCode/binary, ".svg">>
230+
},
231+
Details#{geo => EnrichedGeo};
232+
put_geo_enrichments(Details, _Countries, _EuCountries, _CountriesFlags,
233+
_CountryFlagBaseUrl, _CountriesCurrencies, _Continents) ->
234+
Details.
235+
236+
get_config(Key, Settings, Default) ->
237+
proplists:get_value(Key, Settings,
238+
application:get_env(ipinfo, Key, Default)).
239+
240+
read_json(JsonFile) ->
241+
case file:read_file(JsonFile) of
242+
{ok, Binary} ->
243+
case jsx:is_json(Binary) of
244+
true ->
245+
{ok, jsx:decode(Binary, [return_maps])};
246+
false ->
247+
{error, invalid_json}
248+
end;
249+
{error, Reason} ->
250+
{error, Reason}
251+
end.

test/ipinfo_plus_SUITE.erl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
-module(ipinfo_plus_SUITE).
2+
3+
-include_lib("eunit/include/eunit.hrl").
4+
-include_lib("common_test/include/ct.hrl").
5+
6+
-export([
7+
all/0,
8+
init_per_suite/1,
9+
end_per_suite/1
10+
]).
11+
12+
-export([
13+
details_test/1
14+
]).
15+
16+
all() ->
17+
[details_test].
18+
19+
init_per_suite(Config) ->
20+
{ok, _} = application:ensure_all_started(ipinfo),
21+
Token = case os:getenv("IPINFO_TOKEN") of
22+
false -> nil;
23+
TokenStr -> list_to_binary(TokenStr)
24+
end,
25+
[{token, Token} | Config].
26+
27+
end_per_suite(_Config) ->
28+
ok = application:stop(ipinfo).
29+
30+
details_test(Config) ->
31+
Token = proplists:get_value(token, Config),
32+
{ok, IpInfoPlus} = ipinfo_plus:create(Token),
33+
{ok, Details} = ipinfo_plus:details(IpInfoPlus, <<"8.8.8.8">>),
34+
35+
% Verify required fields
36+
?assertEqual(<<"8.8.8.8">>, maps:get(ip, Details)),
37+
?assertNotEqual(nil, maps:get(hostname, Details)),
38+
39+
% Check geo object
40+
#{geo := Geo} = Details,
41+
?assertNotEqual(nil, maps:get(city, Geo)),
42+
?assertNotEqual(nil, maps:get(region, Geo)),
43+
?assertNotEqual(nil, maps:get(region_code, Geo)),
44+
?assertNotEqual(nil, maps:get(country, Geo)),
45+
?assertNotEqual(nil, maps:get(country_code, Geo)),
46+
?assertNotEqual(nil, maps:get(continent, Geo)),
47+
?assertNotEqual(nil, maps:get(continent_code, Geo)),
48+
?assertNotEqual(nil, maps:get(latitude, Geo)),
49+
?assertNotEqual(nil, maps:get(longitude, Geo)),
50+
?assertNotEqual(nil, maps:get(timezone, Geo)),
51+
?assertNotEqual(nil, maps:get(postal_code, Geo)),
52+
?assertNotEqual(nil, maps:get(dma_code, Geo)),
53+
?assertNotEqual(nil, maps:get(geoname_id, Geo)),
54+
?assertNotEqual(nil, maps:get(radius, Geo)),
55+
?assertNotEqual(nil, maps:get(country_name, Geo)),
56+
?assertNotEqual(nil, maps:get(is_eu, Geo)),
57+
?assertNotEqual(nil, maps:get(country_flag, Geo)),
58+
?assertNotEqual(nil, maps:get(country_currency, Geo)),
59+
?assertNotEqual(nil, maps:get(country_flag_url, Geo)),
60+
61+
% Check as object
62+
As = maps:get(as, Details),
63+
?assertNotEqual(nil, maps:get(asn, As)),
64+
?assertNotEqual(nil, maps:get(name, As)),
65+
?assertNotEqual(nil, maps:get(domain, As)),
66+
?assertNotEqual(nil, maps:get(type, As)),
67+
?assertNotEqual(nil, maps:get(last_changed, As)),
68+
69+
% Check mobile and anonymous objects
70+
?assertNotEqual(nil, maps:get(mobile, Details)),
71+
Anonymous = maps:get(anonymous, Details),
72+
?assertNotEqual(nil, maps:get(is_proxy, Anonymous)),
73+
?assertNotEqual(nil, maps:get(is_relay, Anonymous)),
74+
?assertNotEqual(nil, maps:get(is_tor, Anonymous)),
75+
?assertNotEqual(nil, maps:get(is_vpn, Anonymous)),
76+
77+
% Check network flags
78+
?assertNotEqual(nil, maps:get(is_anonymous, Details)),
79+
?assertNotEqual(nil, maps:get(is_anycast, Details)),
80+
?assertNotEqual(nil, maps:get(is_hosting, Details)),
81+
?assertNotEqual(nil, maps:get(is_mobile, Details)),
82+
?assertNotEqual(nil, maps:get(is_satellite, Details)).

0 commit comments

Comments
 (0)