protocol-caldav 1.0.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8094e6e1eb67a07518af3ee93dacaf90c3503df5591c448dc5fb6254017a4e2a
4
- data.tar.gz: 8879d2477ddf90750f87451574ece2b6ab30b2b014c70c3a916487a56c532f49
3
+ metadata.gz: 4506a98c42a98f929d194a834d1cd3b9e3ba5d75824a4a2f2921d7fb67a8ee8f
4
+ data.tar.gz: 92c8aa4a1b645aacca6f03f5343cbf57d842120019401436e6f3d9781841f0ac
5
5
  SHA512:
6
- metadata.gz: 6a378a3b95b2d9825d43b6b2e1917d41558a3146ae96cdd8425d2ba0ece33ff06731ba144abed4691b7e45bee2be40fd927e99d1d8b46c145ef194e7322acea2
7
- data.tar.gz: 1cc5776a7b5148f7c3672fe6d814ee361eb4be0c93b0ce1f0dfb1feaf8a21d8706769629f295686b2d3226cda64021b4d1e68d2f3d3ec7834334b779847a36ce
6
+ metadata.gz: 7937306d109796181723817e98460e5c61efdc41c38c7a629eff641f12b12715930f16ce2042fcbada7e296541ff51a484c54465456ae876db052980a7a8d45a
7
+ data.tar.gz: 28ee48c158bba16390fb0d3f8fe855d0adcde6ecc5cb569cbf63a8dba01a27623a5a2b2cbf7388a7c2412256be53c87e8e64cf7d79d8f0224d87934e9c7aef23
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "scampi"
5
3
  require "builder"
6
4
 
7
5
  require "protocol/caldav"
@@ -21,10 +19,18 @@ module Protocol
21
19
  end
22
20
 
23
21
  def update_attrs(updates)
24
- @displayname = updates[:displayname] if updates.key?(:displayname)
25
- @description = updates[:description] if updates.key?(:description)
26
- @color = updates[:color] if updates.key?(:color)
27
- @props = (@props || {}).merge(updates[:props]) if updates.key?(:props)
22
+ if updates.key?(:displayname)
23
+ @displayname = updates[:displayname]
24
+ end
25
+ if updates.key?(:description)
26
+ @description = updates[:description]
27
+ end
28
+ if updates.key?(:color)
29
+ @color = updates[:color]
30
+ end
31
+ if updates.key?(:props)
32
+ @props = (@props || {}).merge(updates[:props])
33
+ end
28
34
  self
29
35
  end
30
36
 
@@ -33,21 +39,31 @@ module Protocol
33
39
  XmlBuilder.propstat_ok(xml) do
34
40
  xml.tag!("d:resourcetype") do
35
41
  xml.tag!("d:collection")
36
- xml.tag!("c:calendar") if @type == :calendar
37
- xml.tag!("cr:addressbook") if @type == :addressbook
42
+ if @type == :calendar
43
+ xml.tag!("c:calendar")
44
+ end
45
+ if @type == :addressbook
46
+ xml.tag!("cr:addressbook")
47
+ end
38
48
  end
39
49
 
40
- xml.tag!("d:displayname", @displayname) if @displayname
41
- xml.tag!("c:calendar-description", @description) if @description
42
- xml.tag!("x:calendar-color", @color) if @color
50
+ if @displayname
51
+ xml.tag!("d:displayname", @displayname)
52
+ end
53
+ if @description
54
+ xml.tag!("c:calendar-description", @description)
55
+ end
56
+ if @color
57
+ xml.tag!("x:calendar-color", @color)
58
+ end
43
59
 
44
60
  item_etags = @path.storage_class.list_items(@path.to_s).map { |_, data| data[:etag] }
45
61
  ctag = CTag.compute(
46
- path: @path.to_s,
62
+ path: @path.to_s,
47
63
  displayname: @displayname,
48
64
  description: @description,
49
- color: @color,
50
- item_etags: item_etags
65
+ color: @color,
66
+ item_etags: item_etags,
51
67
  )
