1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
require "nokogiri"

module ProTacts
  # The XML the DAV routes answer with (web/dav.rb). Each namespace is an
  # object whose methods are the elements this server sends in it, so an
  # element outside them is a NoMethodError, and adding one is a change
  # here — the real decision AGENTS.md says adding a property is.
  module DavXml
    PREFIXES = {
      "d" => "DAV:",
      "card" => "urn:ietf:params:xml:ns:carddav",
      "cs" => "http://calendarserver.org/ns/",
    }.freeze #: Hash[String, String]

    # A whole body under the DAV: element root, declaring "d" and each of
    # prefixes, with the children the block writes. Writing through a
    # namespace left undeclared raises.
    #: (String root, *String prefixes) { (DAV d, CardDAV card, CalendarServer cs) -> void } -> String
    def self.document(root, *prefixes)
      namespaces = ["d", *prefixes].to_h { |prefix|
        ["xmlns:#{prefix}", PREFIXES.fetch(prefix)] #: [String, String]
      }

      builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |x|
        x["d"].send(root, namespaces) do
          yield DAV.new(x, "d"), CardDAV.new(x, "card"), CalendarServer.new(x, "cs")
        end
      end
      builder.to_xml
    end

    # One namespace's elements. The builder yields itself to an element's
    # block, so `it` inside one is the builder rather than an enclosing
    # block's element.
    class Namespace
      # @rbs @x: Nokogiri::XML::Builder
      # @rbs @prefix: String

      #: (Nokogiri::XML::Builder x, String prefix) -> void
      def initialize(x, prefix)
        @x = x
        @prefix = prefix
      end

      private

      #: (String name, ?String? text) ?{ () -> void } -> void
      def tag(name, text = nil, &)
        element = @x[@prefix]
        text ? element.send(name, text, &) : element.send(name, &)
      end
    end

    class DAV < Namespace
      # A member answered with a 200, holding the properties the block
      # writes.
      #: (String location) { () -> void } -> void
      def found(location, &)
        response do
          href location
          propstat do
            prop(&)
            status "HTTP/1.1 200 OK"
          end
        end
      end

      # A member the collection does not answer for: the multiget miss,
      # reported as a 404 inside the 207 rather than failing the request
      # (RFC 6352 section 8.7), and the removal shape a sync-collection
      # delta reports (RFC 6578 section 3.2 — href and 404, no propstat).
      #: (String location) -> void
      def missing(location)
        response do
          href location
          status "HTTP/1.1 404 Not Found"
        end
      end

      #: () { () -> void } -> void
      def response(&) = tag("response", &)
      #: () { () -> void } -> void
      def propstat(&) = tag("propstat", &)
      #: () { () -> void } -> void
      def prop(&) = tag("prop", &)
      #: (String location) -> void
      def href(location) = tag("href", location)
      #: (String line) -> void
      def status(line) = tag("status", line)

      #: (String etag) -> void
      def getetag(etag) = tag("getetag", etag)
      #: (String token) -> void
      def sync_token(token) = tag("sync-token", token)
      #: () { () -> void } -> void
      def current_user_principal(&) = tag("current-user-principal", &)
      #: () { () -> void } -> void
      def resourcetype(&) = tag("resourcetype", &)
      #: () -> void
      def collection = tag("collection")

      #: () { () -> void } -> void
      def supported_report_set(&) = tag("supported-report-set", &)
      # Also the precondition RFC 3253 section 3.6 names, empty.
      #: () ?{ () -> void } -> void
      def supported_report(&) = tag("supported-report", &)
      #: () { () -> void } -> void
      def report(&) = tag("report", &)
      #: () -> void
      def sync_collection = tag("sync-collection")
      #: () -> void
      def valid_sync_token = tag("valid-sync-token")

      #: () { () -> void } -> void
      def current_user_privilege_set(&) = tag("current-user-privilege-set", &)
      #: () { () -> void } -> void
      def privilege(&) = tag("privilege", &)
      #: () -> void
      def read = tag("read")
      #: () -> void
      def write = tag("write")
      #: () -> void
      def bind = tag("bind")
      #: () -> void
      def unbind = tag("unbind")
    end

    class CardDAV < Namespace
      #: () { () -> void } -> void
      def addressbook_home_set(&) = tag("addressbook-home-set", &)
      #: () -> void
      def addressbook = tag("addressbook")

      # The card's carriage returns go out as character references: a raw
      # CR in text is normalized away by the client's parser (XML 1.0
      # section 2.11), which would hand it LF-only lines.
      #: (String card) -> void
      def address_data(card) = tag("address-data", card)

      # The preconditions of RFC 6352 section 6.3.2.1, each holding what
      # the block writes.
      #: () ?{ () -> void } -> void
      def supported_address_data(&) = tag("supported-address-data", &)
      #: () ?{ () -> void } -> void
      def valid_address_data(&) = tag("valid-address-data", &)
      #: () ?{ () -> void } -> void
      def no_uid_conflict(&) = tag("no-uid-conflict", &)
    end

    class CalendarServer < Namespace
      #: (String ctag) -> void
      def getctag(ctag) = tag("getctag", ctag)
    end
  end
end