1 /**
2  * The client API
3  */
4 module eskomcalendar.calendar;
5 
6 import std.stdio;
7 import std.json;
8 import std.net.curl : get, CurlException;
9 import std.conv : to;
10 import eskomcalendar.schedule;
11 import std.datetime : SysTime;
12 import eskomcalendar.exceptions;
13 
14 /** 
15  * Client API wrapper for the eskom-calendar service
16  */
17 public class EskomCalendar
18 {
19     private string calendarServer;
20 
21     /** 
22      * Constructs a new `EskomCalendar` using the provided
23      * custom server
24      *
25      * Params:
26      *   calendarServer = URL of the server to use
27      */
28     this(string calendarServer)
29     {
30         import std.string : stripRight;
31         this.calendarServer = stripRight(calendarServer, "/");
32     }
33 
34     /** 
35      * Constructs a new `EskomCalendar` using the
36      * default reference server
37      */
38     this()
39     {
40         this("https://eskom-calendar-api.shuttleapp.rs/v0.0.1/");
41     }
42 
43     /** 
44      * Performs an HTTP GET request to the provided URL
45      * and wraps any exceptions in our `EskomCalendarException`
46      * type
47      *
48      * Params:
49      *   url = the URL to perform a GET on
50      * Returns: the response body
51      */
52     private final string doGet(string url)
53     {
54         try
55         {
56             return cast(string)get(url);
57         }
58         catch(CurlException e)
59         {
60             throw new EskomCalendarException(ErrType.NETWORK_ERROR, "Could not connect to server '"~calendarServer~"'");
61         }
62     }
63 
64     /** 
65      * Get schedules from a given area
66      *
67      * Params:
68      *   area = the area to check for schedules
69      *   startTime = the lower bound to filter by (none by default)
70      *   finishTime = the upper bound to filter by (none by default)
71      * Returns: an array of `Schedule`(s)
72      */
73     public Schedule[] getSchedules(string area, SysTime startTime = SysTime.min(), SysTime finishTime = SysTime.max())
74     {
75         Schedule[] schedules;
76 
77         scope(exit)
78         {
79             version(unittest)
80             {
81                 writeln("Exiting with '"~to!(string)(schedules.length)~" schedules for area '"~area~"' between '"~startTime.toSimpleString()~"' and '"~finishTime.toSimpleString()~"'");
82             }
83         }
84 
85         /** 
86          * Fetch the schedules and parse
87          */
88         string data = doGet(calendarServer~"/outages/"~area);
89         JSONValue[] schedulesJSON = parseJSON(data).array();
90         foreach(JSONValue schedule; schedulesJSON)
91         {
92             Schedule curSchedule = Schedule.fromJSON(schedule);
93 
94             if(curSchedule.getStart() >= startTime && curSchedule.getFinish() <= finishTime)
95             {
96                 schedules ~= curSchedule;
97             }
98         }
99 
100         if(schedules.length == 0)
101         {
102             throw new EskomCalendarException(ErrType.NO_SCHEDULES_AVAILABLE, "No schedules for area '"~area~"'");
103         }
104 
105         return schedules;
106     }
107 
108     /** 
109      * Gets any schedules in the given area that would be valid for
110      * the 24 hours of today's date
111      *
112      * Params:
113      *   area = the area to check for schedules
114      * Returns: an array of `Schedule`(s) 
115      */
116     public Schedule[] getTodaySchedules(string area)
117     {
118         import std.datetime.systime :  Clock;
119         import std.datetime.date : Date, DateTime;
120         import std.datetime.systime : SysTime;
121         import core.thread : dur;
122         
123 
124         // Get just the date of today (no time)
125         Date todayDate = cast(Date)Clock.currTime();
126 
127         // Get the start and end date+times but with time zeroed out
128         SysTime startTime = cast(SysTime)todayDate;
129         SysTime endTime = cast(SysTime)todayDate;
130 
131         // Make end date+time 24 hours later
132         endTime += dur!("hours")(24);
133 
134         version(unittest)
135         {
136             writeln("startTime: ", startTime);
137             writeln("endTime: ", endTime);
138         }
139         
140         return getSchedules(area, startTime, endTime);
141     }
142 
143     /** 
144      * Gets schedules from a given time
145      *
146      * Params:
147      *   area = the area to check for schedules
148      *   startTime = the time to check from
149      * Returns: an array of `Schedule`(s) 
150      */
151     public Schedule[] getSchedulesFrom(string area, SysTime startTime)
152     {
153         return getSchedules(area, startTime);
154     }
155 
156     /** 
157      * Gets schedules up until a given time
158      *
159      * Params:
160      *   area = the area to check for schedules
161      *   finishTime = the time to check til
162      * Returns: an array of `Schedule`(s) 
163      */
164     public Schedule[] getSchedulesUntil(string area, SysTime finishTime)
165     {
166         return getSchedules(area, SysTime.min(), finishTime);
167     }
168 
169     /** 
170      * Get all the areas
171      *
172      * Returns: an array of `string` of the area names
173      */
174     public string[] getAreas()
175     {
176         return getAreas("");
177     }
178 
179     /** 
180      * Get all areas matching a given regular expression
181      *
182      * Params:
183      *   regex = the regular expression
184      * Returns: an array of `string` of the area names
185      */
186     public string[] getAreas(string regex)
187     {
188         // Apply any URL escaping needed
189         import std.uri : encode;
190         regex = encode(regex);
191 
192         string data = doGet(calendarServer~"/list_areas/"~regex);
193         JSONValue[] areas = parseJSON(data).array();
194 
195         string[] areasStr;
196         foreach(JSONValue area; areas)
197         {
198             areasStr ~= area.str();
199         }
200 
201         if(areasStr.length == 0)
202         {
203             throw new EskomCalendarException(ErrType.NO_AREAS_AVAILABLE);
204         }
205 
206         return areasStr;
207     }
208 }
209 
210 /**
211  * Get the schedules that will occur within today's 24 hours
212  * in the `western-cape-worscester` area
213  */
214 unittest
215 {
216     EskomCalendar calendar = new EskomCalendar();
217 
218     try
219     {
220         Schedule[] schedules = calendar.getTodaySchedules("western-cape-worscester");
221         foreach(Schedule schedule; schedules)
222         {
223             writeln("Today: "~schedule.toString());
224         }
225     }
226     catch(EskomCalendarException e)
227     {
228         writeln("Crashed with '"~e.toString()~"'");
229         assert(false);
230     }
231 }
232 
233 /** 
234  * Get the first 10 areas and then all schedules
235  * of each said area
236  */
237 unittest
238 {
239     EskomCalendar calendar = new EskomCalendar();
240 
241     /** 
242      * Get all areas available
243      * and take a subset of them
244      */
245     string[] areas = calendar.getAreas()[0..10];
246 
247     /**
248      * Get the schedules per-each of them
249      */
250     foreach(string area; areas)
251     {
252         Schedule[] schedules = calendar.getSchedules(area);
253     }
254 }
255 
256 /**
257  * Get the schedules for the `western-cape-worscester` area
258  */
259 unittest
260 {
261     EskomCalendar calendar = new EskomCalendar();
262 
263     try
264     {
265         Schedule[] schedules = calendar.getSchedules("western-cape-worscester");
266         assert(schedules.length > 5);
267 
268         foreach(Schedule schedule; schedules)
269         {
270             writeln(schedule);
271         }
272     }
273     catch(EskomCalendarException e)
274     {
275         writeln("Crashed with '"~e.toString()~"'");
276         assert(false);
277     }
278 }
279 
280 /**
281  * Get all areas
282  */
283 unittest
284 {
285     EskomCalendar calendar = new EskomCalendar();
286 
287     try
288     {
289         string[] areas = calendar.getAreas();
290         writeln(areas);
291         assert(areas.length > 40);
292     }
293     catch(EskomCalendarException e)
294     {
295         writeln("Crashed with '"~e.toString()~"'");
296         assert(false);
297     }
298 }
299 
300 /**
301  * Test failing network connection
302  */
303 unittest
304 {
305     EskomCalendar calendar = new EskomCalendar("http://sdhjdshkjdas.com");
306 
307     try
308     {
309         calendar.getAreas();
310         assert(false);
311     }
312     catch(EskomCalendarException e)
313     {
314         assert(e.getError() == ErrType.NETWORK_ERROR);
315     }
316 }