52
68
  xml.tag!("cs:getctag", ctag)
53
69
  xml.tag!("d:sync-token", "http://caldav.local/sync/#{ctag}")
@@ -77,12 +93,20 @@ module Protocol
77
93
  XmlBuilder.response(xml, href: @path.to_s) do
78
94
  XmlBuilder.propstat_ok(xml) do
79
95
  xml.tag!("d:resourcetype")
80
- xml.tag!("d:displayname") if @displayname
81
- xml.tag!("c:calendar-description") if @description
82
- xml.tag!("x:calendar-color") if @color
96
+ if @displayname
97
+ xml.tag!("d:displayname")
98
+ end
99
+ if @description
100
+ xml.tag!("c:calendar-description")
101
+ end
102
+ if @color
103
+ xml.tag!("x:calendar-color")
104
+ end
83
105
  xml.tag!("cs:getctag")
84
106
  xml.tag!("d:sync-token")
85
- xml.tag!("c:supported-calendar-component-set") if @type == :calendar
107
+ if @type == :calendar
108
+ xml.tag!("c:supported-calendar-component-set")
109
+ end
86
110
  end
87
111
  end
88
112
  end
@@ -106,75 +130,75 @@ module Protocol
106
130
  end
107
131
  end
108
132
 
109
- test do
110
- def normalize(xml)
111
- xml.gsub(/>\s+</, '><').strip
112
- end
133
+ __END__
113
134
 
114
- # Minimal mock storage for Collection tests
115
- class MockStorageForCollection < Protocol::Caldav::Storage
116
- def list_items(_path)
117
- []
118
- end
135
+ def normalize(xml)
136
+ xml.gsub(/>\s+</, '><').strip
137
+ end
138
+
139
+ # Minimal mock storage for Collection tests
140
+ class MockStorageForCollection < Protocol::Caldav::Storage
141
+ def list_items(_path)
142
+ []
119
143
  end
144
+ end
120
145
 
121
- describe "Protocol::Caldav::Collection" do
122
- def make_collection(type: :calendar, displayname: "Work", **opts)
123
- storage = MockStorageForCollection.new
124
- path = Protocol::Caldav::Path.new("/calendars/admin/work/", storage_class: storage)
125
- Protocol::Caldav::Collection.new(path: path, type: type, displayname: displayname, **opts)
126
- end
146
+ describe "Protocol::Caldav::Collection" do
147
+ def make_collection(type: :calendar, displayname: "Work", **opts)
148
+ storage = MockStorageForCollection.new
149
+ path = Protocol::Caldav::Path.new("/calendars/admin/work/", storage_class: storage)
150
+ Protocol::Caldav::Collection.new(path: path, type: type, displayname: displayname, **opts)
151
+ end
127
152
 
128
- it "renders calendar resourcetype" do
129
- xml = make_collection(type: :calendar).to_propfind_xml
130
- xml.should.include "<c:calendar/>"
131
- end
153
+ it "renders calendar resourcetype" do
154
+ xml = make_collection(type: :calendar).to_propfind_xml
155
+ xml.should.include "<c:calendar/>"
156
+ end
132
157
 
133
- it "renders addressbook resourcetype" do
134
- xml = make_collection(type: :addressbook).to_propfind_xml
135
- xml.should.include "<cr:addressbook/>"
136
- end
158
+ it "renders addressbook resourcetype" do
159
+ xml = make_collection(type: :addressbook).to_propfind_xml
160
+ xml.should.include "<cr:addressbook/>"
161
+ end
137
162
 
138
- it "includes displayname when set" do
139
- xml = make_collection(displayname: "Work").to_propfind_xml
140
- xml.should.include "<d:displayname>Work</d:displayname>"
141
- end
163
+ it "includes displayname when set" do
164
+ xml = make_collection(displayname: "Work").to_propfind_xml
165
+ xml.should.include "<d:displayname>Work</d:displayname>"
166
+ end
142
167
 
