caldav.rb 0.1.0 → 0.2.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: 1bade6bf8d4171a511851eab788d9a4e0c6926ee1cfa8811ce61ba1a8b4e61ba
4
- data.tar.gz: 428761e5e9978cab9cae7ae1e15a38cbec9dade3b3547b2be5168851abc46645
3
+ metadata.gz: 78f2d9632906e82c83ef2a9f7368fedd3a6e6ec473b1bf5225963ca3e298fef0
4
+ data.tar.gz: fd60c2113bc0059685560b89052edcfd70c2e85bda4359e2be03b48a49196688
5
5
  SHA512:
6
- metadata.gz: 7f43dad1403215dc884edd5ad4cd54e7f5d1ef30f72926c949be39c30ddc141fde805b895f057533e6a7acd5b2a2fe822de29557f47971ba1e308b147650fa70
7
- data.tar.gz: 96ce0a9f7c0894180276d7d33e22b4b5df287ddd1fdbfc9bdbfed589021cb14e650bc10d77a6ce33e46d0b9a84684363f4c21d63a5deb8cc87ca7b5a9b20c55c
6
+ metadata.gz: e4b935fdde691594b182f27fe4aed10e43017a986b1d4de04ec47443c7090d0485714b2ec424ddf760de9363d484e7be899a9d8750757e99d5919273bef401ba
7
+ data.tar.gz: d26b9a87746ea1e8abdfc2c20f31c641e6980a78d95ccceb0eb1ba1f3228a3540b5b6e5e0ebd5c4447426d74226b236dded24893e3b0212ef8480ff1d74a6a70
data/CHANGELOG CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260811
4
+
5
+ 0.2.0: Begin Layer 2 (Objects) — Calendar value objects and object-returning discovery.
6
+
7
+ 1. + CalDAV::Calendar, a value object (path, display_name, ctag, color, description, timezone, supported_components), with .from_resource mapping a CalDAV::Resource, and .all(client:, home:) discovering a home's calendars as Calendars — the eodhd model-class-method form; the client stays the low-level raw path.
8
+ 2. + the object-layer entry points: require 'CalDAV/Objects' (default, with extensions) and require 'CalDAV/Objects/Core' (objects without the extension accessors).
9
+ 3. + a composable vendor-extension layer: lib/CalDAV/Extensions/CalendarServer.rb (getctag) and Apple.rb (calendar-color). Each is a module that prepends onto the client to add its namespace and property to the calendar-discovery request and mixes a reader into CalDAV::Resource, composing via super so vendors stack rather than clobber. Layer 1 stays strictly RFC 4791 (empty base hooks); the object layer loads both by default, 'CalDAV/Objects/Core' loads neither, and a consumer may require a single vendor. from_resource reads ctag/color opportunistically.
10
+ 4. + CalDAV::Resource#display_name (DAV:displayname).
11
+ 5. ~ CalDAV#calendars: returns CalDAV::Resource objects rather than href strings, from a fuller PROPFIND (displayname, calendar-description, calendar-timezone, supported-calendar-component-set). Callers wanting raw read the resources directly; Calendar.all maps them.
12
+
13
+
3
14
  ## 20260623
4
15
 
5
16
  0.1.0: Complete the RFC 4791 §5.2 navigation surface.
@@ -14,6 +25,7 @@
14
25
  8. ~ README navigation-accessor list.
15
26
  9. ~ VERSION: /0.0.1/0.1.0/
16
27
 
28
+
17
29
  ## 20260621
18
30
 
19
31
  0.0.1: Lowercase load file; require VERSION; MultiStatus tests.
@@ -24,6 +36,7 @@
24
36
  4. + CalDAV::MultiStatus unit test, covering the two overrides over WebDAV::MultiStatus: parse_response (wraps each response as a CalDAV::Resource, in order) and parse_properties (a pretty-printed nested property is kept as serialised markup rather than collapsing to inter-tag whitespace).
25
37
  5. ~ VERSION: /0.0.0/0.0.1/
26
38
 
39
+
27
40
  ## 20260620
28
41
 
29
42
  0.0.0: Initial release.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # caldav.rb
2
2
 
