1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 2 18, 2015
4  * License: Subject to the	 terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module vibedav.icalendar;
8 
9 import std..string;
10 import std.stdio;
11 
12 private mixin template vAccessTpl()
13 {
14 	private
15 	{
16 		string[string] uniqueValues;
17 		string[][string] optionalValues;
18 
19 		bool setUnique(string value, string key)
20 		{
21 			foreach (k; OptionalUnique)
22 			{
23 				if (key == k)
24 				{
25 					if (k in uniqueValues)
26 						throw new Exception("Key is `" ~ key ~ "` already set.");
27 
28 					uniqueValues[key] = value;
29 					return true;
30 				}
31 			}
32 
33 			return false;
34 		}
35 
36 		bool setOptional(string value, string key)
37 		{
38 			foreach (k; Optional)
39 			{
40 				if (key == k)
41 				{
42 					optionalValues[key] ~= value;
43 					return true;
44 				}
45 			}
46 
47 			if (key.length > 2 && key[0 .. 2] == "X-")
48 				return true;
49 
50 			return false;
51 		}
52 
53 		bool setOptionalOr(string value, string key)
54 		{
55 			bool exists;
56 
57 			foreach (k; OptionalOr)
58 			{
59 				if (k in uniqueValues)
60 					return false;
61 
62 				if (k == key)
63 					exists = true;
64 			}
65 
66 			if (!exists)
67 				return false;
68 
69 			uniqueValues[key] = value;
70 
71 			return true;
72 		}
73 
74 		string asString(string type)()
75 		{
76 			string a;
77 
78 			foreach (key, value; uniqueValues)
79 				a ~= key ~ ":" ~ value ~ "\n";
80 
81 			foreach (key, valueList; optionalValues)
82 				foreach (value; valueList)
83 					a ~= key ~ ":" ~ value ~ "\n";
84 
85 			return "BEGIN:" ~ type ~ "\n" ~ a ~ "END:" ~ type;
86 		}
87 	}
88 
89 	void opIndexAssign(string value, string key)
90 	{
91 		if (!setUnique(value, key) && !setOptional(value, key) && !setOptionalOr(value, key))
92 			throw new Exception("Invalid key `" ~ key ~ "`");
93 	}
94 
95 	string opIndex(string key)
96 	{
97 		return uniqueValues[key];
98 	}
99 
100 	void remove(string key)
101 	{
102 		optionalValues.remove(key);
103 	}
104 }
105 
106 struct vEvent
107 {
108 	/// the following are optional,
109 	/// but MUST NOT occur more than once
110 	enum OptionalUnique = [
111 			"CLASS", "CREATED", "DESCRIPTION", "DTSTART", "GEO", "LAST-MOD", "LOCATION", "ORGANIZER",
112 			"PRIORITY", "DTSTAMP", "SEQ", "STATUS", "SUMMARY", "TRANSP", "UID",
113 			"URL", "RECURID", "SEQUENCE"
114 		];
115 
116 	//either 'dtend' or 'duration' may appear in
117 	//a 'eventprop', but 'dtend' and 'duration'
118 	//MUST NOT occur in the same 'eventprop'
119 	enum string[] OptionalOr = ["DTEND", "DURATION"];
120 
121 	/// the following are optional,
122 	/// and MAY occur more than once
123 	enum Optional = [
124 			"ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE",
125 			"EXRULE", "RSTATUS", "RELATED", "RESOURCES", "RDATE", "RRULE"
126 		];
127 
128 	mixin vAccessTpl;
129 
130 	string toString()
131 	{
132 		return asString!"VEVENT";
133 	}
134 }
135 
136 @("Set unique property")
137 unittest
138 {
139 	vEvent event;
140 
141 	event["CLASS"] = "value";
142 
143 	assert(event.toString == "BEGIN:VEVENT\nCLASS:value\nEND:VEVENT");
144 }
145 
146 @("Set unique property twice throw Exception")
147 unittest
148 {
149 	vEvent event;
150 
151 	bool failed;
152 
153 	event["CLASS"] = "value1";
154 	try
155 	{
156 		event["CLASS"] = "value2";
157 	}
158 	catch (Exception e)
159 	{
160 		failed = true;
161 	}
162 
163 	assert(failed);
164 	assert(event.toString == "BEGIN:VEVENT\nCLASS:value1\nEND:VEVENT");
165 }
166 
167 @("Set optional properties")
168 unittest
169 {
170 	vEvent event;
171 
172 	event["ATTACH"] = "value1";
173 	event["ATTACH"] = "value2";
174 	assert(event.toString == "BEGIN:VEVENT\nATTACH:value1\nATTACH:value2\nEND:VEVENT");
175 }
176 
177 struct vTodo
178 {
179 	/// the following are optional,
180 	/// but MUST NOT occur more than once
181 	enum OptionalUnique = [
182 			"CLASS", "COMPLETED", "CREATED", "DESCRIPTION", "DTSTAMP", "DTSTART", "GEO", "LAST-MOD", "LOCATION",
183 			"ORGANIZER", "PERCENT", "PRIORITY", "RECURID", "SEQ", "STATUS",
184 			"SUMMARY", "UID", "URL"
185 		];
186 
187 	/// either 'due' or 'duration' may appear in
188 	/// a 'todoprop', but 'due' and 'duration'
189 	/// MUST NOT occur in the same 'todoprop'
190 	enum string[] OptionalOr = ["DUE", "DURATION"];
191 
192 	/// the following are optional,
193 	/// and MAY occur more than once
194 	enum Optional = [
195 			"ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE",
196 			"EXRULE", "RSTATUS", "RELATED", "RESOURCES", "RDATE", "RRULE"
197 		];
198 
199 	mixin vAccessTpl;
200 
201 	string toString()
202 	{
203 		return asString!"VTODO";
204 	}
205 }
206 
207 /// Provide a grouping of component properties that describe a
208 /// journal entry.
209 struct vJournal
210 {
211 
212 	/// the following are optional,
213 	/// but MUST NOT occur more than once
214 	enum OptionalUnique = [
215 			"CLASS", "CREATED", "DESCRIPTION", "DTSTART", "DTSTAMP", "LAST-MOD",
216 			"ORGANIZER", "RECURID", "SEQ", "STATUS", "SUMMARY", "UID", "URL"
217 		];
218 
219 	enum string[] OptionalOr = [];
220 
221 	/// the following are optional,
222 	/// and MAY occur more than once
223 	enum Optional = [
224 			"ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE",
225 			"EXRULE", "RELATED", "RDATE" "RRULE", "RSTATUS"
226 		];
227 
228 	mixin vAccessTpl;
229 
230 	string toString()
231 	{
232 		return asString!"VJOURNAL";
233 	}
234 }
235 
236 struct vFreeBussy
237 {
238 	/// the following are optional,
239 	/// but MUST NOT occur more than once
240 	enum OptionalUnique = [
241 			"CONTACT", "DTSTART", "DTEND", "DURATION", "DTSTAMP", "ORGANIZER", "UID", "URL"
242 		];
243 
244 	enum string[] OptionalOr = [];
245 
246 	/// the following are optional,
247 	/// and MAY occur more than once
248 	enum Optional = ["ATTENDEE", "COMMENT", "FREEBUSY", "RSTATUS"];
249 
250 	mixin vAccessTpl;
251 
252 	string toString()
253 	{
254 		return asString!"VFREEBUSSY";
255 	}
256 }
257 
258 struct vTimezone
259 {
260 
261 	/// 'tzid' is required, but MUST NOT occur more
262 	/// than once
263 	enum Required = ["TZID"];
264 
265 	enum string[] Optional = [];
266 
267 	/// 'last-mod' and 'tzurl' are optional,
268 	/// but MUST NOT occur more than once
269 	enum OptionalUnique = ["LAST-MOD", "TZURL"];
270 
271 	/// one of 'standardc' or 'daylightc' MUST occur
272 	/// and each MAY occur more than once.
273 	enum string[] OptionalOr = ["STANDARDC", "DAYLIGHTC"];
274 
275 	mixin vAccessTpl;
276 
277 	string toString()
278 	{
279 		return asString!"VTIMEZONE";
280 	}
281 }
282 
283 struct vAlarm
284 {
285 
286 	enum string[] OptionalUnique = [];
287 
288 	enum string[] OptionalOr = [];
289 
290 	enum Optional = [
291 			"action", "attach", "description", "trigger", "summary",
292 			"attendee", "duration", "repeat", "attach"
293 		];
294 
295 	mixin vAccessTpl;
296 
297 	string toString()
298 	{
299 		return asString!"VALARM";
300 	}
301 }
302 
303 struct iCalendar
304 {
305 	vEvent[] vEvents;
306 	vTodo[] vTodos;
307 	vJournal[] vJournals;
308 	vFreeBussy[] vFreeBussys;
309 	vTimezone[] vTimezones;
310 	vAlarm[] vAlarms;
311 }
312 
313 vEvent parseVEvent(string[] data)
314 {
315 	vEvent ev;
316 
317 	foreach (item; data)
318 	{
319 		auto valueSep = item.indexOf(":");
320 		auto metaSep = item.indexOf(";");
321 		string key;
322 		string val;
323 
324 		if (metaSep != -1 && metaSep < valueSep)
325 		{
326 			key = item[0 .. metaSep];
327 			val = item[metaSep + 1 .. $];
328 		}
329 		else
330 		{
331 			auto row = item.split(":");
332 
333 			assert(row.length == 2);
334 
335 			key = row[0];
336 			val = row[1];
337 		}
338 
339 		ev[key] = val;
340 	}
341 
342 	return ev;
343 }
344 
345 iCalendar parseICalendar(string data)
346 {
347 	iCalendar calendar;
348 
349 	auto rows = data.split("\n");
350 
351 	string[] tmpData;
352 	bool found;
353 
354 	foreach (row; rows)
355 	{
356 		row = row.strip;
357 
358 		if (row == "BEGIN:VEVENT")
359 			found = true;
360 		else if (row == "END:VEVENT")
361 		{
362 			found = false;
363 			calendar.vEvents ~= parseVEvent(tmpData);
364 			tmpData = [];
365 		}
366 		else if (found)
367 			tmpData ~= row;
368 	}
369 
370 	return calendar;
371 }
372 
373 @("Parse VEVENT")
374 unittest
375 {
376 
377 	string data = "BEGIN:VCALENDAR
378 VERSION:2.0
379 PRODID:-//Apple Inc.//Mac OS X 10.10.3//EN
380 CALSCALE:GREGORIAN
381 BEGIN:VEVENT
382 CREATED:20150412T100602Z
383 UID:675FC7E1-B891-4C58-9B60-000000000000
384 DTEND:20150401T010000Z
385 TRANSP:OPAQUE
386 SUMMARY:some name2
387 DTSTART:20150401T000000Z
388 DTSTAMP:20150412T100602Z
389 SEQUENCE:0
390 END:VEVENT
391 END:VCALENDAR";
392 
393 	auto parsed = data.parseICalendar;
394 
395 	assert(parsed.vEvents.length == 1);
396 	assert(parsed.vEvents[0]["DTSTART"] == "20150401T000000Z");
397 }
398 
399 @("Parse VEVENT with timezone")
400 unittest
401 {
402 
403 	string data = "BEGIN:VCALENDAR
404 VERSION:2.0
405 PRODID:-//Apple Inc.//Mac OS X 10.10.3//EN
406 CALSCALE:GREGORIAN
407 BEGIN:VEVENT
408 DTEND;TZID=Europe/Bucharest:20150401T010000Z
409 END:VEVENT
410 END:VCALENDAR";
411 
412 	auto parsed = data.parseICalendar;
413 
414 	assert(parsed.vEvents.length == 1);
415 	assert(parsed.vEvents[0]["DTEND"] == "TZID=Europe/Bucharest:20150401T010000Z");
416 }