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.
@@ -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
@@ -23,10 +20,14 @@ module Protocol
23
20
 
24
21
  items.each do |item|
25
22
  component = Parser.parse(item.body)
26
- next unless component
23
+ unless component
24
+ next
25
+ end
27
26
 
28
27
  component.find_components('VEVENT').each do |vevent|
29
- next if vevent.find_property('RECURRENCE-ID') # skip overrides in this pass
28
+ if vevent.find_property('RECURRENCE-ID')
29
+ next
30
+ end # skip overrides in this pass
30
31
 
31
32
  status = vevent.find_property('STATUS')&.value&.strip&.upcase
32
33
  transp = vevent.find_property('TRANSP')&.value&.strip&.upcase
@@ -35,62 +36,91 @@ module Protocol
35
36
 
36
37
  rrule = vevent.find_property('RRULE')
37
38
  if rrule
38
- collect_recurring_periods(vevent, rrule, range_start, range_end, is_free, busy_periods, free_periods)
39
+ collect_recurring_periods(
40
+ vevent,
41
+ rrule,
42
+ range_start,
43
+ range_end,
44
+ is_free,
45
+ busy_periods,
46
+ free_periods,
47
+ )
39
48
  else
40
- collect_single_period(vevent, range_start, range_end, is_free, busy_periods, free_periods)
49
+ collect_single_period(
50
+ vevent,
51
+ range_start,
52
+ range_end,
53
+ is_free,
54
+ busy_periods,
55
+ free_periods,
56
+ )
41
57
  end
42
58
  end
43
59
  end
44
60
 
45
- serialize_freebusy(busy_periods, free_periods, range_start, range_end)
61
+ serialize_freebusy(
62
+ busy_periods,
63
+ free_periods,
64
+ range_start,
65
+ range_end,
66
+ )
46
67
  end
47
68
 
48
69
  def collect_single_period(vevent, range_start, range_end, is_free, busy_periods, free_periods)
49
70
  dtstart = Filter::Match.send(:parse_ical_datetime, vevent, 'DTSTART')
50
71
  dtend = Filter::Match.send(:parse_ical_datetime, vevent, 'DTEND')
51
- return unless dtstart
52
-
53
- dtend ||= dtstart + 3600 # default 1 hour
54
- return unless dtstart < range_end && dtend > range_start
55
-
56
- period = "#{format_utc(dtstart)}/#{format_utc(dtend)}"
57
- if is_free
58
- free_periods << period
72
+ if dtstart
73
+ dtend ||= dtstart + 3600
74
+ if dtstart < range_end && dtend > range_start
75
+ period = "#{format_utc(dtstart)}/#{format_utc(dtend)}"
76
+ if is_free
77
+ free_periods << period
78
+ else
79
+ busy_periods << period
80
+ end
81
+ else
82
+ nil
83
+ end
59
84
  else
60
- busy_periods << period
85
+ nil
61
86
  end
62
87
  end
63
88
 
64
89
  def collect_recurring_periods(vevent, rrule, range_start, range_end, is_free, busy_periods, free_periods)
65
90
  dtstart = Filter::Match.send(:parse_ical_datetime, vevent, 'DTSTART')
66
- return unless dtstart
67
-
68
- dtend = Filter::Match.send(:parse_ical_datetime, vevent, 'DTEND')
69
- duration = dtend ? (dtend - dtstart).to_i : 3600
70
-
71
- exdates = vevent.find_all_properties('EXDATE').filter_map do |ex|
72
- Filter::Match.send(:parse_datetime_string, ex.value.strip)
73
- end
74
-
75
- occurrences = Rrule.expand(
76
- dtstart: dtstart,
77
- rrule_value: rrule.value.strip,
78
- range_start: range_start - duration,
79
- range_end: range_end,
80
- exdates: exdates,
81
- max_count: 10000
82
- )
83
-
84
- occurrences.each do |occ_start|
85
- occ_end = occ_start + duration
86
- next unless occ_start < range_end && occ_end > range_start
87
-
88
- period = "#{format_utc(occ_start)}/#{format_utc(occ_end)}"
89
- if is_free
90
- free_periods << period
91
+ if dtstart
92
+ dtend = Filter::Match.send(:parse_ical_datetime, vevent, 'DTEND')
93
+ if dtend
94
+ duration = (dtend - dtstart).to_i
91
95
  else