143
- it "omits displayname when nil" do
144
- xml = make_collection(displayname: nil).to_propfind_xml
145
- xml.should.not.include "displayname"
146
- end
168
+ it "omits displayname when nil" do
169
+ xml = make_collection(displayname: nil).to_propfind_xml
170
+ xml.should.not.include "displayname"
171
+ end
147
172
 
148
- it "includes ctag" do
149
- xml = make_collection.to_propfind_xml
150
- xml.should.include "<cs:getctag>"
151
- end
173
+ it "includes ctag" do
174
+ xml = make_collection.to_propfind_xml
175
+ xml.should.include "<cs:getctag>"
176
+ end
152
177
 
153
- it "includes supported-calendar-component-set for calendars" do
154
- xml = make_collection(type: :calendar).to_propfind_xml
155
- xml.should.include "supported-calendar-component-set"
156
- end
178
+ it "includes supported-calendar-component-set for calendars" do
179
+ xml = make_collection(type: :calendar).to_propfind_xml
180
+ xml.should.include "supported-calendar-component-set"
181
+ end
157
182
 
158
- it "omits supported-calendar-component-set for addressbooks" do
159
- xml = make_collection(type: :addressbook).to_propfind_xml
160
- xml.should.not.include "supported-calendar-component-set"
161
- end
183
+ it "omits supported-calendar-component-set for addressbooks" do
184
+ xml = make_collection(type: :addressbook).to_propfind_xml
185
+ xml.should.not.include "supported-calendar-component-set"
186
+ end
162
187
 
163
- it "escapes special characters in displayname" do
164
- xml = make_collection(displayname: "Work & <Personal>").to_propfind_xml
165
- xml.should.include "Work &amp; &lt;Personal&gt;"
166
- end
188
+ it "escapes special characters in displayname" do
189
+ xml = make_collection(displayname: "Work & <Personal>").to_propfind_xml
190
+ xml.should.include "Work &amp; &lt;Personal&gt;"
191
+ end
167
192
 
168
- it "update_attrs modifies named fields" do
169
- col = make_collection(displayname: "Old")
170
- col.update_attrs(displayname: "New")
171
- col.displayname.should.equal "New"
172
- end
193
+ it "update_attrs modifies named fields" do
194
+ col = make_collection(displayname: "Old")
195
+ col.update_attrs(displayname: "New")
196
+ col.displayname.should.equal "New"
197
+ end
173
198
 
174
- it "update_attrs leaves unmentioned fields alone" do
175
- col = make_collection(displayname: "Work", description: "Desc")
176
- col.update_attrs(displayname: "New")
177
- col.description.should.equal "Desc"
178
- end
199
+ it "update_attrs leaves unmentioned fields alone" do
200
+ col = make_collection(displayname: "Work", description: "Desc")
201
+ col.update_attrs(displayname: "New")
202
+ col.description.should.equal "Desc"
179
203
  end
180
204
  end
@@ -1,15 +1,12 @@
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 Constants
9
6
  DAV_HEADERS = {
10
- 'dav' => '1, 2, 3, calendar-access, addressbook, extended-mkcol',
11
- 'allow' => 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCALENDAR, MKCOL, MOVE, REPORT',
12
- 'content-type' => 'text/xml; charset=utf-8'
7
+ 'dav' => '1, 2, 3, calendar-access, addressbook, extended-mkcol',
8
+ 'allow' => 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCALENDAR, MKCOL, MOVE, REPORT',
9
+ 'content-type' => 'text/xml; charset=utf-8',
13
10
  }.freeze
14
11
 
15
12
  DAV_NS = 'DAV:'
@@ -24,31 +21,30 @@ module Protocol
24
21
  end
25
22
  end
26
23
 
24
+ __END__
27
25
 
28
- test do
29
- describe "Protocol::Caldav::Constants" do
30
- it "DAV_HEADERS is frozen" do
31
- Protocol::Caldav::Constants::DAV_HEADERS.should.be.frozen
32
- end
26
+ describe "Protocol::Caldav::Constants" do
27
+ it "DAV_HEADERS is frozen" do
28
+ Protocol::Caldav::Constants::DAV_HEADERS.should.be.frozen
29
+ end
33
30
 
