protocol-caldav 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/protocol/caldav/collection.rb +99 -75
- data/lib/protocol/caldav/constants.rb +23 -27
- data/lib/protocol/caldav/content_line.rb +94 -91
- data/lib/protocol/caldav/ctag.rb +46 -50
- data/lib/protocol/caldav/etag.rb +24 -28
- data/lib/protocol/caldav/filter/addressbook.rb +41 -28
- data/lib/protocol/caldav/filter/calendar.rb +65 -44
- data/lib/protocol/caldav/filter/match.rb +632 -556
- data/lib/protocol/caldav/filter/parser.rb +273 -252
- data/lib/protocol/caldav/ical/component.rb +49 -46
- data/lib/protocol/caldav/ical/expand.rb +148 -134
- data/lib/protocol/caldav/ical/freebusy.rb +106 -74
- data/lib/protocol/caldav/ical/parser.rb +139 -139
- data/lib/protocol/caldav/ical/property.rb +22 -21
- data/lib/protocol/caldav/ical/rrule.rb +346 -235
- data/lib/protocol/caldav/item.rb +41 -43
- data/lib/protocol/caldav/multistatus.rb +43 -46
- data/lib/protocol/caldav/path.rb +82 -79
- data/lib/protocol/caldav/storage.rb +24 -28
- data/lib/protocol/caldav/vcard/card.rb +25 -27
- data/lib/protocol/caldav/vcard/parser.rb +56 -59
- data/lib/protocol/caldav/version.rb +1 -1
- data/lib/protocol/caldav/xml.rb +84 -80
- data/lib/protocol/caldav/xml_builder.rb +4 -2
- metadata +18 -4
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "scampi"
|
|
5
|
-
|
|
6
3
|
require "protocol/caldav"
|
|
7
4
|
|
|
8
5
|
module Protocol
|
|
@@ -14,43 +11,57 @@ module Protocol
|
|
|
14
11
|
# --- Calendar (RFC 4791 §9.7) ---
|
|
15
12
|
|
|
16
13
|
def calendar?(filter, component)
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if component
|
|
15
|
+
comp_filter_matches?(filter, component)
|
|
16
|
+
else
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# --- Addressbook (RFC 6352 §10.5) ---
|
|
22
22
|
|
|
23
23
|
def addressbook?(filter, card)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
if card
|
|
25
|
+
if filter.prop_filters.empty?
|
|
26
|
+
true
|
|
27
|
+
else
|
|
28
|
+
if filter.test == 'allof'
|
|
29
|
+
filter.prop_filters.all? { |pf| card_prop_filter_matches?(pf, card) }
|
|
30
|
+
else
|
|
31
|
+
filter.prop_filters.any? { |pf| card_prop_filter_matches?(pf, card) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
29
34
|
else
|
|
30
|
-
|
|
35
|
+
false
|
|
31
36
|
end
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
# --- Private: Calendar matching ---
|
|
35
40
|
|
|
36
41
|
def comp_filter_matches?(filter, component)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Time-range check on this component
|
|
41
|
-
return false if filter.time_range && !time_range_matches?(filter.time_range, component)
|
|
42
|
-
|
|
43
|
-
# All nested prop-filters must match (AND)
|
|
44
|
-
return false unless filter.prop_filters.all? { |pf| prop_filter_matches?(pf, component) }
|
|
45
|
-
|
|
46
|
-
# All nested comp-filters must match
|
|
47
|
-
filter.comp_filters.all? do |cf|
|
|
48
|
-
children = component.find_components(cf.name)
|
|
49
|
-
if cf.is_not_defined
|
|
50
|
-
children.empty?
|
|
42
|
+
if component.name.casecmp?(filter.name)
|
|
43
|
+
if filter.is_not_defined
|
|
44
|
+
false
|
|
51
45
|
else
|
|
52
|
-
|
|
46
|
+
if filter.time_range && !time_range_matches?(filter.time_range, component)
|
|
47
|
+
false
|
|
48
|
+
else
|
|
49
|
+
if filter.prop_filters.all? { |pf| prop_filter_matches?(pf, component) }
|
|
50
|
+
filter.comp_filters.all? do |cf|
|
|
51
|
+
children = component.find_components(cf.name)
|
|
52
|
+
if cf.is_not_defined
|
|
53
|
+
children.empty?
|
|
54
|
+
else
|
|
55
|
+
children.any? { |child| comp_filter_matches?(cf, child) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
53
62
|
end
|
|
63
|
+
else
|
|
64
|
+
false
|
|
54
65
|
end
|
|
55
66
|
end
|
|
56
67
|
|
|
@@ -58,83 +69,115 @@ module Protocol
|
|
|
58
69
|
# Handle recurring events via RRULE expansion
|
|
59
70
|
rrule_prop = component.find_property('RRULE')
|
|
60
71
|
if rrule_prop
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
filter_end = tr.end_time ? parse_datetime_string(tr.end_time) : Time.utc(9999)
|
|
66
|
-
|
|
67
|
-
comp_start = parse_ical_datetime(component, 'DTSTART')
|
|
68
|
-
|
|
69
|
-
# VTODO special cases per RFC 4791 §9.9
|
|
70
|
-
if component.name.casecmp?('VTODO')
|
|
71
|
-
due = parse_ical_datetime(component, 'DUE')
|
|
72
|
-
completed = parse_ical_datetime(component, 'COMPLETED')
|
|
73
|
-
created = parse_ical_datetime(component, 'CREATED')
|
|
74
|
-
|
|
75
|
-
if comp_start && due
|
|
76
|
-
return comp_start < filter_end && due > filter_start
|
|
77
|
-
elsif comp_start
|
|
78
|
-
return comp_start < filter_end && (comp_start + 1) > filter_start
|
|
79
|
-
elsif due
|
|
80
|
-
return (due - 1) < filter_end && due > filter_start
|
|
81
|
-
elsif completed
|
|
82
|
-
return completed >= filter_start && completed < filter_end
|
|
83
|
-
elsif created
|
|
84
|
-
return created >= filter_start && created < filter_end
|
|
72
|
+
rrule_time_range_matches?(tr, component, rrule_prop)
|
|
73
|
+
else
|
|
74
|
+
if tr.start_time
|
|
75
|
+
filter_start = parse_datetime_string(tr.start_time)
|
|
85
76
|
else
|
|
86
|
-
|
|
77
|
+
filter_start = Time.at(0).utc
|
|
78
|
+
end
|
|
79
|
+
if tr.end_time
|
|
80
|
+
filter_end = parse_datetime_string(tr.end_time)
|
|
81
|
+
else
|
|
82
|
+
filter_end = Time.utc(9999)
|
|
83
|
+
end
|
|
84
|
+
comp_start = parse_ical_datetime(component, 'DTSTART')
|
|
85
|
+
|
|
86
|
+
if component.name.casecmp?('VTODO')
|
|
87
|
+
due = parse_ical_datetime(component, 'DUE')
|
|
88
|
+
completed = parse_ical_datetime(component, 'COMPLETED')
|
|
89
|
+
created = parse_ical_datetime(component, 'CREATED')
|
|
90
|
+
|
|
91
|
+
if comp_start && due
|
|
92
|
+
comp_start < filter_end && due > filter_start
|
|
93
|
+
elsif comp_start
|
|
94
|
+
comp_start < filter_end && (comp_start + 1) > filter_start
|
|
95
|
+
elsif due
|
|
96
|
+
(due - 1) < filter_end && due > filter_start
|
|
97
|
+
elsif completed
|
|
98
|
+
completed >= filter_start && completed < filter_end
|
|
99
|
+
elsif created
|
|
100
|
+
created >= filter_start && created < filter_end
|
|
101
|
+
else
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
elsif component.name.casecmp?('VJOURNAL')
|
|
105
|
+
if comp_start
|
|
106
|
+
comp_start >= filter_start && comp_start < filter_end
|
|
107
|
+
else
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
comp_end = parse_ical_datetime(component, 'DTEND') ||
|
|
112
|
+
parse_duration_end(component, comp_start) ||
|
|
113
|
+
(comp_start ? comp_start + 1 : nil)
|
|
114
|
+
|
|
115
|
+
if comp_start
|
|
116
|
+
comp_start < filter_end && (comp_end || comp_start + 1) > filter_start
|
|
117
|
+
else
|
|
118
|
+
false
|
|
119
|
+
end
|
|
87
120
|
end
|
|
88
121
|
end
|
|
89
|
-
|
|
90
|
-
# VJOURNAL: uses DTSTART only (instantaneous)
|
|
91
|
-
if component.name.casecmp?('VJOURNAL')
|
|
92
|
-
return false unless comp_start
|
|
93
|
-
return comp_start >= filter_start && comp_start < filter_end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# VEVENT
|
|
97
|
-
comp_end = parse_ical_datetime(component, 'DTEND') ||
|
|
98
|
-
parse_duration_end(component, comp_start) ||
|
|
99
|
-
(comp_start ? comp_start + 1 : nil)
|
|
100
|
-
|
|
101
|
-
return false unless comp_start
|
|
102
|
-
|
|
103
|
-
# Half-open overlap: [comp_start, comp_end) overlaps [filter_start, filter_end)
|
|
104
|
-
comp_start < filter_end && (comp_end || comp_start + 1) > filter_start
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
def parse_ical_datetime(component, prop_name)
|
|
108
125
|
prop = component.find_property(prop_name)
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
if prop
|
|
127
|
+
parse_datetime_string(prop.value.strip)
|
|
128
|
+
else
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
111
131
|
end
|
|
112
132
|
|
|
113
133
|
def parse_duration_end(component, start_time)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
if start_time
|
|
135
|
+
dur = component.find_property('DURATION')
|
|
136
|
+
if dur
|
|
137
|
+
val = dur.value.strip
|
|
138
|
+
seconds = 0
|
|
139
|
+
if m = val.match(/P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/)
|
|
140
|
+
seconds += (m[1]&.to_i || 0) * 86400
|
|
141
|
+
seconds += (m[2]&.to_i || 0) * 3600
|
|
142
|
+
seconds += (m[3]&.to_i || 0) * 60
|
|
143
|
+
seconds += (m[4]&.to_i || 0)
|
|
144
|
+
end
|
|
145
|
+
seconds > 0 ? start_time + seconds : nil
|
|
146
|
+
else
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
nil
|
|
124
151
|
end
|
|
125
|
-
seconds > 0 ? start_time + seconds : nil
|
|
126
152
|
end
|
|
127
153
|
|
|
128
154
|
def parse_datetime_string(str)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
155
|
+
if str && !str.empty?
|
|
156
|
+
str = str.strip
|
|
157
|
+
if str.length == 8 # DATE format: YYYYMMDD
|
|
158
|
+
Time.utc(str[0..3].to_i, str[4..5].to_i, str[6..7].to_i)
|
|
159
|
+
elsif str.end_with?('Z') # UTC datetime
|
|
160
|
+
s = str.chomp('Z')
|
|
161
|
+
Time.utc(
|
|
162
|
+
s[0..3].to_i,
|
|
163
|
+
s[4..5].to_i,
|
|
164
|
+
s[6..7].to_i,
|
|
165
|
+
s[9..10].to_i,
|
|
166
|
+
s[11..12].to_i,
|
|
167
|
+
s[13..14].to_i,
|
|
168
|
+
)
|
|
169
|
+
else # Floating datetime -- treat as UTC per RFC 4791 §9.9
|
|
170
|
+
Time.utc(
|
|
171
|
+
str[0..3].to_i,
|
|
172
|
+
str[4..5].to_i,
|
|
173
|
+
str[6..7].to_i,
|
|
174
|
+
str[9..10].to_i,
|
|
175
|
+
str[11..12].to_i,
|
|
176
|
+
str[13..14].to_i,
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
nil
|
|
138
181
|
end
|
|
139
182
|
rescue ArgumentError
|
|
140
183
|
nil
|
|
@@ -144,14 +187,18 @@ module Protocol
|
|
|
144
187
|
properties = component.find_all_properties(filter.name)
|
|
145
188
|
|
|
146
189
|
if filter.is_not_defined
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
190
|
+
properties.empty?
|
|
191
|
+
else
|
|
192
|
+
if properties.empty?
|
|
193
|
+
false
|
|
194
|
+
else
|
|
195
|
+
properties.any? do |prop|
|
|
196
|
+
if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
|
|
197
|
+
next false
|
|
198
|
+
end
|
|
199
|
+
filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
155
202
|
end
|
|
156
203
|
end
|
|
157
204
|
|
|
@@ -159,24 +206,30 @@ module Protocol
|
|
|
159
206
|
param_value = property.param(filter.name)
|
|
160
207
|
|
|
161
208
|
if filter.is_not_defined
|
|
162
|
-
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
return false if param_value.nil?
|
|
166
|
-
|
|
167
|
-
if filter.text_match
|
|
168
|
-
text_match_matches?(filter.text_match, param_value)
|
|
209
|
+
param_value.nil?
|
|
169
210
|
else
|
|
170
|
-
|
|
211
|
+
if param_value.nil?
|
|
212
|
+
false
|
|
213
|
+
else
|
|
214
|
+
if filter.text_match
|
|
215
|
+
text_match_matches?(filter.text_match, param_value)
|
|
216
|
+
else
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
end
|
|
171
220
|
end
|
|
172
221
|
end
|
|
173
222
|
|
|
174
223
|
def text_match_matches?(matcher, value)
|
|
175
|
-
|
|
176
|
-
when 'equals'
|
|
177
|
-
|
|
178
|
-
when '
|
|
179
|
-
|
|
224
|
+
case matcher.match_type
|
|
225
|
+
when 'equals'
|
|
226
|
+
result = collate_equal?(matcher.collation, value, matcher.value)
|
|
227
|
+
when 'starts-with'
|
|
228
|
+
result = collate_starts?(matcher.collation, value, matcher.value)
|
|
229
|
+
when 'ends-with'
|
|
230
|
+
result = collate_ends?(matcher.collation, value, matcher.value)
|
|
231
|
+
else
|
|
232
|
+
result = collate_contains?(matcher.collation, value, matcher.value)
|
|
180
233
|
end
|
|
181
234
|
matcher.negate_condition ? !result : result
|
|
182
235
|
end
|
|
@@ -219,49 +272,65 @@ module Protocol
|
|
|
219
272
|
properties = card.find_all_properties(filter.name)
|
|
220
273
|
|
|
221
274
|
if filter.is_not_defined
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
275
|
+
properties.empty?
|
|
276
|
+
else
|
|
277
|
+
if properties.empty?
|
|
278
|
+
false
|
|
279
|
+
else
|
|
280
|
+
properties.any? do |prop|
|
|
281
|
+
if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
|
|
282
|
+
next false
|
|
283
|
+
end
|
|
284
|
+
filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
230
287
|
end
|
|
231
288
|
end
|
|
232
289
|
|
|
233
290
|
def rrule_time_range_matches?(tr, component, rrule_prop)
|
|
234
291
|
dtstart = parse_ical_datetime(component, 'DTSTART')
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
292
|
+
if dtstart
|
|
293
|
+
if tr.start_time
|
|
294
|
+
filter_start = parse_datetime_string(tr.start_time)
|
|
295
|
+
else
|
|
296
|
+
filter_start = Time.at(0).utc
|
|
297
|
+
end
|
|
298
|
+
if tr.end_time
|
|
299
|
+
filter_end = parse_datetime_string(tr.end_time)
|
|
300
|
+
else
|
|
301
|
+
filter_end = Time.utc(9999)
|
|
302
|
+
end
|
|
303
|
+
exdates = component.find_all_properties('EXDATE').filter_map do |ex|
|
|
304
|
+
parse_datetime_string(ex.value.strip)
|
|
305
|
+
end
|
|
306
|
+
dtend = parse_ical_datetime(component, 'DTEND')
|
|
307
|
+
duration = nil
|
|
239
308
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
309
|
+
if dtend
|
|
310
|
+
duration = dtend - dtstart
|
|
311
|
+
else
|
|
312
|
+
dur_prop = component.find_property('DURATION')
|
|
313
|
+
duration = 1
|
|
243
314
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
315
|
+
if dur_prop
|
|
316
|
+
duration = parse_duration_seconds(dur_prop.value.strip)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
adjusted_start = filter_start - [duration, 0].max
|
|
320
|
+
occurrences = Ical::Rrule.expand(
|
|
321
|
+
dtstart: dtstart,
|
|
322
|
+
rrule_value: rrule_prop.value.strip,
|
|
323
|
+
range_start: adjusted_start,
|
|
324
|
+
range_end: filter_end,
|
|
325
|
+
exdates: exdates,
|
|
326
|
+
max_count: 10000,
|
|
327
|
+
)
|
|
328
|
+
occurrences.any? do |occ_start|
|
|
329
|
+
occ_end = occ_start + duration
|
|
330
|
+
occ_start < filter_end && occ_end > filter_start
|
|
331
|
+
end
|
|
247
332
|
else
|
|
248
|
-
|
|
249
|
-
dur_prop ? parse_duration_seconds(dur_prop.value.strip) : 1
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
adjusted_start = filter_start - [duration, 0].max
|
|
253
|
-
occurrences = Ical::Rrule.expand(
|
|
254
|
-
dtstart: dtstart,
|
|
255
|
-
rrule_value: rrule_prop.value.strip,
|
|
256
|
-
range_start: adjusted_start,
|
|
257
|
-
range_end: filter_end,
|
|
258
|
-
exdates: exdates,
|
|
259
|
-
max_count: 10000
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
occurrences.any? do |occ_start|
|
|
263
|
-
occ_end = occ_start + duration
|
|
264
|
-
occ_start < filter_end && occ_end > filter_start
|
|
333
|
+
false
|
|
265
334
|
end
|
|
266
335
|
end
|
|
267
336
|
|
|
@@ -277,500 +346,507 @@ module Protocol
|
|
|
277
346
|
seconds > 0 ? seconds : 1
|
|
278
347
|
end
|
|
279
348
|
|
|
280
|
-
private_class_method :comp_filter_matches?,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
349
|
+
private_class_method :comp_filter_matches?,
|
|
350
|
+
:prop_filter_matches?,
|
|
351
|
+
:param_filter_matches?,
|
|
352
|
+
:text_match_matches?,
|
|
353
|
+
:collate_contains?,
|
|
354
|
+
:collate_equal?,
|
|
355
|
+
:collate_starts?,
|
|
356
|
+
:collate_ends?,
|
|
357
|
+
:card_prop_filter_matches?,
|
|
358
|
+
:time_range_matches?,
|
|
359
|
+
:parse_ical_datetime,
|
|
360
|
+
:parse_duration_end,
|
|
361
|
+
:parse_datetime_string,
|
|
362
|
+
:rrule_time_range_matches?,
|
|
363
|
+
:parse_duration_seconds
|
|
288
364
|
end
|
|
289
365
|
end
|
|
290
366
|
end
|
|
291
367
|
end
|
|
292
368
|
|
|
293
|
-
|
|
294
|
-
def parse_ical(text)
|
|
295
|
-
Protocol::Caldav::Ical::Parser.parse(text)
|
|
296
|
-
end
|
|
369
|
+
__END__
|
|
297
370
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
371
|
+
def parse_ical(text)
|
|
372
|
+
Protocol::Caldav::Ical::Parser.parse(text)
|
|
373
|
+
end
|
|
301
374
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
375
|
+
def parse_vcard(text)
|
|
376
|
+
Protocol::Caldav::Vcard::Parser.parse(text)
|
|
377
|
+
end
|
|
305
378
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
379
|
+
def comp_filter(name, **opts)
|
|
380
|
+
Protocol::Caldav::Filter::Calendar::CompFilter.new(name: name, **opts)
|
|
381
|
+
end
|
|
309
382
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
383
|
+
def prop_filter(name, **opts)
|
|
384
|
+
Protocol::Caldav::Filter::Calendar::PropFilter.new(name: name, **opts)
|
|
385
|
+
end
|
|
313
386
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
387
|
+
def text_match(value, **opts)
|
|
388
|
+
Protocol::Caldav::Filter::Calendar::TextMatch.new(value: value, **opts)
|
|
389
|
+
end
|
|
317
390
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
391
|
+
def ab_filter(**opts)
|
|
392
|
+
Protocol::Caldav::Filter::Addressbook::Filter.new(**opts)
|
|
393
|
+
end
|
|
321
394
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
395
|
+
def ab_prop_filter(name, **opts)
|
|
396
|
+
Protocol::Caldav::Filter::Addressbook::PropFilter.new(name: name, **opts)
|
|
397
|
+
end
|
|
325
398
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
|
|
330
|
-
Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
|
|
331
|
-
end
|
|
399
|
+
def ab_text_match(value, **opts)
|
|
400
|
+
Protocol::Caldav::Filter::Addressbook::TextMatch.new(value: value, **opts)
|
|
401
|
+
end
|
|
332
402
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
403
|
+
describe "Protocol::Caldav::Filter::Match" do
|
|
404
|
+
describe "CompFilter against component" do
|
|
405
|
+
it "matches when component name equals filter name" do
|
|
406
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
|
|
407
|
+
Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
|
|
408
|
+
end
|
|
337
409
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
410
|
+
it "does not match when names differ" do
|
|
411
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
|
|
412
|
+
Protocol::Caldav::Filter::Match.calendar?(comp_filter("VEVENT"), vcal).should.equal false
|
|
413
|
+
end
|
|
342
414
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
415
|
+
it "with is_not_defined: does not match when component present" do
|
|
416
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
|
|
417
|
+
Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR", is_not_defined: true), vcal).should.equal false
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
it "all nested prop-filters must match (AND semantics)" do
|
|
421
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
422
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
423
|
+
comp_filter("VEVENT", prop_filters: [
|
|
424
|
+
prop_filter("SUMMARY"),
|
|
425
|
+
prop_filter("DESCRIPTION") # absent -> fails
|
|
350
426
|
])
|
|
351
|
-
|
|
352
|
-
|
|
427
|
+
])
|
|
428
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
429
|
+
end
|
|
353
430
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
431
|
+
it "empty filter matches any component of that name" do
|
|
432
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
|
|
433
|
+
Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
|
|
434
|
+
end
|
|
358
435
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
436
|
+
it "is_not_defined on nested comp-filter: matches when absent" do
|
|
437
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
438
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
439
|
+
comp_filter("VTODO", is_not_defined: true)
|
|
440
|
+
])
|
|
441
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
442
|
+
end
|
|
366
443
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
end
|
|
444
|
+
it "is_not_defined on nested comp-filter: does not match when present" do
|
|
445
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
446
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
447
|
+
comp_filter("VEVENT", is_not_defined: true)
|
|
448
|
+
])
|
|
449
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
374
450
|
end
|
|
451
|
+
end
|
|
375
452
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
453
|
+
describe "PropFilter" do
|
|
454
|
+
it "matches when property exists (defined-only check)" do
|
|
455
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Test\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
456
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
457
|
+
comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
|
|
458
|
+
])
|
|
459
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
460
|
+
end
|
|
384
461
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
462
|
+
it "does not match when property absent" do
|
|
463
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
464
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
465
|
+
comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
|
|
466
|
+
])
|
|
467
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
468
|
+
end
|
|
392
469
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
470
|
+
it "with is_not_defined: matches when property absent" do
|
|
471
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
472
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
473
|
+
comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
|
|
474
|
+
])
|
|
475
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
476
|
+
end
|
|
400
477
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
478
|
+
it "with is_not_defined: does not match when property present" do
|
|
479
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSTATUS:CONFIRMED\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
480
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
481
|
+
comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
|
|
482
|
+
])
|
|
483
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
484
|
+
end
|
|
408
485
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
])
|
|
486
|
+
it "multi-value: matches if any instance satisfies" do
|
|
487
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:alice\r\nATTENDEE:bob\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
488
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
489
|
+
comp_filter("VEVENT", prop_filters: [
|
|
490
|
+
prop_filter("ATTENDEE", text_match: text_match("bob"))
|
|
415
491
|
])
|
|
416
|
-
|
|
417
|
-
|
|
492
|
+
])
|
|
493
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
418
494
|
end
|
|
495
|
+
end
|
|
419
496
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
])
|
|
497
|
+
describe "TextMatch" do
|
|
498
|
+
it "contains (default) matches substring" do
|
|
499
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
500
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
501
|
+
comp_filter("VEVENT", prop_filters: [
|
|
502
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting"))
|
|
427
503
|
])
|
|
428
|
-
|
|
429
|
-
|
|
504
|
+
])
|
|
505
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
506
|
+
end
|
|
430
507
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
])
|
|
508
|
+
it "equals matches whole-string equality" do
|
|
509
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
510
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
511
|
+
comp_filter("VEVENT", prop_filters: [
|
|
512
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
|
|
437
513
|
])
|
|
438
|
-
|
|
439
|
-
|
|
514
|
+
])
|
|
515
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
516
|
+
end
|
|
440
517
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
])
|
|
518
|
+
it "equals rejects partial match" do
|
|
519
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
520
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
521
|
+
comp_filter("VEVENT", prop_filters: [
|
|
522
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
|
|
447
523
|
])
|
|
448
|
-
|
|
449
|
-
|
|
524
|
+
])
|
|
525
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
526
|
+
end
|
|
450
527
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
])
|
|
528
|
+
it "starts-with matches prefix" do
|
|
529
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
530
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
531
|
+
comp_filter("VEVENT", prop_filters: [
|
|
532
|
+
prop_filter("SUMMARY", text_match: text_match("Team", match_type: "starts-with"))
|
|
457
533
|
])
|
|
458
|
-
|
|
459
|
-
|
|
534
|
+
])
|
|
535
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
536
|
+
end
|
|
460
537
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
])
|
|
538
|
+
it "ends-with matches suffix" do
|
|
539
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
540
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
541
|
+
comp_filter("VEVENT", prop_filters: [
|
|
542
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "ends-with"))
|
|
467
543
|
])
|
|
468
|
-
|
|
469
|
-
|
|
544
|
+
])
|
|
545
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
546
|
+
end
|
|
470
547
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
])
|
|
548
|
+
it "i;ascii-casemap (default) is case-insensitive" do
|
|
549
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
550
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
551
|
+
comp_filter("VEVENT", prop_filters: [
|
|
552
|
+
prop_filter("SUMMARY", text_match: text_match("meeting"))
|
|
477
553
|
])
|
|
478
|
-
|
|
479
|
-
|
|
554
|
+
])
|
|
555
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
556
|
+
end
|
|
480
557
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
])
|
|
558
|
+
it "i;octet is case-sensitive" do
|
|
559
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
560
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
561
|
+
comp_filter("VEVENT", prop_filters: [
|
|
562
|
+
prop_filter("SUMMARY", text_match: text_match("meeting", collation: "i;octet"))
|
|
487
563
|
])
|
|
488
|
-
|
|
489
|
-
|
|
564
|
+
])
|
|
565
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
566
|
+
end
|
|
490
567
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
])
|
|
568
|
+
it "negate-condition inverts the result" do
|
|
569
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
570
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
571
|
+
comp_filter("VEVENT", prop_filters: [
|
|
572
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
|
|
497
573
|
])
|
|
498
|
-
|
|
499
|
-
|
|
574
|
+
])
|
|
575
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
576
|
+
end
|
|
500
577
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
])
|
|
578
|
+
it "negate-condition on non-match produces true" do
|
|
579
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
580
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
581
|
+
comp_filter("VEVENT", prop_filters: [
|
|
582
|
+
prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
|
|
507
583
|
])
|
|
508
|
-
|
|
509
|
-
|
|
584
|
+
])
|
|
585
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
510
586
|
end
|
|
587
|
+
end
|
|
511
588
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
])
|
|
589
|
+
describe "text-match on SUMMARY only matches SUMMARY property" do
|
|
590
|
+
it "does not match when text appears in DESCRIPTION but not SUMMARY" do
|
|
591
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nDESCRIPTION:Alice is coming\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
592
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
593
|
+
comp_filter("VEVENT", prop_filters: [
|
|
594
|
+
prop_filter("SUMMARY", text_match: text_match("Alice"))
|
|
519
595
|
])
|
|
520
|
-
|
|
521
|
-
|
|
596
|
+
])
|
|
597
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
522
598
|
end
|
|
599
|
+
end
|
|
523
600
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
describe "Addressbook filter" do
|
|
602
|
+
it "anyof returns true if any prop-filter matches" do
|
|
603
|
+
card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
|
|
604
|
+
f = ab_filter(test: "anyof", prop_filters: [
|
|
605
|
+
ab_prop_filter("FN", text_match: ab_text_match("Jane")),
|
|
606
|
+
ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
|
|
607
|
+
])
|
|
608
|
+
Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
|
|
609
|
+
end
|
|
533
610
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
611
|
+
it "allof returns true only if all prop-filters match" do
|
|
612
|
+
card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
|
|
613
|
+
f = ab_filter(test: "allof", prop_filters: [
|
|
614
|
+
ab_prop_filter("FN", text_match: ab_text_match("John")),
|
|
615
|
+
ab_prop_filter("EMAIL", text_match: ab_text_match("jane"))
|
|
616
|
+
])
|
|
617
|
+
Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal false
|
|
618
|
+
end
|
|
542
619
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
620
|
+
it "allof returns true when all match" do
|
|
621
|
+
card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
|
|
622
|
+
f = ab_filter(test: "allof", prop_filters: [
|
|
623
|
+
ab_prop_filter("FN", text_match: ab_text_match("John")),
|
|
624
|
+
ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
|
|
625
|
+
])
|
|
626
|
+
Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
|
|
627
|
+
end
|
|
551
628
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
end
|
|
629
|
+
it "empty filter list returns true" do
|
|
630
|
+
card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEND:VCARD")
|
|
631
|
+
f = ab_filter(prop_filters: [])
|
|
632
|
+
Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
|
|
557
633
|
end
|
|
634
|
+
end
|
|
558
635
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
])
|
|
636
|
+
describe "ParamFilter" do
|
|
637
|
+
it "matches when parameter exists (defined-only check)" do
|
|
638
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
639
|
+
pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
|
|
640
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
641
|
+
comp_filter("VEVENT", prop_filters: [
|
|
642
|
+
prop_filter("ATTENDEE", param_filters: [pf])
|
|
567
643
|
])
|
|
568
|
-
|
|
569
|
-
|
|
644
|
+
])
|
|
645
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
646
|
+
end
|
|
570
647
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
])
|
|
648
|
+
it "does not match when parameter absent" do
|
|
649
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
650
|
+
pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
|
|
651
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
652
|
+
comp_filter("VEVENT", prop_filters: [
|
|
653
|
+
prop_filter("ATTENDEE", param_filters: [pf])
|
|
578
654
|
])
|
|
579
|
-
|
|
580
|
-
|
|
655
|
+
])
|
|
656
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
657
|
+
end
|
|
581
658
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
])
|
|
659
|
+
it "with is_not_defined: matches when parameter absent" do
|
|
660
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
661
|
+
pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT", is_not_defined: true)
|
|
662
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
663
|
+
comp_filter("VEVENT", prop_filters: [
|
|
664
|
+
prop_filter("ATTENDEE", param_filters: [pf])
|
|
589
665
|
])
|
|
590
|
-
|
|
591
|
-
|
|
666
|
+
])
|
|
667
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
668
|
+
end
|
|
592
669
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
670
|
+
it "with text-match: matches when parameter value matches" do
|
|
671
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
672
|
+
pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
|
|
673
|
+
name: "PARTSTAT",
|
|
674
|
+
text_match: text_match("ACCEPTED", match_type: "equals")
|
|
675
|
+
)
|
|
676
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
677
|
+
comp_filter("VEVENT", prop_filters: [
|
|
678
|
+
prop_filter("ATTENDEE", param_filters: [pf])
|
|
679
|
+
])
|
|
680
|
+
])
|
|
681
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
682
|
+
end
|
|
606
683
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
end
|
|
684
|
+
it "with text-match: does not match when parameter value differs" do
|
|
685
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=DECLINED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
686
|
+
pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
|
|
687
|
+
name: "PARTSTAT",
|
|
688
|
+
text_match: text_match("ACCEPTED", match_type: "equals")
|
|
689
|
+
)
|
|
690
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
691
|
+
comp_filter("VEVENT", prop_filters: [
|
|
692
|
+
prop_filter("ATTENDEE", param_filters: [pf])
|
|
693
|
+
])
|
|
694
|
+
])
|
|
695
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
620
696
|
end
|
|
697
|
+
end
|
|
621
698
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
699
|
+
describe "time-range on VEVENT" do
|
|
700
|
+
it "matches overlapping event" do
|
|
701
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260115T090000Z\r\nDTEND:20260115T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
702
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
703
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
704
|
+
comp_filter("VEVENT", time_range: tr)
|
|
705
|
+
])
|
|
706
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
707
|
+
end
|
|
631
708
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
709
|
+
it "does not match event fully before range" do
|
|
710
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251215T090000Z\r\nDTEND:20251215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
711
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
712
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
713
|
+
comp_filter("VEVENT", time_range: tr)
|
|
714
|
+
])
|
|
715
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
716
|
+
end
|
|
640
717
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
718
|
+
it "does not match event fully after range" do
|
|
719
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260215T090000Z\r\nDTEND:20260215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
720
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
721
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
722
|
+
comp_filter("VEVENT", time_range: tr)
|
|
723
|
+
])
|
|
724
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
725
|
+
end
|
|
649
726
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
727
|
+
it "half-open: event ending exactly at range start does not match" do
|
|
728
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251231T230000Z\r\nDTEND:20260101T000000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
729
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
730
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
731
|
+
comp_filter("VEVENT", time_range: tr)
|
|
732
|
+
])
|
|
733
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
734
|
+
end
|
|
658
735
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
end
|
|
736
|
+
it "half-open: event starting exactly at range start matches" do
|
|
737
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T000000Z\r\nDTEND:20260101T010000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
738
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
739
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
740
|
+
comp_filter("VEVENT", time_range: tr)
|
|
741
|
+
])
|
|
742
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
667
743
|
end
|
|
744
|
+
end
|
|
668
745
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
746
|
+
describe "time-range on VJOURNAL" do
|
|
747
|
+
it "matches VJOURNAL with DTSTART in range" do
|
|
748
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260115T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
|
|
749
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
750
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
751
|
+
comp_filter("VJOURNAL", time_range: tr)
|
|
752
|
+
])
|
|
753
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
754
|
+
end
|
|
678
755
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
756
|
+
it "does not match VJOURNAL with DTSTART outside range" do
|
|
757
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260315T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
|
|
758
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
759
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
760
|
+
comp_filter("VJOURNAL", time_range: tr)
|
|
761
|
+
])
|
|
762
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
763
|
+
end
|
|
687
764
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
end
|
|
765
|
+
it "does not match VJOURNAL without DTSTART" do
|
|
766
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nSUMMARY:Undated\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
|
|
767
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
768
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
769
|
+
comp_filter("VJOURNAL", time_range: tr)
|
|
770
|
+
])
|
|
771
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
696
772
|
end
|
|
773
|
+
end
|
|
697
774
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
775
|
+
describe "time-range on VEVENT with RRULE" do
|
|
776
|
+
it "matches recurring event with occurrence in range" do
|
|
777
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=30\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
778
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260115T000000Z", end_time: "20260116T000000Z")
|
|
779
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
780
|
+
comp_filter("VEVENT", time_range: tr)
|
|
781
|
+
])
|
|
782
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
783
|
+
end
|
|
707
784
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
785
|
+
it "does not match recurring event when all occurrences are outside range" do
|
|
786
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
787
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260201T000000Z", end_time: "20260301T000000Z")
|
|
788
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
789
|
+
comp_filter("VEVENT", time_range: tr)
|
|
790
|
+
])
|
|
791
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
792
|
+
end
|
|
716
793
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
794
|
+
it "matches weekly recurring event" do
|
|
795
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260105T090000Z\r\nDTEND:20260105T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
796
|
+
# The 3rd occurrence (Jan 19) should be in this range
|
|
797
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260119T000000Z", end_time: "20260120T000000Z")
|
|
798
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
799
|
+
comp_filter("VEVENT", time_range: tr)
|
|
800
|
+
])
|
|
801
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
802
|
+
end
|
|
726
803
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
end
|
|
804
|
+
it "respects EXDATE when filtering recurring events" do
|
|
805
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20260102T090000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
|
|
806
|
+
# Range covers only Jan 2 — but that's excluded
|
|
807
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260102T000000Z", end_time: "20260102T120000Z")
|
|
808
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
809
|
+
comp_filter("VEVENT", time_range: tr)
|
|
810
|
+
])
|
|
811
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
736
812
|
end
|
|
813
|
+
end
|
|
737
814
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
815
|
+
describe "time-range on VTODO" do
|
|
816
|
+
it "matches VTODO with DTSTART and DUE in range" do
|
|
817
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260115T090000Z\r\nDUE:20260116T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
|
|
818
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
819
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
820
|
+
comp_filter("VTODO", time_range: tr)
|
|
821
|
+
])
|
|
822
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
823
|
+
end
|
|
747
824
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
825
|
+
it "does not match VTODO outside range" do
|
|
826
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260301T090000Z\r\nDUE:20260302T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
|
|
827
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
828
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
829
|
+
comp_filter("VTODO", time_range: tr)
|
|
830
|
+
])
|
|
831
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
|
|
832
|
+
end
|
|
756
833
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
834
|
+
it "matches VTODO with only COMPLETED in range" do
|
|
835
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nCOMPLETED:20260115T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
|
|
836
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
837
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
838
|
+
comp_filter("VTODO", time_range: tr)
|
|
839
|
+
])
|
|
840
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
841
|
+
end
|
|
765
842
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
end
|
|
843
|
+
it "matches VTODO with no dates (always matches per RFC 4791)" do
|
|
844
|
+
vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nSUMMARY:Undated task\r\nEND:VTODO\r\nEND:VCALENDAR")
|
|
845
|
+
tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
|
|
846
|
+
f = comp_filter("VCALENDAR", comp_filters: [
|
|
847
|
+
comp_filter("VTODO", time_range: tr)
|
|
848
|
+
])
|
|
849
|
+
Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
|
|
774
850
|
end
|
|
775
851
|
end
|
|
776
852
|
end
|