92
- busy_periods << period
96
+ duration = 3600
97
+ end
98
+ exdates = vevent.find_all_properties('EXDATE').filter_map do |ex|
99
+ Filter::Match.send(:parse_datetime_string, ex.value.strip)
93
100
  end
101
+ occurrences = Rrule.expand(
102
+ dtstart: dtstart,
103
+ rrule_value: rrule.value.strip,
104
+ range_start: range_start - duration,
105
+ range_end: range_end,
106
+ exdates: exdates,
107
+ max_count: 10000,
108
+ )
109
+ occurrences.each do |occ_start|
110
+ occ_end = occ_start + duration
111
+ unless occ_start < range_end && occ_end > range_start
112
+ next
113
+ end
114
+
115
+ period = "#{format_utc(occ_start)}/#{format_utc(occ_end)}"
116
+ if is_free
117
+ free_periods << period
118
+ else
119
+ busy_periods << period
120
+ end
121
+ end
122
+ else
123
+ nil
94
124
  end
95
125
  end
96
126
 
@@ -120,42 +150,44 @@ module Protocol
120
150
  time.utc.strftime('%Y%m%dT%H%M%SZ')
121
151
  end
122
152
 
123
- private_class_method :collect_single_period, :collect_recurring_periods,
124
- :serialize_freebusy, :format_utc
153
+ private_class_method :collect_single_period,
154
+ :collect_recurring_periods,
155
+ :serialize_freebusy,
156
+ :format_utc
125
157
  end
126
158
  end
127
159
  end
128
160
  end
129
161
 
130
- test do
131
- describe "Protocol::Caldav::Ical::FreeBusy" do
132
- FakeItem = Struct.new(:body)
133
-
134
- it "returns VCALENDAR with VFREEBUSY" do
135
- ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260115T090000Z\r\nDTEND:20260115T100000Z\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR"
136
- items = [FakeItem.new(ical)]
137
- result = Protocol::Caldav::Ical::FreeBusy.generate(
138
- items,
139
- range_start: Time.utc(2026, 1, 1),
140
- range_end: Time.utc(2026, 2, 1)
141
- )
142
- result.should.include "BEGIN:VCALENDAR"
143
- result.should.include "BEGIN:VFREEBUSY"
144
- result.should.include "FREEBUSY;FBTYPE=BUSY:20260115T090000Z/20260115T100000Z"
145
- result.should.include "END:VFREEBUSY"
146
- result.should.include "END:VCALENDAR"
147
- end
162
+ __END__
163
+
164
+ describe "Protocol::Caldav::Ical::FreeBusy" do
165
+ FakeItem = Struct.new(:body)
166
+
167
+ it "returns VCALENDAR with VFREEBUSY" do
168
+ ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260115T090000Z\r\nDTEND:20260115T100000Z\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR"
169
+ items = [FakeItem.new(ical)]
170
+ result = Protocol::Caldav::Ical::FreeBusy.generate(
171
+ items,
172
+ range_start: Time.utc(2026, 1, 1),
173
+ range_end: Time.utc(2026, 2, 1)
174
+ )
175
+ result.should.include "BEGIN:VCALENDAR"
176
+ result.should.include "BEGIN:VFREEBUSY"
177
+ result.should.include "FREEBUSY;FBTYPE=BUSY:20260115T090000Z/20260115T100000Z"
178
+ result.should.include "END:VFREEBUSY"
179
+ result.should.include "END:VCALENDAR"
180
+ end
148
181
 
149
- it "empty items list returns empty freebusy" do
150
- result = Protocol::Caldav::Ical::FreeBusy.generate(
151
- [],
152
- range_start: Time.utc(2026, 1, 1),
153
- range_end: Time.utc(2026, 2, 1)
154
- )
155
- result.should.include "BEGIN:VCALENDAR"
156
- result.should.include "BEGIN:VFREEBUSY"
157
- result.should.include "END:VFREEBUSY"
158
- result.should.not.include "FREEBUSY;FBTYPE=BUSY"
159
- end
182
+ it "empty items list returns empty freebusy" do
183
+ result = Protocol::Caldav::Ical::FreeBusy.generate(
184
+ [],
185
+ range_start: Time.utc(2026, 1, 1),
186
+ range_end: Time.utc(2026, 2, 1)
187
+ )
188
+ result.should.include "BEGIN:VCALENDAR"
189
+ result.should.include "BEGIN:VFREEBUSY"
190
+ result.should.include "END:VFREEBUSY"
191
+ result.should.not.include "FREEBUSY;FBTYPE=BUSY"
160
192
  end