34
- it "DAV_HEADERS includes calendar-access" do
35
- Protocol::Caldav::Constants::DAV_HEADERS['dav'].should.include 'calendar-access'
36
- end
31
+ it "DAV_HEADERS includes calendar-access" do
32
+ Protocol::Caldav::Constants::DAV_HEADERS['dav'].should.include 'calendar-access'
33
+ end
37
34
 
38
- it "DAV_HEADERS includes addressbook" do
39
- Protocol::Caldav::Constants::DAV_HEADERS['dav'].should.include 'addressbook'
40
- end
35
+ it "DAV_HEADERS includes addressbook" do
36
+ Protocol::Caldav::Constants::DAV_HEADERS['dav'].should.include 'addressbook'
37
+ end
41
38
 
42
- it "defines all required namespace URIs" do
43
- Protocol::Caldav::Constants::DAV_NS.should.equal 'DAV:'
44
- Protocol::Caldav::Constants::CALDAV_NS.should.equal 'urn:ietf:params:xml:ns:caldav'
45
- Protocol::Caldav::Constants::CARDDAV_NS.should.equal 'urn:ietf:params:xml:ns:carddav'
46
- Protocol::Caldav::Constants::CALSERVER_NS.should.equal 'http://calendarserver.org/ns/'
47
- end
39
+ it "defines all required namespace URIs" do
40
+ Protocol::Caldav::Constants::DAV_NS.should.equal 'DAV:'
41
+ Protocol::Caldav::Constants::CALDAV_NS.should.equal 'urn:ietf:params:xml:ns:caldav'
42
+ Protocol::Caldav::Constants::CARDDAV_NS.should.equal 'urn:ietf:params:xml:ns:carddav'
43
+ Protocol::Caldav::Constants::CALSERVER_NS.should.equal 'http://calendarserver.org/ns/'
44
+ end
48
45
 
49
- it "defines media types" do
50
- Protocol::Caldav::Constants::CALENDAR_MEDIA_TYPE.should.equal 'text/calendar'
51
- Protocol::Caldav::Constants::VCARD_MEDIA_TYPE.should.equal 'text/vcard'
52
- end
46
+ it "defines media types" do
47
+ Protocol::Caldav::Constants::CALENDAR_MEDIA_TYPE.should.equal 'text/calendar'
48
+ Protocol::Caldav::Constants::VCARD_MEDIA_TYPE.should.equal 'text/vcard'
53
49
  end
54
50
  end
@@ -1,8 +1,5 @@
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 ContentLine
@@ -31,124 +28,130 @@ module Protocol
31
28
  end
32
29
  end
33
30
 
34
- return nil unless colon_idx
35
-
36
- left = line[0...colon_idx]
37
- value = line[(colon_idx + 1)..]
38
-
39
- parts = split_params(left)
40
- name = parts.shift
41
- params = {}
42
- parts.each do |param_str|
43
- key, val = param_str.split('=', 2)
44
- next unless key
45
- val = val[1..-2] if val&.start_with?('"') && val&.end_with?('"')
46
- params[key.upcase] = val || ''
31
+ if colon_idx
32
+ left = line[0...colon_idx]
33
+ value = line[(colon_idx + 1)..]
34
+ parts = split_params(left)
35
+ name = parts.shift
36
+ params = {}
37
+ parts.each do |param_str|
38
+ key, val = param_str.split('=', 2)
39
+ unless key
40
+ next
41
+ end
42
+ if val&.start_with?('"') && val&.end_with?('"')
43
+ val = val[1..-2]
44
+ end
45
+ params[key.upcase] = val || ''
46
+ end
47
+ [name, params, value]
48
+ else
49
+ nil
47
50
  end
48
-
49
- [name, params, value]
50
51
  end
51
52
 
52
53
  # Split the left side of a content line by semicolons,
53
54
  # respecting quoted values.
54
55
  def split_params(str)
