Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 221 lines (195 sloc) 8.314 kb
fccc685 Initial open-source release
MLstate authored
1 /*
2 Copyright © 2011 MLstate
3
3143758 Frederic Ye [doc] lib: changed files headers to MIT license
Aqua-Ye authored
4 This file is part of Opa.
fccc685 Initial open-source release
MLstate authored
5
3143758 Frederic Ye [doc] lib: changed files headers to MIT license
Aqua-Ye authored
6 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
fccc685 Initial open-source release
MLstate authored
7
3143758 Frederic Ye [doc] lib: changed files headers to MIT license
Aqua-Ye authored
8 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
fccc685 Initial open-source release
MLstate authored
9
3143758 Frederic Ye [doc] lib: changed files headers to MIT license
Aqua-Ye authored
10 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
fccc685 Initial open-source release
MLstate authored
11 */
12 /*
13 * Author : Adam Koprowski <adam.koprowski@mlstate.com>
14 **/
15
16 import stdlib.web.client
17
18 /**
19 * An API to World Weather Online provider of weather forecasts.
20 *
21 * @author Adam Koprowski, 2010
22 * @category api
23 * @destination public
24 */
25
26 /**
27 * {1 About this module}
28 *
29 * This module provides an API to obtain weather forecasts from the World
30 * Weather Online provider.
31 *
32 * {1 Where should I start?}
33 *
34 * Take a look at the {!WorldWeather.get_weather} function which requests
35 * a weater forecasts and calls a provided callback once the weather is
36 * available.
37 */
38
39 /**
40 * A configuration of the forecast request.
41 * - [api_key] is the key API obtained from World Weather Online
42 * - [num_of_days] is the number of days of the forecast (which will
43 * always begin as of today).
44 **/
45 type WorldWeather.config = {
46 api_key : string
47 num_of_days : int
48 }
49
50 /**
51 * A single day weather result.
52 * - [date] a date of the forecast
53 * - [tempMin] minimal temperature (in C)
54 * - [tempMax] maximal temperature (in C)
55 * - [iconUrl] an URL to the icon representing the weather forecast for the day
56 * - [desc] a textual description (in English) of the weather forecast for the day
57 **/
58 type WorldWeather.daily_weather = {
59 date : Date.date
60 tempMin : int
61 tempMax : int
62 iconUrl : string
63 desc : string
64 }
65
66 /**
67 * A weather report.
68 * - [location] a location for which the weather forecast was prepared. Note that
69 * this does *not* need to be the same as in the request. For instance if making
70 * a request for city only (which may be ambiguous) this field will usually contain
71 * more information (i.e. city + country).
72 * - [weather_data] a list of daily weather reports.
73 **/
74 type WorldWeather.weather_report = {
75 location : string
76 weather_data : list(WorldWeather.daily_weather)
77 }
78
79 /**
80 * @private
81 **/
82 type WorldWeather.request = {
83 location : string
84 config : WorldWeather.config
85 callback : option(WorldWeather.weather_report) -> void
86 }
87
88 WorldWeather = {{
89
90 @private cache_expiry = Duration.min(15)
91 @private cache_gc_interval = Duration.min(30)
92
93 @private @server rec val cache_channel =
94 Session.make({last_gc = Date.epoch; cache = StringMap.empty},
95 s_onmessage)
96
97 @private date_scanner = Date.generate_scanner("%Y-%m-%d")
98
99 // we periodically perform garbage collection on the cache
100 @private @server garbage_collection(cache_channel)=
101 interval_ms = Duration.in_milliseconds(cache_gc_interval)
102 garbage_collection() = Session.send(cache_channel, {gc})
103 Scheduler.timer(interval_ms, garbage_collection)
104
105 @private cache_expired(cached_at) =
106 cache_delay = Duration.between(Date.now(), cached_at)
107 Duration.is_negative(Duration.add(cache_delay, cache_expiry))
108
109 @private s_onmessage(~{last_gc cache}, msg) =
110 do if last_gc == Date.epoch then garbage_collection(cache_channel)
111 match msg with
112 | {gc} ->
113 check_entry((cached_at, val)) =
114 if cache_expired(cached_at) then
115 none
116 else
117 some((cached_at, val))
118 new_cache = StringMap.filter_map(check_entry, cache) // FIXME, we just need StringMap.filter here
119 do jlog("GC WorldWeatherOnline cache from {StringMap.size(cache)} to {StringMap.size(new_cache)} values")
120 {set = {last_gc=Date.now(); cache=new_cache}}
121 // a request to update cache
122 | {update = ~{location response}} ->
123 cache_entry = (Date.now(), response)
124 {set = {~last_gc cache=StringMap.add(location, cache_entry, cache)}}
125 // a request to check cache
126 | {check = ~{location callback : _ -> void}} ->
127 do
128 match StringMap.get(location, cache) with
129 | {none} ->
130 callback(none)
131 | {some=(cached_at, response : option(WorldWeather.weather_report))} ->
132 if cache_expired(cached_at) then
133 // cache expired -- say value is not cached
134 callback(none)
135 else
136 // use cached response
137 callback(some(response))
138 {unchanged}
139
140 @private ask_for_weather(location, config, callback) =
141 format = "xml"
142 api_key = config.api_key
143 ndays = config.num_of_days
144 uri =
145 { Uri.default_absolute with
146 domain = "free.worldweatheronline.com"
147 path = ["feed", "weather.ashx"] ;
148 query = [("q", location), ("format", format), ("key", api_key), ("num_of_days", "{ndays}")]
149 } <: Uri.uri
150 res = match WebClient.Get.try_get(uri) with
151 | {success = { ~content ... }} -> some(content)
152 | _ -> none
153 callback(res)
154
155 @private decode_weather(s) =
156 doc = Xmlns.try_parse_document(s)
157 Option.switch((doc -> Xml_parser.try_parse(world_weather_parser, doc.element)), none, doc)
158
159 @private world_weather_parser =
160 request = xml_parser
161 <request>
162 <type>_*</>
163 <query>query={Xml.Rule.string}</>
164 </> -> query
165 current = xml_parser <current_condition>_*</current_condition> -> void
166 entry = xml_parser
167 <weather>
168 <date>dateStr={Xml.Rule.string}</>
169 <tempMaxC>tempMax={Xml.Rule.integer}</>
170 <tempMaxF>_*</>
171 <tempMinC>tempMin={Xml.Rule.integer}</>
172 <tempMinF>_*</>
173 <windspeedMiles>_*</>
174 <windspeedKmph>_*</>
175 <winddirection>_*</>
176 <winddir16Point>_*</>
177 <winddirDegree>_*</>
178 <weatherCode>_*</>
179 <weatherIconUrl>iconUrl={Xml.Rule.string}</>
180 <weatherDesc>desc={Xml.Rule.string}</>
181 <precipMM>_*</>
182 </weather>
183 -> date = Date.of_formatted_string(date_scanner, dateStr) ? Date.epoch
184 ~{date tempMin tempMax iconUrl desc}
185 xml_parser <data>location={request} _current={current} weather={entry}+</data> -> {~location weather_data=List.rev(weather)}
186
187 @private weather_callback(location, config, callback) =
188 // value is cached -- simply use the cached value
189 | {some=response} -> callback(response)
190 // value not cached -- we need to fetch it
191 | {none} ->
192 cb(s) =
193 doc = Option.bind(Xmlns.try_parse_document, s)
194 response = Option.switch((doc -> Xml_parser.try_parse(world_weather_parser, doc.element)), none, doc)
195 // update cache
196 do Session.send(cache_channel, {update = ~{location response}})
197 // call client callback
198 callback(response)
199 ask_for_weather(location, config, cb)
200
201 /**
202 * This function given an API key for World Weather Online, creates a default
203 * configuration.
204 **/
205 default_config(api_key : string) : WorldWeather.config =
206 { ~api_key; num_of_days = 4}
207
208 /**
209 * Request for a weather forecast
210 *
211 * @param location location of the forecast request ("city" or "city, country")
212 * @param config a configuration of the request, see {!WorldWeather.config}.
213 * @param callback a callback function which will be called (with the weather
214 * report as its only parameter) once the weather forecast was obtained.
215 **/
216 get_weather(location : string, config : WorldWeather.config, callback : option(WorldWeather.weather_report) -> void) : void =
217 // first, check the cache
218 Session.send(cache_channel, {check = {~location callback=weather_callback(location, config, callback)}})
219
220 }}
Something went wrong with that request. Please try again.