161
193
  end
@@ -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
@@ -12,45 +9,49 @@ module Protocol
12
9
  module_function
13
10
 
14
11
  def parse(text)
15
- return nil if text.nil? || text.strip.empty?
16
-
17
- # Strip BOM
18
- text = text.sub(/\A\xEF\xBB\xBF/, '')
19
-
20
- lines = ContentLine.unfold(text).split("\n").map(&:strip).reject(&:empty?)
21
- stack = []
22
- current = nil
23
-
24
- lines.each do |line|
25
- parsed = ContentLine.parse_line(line)
26
- next unless parsed
27
-
28
- name, params, value = parsed
29
-
30
- if name.casecmp?('BEGIN')
31
- comp = Component.new(name: value.strip.upcase)
32
- if current
33
- stack.push(current)
12
+ if text.nil? || text.strip.empty?
13
+ nil
14
+ else
15
+ text = text.sub(/\A\xEF\xBB\xBF/, '')
16
+ lines = ContentLine.unfold(text).split("\n").map(&:strip).reject(&:empty?)
17
+ stack = []
18
+ current = nil
19
+ lines.each do |line|
20
+ parsed = ContentLine.parse_line(line)
21
+ unless parsed
22
+ next
34
23
  end
35
- current = comp
36
- elsif name.casecmp?('END')
37
- end_name = value.strip.upcase
38
- raise ParseError, "Mismatched END:#{end_name} (expected END:#{current&.name})" if current.nil? || !current.name.casecmp?(end_name)
39
24
 
40
- if stack.empty?
41
- return current
25
+ name, params, value = parsed
26
+
27
+ if name.casecmp?('BEGIN')
28
+ comp = Component.new(name: value.strip.upcase)
29
+ if current
30
+ stack.push(current)
31
+ end
32
+ current = comp
33
+ elsif name.casecmp?('END')
34
+ end_name = value.strip.upcase
35
+ if current.nil? || !current.name.casecmp?(end_name)
36
+ raise ParseError, "Mismatched END:#{end_name} (expected END:#{current&.name})"
37
+ end
38
+
39
+ if stack.empty?
40
+ break
41
+ else
42
+ parent = stack.pop
43
+ parent.components << current
44
+ current = parent
45
+ end
42
46
  else
43
- parent = stack.pop
44
- parent.components << current
45
- current = parent
47
+ current&.properties&.push(Property.new(name: name, params: params, value: value))
46
48
  end
47
- else
48
- current&.properties&.push(Property.new(name: name, params: params, value: value))
49
49
  end
50
+ if current && stack.any?
51
+ raise ParseError, "Unclosed component: #{current&.name}"
52
+ end
53
+ current
50
54
  end
51
-
52
- raise ParseError, "Unclosed component: #{current&.name}" if current && stack.any?
53
- current
54
55
  end
55
56
  end
56
57
  end
@@ -59,133 +60,132 @@ module Protocol
59
60
  end
60
61
  end
61
62
 
63
+ __END__
62
64
 
63
- test do
64
- describe "Protocol::Caldav::Ical::Parser" do
65
- def parse(text)
66
- Protocol::Caldav::Ical::Parser.parse(text)
67
- end
65
+ describe "Protocol::Caldav::Ical::Parser" do
66
+ def parse(text)
67
+ Protocol::Caldav::Ical::Parser.parse(text)
68
+ end
68
69
 
69
- describe "line unfolding" do
70
- it "unfolds CRLF SPACE continuations" do
71
- ical = "BEGIN:VCALENDAR\r\nSUMMARY:Annual\r\n planning\r\nEND:VCALENDAR"
72
- c = parse(ical)
73
- c.find_property("SUMMARY").value.should.equal "Annualplanning"
74
- end
70
+ describe "line unfolding" do
71
+ it "unfolds CRLF SPACE continuations" do
72
+ ical = "BEGIN:VCALENDAR\r\nSUMMARY:Annual\r\n planning\r\nEND:VCALENDAR"
73
+ c = parse(ical)
74
+ c.find_property("SUMMARY").value.should.equal "Annualplanning"
75
+ end
75
76
 