55
- parts = []
56
- current = +''
57
- in_quotes = false
56
+ [].tap do |parts|
57
+ current = +''
58
+ in_quotes = false
59
+
60
+ str.each_char do |ch|
61
+ if ch == '"'
62
+ in_quotes = !in_quotes
63
+ current << ch
64
+ elsif ch == ';' && !in_quotes
65
+ parts << current
66
+ current = +''
67
+ else
68
+ current << ch
69
+ end
70
+ end
58
71
 
59
- str.each_char do |ch|
60
- if ch == '"'
61
- in_quotes = !in_quotes
62
- current << ch
63
- elsif ch == ';' && !in_quotes
72
+ unless current.empty?
64
73
  parts << current
65
- current = +''
66
- else
67
- current << ch
68
74
  end
69
75
  end
70
- parts << current unless current.empty?
71
- parts
72
76
  end
73
77
  end
74
78
  end
75
79
  end
76
80
 
81
+ __END__
77
82
 
78
- test do
79
- describe "Protocol::Caldav::ContentLine" do
80
- describe ".unfold" do
81
- it "unfolds CRLF SPACE continuations" do
82
- Protocol::Caldav::ContentLine.unfold("SUMMARY:Annual\r\n planning").should.equal "SUMMARY:Annualplanning"
83
- end
83
+ describe "Protocol::Caldav::ContentLine" do
84
+ describe ".unfold" do
85
+ it "unfolds CRLF SPACE continuations" do
86
+ Protocol::Caldav::ContentLine.unfold("SUMMARY:Annual\r\n planning").should.equal "SUMMARY:Annualplanning"
87
+ end
84
88
 
85
- it "unfolds CRLF TAB continuations" do
86
- Protocol::Caldav::ContentLine.unfold("SUMMARY:Annual\r\n\tplanning").should.equal "SUMMARY:Annualplanning"
87
- end
89
+ it "unfolds CRLF TAB continuations" do
90
+ Protocol::Caldav::ContentLine.unfold("SUMMARY:Annual\r\n\tplanning").should.equal "SUMMARY:Annualplanning"
91
+ end
88
92
 
89
- it "does not unfold CRLF followed by non-whitespace" do
90
- result = Protocol::Caldav::ContentLine.unfold("SUMMARY:A\r\nDTSTART:B")
91
- result.should.equal "SUMMARY:A\nDTSTART:B"
92
- end
93
+ it "does not unfold CRLF followed by non-whitespace" do
94
+ result = Protocol::Caldav::ContentLine.unfold("SUMMARY:A\r\nDTSTART:B")
95
+ result.should.equal "SUMMARY:A\nDTSTART:B"
96
+ end
93
97
 
94
- it "removes the space/tab character (not kept in value)" do
95
- Protocol::Caldav::ContentLine.unfold("X:hello\r\n world").should.equal "X:helloworld"
96
- end
98
+ it "removes the space/tab character (not kept in value)" do
99
+ Protocol::Caldav::ContentLine.unfold("X:hello\r\n world").should.equal "X:helloworld"
100
+ end
97
101
 
98
- it "handles three consecutive folded lines" do
99
- Protocol::Caldav::ContentLine.unfold("X:a\r\n b\r\n c").should.equal "X:abc"
100
- end
102
+ it "handles three consecutive folded lines" do
103
+ Protocol::Caldav::ContentLine.unfold("X:a\r\n b\r\n c").should.equal "X:abc"
104
+ end
101
105
 
102
- it "handles LF-only line endings" do
103
- Protocol::Caldav::ContentLine.unfold("X:a\n b").should.equal "X:ab"
104
- end
106
+ it "handles LF-only line endings" do
107
+ Protocol::Caldav::ContentLine.unfold("X:a\n b").should.equal "X:ab"
105
108
  end
109
+ end
106
110
 