3
- A CalDAV client library for Ruby, built on [webdav](https://github.com/thoran/webdav) and implementing RFC 4791. This release ships **Layer 1 (Protocol)** of a planned three-layer ecosystem.
3
+ A CalDAV client library for Ruby, built on [webdav](https://github.com/thoran/webdav) and implementing RFC 4791. It ships **Layer 1 (Protocol)** and the beginning of **Layer 2 (Objects)** — Calendar objects — of a planned three-layer ecosystem.
4
4
 
5
5
 
6
6
  ## Installation
@@ -18,11 +18,11 @@ gem 'caldav.rb'
18
18
 
19
19
  ## Concepts
20
20
 
21
- caldav.rb is designed in three layers, each a strict superset of the one below. This release ships Layer 1 only; the higher layers are future releases with reserved require paths.
21
+ caldav.rb is designed in three layers, each a strict superset of the one below. Layer 1 is complete; Layer 2 has begun with Calendar objects; its Event and Principal objects, and all of Layer 3, are future releases.
22
22
 
23
- - **Layer 1 — Protocol** (this release). The CalDAV verbs, multistatus responses, and namespace-aware navigation accessors. The equivalent of a raw protocol library: it returns CalDAV-typed responses but does not parse iCalendar payloads or construct domain objects. Most users will want Layer 2 once it ships; until then, the protocol layer is usable directly by reading `resource.calendar_data` and parsing it with the `icalendar` gem.
24
- - **Layer 2 — Objects** (future, `require 'caldav.rb/objects'`). `CalDAV::Event`, `CalDAV::Calendar`, and `CalDAV::Principal` value objects with iCalendar parsing and convenience predicates.
25
- - **Layer 3 — Queryable** (future, `require 'caldav.rb/queryable'`). A [Namo](https://github.com/thoran/namo)-backed `CalDAV::Query::Calendar` exposing events as queryable rows with derived columns.
23
+ - **Layer 1 — Protocol.** The CalDAV verbs, multistatus responses, and namespace-aware navigation accessors. The equivalent of a raw protocol library: it returns CalDAV-typed responses but does not parse iCalendar payloads or construct domain objects. For events, read `resource.calendar_data` and parse it with the `icalendar` gem until Layer 2's `CalDAV::Event` lands.
24
+ - **Layer 2 — Objects** (`require 'CalDAV/Objects'`). Value objects over the protocol layer. `CalDAV::Calendar` and object-returning discovery (`CalDAV::Calendar.all`) ship now, with vendor properties (getctag, calendar-color) available and separable (`require 'CalDAV/Objects/Core'` to omit them). `CalDAV::Event` (with iCalendar parsing) and `CalDAV::Principal` are still to come.
25
+ - **Layer 3 — Queryable** (future, `require 'CalDAV/Queryable'`). A [Namo](https://github.com/thoran/namo)-backed `CalDAV::Query::Calendar` exposing events as queryable rows with derived columns.
26
26
 
27
27
 
28
28
  ## Usage
@@ -33,12 +33,43 @@ require 'caldav.rb'
33
33
  caldav = CalDAV.new('https://caldav.example.com/dav/', username: 'user', password: 'pass')
34
34
  ```
35
35
 
36
+ The object layer (`require 'CalDAV/Objects'`) is the high-level API; today it covers calendar discovery. Everything else below is the protocol layer it is built on — which you can always use directly for raw access.
37
+
36
38
  ### Discovery
37
39
 
40
+ With the object layer, listing calendars returns `CalDAV::Calendar` objects — `Calendar.all` walks principal → home-set → calendars for you:
41
+
42
+ ```ruby
43
+ require 'CalDAV/Objects'
44
+ CalDAV::Calendar.all(client: caldav).each{|calendar| puts calendar.display_name}
45
+ ```
46
+
47
+ Each `Calendar` exposes its collection's properties:
48
+
49
+ ```ruby
50
+ calendar = CalDAV::Calendar.all(client: caldav).first
51
+ calendar.path # "/dav/calendars/user/work/"
52
+ calendar.description # the calendar-description string
53
+ calendar.timezone # the raw VTIMEZONE string, if the server sets one
54
+ calendar.supported_components # the raw supported-component-set (unparsed markup)
55
+ calendar.ctag # change tag for cheap sync-detection (vendor extension)
56
+ calendar.color # the calendar colour (vendor extension)
57
+ calendar.to_s # display name, or path when unnamed
58
+ ```
59
+
60
+ `ctag` and `color` come from the vendor extensions, loaded by default. Require the core objects instead to omit them — the readers stay, but return `nil`:
61
+
62
+ ```ruby
63
+ require 'CalDAV/Objects/Core' # strictly RFC 4791 — no vendor extensions
64
+ CalDAV::Calendar.all(client: caldav).first.ctag # => nil
65
+ ```
66
+
67
+ Drop to the protocol layer when you want the raw resources, or aren't loading the object layer — the same walk, step by step:
68
+
38
69
  ```ruby
39
70
  principal = caldav.current_user_principal
40
71
  home = caldav.calendar_home_set(principal)
41
- caldav.calendars(home).each{|href| puts href}
72
+ caldav.calendars(home).each{|resource| puts resource.href} # CalDAV::Resource objects
42
73
  ```
43
74
 
44
75
  Each discovery helper defaults its argument to the result of the previous step, so `caldav.calendars` alone walks principal → home-set → calendars.
@@ -77,18 +108,30 @@ caldav.mkcalendar('/calendars/user/new/', body: mkcalendar_xml)
77
108
 
78
109
  ## Methods
79
110
 
80
- ### Protocol verbs (RFC 4791)
111
+ ### Layer 1 — Protocol (`require 'caldav.rb'`)
112
+
113
+ Raw CalDAV-typed responses and navigation resources; no domain objects.
114
+
115
+ #### Protocol verbs (RFC 4791)
81
116
 
82
117
  - `mkcalendar(path, body:)` — §5.3.1. Create a calendar collection. Returns a `WebDAV::Response`.
83
118
  - `calendar_query(path, body:, depth:)` — §7.8. REPORT with a `<c:calendar-query>` body. Returns a `CalDAV::MultiStatus`.
84
119
  - `calendar_multiget(path, body:, depth:)` — §7.9. REPORT with a `<c:calendar-multiget>` body. Returns a `CalDAV::MultiStatus`.
85
120
  - `freebusy_query(path, body:, depth:)` — §7.10. REPORT with a `<c:free-busy-query>` body. Returns a raw `WebDAV::Response` carrying VFREEBUSY iCalendar data — **not** a multistatus. This asymmetry is inherent to the CalDAV spec.
86
121
 
87
- ### Discovery
122
+ #### Discovery
88
123
 
89
124
  - `current_user_principal(path = base URL path)` — returns the principal URL string. PROPFINDs the base URL's path by default; pass a path to start discovery elsewhere.
90
125
  - `calendar_home_set(principal = current_user_principal)` — returns the calendar-home-set URL string.
91
- - `calendars(home = calendar_home_set)` — returns an array of calendar collection URL strings.
126
+ - `calendars(home = calendar_home_set)` — returns the calendar-collection `CalDAV::Resource`s.
127
+
128
+ ### Layer 2 — Objects (`require 'CalDAV/Objects'`)
129
+
130
+ Value objects built over Layer 1. The default require also loads the vendor extensions (`getctag`, `calendar-color`); `require 'CalDAV/Objects/Core'` omits them.
131
+
132
+ - `CalDAV::Calendar.all(client:, home: nil)` — discovers a home's calendars and returns `CalDAV::Calendar` objects. `home` defaults to the client's discovered calendar-home-set.
133
+ - `CalDAV::Calendar.from_resource(resource)` — maps a single `CalDAV::Resource` to a `CalDAV::Calendar`.
134
+ - `CalDAV::Calendar` readers — `path`, `display_name`, `description`, `timezone`, `supported_components`, `ctag`, `color`; `ctag` and `color` are `nil` unless the vendor extensions are loaded. `to_s` gives the display name or path.
92
135
 
93
136
 
94
137
  ## Responses
@@ -96,6 +139,7 @@ caldav.mkcalendar('/calendars/user/new/', body: mkcalendar_xml)
96
139
  The REPORT verbs return `CalDAV::MultiStatus`, a type-preserving subclass of `WebDAV::MultiStatus` whose `resources` are `CalDAV::Resource` objects. Each `CalDAV::Resource` adds CalDAV-namespace navigation accessors over the underlying webdav resource:
97
140
 
98
141
  - `href` — the resource URL
142
+ - `display_name` — the human-readable name from `<d:displayname>`
99
143
  - `calendar_data` — the iCalendar string from `<c:calendar-data>`
100
144
  - `calendar_description` — `<c:calendar-description>`
101
145
  - `supported_calendar_component_set` — `<c:supported-calendar-component-set>`
@@ -113,13 +157,13 @@ These are strictly navigation: they return raw strings and values, never parsed
113
157
 
114
158
  ## Limitations
115
159
 
116
- This is a Layer 1 protocol release. Known boundaries:
160
+ The client reads but does not write, and the object layer is only beginning. Known boundaries:
117
161
 
118
- - **Read-only.** Conditional writes need `If-Match`, which webdav's `put` does not yet expose; writes arrive with Layer 2.
119
- - **No iCalendar parsing.** Use the `icalendar` gem on `resource.calendar_data` if you need parsed events now.
162
+ - **Read-only.** No creating, updating or deleting yet — conditional writes need `If-Match`, which webdav's `put` does not yet expose. Writes are a later increment, beyond Layer 2's read-only objects, not part of them.
163
+ - **No iCalendar parsing.** Use the `icalendar` gem on `resource.calendar_data` if you need parsed events now, until Layer 2's `CalDAV::Event` lands.
120
164
  - **Basic auth only.** No OAuth.
121
165
  - **Tested against one real CalDAV server.** Other servers should work but are unverified.
122
- - **No sync-collection.** Incremental sync is deferred.
166
+ - **No sync-collection.** `getctag` gives coarse per-collection change-detection now; fine-grained sync-collection (RFC 6578) is deferred.
123
167
 
124
168
 
125
169
  ## Dependencies
@@ -0,0 +1,67 @@
1
+ # CalDAV/Calendar.rb
2
+ # CalDAV::Calendar
3
+
4
+ require 'webdav'
5
+
6
+ class CalDAV < WebDAV
7
+ class Calendar
8
+ attr_reader\
9
+ :path,
10
+ :display_name,
11
+ :ctag,
12
+ :color,
13
+ :description,
14
+ :timezone,
15
+ :supported_components
16
+
17
+ class << self
18
+ # Discovers the calendars in a home collection and returns them as
19
+ # Calendars — the high-level, object-returning entry point. The CalDAV
20
+ # client stays the low-level path for anyone wanting the raw resources.
21
+ # home defaults to the client's discovered calendar-home-set.
22
+ def all(client:, home: nil)
23
+ load(client: client, home: home)
24
+ end
25
+
26
+ # Builds a Calendar from a CalDAV::Resource (as returned by a calendar-home
27
+ # PROPFIND). Strictly a mapping over the resource's navigation accessors — no
28
+ # parsing. ctag and color are read opportunistically: populated only when
29
+ # the extension accessors are loaded (require 'CalDAV/Objects' pulls them
30
+ # in; 'CalDAV/Objects/Core' does not), and nil otherwise.
31
+ def from_resource(resource)
32
+ new(
33
+ path: resource.href,
34
+ display_name: resource.display_name,
35
+ ctag: (resource.getctag if resource.respond_to?(:getctag)),
36
+ color: (resource.calendar_color if resource.respond_to?(:calendar_color)),
37
+ description: resource.calendar_description,
38
+ timezone: resource.calendar_timezone,
39
+ supported_components: resource.supported_calendar_component_set
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def load(client:, home: nil)
46
+ resources = home ? client.calendars(home) : client.calendars
47
+ resources.collect{|resource| from_resource(resource)}
48
+ end
49
+ end
50
+
51
+ def to_s
52
+ display_name || path
53
+ end
54
+
55
+ private
56
+
57
+ def initialize(path:, display_name: nil, ctag: nil, color: nil, description: nil, timezone: nil, supported_components: nil)
58
+ @path = path
59
+ @display_name = display_name
60
+ @ctag = ctag
61
+ @color = color
62
+ @description = description
63
+ @timezone = timezone
64
+ @supported_components = supported_components
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ # CalDAV/Extensions/Apple.rb
2
+ # CalDAV::Extensions::Apple
3
+
4
+ # The apple calendar-color property. A non-RFC-4791 vendor extension: it prepends
5
+ # its namespace and property onto the calendar-discovery request and mixes its
6
+ # reader into Resource. Composes with other extensions via super. Loaded with the
7
+ # object layer; omitted by 'CalDAV/Objects/Core'.
8
+
9
+ require_relative '../../caldav'
10
+
11
+ class CalDAV < WebDAV
12
+ module Extensions
13
+ module Apple
14
+ NAMESPACE = 'http://apple.com/ns/ical/'
15
+
16
+ private def calendars_request_namespaces
17
+ %(#{super} xmlns:ic="#{NAMESPACE}")
18
+ end
19
+
20
+ private def calendars_request_properties
21
+ "#{super}\n <ic:calendar-color/>"
22
+ end
23
+
24
+ module Reader
25
+ def calendar_color
26
+ property(CalDAV::Extensions::Apple::NAMESPACE, 'calendar-color')
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ prepend Extensions::Apple
33
+ Resource.include Extensions::Apple::Reader
34
+ end
@@ -0,0 +1,35 @@
1
+ # CalDAV/Extensions/CalendarServer.rb
2
+ # CalDAV::Extensions::CalendarServer
3
+
4
+ # The calendarserver getctag property (calendar-collection sync token). A
5
+ # non-RFC-4791 vendor extension: it prepends its namespace and property onto the
6
+ # calendar-discovery request and mixes its reader into Resource. Composes with
7
+ # other extensions via super. Loaded with the object layer; omitted by
8
+ # 'CalDAV/Objects/Core'.
9
+
10
+ require_relative '../../caldav'
11
+
12
+ class CalDAV < WebDAV
13
+ module Extensions
14
+ module CalendarServer
15
+ NAMESPACE = 'http://calendarserver.org/ns/'
16
+
17
+ private def calendars_request_namespaces
18
+ %(#{super} xmlns:cs="#{NAMESPACE}")
19
+ end
20
+
21
+ private def calendars_request_properties
22
+ "#{super}\n <cs:getctag/>"
23
+ end
24
+
25
+ module Reader
26
+ def getctag
27
+ property(CalDAV::Extensions::CalendarServer::NAMESPACE, 'getctag')
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ prepend Extensions::CalendarServer
34
+ Resource.include Extensions::CalendarServer::Reader
35
+ end
@@ -0,0 +1,9 @@
1
+ # CalDAV/Objects/Core.rb
2
+ # CalDAV object layer, core (no extension accessors)
3
+
4
+ # The object layer without the extension-namespace accessors — a strictly
5
+ # RFC 4791 object surface. `require 'CalDAV/Objects'` loads this plus the
6
+ # extensions; require this path directly to opt out of them.
7
+
8
+ require_relative '../../caldav'
9
+ require_relative '../Calendar'
@@ -0,0 +1,11 @@
1
+ # CalDAV/Objects.rb
2
+ # CalDAV Objects layer (Layer 2), default: with vendor extensions
3
+
4
+ # Value objects built over the Layer 1 protocol/navigation surface. Opt-in via
5
+ # `require 'CalDAV/Objects'`. This default form also loads the vendor extensions
6
+ # (calendarserver getctag, apple calendar-color); require 'CalDAV/Objects/Core'
7
+ # for the objects without them, a strictly RFC 4791 consumer.
8
+
9
+ require_relative './Objects/Core'
10
+ require_relative './Extensions/Apple'
11
+ require_relative './Extensions/CalendarServer'
@@ -19,6 +19,10 @@ class CalDAV < WebDAV
19
19
  hash[:propstats]
20
20
  end
21
21
 
22
+ def display_name
23
+ property(DAV_NAMESPACE, 'displayname')
24
+ end
25
+
22
26
  # Navigation accessors. Strictly navigation, not parsing: they return the
23
27
  # underlying iCalendar string or value.
24
28
 
@@ -4,5 +4,5 @@
4
4
  class WebDAV; end
5
5
 
6
6
  class CalDAV < WebDAV
7
- VERSION = '0.1.0'
7
+ VERSION = '0.2.0'
8
8
  end
data/lib/caldav.rb CHANGED
@@ -37,8 +37,10 @@ class CalDAV < WebDAV
37
37
  handle_response(response)
38
38
  end
39
39
 
40
- # Discovery helpers. Return paths/strings. And object-oriented discovery API
41
- # (Principal/Calendar objects) is is planned.
40
+ # Discovery helpers. current_user_principal and calendar_home_set return href
41
+ # strings; calendars returns CalDAV::Resource objects (the navigation surface),
42
+ # which the object layer (CalDAV::Calendar.all, via require 'CalDAV/Objects')
43
+ # maps into value objects.
42
44
 
43
45
  def current_user_principal(path = discovery_path)
44
46
  resource = propfind_as_caldav(path, body: current_user_principal_body, depth: '0').resources.first
@@ -52,7 +54,7 @@ class CalDAV < WebDAV
52
54
 
53
55
  def calendars(home = calendar_home_set)
54
56
  resources = propfind_as_caldav(home, body: calendars_body, depth: '1').resources
55
- resources.select{|resource| resource.is_calendar?}.collect{|resource| resource.href}
57
+ resources.select{|resource| resource.is_calendar?}
56
58
  end
57
59
 
58
60
  private
@@ -126,11 +128,21 @@ class CalDAV < WebDAV
126
128
  def calendars_body
127
129
  <<~XML
128
130
  <?xml version="1.0" encoding="UTF-8"?>
129
- <d:propfind xmlns:d="DAV:">
131
+ <d:propfind xmlns:d="DAV:" xmlns:c="#{NAMESPACE}"#{calendars_request_namespaces}>
130
132
  <d:prop>
133
+ <d:displayname/>
131
134
  <d:resourcetype/>
135
+ <c:calendar-description/>
136
+ <c:calendar-timezone/>
137
+ <c:supported-calendar-component-set/>#{calendars_request_properties}
132
138
  </d:prop>
133
139
  </d:propfind>
134
140
  XML
135
141
  end
142
+
143
+ # Root of the calendar-discovery extension chain. Extension modules prepend
144
+ # onto these and super, so their contributions compose rather than clobber;
145
+ # empty here keeps the request strictly RFC 4791.
146
+ def calendars_request_namespaces; ''; end
147
+ def calendars_request_properties; ''; end
136
148
  end
@@ -0,0 +1,169 @@
1
+ # test/CalDAV/Calendar_test.rb
2
+
3
+ require_relative '../helper'
4
+ require_relative '../../lib/CalDAV/Objects'
5
+
6
+ describe CalDAV::Calendar do
7
+ let(:calendar) do
8
+ CalDAV::Calendar.new(
9
+ path: '/dav/calendars/user/test/work/',
10
+ display_name: 'Work',
11
+ ctag: 'abc123',
12
+ color: '#FF0000',
13
+ description: 'Work calendar',
14
+ supported_components: 'VEVENT'
15
+ )
16
+ end
17
+
18
+ describe "#path" do
19
+ it "returns the path" do
20
+ _(calendar.path).must_equal '/dav/calendars/user/test/work/'
21
+ end
22
+ end
23
+
24
+ describe "#display_name" do
25
+ it "returns the display name" do
26
+ _(calendar.display_name).must_equal 'Work'
27
+ end
28
+ end
29
+
30
+ describe "#ctag" do
31
+ it "returns the ctag" do
32
+ _(calendar.ctag).must_equal 'abc123'
33
+ end
34
+ end
35
+
36
+ describe "#color" do
37
+ it "returns the color" do
38
+ _(calendar.color).must_equal '#FF0000'
39
+ end
40
+ end
41
+
42
+ describe "#to_s" do
43
+ it "returns the display name" do
44
+ _(calendar.to_s).must_equal 'Work'
45
+ end
46
+
47
+ it "falls back to path when display_name is nil" do
48
+ c = CalDAV::Calendar.new(path: '/dav/work/')
49
+ _(c.to_s).must_equal '/dav/work/'
50
+ end
51
+ end
52
+
53
+ describe ".from_resource" do
54
+ let(:multistatus) do
55
+ CalDAV::MultiStatus.new(MockResponse.new(code: '207', message: 'Multi-Status', body: <<~XML))
56
+ <?xml version="1.0" encoding="UTF-8"?>
57
+ <d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:ic="http://apple.com/ns/ical/">
58
+ <d:response>
59
+ <d:href>/calendars/user/work/</d:href>
60
+ <d:propstat>
61
+ <d:prop>
62
+ <d:displayname>Work</d:displayname>
63
+ <d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
64
+ <cs:getctag>abc123</cs:getctag>
65
+ <ic:calendar-color>#FF0000</ic:calendar-color>
66
+ <c:calendar-description>Work calendar</c:calendar-description>
67
+ <c:calendar-timezone>BEGIN:VTIMEZONE</c:calendar-timezone>
68
+ </d:prop>
69
+ <d:status>HTTP/1.1 200 OK</d:status>
70
+ </d:propstat>
71
+ </d:response>
72
+ </d:multistatus>
73
+ XML
74
+ end
75
+ let(:built){CalDAV::Calendar.from_resource(multistatus.resources.first)}
76
+
77
+ it "maps href to path" do
78
+ _(built.path).must_equal '/calendars/user/work/'
79
+ end
80
+
81
+ it "maps displayname to display_name" do
82
+ _(built.display_name).must_equal 'Work'
83
+ end
84
+
85
+ it "maps calendar-description to description" do
86
+ _(built.description).must_equal 'Work calendar'
87
+ end
88
+
89
+ it "maps calendar-timezone to timezone" do
90
+ _(built.timezone).must_equal 'BEGIN:VTIMEZONE'
91
+ end
92
+
93
+ it "maps getctag to ctag (extension, loaded by default)" do
94
+ _(built.ctag).must_equal 'abc123'
95
+ end
96
+
97
+ it "maps calendar-color to color (extension, loaded by default)" do
98
+ _(built.color).must_equal '#FF0000'
99
+ end
100
+
101
+ it "leaves ctag and color nil when those properties are absent" do
102
+ body = <<~XML
103
+ <?xml version="1.0" encoding="UTF-8"?>
104
+ <d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
105
+ <d:response>
106
+ <d:href>/calendars/user/plain/</d:href>
107
+ <d:propstat>
108
+ <d:prop><d:displayname>Plain</d:displayname></d:prop>
109
+ <d:status>HTTP/1.1 200 OK</d:status>
110
+ </d:propstat>
111
+ </d:response>
112
+ </d:multistatus>
113
+ XML
114
+ plain = CalDAV::Calendar.from_resource(CalDAV::MultiStatus.new(MockResponse.new(code: '207', message: 'Multi-Status', body: body)).resources.first)
115
+ _(plain.ctag).must_be_nil
116
+ _(plain.color).must_be_nil
117
+ end
118
+ end
119
+
120
+ describe ".all" do
121
+ let(:caldav){CalDAV.new('https://caldav.example.com/', username: 'u', password: 'p')}
122
+ let(:home_response) do
123
+ MockResponse.new(code: '207', message: 'Multi-Status', body: <<~XML)
124
+ <?xml version="1.0" encoding="UTF-8"?>
125
+ <d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:ic="http://apple.com/ns/ical/">
126
+ <d:response>
127
+ <d:href>/dav/calendars/user/abc/</d:href>
128
+ <d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>
129
+ </d:response>
130
+ <d:response>
131
+ <d:href>/dav/calendars/user/abc/work/</d:href>
132
+ <d:propstat>
133
+ <d:prop>
134
+ <d:displayname>Work</d:displayname>
135
+ <d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
136
+ <cs:getctag>ctag-1</cs:getctag>
137
+ <ic:calendar-color>#FF0000</ic:calendar-color>
138
+ <c:calendar-description>Work calendar</c:calendar-description>
139
+ </d:prop>
140
+ <d:status>HTTP/1.1 200 OK</d:status>
141
+ </d:propstat>
142
+ </d:response>
143
+ </d:multistatus>
144
+ XML
145
+ end
146
+
147
+ it "returns Calendar objects for the calendar collections only" do
148
+ caldav.stub(:request, home_response) do
149
+ calendars = CalDAV::Calendar.all(client: caldav, home: '/dav/calendars/user/abc/')
150
+ _(calendars.length).must_equal 1
151
+ _(calendars.first).must_be_kind_of CalDAV::Calendar
152
+ _(calendars.first.path).must_equal '/dav/calendars/user/abc/work/'
153
+ _(calendars.first.display_name).must_equal 'Work'
154
+ end
155
+ end
156
+
157
+ it "maps the extension properties when the extensions are loaded" do
158
+ caldav.stub(:request, home_response) do
159
+ work = CalDAV::Calendar.all(client: caldav, home: '/dav/calendars/user/abc/').first
160
+ _(work.ctag).must_equal 'ctag-1'
161
+ _(work.color).must_equal '#FF0000'
162
+ end
163
+ end
164
+
165
+ it "requires a client" do
166
+ _{CalDAV::Calendar.all(home: '/dav/calendars/user/abc/')}.must_raise ArgumentError
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,30 @@
1
+ # test/CalDAV/Extensions/Apple_test.rb
2
+
3
+ require_relative '../../helper'
4
+ require 'CalDAV/Objects' # loads the vendor extensions
5
+
6
+ describe "CalDAV::Extensions::Apple" do
7
+ let(:resource) do
8
+ body = <<~XML
9
+ <?xml version="1.0" encoding="UTF-8"?>
10
+ <d:multistatus xmlns:d="DAV:" xmlns:ic="http://apple.com/ns/ical/">
11
+ <d:response>
12
+ <d:href>/calendars/user/work/</d:href>
13
+ <d:propstat><d:prop><ic:calendar-color>#FF0000</ic:calendar-color></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>
14
+ </d:response>
15
+ </d:multistatus>
16
+ XML
17
+ CalDAV::MultiStatus.new(MockResponse.new(code: '207', message: 'Multi-Status', body: body)).resources.first
18
+ end
19
+
20
+ it "reads calendar-color from the apple namespace" do
21
+ _(resource.calendar_color).must_equal '#FF0000'
22
+ end
23
+
24
+ it "contributes its namespace and property to the calendars request" do
25
+ caldav = CalDAV.new('https://caldav.example.com/', username: 'u', password: 'p')
26
+ body = caldav.send(:calendars_body)
27
+ _(body).must_include 'xmlns:ic="http://apple.com/ns/ical/"'
28
+ _(body).must_include '<ic:calendar-color/>'
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # test/CalDAV/Extensions/CalendarServer_test.rb
2
+
3
+ require_relative '../../helper'
4
+ require 'CalDAV/Objects' # loads the vendor extensions
5
+
6
+ describe "CalDAV::Extensions::CalendarServer" do
7
+ let(:resource) do
8
+ body = <<~XML
9
+ <?xml version="1.0" encoding="UTF-8"?>
10
+ <d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
11
+ <d:response>
12
+ <d:href>/calendars/user/work/</d:href>
13
+ <d:propstat><d:prop><cs:getctag>abc123</cs:getctag></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>
14
+ </d:response>
15
+ </d:multistatus>
16
+ XML
17
+ CalDAV::MultiStatus.new(MockResponse.new(code: '207', message: 'Multi-Status', body: body)).resources.first
18
+ end
19
+
20
+ it "reads getctag from the calendarserver namespace" do
21
+ _(resource.getctag).must_equal 'abc123'
22
+ end
23
+
24
+ it "contributes its namespace and property to the calendars request" do
25
+ caldav = CalDAV.new('https://caldav.example.com/', username: 'u', password: 'p')
26
+ body = caldav.send(:calendars_body)
27
+ _(body).must_include 'xmlns:cs="http://calendarserver.org/ns/"'
28
+ _(body).must_include '<cs:getctag/>'
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # test/CalDAV/Objects/Core_test.rb
2
+
3
+ require_relative '../../helper'
4
+ require 'CalDAV/Objects/Core'
5
+
6
+ # The core path loads the object layer without the extension accessors. Their
7
+ # absence can't be asserted in-process — requires are global, and another test
8
+ # loads the full 'CalDAV/Objects' — so this covers only that the path itself
9
+ # resolves and yields the objects; the opt-out is a structural property of
10
+ # Core.rb not requiring Resource/Extensions.
11
+ describe "CalDAV/Objects/Core" do
12
+ it "loads the object layer through the core require path" do
13
+ _(defined?(CalDAV::Calendar)).must_equal 'constant'
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # test/CalDAV/Objects_test.rb
2
+
3
+ require_relative '../helper'
4
+
5
+ # Exercise the Layer 2 entry point through its real public require path, so this
6
+ # guards the path itself (the CalDAV-cased, portable form) — not just the file's
7
+ # contents. lib is on the load path under rake's TestTask, as it is for a
8
+ # consumer of the installed gem.
9
+ require 'CalDAV/Objects'
10
+
11
+ describe "CalDAV/Objects" do
12
+ it "loads the object layer through the require path" do
13
+ _(defined?(CalDAV::Calendar)).must_equal 'constant'
14
+ end
15
+
16
+ it "pulls in the Layer 1 base it builds on" do
17
+ _(CalDAV.ancestors).must_include(WebDAV)
18
+ _(defined?(CalDAV::Resource)).must_equal 'constant'
19
+ end
20
+
21
+ it "loads the extension accessors by default" do
22
+ _(CalDAV::Resource.method_defined?(:getctag)).must_equal true
23
+ _(CalDAV::Resource.method_defined?(:calendar_color)).must_equal true
24
+ end
25
+
26
+ it "composes every loaded extension into the calendars request" do
27
+ caldav = CalDAV.new('https://caldav.example.com/', username: 'u', password: 'p')
28
+ body = caldav.send(:calendars_body)
29
+ _(body).must_include 'xmlns:cs="http://calendarserver.org/ns/"'
30
+ _(body).must_include '<cs:getctag/>'
31
+ _(body).must_include 'xmlns:ic="http://apple.com/ns/ical/"'
32
+ _(body).must_include '<ic:calendar-color/>'
33
+ end
34
+ end
@@ -12,6 +12,7 @@ describe CalDAV::Resource do
12
12
  <d:propstat>
13
13
  <d:prop>
14
14
  <d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
15
+ <d:displayname>Work</d:displayname>
15
16
  <c:calendar-description>Work calendar</c:calendar-description>
16
17
  <c:supported-calendar-data><c:calendar-data content-type="text/calendar" version="2.0"/></c:supported-calendar-data>
17
18
  <c:max-resource-size>10485760</c:max-resource-size>
@@ -59,6 +60,10 @@ describe CalDAV::Resource do
59
60
  _(calendar_resource.href).must_equal '/calendars/user/work/'
60
61
  end
61
62
 
63
+ it "reads displayname" do
64
+ _(calendar_resource.display_name).must_equal 'Work'
65
+ end
66
+
62
67
  it "reads calendar-description" do
63
68
  _(calendar_resource.calendar_description).must_equal 'Work calendar'
64
69
  end
@@ -173,9 +173,12 @@ describe CalDAV do
173
173
  XML
174
174
  end
175
175
 
176
- it "returns only the calendar collection hrefs as bare URL strings" do
176
+ it "returns only the calendar-collection resources" do
177
177
  caldav.stub(:request, response) do
178
- _(caldav.calendars('/dav/calendars/user/abc/')).must_equal ['/dav/calendars/user/abc/work/']
178
+ result = caldav.calendars('/dav/calendars/user/abc/')
179
+ _(result.length).must_equal 1
180
+ _(result.first).must_be_kind_of CalDAV::Resource
181
+ _(result.first.href).must_equal '/dav/calendars/user/abc/work/'
179
182
  end
180
183
  end
181
184
  end
@@ -23,7 +23,7 @@ describe 'CalDAV integration' do
23
23
  XML
24
24
  end
25
25
 
26
- it "discovers principal, home-set and calendars as bare URL strings" do
26
+ it "discovers principal and home-set as strings, and calendars as resources" do
27
27
  with_caldav_cassette('discovery') do
28
28
  principal = caldav.current_user_principal
29
29
  _(principal).must_be_kind_of String
@@ -33,9 +33,10 @@ describe 'CalDAV integration' do
33
33
  _(home).wont_include '<'
34
34
  calendars = caldav.calendars(home)
35
35
  _(calendars).must_be_kind_of Array
36
- calendars.each do |href|
37
- _(href).must_be_kind_of String
38
- _(href).wont_include '<'
36
+ calendars.each do |resource|
37
+ _(resource).must_be_kind_of CalDAV::Resource
38
+ _(resource.href).must_be_kind_of String
39
+ _(resource.href).wont_include '<'
39
40
  end
40
41
  end
41
42
  end
@@ -44,7 +45,7 @@ describe 'CalDAV integration' do
44
45
  with_caldav_cassette('calendar_query') do
45
46
  calendar = caldav.calendars.first
46
47
  skip 'no calendars on this account' unless calendar
47
- result = caldav.calendar_query(calendar, body: events_query_body)
48
+ result = caldav.calendar_query(calendar.href, body: events_query_body)
48
49
  _(result).must_be_kind_of CalDAV::MultiStatus
49
50
  result.resources.each{|resource| _(resource).must_be_kind_of CalDAV::Resource}
50
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caldav.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -105,15 +105,25 @@ files:
105
105
  - README.md
106
106
  - Rakefile
107
107
  - caldav.rb.gemspec
108
+ - lib/CalDAV/Calendar.rb
109
+ - lib/CalDAV/Extensions/Apple.rb
110
+ - lib/CalDAV/Extensions/CalendarServer.rb
108
111
  - lib/CalDAV/MultiStatus.rb
112
+ - lib/CalDAV/Objects.rb
113
+ - lib/CalDAV/Objects/Core.rb
109
114
  - lib/CalDAV/Resource.rb
110
115
  - lib/CalDAV/VERSION.rb
111
116
  - lib/Net/HTTP/Mkcalendar.rb
112
117
  - lib/caldav.rb
118
+ - test/CalDAV/Calendar_test.rb
119
+ - test/CalDAV/Extensions/Apple_test.rb
120
+ - test/CalDAV/Extensions/CalendarServer_test.rb
113
121
  - test/CalDAV/MultiStatus_test.rb
122
+ - test/CalDAV/Objects/Core_test.rb
123
+ - test/CalDAV/Objects_test.rb
114
124
  - test/CalDAV/Resource_test.rb
125
+ - test/CalDAV_test.rb
115
126
  - test/Net/HTTP/Mkcalendar_test.rb
116
- - test/caldav_test.rb
117
127
  - test/helper.rb
118
128
  - test/integration_test.rb
119
129
  - test/vcr_helper.rb
@@ -136,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
146
  - !ruby/object:Gem::Version
137
147
  version: '0'
138
148
  requirements: []
139
- rubygems_version: 4.0.14
149
+ rubygems_version: 4.0.18
140
150
  specification_version: 4
141
151
  summary: A Ruby CalDAV client library.
142
152
  test_files: []