76
- it "unfolds CRLF TAB continuations" do
77
- ical = "BEGIN:VCALENDAR\r\nSUMMARY:Annual\r\n\tplanning\r\nEND:VCALENDAR"
78
- c = parse(ical)
79
- c.find_property("SUMMARY").value.should.equal "Annualplanning"
80
- end
77
+ it "unfolds CRLF TAB continuations" do
78
+ ical = "BEGIN:VCALENDAR\r\nSUMMARY:Annual\r\n\tplanning\r\nEND:VCALENDAR"
79
+ c = parse(ical)
80
+ c.find_property("SUMMARY").value.should.equal "Annualplanning"
81
+ end
81
82
 
82
- it "handles three consecutive folded lines" do
83
- ical = "BEGIN:VCALENDAR\r\nX-LONG:a\r\n b\r\n c\r\nEND:VCALENDAR"
84
- c = parse(ical)
85
- c.find_property("X-LONG").value.should.equal "abc"
86
- end
83
+ it "handles three consecutive folded lines" do
84
+ ical = "BEGIN:VCALENDAR\r\nX-LONG:a\r\n b\r\n c\r\nEND:VCALENDAR"
85
+ c = parse(ical)
86
+ c.find_property("X-LONG").value.should.equal "abc"
87
87
  end
88
+ end
88
89
 
89
- describe "component nesting" do
90
- it "parses a flat VCALENDAR with no children" do
91
- c = parse("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR")
92
- c.name.should.equal "VCALENDAR"
93
- c.components.should.equal []
94
- end
90
+ describe "component nesting" do
91
+ it "parses a flat VCALENDAR with no children" do
92
+ c = parse("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR")
93
+ c.name.should.equal "VCALENDAR"
94
+ c.components.should.equal []
95
+ end
95
96
 
96
- it "parses one VEVENT inside VCALENDAR" do
97
- ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Test\r\nEND:VEVENT\r\nEND:VCALENDAR"
98
- c = parse(ical)
99
- c.components.length.should.equal 1
100
- c.components[0].name.should.equal "VEVENT"
101
- c.components[0].find_property("SUMMARY").value.should.equal "Test"
102
- end
97
+ it "parses one VEVENT inside VCALENDAR" do
98
+ ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Test\r\nEND:VEVENT\r\nEND:VCALENDAR"
99
+ c = parse(ical)
100
+ c.components.length.should.equal 1
101
+ c.components[0].name.should.equal "VEVENT"
102
+ c.components[0].find_property("SUMMARY").value.should.equal "Test"
103
+ end
103
104
 
104
- it "parses multiple sibling VEVENTs" do
105
- ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:A\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nSUMMARY:B\r\nEND:VEVENT\r\nEND:VCALENDAR"
106
- c = parse(ical)
107
- c.components.length.should.equal 2
108
- end
105
+ it "parses multiple sibling VEVENTs" do
106
+ ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:A\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nSUMMARY:B\r\nEND:VEVENT\r\nEND:VCALENDAR"
107
+ c = parse(ical)
108
+ c.components.length.should.equal 2
109
+ end
109
110
 
110
- it "parses VALARM nested inside VEVENT" do
111
- ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nEND:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR"
112
- c = parse(ical)
113
- vevent = c.components[0]
114
- vevent.components.length.should.equal 1
115
- vevent.components[0].name.should.equal "VALARM"
116
- end
111
+ it "parses VALARM nested inside VEVENT" do
112
+ ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nEND:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR"
113
+ c = parse(ical)
114
+ vevent = c.components[0]
115
+ vevent.components.length.should.equal 1
116
+ vevent.components[0].name.should.equal "VALARM"
117
+ end
117
118
 
118
- it "raises on mismatched END" do
119
- ical = "BEGIN:VEVENT\r\nEND:VTODO"
120
- lambda { parse(ical) }.should.raise Protocol::Caldav::ParseError
121
- end
119
+ it "raises on mismatched END" do
120
+ ical = "BEGIN:VEVENT\r\nEND:VTODO"
121
+ lambda { parse(ical) }.should.raise Protocol::Caldav::ParseError
122
+ end
122
123
 