107
- describe ".parse_line" do
108
- it "parses a property with no parameters" do
109
- name, params, value = Protocol::Caldav::ContentLine.parse_line("SUMMARY:Hello")
110
- name.should.equal "SUMMARY"
111
- params.should.equal({})
112
- value.should.equal "Hello"
113
- end
111
+ describe ".parse_line" do
112
+ it "parses a property with no parameters" do
113
+ name, params, value = Protocol::Caldav::ContentLine.parse_line("SUMMARY:Hello")
114
+ name.should.equal "SUMMARY"
115
+ params.should.equal({})
116
+ value.should.equal "Hello"
117
+ end
114
118
 
115
- it "parses a property with one parameter" do
116
- name, params, value = Protocol::Caldav::ContentLine.parse_line("DTSTART;TZID=America/New_York:20260101T090000")
117
- name.should.equal "DTSTART"
118
- params["TZID"].should.equal "America/New_York"
119
- value.should.equal "20260101T090000"
120
- end
119
+ it "parses a property with one parameter" do
120
+ name, params, value = Protocol::Caldav::ContentLine.parse_line("DTSTART;TZID=America/New_York:20260101T090000")
121
+ name.should.equal "DTSTART"
122
+ params["TZID"].should.equal "America/New_York"
123
+ value.should.equal "20260101T090000"
124
+ end
121
125
 
122
- it "parses a parameter with a quoted value" do
123
- name, params, value = Protocol::Caldav::ContentLine.parse_line('ATTENDEE;CN="Smith, John":mailto:j@e.com')
124
- params["CN"].should.equal "Smith, John"
125
- value.should.equal "mailto:j@e.com"
126
- end
126
+ it "parses a parameter with a quoted value" do
127
+ name, params, value = Protocol::Caldav::ContentLine.parse_line('ATTENDEE;CN="Smith, John":mailto:j@e.com')
128
+ params["CN"].should.equal "Smith, John"
129
+ value.should.equal "mailto:j@e.com"
130
+ end
127
131
 
128
- it "handles a colon inside a quoted parameter value" do
129
- name, params, value = Protocol::Caldav::ContentLine.parse_line('X;FOO="a:b":realvalue')
130
- params["FOO"].should.equal "a:b"
131
- value.should.equal "realvalue"
132
- end
132
+ it "handles a colon inside a quoted parameter value" do
133
+ name, params, value = Protocol::Caldav::ContentLine.parse_line('X;FOO="a:b":realvalue')
134
+ params["FOO"].should.equal "a:b"
135
+ value.should.equal "realvalue"
136
+ end
133
137
 
134
- it "handles a property value containing colons" do
135
- _, _, value = Protocol::Caldav::ContentLine.parse_line("URL:https://example.com:8080/path")
136
- value.should.equal "https://example.com:8080/path"
137
- end
138
+ it "handles a property value containing colons" do
139
+ _, _, value = Protocol::Caldav::ContentLine.parse_line("URL:https://example.com:8080/path")
140
+ value.should.equal "https://example.com:8080/path"
141
+ end
138
142
 
139
- it "handles an empty property value" do
140
- _, _, value = Protocol::Caldav::ContentLine.parse_line("DESCRIPTION:")
141
- value.should.equal ""
142
- end
143
+ it "handles an empty property value" do
144
+ _, _, value = Protocol::Caldav::ContentLine.parse_line("DESCRIPTION:")
145
+ value.should.equal ""
146
+ end
143
147
 
144
- it "returns nil for a line with no colon" do
145
- Protocol::Caldav::ContentLine.parse_line("NOCOLON").should.be.nil
146
- end
148
+ it "returns nil for a line with no colon" do
149
+ Protocol::Caldav::ContentLine.parse_line("NOCOLON").should.be.nil
150
+ end
147
151
 
148
- it "uppercases parameter names" do
149
- _, params, _ = Protocol::Caldav::ContentLine.parse_line("X;tzid=utc:val")
150
- params.key?("TZID").should.equal true
151
- end
152
+ it "uppercases parameter names" do
153
+ _, params, _ = Protocol::Caldav::ContentLine.parse_line("X;tzid=utc:val")
154
+ params.key?("TZID").should.equal true
152
155
  end
153
156
  end
154
157
  end