-
Notifications
You must be signed in to change notification settings - Fork 125
/
worldweather.opa
226 lines (201 loc) · 7.68 KB
/
worldweather.opa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
Copyright © 2011 MLstate
This file is part of OPA.
OPA is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License, version 3, as published by
the Free Software Foundation.
OPA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
more details.
You should have received a copy of the GNU Affero General Public License
along with OPA. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Author : Adam Koprowski <adam.koprowski@mlstate.com>
**/
import stdlib.web.client
/**
* An API to World Weather Online provider of weather forecasts.
*
* @author Adam Koprowski, 2010
* @category api
* @destination public
*/
/**
* {1 About this module}
*
* This module provides an API to obtain weather forecasts from the World
* Weather Online provider.
*
* {1 Where should I start?}
*
* Take a look at the {!WorldWeather.get_weather} function which requests
* a weater forecasts and calls a provided callback once the weather is
* available.
*/
/**
* A configuration of the forecast request.
* - [api_key] is the key API obtained from World Weather Online
* - [num_of_days] is the number of days of the forecast (which will
* always begin as of today).
**/
type WorldWeather.config = {
api_key : string
num_of_days : int
}
/**
* A single day weather result.
* - [date] a date of the forecast
* - [tempMin] minimal temperature (in C)
* - [tempMax] maximal temperature (in C)
* - [iconUrl] an URL to the icon representing the weather forecast for the day
* - [desc] a textual description (in English) of the weather forecast for the day
**/
type WorldWeather.daily_weather = {
date : Date.date
tempMin : int
tempMax : int
iconUrl : string
desc : string
}
/**
* A weather report.
* - [location] a location for which the weather forecast was prepared. Note that
* this does *not* need to be the same as in the request. For instance if making
* a request for city only (which may be ambiguous) this field will usually contain
* more information (i.e. city + country).
* - [weather_data] a list of daily weather reports.
**/
type WorldWeather.weather_report = {
location : string
weather_data : list(WorldWeather.daily_weather)
}
/**
* @private
**/
type WorldWeather.request = {
location : string
config : WorldWeather.config
callback : option(WorldWeather.weather_report) -> void
}
WorldWeather = {{
@private cache_expiry = Duration.min(15)
@private cache_gc_interval = Duration.min(30)
@private @server rec val cache_channel =
Session.make({last_gc = Date.epoch; cache = StringMap.empty},
s_onmessage)
@private date_scanner = Date.generate_scanner("%Y-%m-%d")
// we periodically perform garbage collection on the cache
@private @server garbage_collection(cache_channel)=
interval_ms = Duration.in_milliseconds(cache_gc_interval)
garbage_collection() = Session.send(cache_channel, {gc})
Scheduler.timer(interval_ms, garbage_collection)
@private cache_expired(cached_at) =
cache_delay = Duration.between(Date.now(), cached_at)
Duration.is_negative(Duration.add(cache_delay, cache_expiry))
@private s_onmessage(~{last_gc cache}, msg) =
do if last_gc == Date.epoch then garbage_collection(cache_channel)
match msg with
| {gc} ->
check_entry((cached_at, val)) =
if cache_expired(cached_at) then
none
else
some((cached_at, val))
new_cache = StringMap.filter_map(check_entry, cache) // FIXME, we just need StringMap.filter here
do jlog("GC WorldWeatherOnline cache from {StringMap.size(cache)} to {StringMap.size(new_cache)} values")
{set = {last_gc=Date.now(); cache=new_cache}}
// a request to update cache
| {update = ~{location response}} ->
cache_entry = (Date.now(), response)
{set = {~last_gc cache=StringMap.add(location, cache_entry, cache)}}
// a request to check cache
| {check = ~{location callback : _ -> void}} ->
do
match StringMap.get(location, cache) with
| {none} ->
callback(none)
| {some=(cached_at, response : option(WorldWeather.weather_report))} ->
if cache_expired(cached_at) then
// cache expired -- say value is not cached
callback(none)
else
// use cached response
callback(some(response))
{unchanged}
@private ask_for_weather(location, config, callback) =
format = "xml"
api_key = config.api_key
ndays = config.num_of_days
uri =
{ Uri.default_absolute with
domain = "free.worldweatheronline.com"
path = ["feed", "weather.ashx"] ;
query = [("q", location), ("format", format), ("key", api_key), ("num_of_days", "{ndays}")]
} <: Uri.uri
res = match WebClient.Get.try_get(uri) with
| {success = { ~content ... }} -> some(content)
| _ -> none
callback(res)
@private decode_weather(s) =
doc = Xmlns.try_parse_document(s)
Option.switch((doc -> Xml_parser.try_parse(world_weather_parser, doc.element)), none, doc)
@private world_weather_parser =
request = xml_parser
<request>
<type>_*</>
<query>query={Xml.Rule.string}</>
</> -> query
current = xml_parser <current_condition>_*</current_condition> -> void
entry = xml_parser
<weather>
<date>dateStr={Xml.Rule.string}</>
<tempMaxC>tempMax={Xml.Rule.integer}</>
<tempMaxF>_*</>
<tempMinC>tempMin={Xml.Rule.integer}</>
<tempMinF>_*</>
<windspeedMiles>_*</>
<windspeedKmph>_*</>
<winddirection>_*</>
<winddir16Point>_*</>
<winddirDegree>_*</>
<weatherCode>_*</>
<weatherIconUrl>iconUrl={Xml.Rule.string}</>
<weatherDesc>desc={Xml.Rule.string}</>
<precipMM>_*</>
</weather>
-> date = Date.of_formatted_string(date_scanner, dateStr) ? Date.epoch
~{date tempMin tempMax iconUrl desc}
xml_parser <data>location={request} _current={current} weather={entry}+</data> -> {~location weather_data=List.rev(weather)}
@private weather_callback(location, config, callback) =
// value is cached -- simply use the cached value
| {some=response} -> callback(response)
// value not cached -- we need to fetch it
| {none} ->
cb(s) =
doc = Option.bind(Xmlns.try_parse_document, s)
response = Option.switch((doc -> Xml_parser.try_parse(world_weather_parser, doc.element)), none, doc)
// update cache
do Session.send(cache_channel, {update = ~{location response}})
// call client callback
callback(response)
ask_for_weather(location, config, cb)
/**
* This function given an API key for World Weather Online, creates a default
* configuration.
**/
default_config(api_key : string) : WorldWeather.config =
{ ~api_key; num_of_days = 4}
/**
* Request for a weather forecast
*
* @param location location of the forecast request ("city" or "city, country")
* @param config a configuration of the request, see {!WorldWeather.config}.
* @param callback a callback function which will be called (with the weather
* report as its only parameter) once the weather forecast was obtained.
**/
get_weather(location : string, config : WorldWeather.config, callback : option(WorldWeather.weather_report) -> void) : void =
// first, check the cache
Session.send(cache_channel, {check = {~location callback=weather_callback(location, config, callback)}})
}}