123
- it "handles BEGIN/END names case-insensitively" do
124
- c = parse("begin:vcalendar\r\nend:vcalendar")
125
- c.name.should.equal "VCALENDAR"
126
- end
124
+ it "handles BEGIN/END names case-insensitively" do
125
+ c = parse("begin:vcalendar\r\nend:vcalendar")
126
+ c.name.should.equal "VCALENDAR"
127
+ end
127
128
 
128
- it "preserves component order" do
129
- ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR"
130
- c = parse(ical)
131
- c.components[0].name.should.equal "VEVENT"
132
- c.components[1].name.should.equal "VTODO"
133
- end
129
+ it "preserves component order" do
130
+ ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR"
131
+ c = parse(ical)
132
+ c.components[0].name.should.equal "VEVENT"
133
+ c.components[1].name.should.equal "VTODO"
134
134
  end
135
+ end
135
136
 
136
- describe "property parsing" do
137
- it "parses a property with no parameters" do
138
- c = parse("BEGIN:VCALENDAR\r\nSUMMARY:Hello\r\nEND:VCALENDAR")
139
- c.find_property("SUMMARY").value.should.equal "Hello"
140
- c.find_property("SUMMARY").params.should.equal({})
141
- end
137
+ describe "property parsing" do
138
+ it "parses a property with no parameters" do
139
+ c = parse("BEGIN:VCALENDAR\r\nSUMMARY:Hello\r\nEND:VCALENDAR")
140
+ c.find_property("SUMMARY").value.should.equal "Hello"
141
+ c.find_property("SUMMARY").params.should.equal({})
142
+ end
142
143
 
143
- it "parses a property with one parameter" do
144
- c = parse("BEGIN:VCALENDAR\r\nDTSTART;TZID=America/New_York:20260101T090000\r\nEND:VCALENDAR")
145
- p = c.find_property("DTSTART")
146
- p.params["TZID"].should.equal "America/New_York"
147
- p.value.should.equal "20260101T090000"
148
- end
144
+ it "parses a property with one parameter" do
145
+ c = parse("BEGIN:VCALENDAR\r\nDTSTART;TZID=America/New_York:20260101T090000\r\nEND:VCALENDAR")
146
+ p = c.find_property("DTSTART")
147
+ p.params["TZID"].should.equal "America/New_York"
148
+ p.value.should.equal "20260101T090000"
149
+ end
149
150
 
150
- it "handles a property value containing colons" do
151
- c = parse("BEGIN:VCALENDAR\r\nURL:https://example.com:8080/path\r\nEND:VCALENDAR")
152
- c.find_property("URL").value.should.equal "https://example.com:8080/path"
153
- end
151
+ it "handles a property value containing colons" do
152
+ c = parse("BEGIN:VCALENDAR\r\nURL:https://example.com:8080/path\r\nEND:VCALENDAR")
153
+ c.find_property("URL").value.should.equal "https://example.com:8080/path"
154
+ end
154
155
 
155
- it "handles an empty property value" do
156
- c = parse("BEGIN:VCALENDAR\r\nDESCRIPTION:\r\nEND:VCALENDAR")
157
- c.find_property("DESCRIPTION").value.should.equal ""
158
- end
156
+ it "handles an empty property value" do
157
+ c = parse("BEGIN:VCALENDAR\r\nDESCRIPTION:\r\nEND:VCALENDAR")
158
+ c.find_property("DESCRIPTION").value.should.equal ""
159
+ end
159
160
 
160
- it "handles X-prefix custom properties" do
161
- c = parse("BEGIN:VCALENDAR\r\nX-WR-CALNAME:My Calendar\r\nEND:VCALENDAR")
162
- c.find_property("X-WR-CALNAME").value.should.equal "My Calendar"
163
- end
161
+ it "handles X-prefix custom properties" do
162
+ c = parse("BEGIN:VCALENDAR\r\nX-WR-CALNAME:My Calendar\r\nEND:VCALENDAR")
163
+ c.find_property("X-WR-CALNAME").value.should.equal "My Calendar"
164
164
  end
165
+ end
165
166
 
166
- describe "edge cases" do
167
- it "returns nil for empty input" do
168
- parse("").should.be.nil
169
- end
167
+ describe "edge cases" do
168
+ it "returns nil for empty input" do
169
+ parse("").should.be.nil
170
+ end
170
171
 
171
- it "returns nil for nil input" do
172
- parse(nil).should.be.nil
173
- end
172
+ it "returns nil for nil input" do
173
+ parse(nil).should.be.nil
174
+ end
174
175
 
175
- it "tolerates trailing whitespace and blank lines" do
176
- c = parse("BEGIN:VCALENDAR\r\n\r\nVERSION:2.0\r\n\r\nEND:VCALENDAR\r\n \r\n")
177
- c.name.should.equal "VCALENDAR"
178
- end
176
+ it "tolerates trailing whitespace and blank lines" do
177
+ c = parse("BEGIN:VCALENDAR\r\n\r\nVERSION:2.0\r\n\r\nEND:VCALENDAR\r\n \r\n")
178
+ c.name.should.equal "VCALENDAR"
179
+ end
179
180
 
180
- it "tolerates BOM at start of file" do
181
- c = parse("\xEF\xBB\xBFBEGIN:VCALENDAR\r\nEND:VCALENDAR")
182
- c.name.should.equal "VCALENDAR"
183
- end
181
+ it "tolerates BOM at start of file" do
182
+ c = parse("\xEF\xBB\xBFBEGIN:VCALENDAR\r\nEND:VCALENDAR")
183
+ c.name.should.equal "VCALENDAR"
184
+ end
184
185
 
185
- it "tolerates LF-only line endings" do
186
- c = parse("BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR")
187
- c.name.should.equal "VCALENDAR"
188
- end
186
+ it "tolerates LF-only line endings" do
187
+ c = parse("BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR")
188
+ c.name.should.equal "VCALENDAR"
189
189
  end
190
190
  end
191
191
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "scampi"
5
-
6
3
  module Protocol
7
4
  module Caldav
8
5
  module Ical
9
- Property = Struct.new(:name, :params, :value, keyword_init: true) do
6
+ Property = Struct.new(
7
+ :name,
8
+ :params,
9
+ :value,
10
+ keyword_init: true,
11
+ ) do
10
12
  def param(key)
11
13
  params[key.upcase]
12
14
  end
@@ -15,25 +17,24 @@ module Protocol
15
17
  end
16
18
  end
17
19
 
20
+ __END__
18
21
 
19
- test do
20
- describe "Protocol::Caldav::Ical::Property" do
21
- it "exposes name, params, value" do
22
- p = Protocol::Caldav::Ical::Property.new(name: "SUMMARY", params: {}, value: "Hello")
23
- p.name.should.equal "SUMMARY"
24
- p.params.should.equal({})
25
- p.value.should.equal "Hello"
26
- end
22
+ describe "Protocol::Caldav::Ical::Property" do
23
+ it "exposes name, params, value" do
24
+ p = Protocol::Caldav::Ical::Property.new(name: "SUMMARY", params: {}, value: "Hello")
25
+ p.name.should.equal "SUMMARY"
26
+ p.params.should.equal({})
27
+ p.value.should.equal "Hello"
28
+ end
27
29
 
28
- it "param lookup is case-insensitive on parameter names" do
29
- p = Protocol::Caldav::Ical::Property.new(name: "DTSTART", params: {"TZID" => "UTC"}, value: "x")
30
- p.param("tzid").should.equal "UTC"
31
- p.param("TZID").should.equal "UTC"
32
- end
30
+ it "param lookup is case-insensitive on parameter names" do
31
+ p = Protocol::Caldav::Ical::Property.new(name: "DTSTART", params: {"TZID" => "UTC"}, value: "x")
32
+ p.param("tzid").should.equal "UTC"
33
+ p.param("TZID").should.equal "UTC"
34
+ end
33
35
 
34
- it "value is the raw string, no type coercion" do
35
- p = Protocol::Caldav::Ical::Property.new(name: "DTSTART", params: {}, value: "20260101T090000Z")
36
- p.value.should.equal "20260101T090000Z"
37
- end
36
+ it "value is the raw string, no type coercion" do
37
+ p = Protocol::Caldav::Ical::Property.new(name: "DTSTART", params: {}, value: "20260101T090000Z")
38
+ p.value.should.equal "20260101T090000Z"
38
39
  end
39
40
  end