ruby-vobject 0.1.0 → 1.0.1

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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.hound.yml +3 -0
  3. data/.rubocop.tb.yml +650 -0
  4. data/.rubocop.yml +1077 -0
  5. data/.travis.yml +16 -3
  6. data/Gemfile +1 -1
  7. data/LICENSE.txt +21 -17
  8. data/README.adoc +142 -0
  9. data/Rakefile +1 -1
  10. data/lib/c.rb +173 -0
  11. data/lib/error.rb +19 -0
  12. data/lib/vcalendar.rb +77 -0
  13. data/lib/vcard.rb +67 -0
  14. data/lib/vobject.rb +13 -170
  15. data/lib/vobject/component.rb +87 -36
  16. data/lib/vobject/parameter.rb +116 -0
  17. data/lib/vobject/parametervalue.rb +26 -0
  18. data/lib/vobject/property.rb +134 -55
  19. data/lib/vobject/propertyvalue.rb +46 -0
  20. data/lib/vobject/vcalendar/component.rb +106 -0
  21. data/lib/vobject/vcalendar/grammar.rb +595 -0
  22. data/lib/vobject/vcalendar/paramcheck.rb +259 -0
  23. data/lib/vobject/vcalendar/propertyparent.rb +98 -0
  24. data/lib/vobject/vcalendar/propertyvalue.rb +606 -0
  25. data/lib/vobject/vcalendar/typegrammars.rb +605 -0
  26. data/lib/vobject/vcard/v3_0/component.rb +40 -0
  27. data/lib/vobject/vcard/v3_0/grammar.rb +176 -0
  28. data/lib/vobject/vcard/v3_0/paramcheck.rb +111 -0
  29. data/lib/vobject/vcard/v3_0/parameter.rb +17 -0
  30. data/lib/vobject/vcard/v3_0/property.rb +18 -0
  31. data/lib/vobject/vcard/v3_0/propertyvalue.rb +401 -0
  32. data/lib/vobject/vcard/v3_0/typegrammars.rb +426 -0
  33. data/lib/vobject/vcard/v4_0/component.rb +40 -0
  34. data/lib/vobject/vcard/v4_0/grammar.rb +225 -0
  35. data/lib/vobject/vcard/v4_0/paramcheck.rb +270 -0
  36. data/lib/vobject/vcard/v4_0/parameter.rb +18 -0
  37. data/lib/vobject/vcard/v4_0/property.rb +63 -0
  38. data/lib/vobject/vcard/v4_0/propertyvalue.rb +404 -0
  39. data/lib/vobject/vcard/v4_0/typegrammars.rb +540 -0
  40. data/lib/vobject/vcard/version.rb +3 -0
  41. data/lib/vobject/version.rb +1 -1
  42. data/ruby-vobject.gemspec +19 -11
  43. metadata +93 -17
  44. data/README.md +0 -94
@@ -0,0 +1,426 @@
1
+ require "rsec"
2
+ require "set"
3
+ require "uri"
4
+ require "date"
5
+ include Rsec::Helpers
6
+ require "vobject/vcard/version"
7
+ require "vobject"
8
+ require_relative "./propertyvalue"
9
+
10
+ module Vcard::V3_0
11
+ class Typegrammars
12
+ class << self
13
+ # property value types, each defining their own parser
14
+
15
+ def binary
16
+ binary = seq(/[a-zA-Z0-9+\/]*/.r, /={0,2}/.r) do |b, q|
17
+ if (b.length + q.length) % 4 == 0
18
+ PropertyValue::Binary.new(b + q)
19
+ else
20
+ { error: "Malformed binary coding" }
21
+ end
22
+ end
23
+ binary.eof
24
+ end
25
+
26
+ def phone_number
27
+ # This is on the lax side; there should be up to 15 digits
28
+ # Will allow letters
29
+ phone_number = /[0-9() +A-Z-]+/i.r.map { |p| PropertyValue::Phonenumber.new p }
30
+ phone_number.eof
31
+ end
32
+
33
+ def geovalue
34
+ float = prim(:double)
35
+ geovalue = seq(float << ";".r, float) do |a, b|
36
+ if a <= 180.0 && a >= -180.0 && b <= 180 && b > -180
37
+ PropertyValue::Geovalue.new(lat: a, long: b)
38
+ else
39
+ { error: "Latitude/Longitude outside of range -180..180" }
40
+ end
41
+ end
42
+ geovalue.eof
43
+ end
44
+
45
+ def classvalue
46
+ iana_token = /[a-zA-Z\d\-]+/.r
47
+ xname = seq(/[xX]-/, /[a-zA-Z0-9-]+/.r).map(&:join)
48
+ classvalue = (/PUBLIC/i.r | /PRIVATE/i.r | /CONFIDENTIAL/i.r | iana_token | xname).map do |m|
49
+ PropertyValue::ClassValue.new m
50
+ end
51
+ classvalue.eof
52
+ end
53
+
54
+ def integer
55
+ integer = prim(:int32).map { |i| PropertyValue::Integer.new i }
56
+ integer.eof
57
+ end
58
+
59
+ def float_t
60
+ float_t = prim(:double).map { |f| PropertyValue::Float.new f }
61
+ float_t.eof
62
+ end
63
+
64
+ def iana_token
65
+ iana_token = /[a-zA-Z\d\-]+/.r.map { |x| PropertyValue::Ianatoken.new x }
66
+ iana_token.eof
67
+ end
68
+
69
+ def versionvalue
70
+ versionvalue = "3.0".r.map { |v| PropertyValue::Version.new v }
71
+ versionvalue.eof
72
+ end
73
+
74
+ def profilevalue
75
+ profilevalue = /VCARD/i.r.map { |v| PropertyValue::Profilevalue.new v }
76
+ profilevalue.eof
77
+ end
78
+
79
+ def uri
80
+ uri = /\S+/.r.map do |s|
81
+ if s =~ URI::DEFAULT_PARSER.make_regexp
82
+ PropertyValue::Uri.new(s)
83
+ else
84
+ { error: "Invalid URI" }
85
+ end
86
+ end
87
+ uri.eof
88
+ end
89
+
90
+ def text_t
91
+ text_t = C::TEXT3.map { |t| PropertyValue::Text.new(unescape(t)) }
92
+ text_t.eof
93
+ end
94
+
95
+ def textlist
96
+ text = C::TEXT3
97
+ textlist1 =
98
+ seq(text << ",".r, lazy { textlist1 }) { |a, b| [unescape(a), b].flatten } |
99
+ text.map { |t| [unescape(t)] }
100
+ textlist = textlist1.map { |m| PropertyValue::Textlist.new m }
101
+ textlist.eof
102
+ end
103
+
104
+ def org
105
+ text = C::TEXT3
106
+ org1 =
107
+ seq(text << ";".r, lazy { org1 }) { |a, b| [unescape(a), b].flatten } |
108
+ text.map { |t| [unescape(t)] }
109
+ org = org1.map { |o| PropertyValue::Org.new o }
110
+ org.eof
111
+ end
112
+
113
+ def date_t
114
+ date_t = seq(/[0-9]{4}/.r, /-/.r._? >> /[0-9]{2}/.r, /-/.r._? >> /[0-9]{2}/.r) do |yy, mm, dd|
115
+ PropertyValue::Date.new(year: yy, month: mm, day: dd)
116
+ end
117
+ date_t.eof
118
+ end
119
+
120
+ def time_t
121
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
122
+ { sign: s, hour: h, min: m }
123
+ end
124
+ zone = utc_offset.map { |u| u } |
125
+ /Z/i.r.map { "Z" }
126
+ hour = /[0-9]{2}/.r
127
+ minute = /[0-9]{2}/.r
128
+ second = /[0-9]{2}/.r
129
+ secfrac = seq(",".r >> /[0-9]+/)
130
+ time_t = seq(hour << /:/._?, minute << /:/._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
131
+ h = { hour: h, min: m, sec: s }
132
+ h[:zone] = z[0] unless z.empty?
133
+ h[:secfrac] = f[0] unless f.empty?
134
+ PropertyValue::Time.new(h)
135
+ end
136
+ time_t.eof
137
+ end
138
+
139
+ def date_time
140
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
141
+ { sign: s, hour: h, min: m }
142
+ end
143
+ zone = utc_offset.map { |u| u } |
144
+ /Z/i.r.map { "Z" }
145
+ hour = /[0-9]{2}/.r
146
+ minute = /[0-9]{2}/.r
147
+ second = /[0-9]{2}/.r
148
+ secfrac = seq(",".r >> /[0-9]+/)
149
+ date = seq(/[0-9]{4}/.r, /-/.r._?, /[0-9]{2}/.r, /-/.r._?, /[0-9]{2}/.r) do |yy, _, mm, _, dd|
150
+ { year: yy, month: mm, day: dd }
151
+ end
152
+ time = seq(hour << /:/.r._?, minute << /:/.r._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
153
+ h = { hour: h, min: m, sec: s }
154
+ h[:zone] = if z.empty?
155
+ ""
156
+ else
157
+ z[0]
158
+ end
159
+ h[:secfrac] = f[0] unless f.empty?
160
+ h
161
+ end
162
+ date_time = seq(date << "T".r, time) do |d, t|
163
+ PropertyValue::DateTimeLocal.new(d.merge(t))
164
+ end
165
+ date_time.eof
166
+ end
167
+
168
+ def date_or_date_time
169
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r << /:/.r._?, /[0-9]{2}/.r) do |s, h, m|
170
+ { sign: s, hour: h, min: m }
171
+ end
172
+ zone = utc_offset.map { |u| u } |
173
+ /Z/i.r.map { "Z" }
174
+ hour = /[0-9]{2}/.r
175
+ minute = /[0-9]{2}/.r
176
+ second = /[0-9]{2}/.r
177
+ secfrac = seq(",".r >> /[0-9]+/)
178
+ date = seq(/[0-9]{4}/.r << /-/.r._?, /[0-9]{2}/.r << /-/.r._?, /[0-9]{2}/.r) do |yy, mm, dd|
179
+ { year: yy, month: mm, day: dd }
180
+ end
181
+ time = seq(hour << /:/.r._?, minute << /:/.r._?, second, secfrac._?, zone._?) do |h, m, s, f, z|
182
+ h = { hour: h, min: m, sec: s }
183
+ h[:zone] = z[0] unless z.empty?
184
+ h[:secfrac] = f[0] unless f.empty?
185
+ h
186
+ end
187
+ date_or_date_time = seq(date << "T".r, time) do |d, t|
188
+ PropertyValue::DateTimeLocal.new(d.merge(t))
189
+ end | date.map { |d| PropertyValue::Date.new(d) }
190
+ date_or_date_time.eof
191
+ end
192
+
193
+ def utc_offset
194
+ utc_offset = seq(C::SIGN, /[0-9]{2}/.r, /:/.r._?, /[0-9]{2}/.r) do |s, h, _, m|
195
+ PropertyValue::Utcoffset.new(sign: s, hour: h, min: m)
196
+ end
197
+ utc_offset.eof
198
+ end
199
+
200
+ def kindvalue
201
+ iana_token = /[a-zA-Z\d\-]+/.r
202
+ xname = seq(/[xX]-/, /[a-zA-Z0-9-]+/.r).map(&:join)
203
+ kindvalue = (/individual/i.r | /group/i.r | /org/i.r | /location/i.r |
204
+ iana_token | xname).map do |k|
205
+ PropertyValue::Kindvalue.new(k)
206
+ end
207
+ kindvalue.eof
208
+ end
209
+
210
+ def fivepartname
211
+ text = C::TEXT3
212
+ component = seq(text << ",".r, lazy { component }) do |a, b|
213
+ [unescape(a), b].flatten
214
+ end | text.map { |t| [unescape(t)] }
215
+ fivepartname1 = seq(component << ";".r, component << ";".r, component << ";".r,
216
+ component << ";".r, component) do |a, b, c, d, e|
217
+ a = a[0] if a.length == 1
218
+ b = b[0] if b.length == 1
219
+ c = c[0] if c.length == 1
220
+ d = d[0] if d.length == 1
221
+ e = e[0] if e.length == 1
222
+ { surname: a, givenname: b, middlename: c, honprefix: d, honsuffix: e }
223
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component) do |a, b, c, d|
224
+ a = a[0] if a.length == 1
225
+ b = b[0] if b.length == 1
226
+ c = c[0] if c.length == 1
227
+ d = d[0] if d.length == 1
228
+ { surname: a, givenname: b, middlename: c, honprefix: d, honsuffix: "" }
229
+ end | seq(component << ";".r, component << ";".r, component) do |a, b, c|
230
+ a = a[0] if a.length == 1
231
+ b = b[0] if b.length == 1
232
+ c = c[0] if c.length == 1
233
+ { surname: a, givenname: b, middlename: c, honprefix: "", honsuffix: "" }
234
+ end | seq(component << ";".r, component) do |a, b|
235
+ a = a[0] if a.length == 1
236
+ b = b[0] if b.length == 1
237
+ { surname: a, givenname: b, middlename: "", honprefix: "", honsuffix: "" }
238
+ end | component.map do |a|
239
+ a = a[0] if a.length == 1
240
+ { surname: a, givenname: "", middlename: "", honprefix: "", honsuffix: "" }
241
+ end
242
+ fivepartname = fivepartname1.map { |n| PropertyValue::Fivepartname.new(n) }
243
+ fivepartname.eof
244
+ end
245
+
246
+ def address
247
+ text = C::TEXT3
248
+ component = seq(text << ",".r, lazy { component }) do |a, b|
249
+ [unescape(a), b].flatten
250
+ end | text.map { |t| [unescape(t)] }
251
+ address1 = seq(component << ";".r, component << ";".r, component << ";".r, component << ";".r,
252
+ component << ";".r, component << ";".r, component) do |a, b, c, d, e, f, g|
253
+ a = a[0] if a.length == 1
254
+ b = b[0] if b.length == 1
255
+ c = c[0] if c.length == 1
256
+ d = d[0] if d.length == 1
257
+ e = e[0] if e.length == 1
258
+ f = f[0] if f.length == 1
259
+ g = g[0] if g.length == 1
260
+ { pobox: a, ext: b, street: c,
261
+ locality: d, region: e, code: f, country: g }
262
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component << ";".r,
263
+ component << ";".r, component) do |a, b, c, d, e, f|
264
+ a = a[0] if a.length == 1
265
+ b = b[0] if b.length == 1
266
+ c = c[0] if c.length == 1
267
+ d = d[0] if d.length == 1
268
+ e = e[0] if e.length == 1
269
+ f = f[0] if f.length == 1
270
+ { pobox: a, ext: b, street: c,
271
+ locality: d, region: e, code: f, country: "" }
272
+ end | seq(component << ";".r, component << ";".r, component << ";".r,
273
+ component << ";".r, component) do |a, b, c, d, e|
274
+ a = a[0] if a.length == 1
275
+ b = b[0] if b.length == 1
276
+ c = c[0] if c.length == 1
277
+ d = d[0] if d.length == 1
278
+ e = e[0] if e.length == 1
279
+ { pobox: a, ext: b, street: c,
280
+ locality: d, region: e, code: "", country: "" }
281
+ end | seq(component << ";".r, component << ";".r, component << ";".r, component) do |a, b, c, d|
282
+ a = a[0] if a.length == 1
283
+ b = b[0] if b.length == 1
284
+ c = c[0] if c.length == 1
285
+ d = d[0] if d.length == 1
286
+ { pobox: a, ext: b, street: c,
287
+ locality: d, region: "", code: "", country: "" }
288
+ end | seq(component << ";".r, component << ";".r, component) do |a, b, c|
289
+ a = a[0] if a.length == 1
290
+ b = b[0] if b.length == 1
291
+ c = c[0] if c.length == 1
292
+ { pobox: a, ext: b, street: c,
293
+ locality: "", region: "", code: "", country: "" }
294
+ end | seq(component << ";".r, component) do |a, b|
295
+ a = a[0] if a.length == 1
296
+ b = b[0] if b.length == 1
297
+ { pobox: a, ext: b, street: "",
298
+ locality: "", region: "", code: "", country: "" }
299
+ end | component.map do |a|
300
+ a = a[0] if a.length == 1
301
+ { pobox: a, ext: "", street: "",
302
+ locality: "", region: "", code: "", country: "" }
303
+ end
304
+ address = address1.map { |n| PropertyValue::Address.new(n) }
305
+ address.eof
306
+ end
307
+
308
+ def registered_propname
309
+ registered_propname = C::NAME_VCARD
310
+ registered_propname.eof
311
+ end
312
+
313
+ def registered_propname?(x)
314
+ p = registered_propname.parse(x)
315
+ not(Rsec::INVALID[p])
316
+ end
317
+
318
+ # text escapes: \\ \; \, \N \n
319
+ def unescape(x)
320
+ # temporarily escape \\ as \007f, which is disallowed in any text
321
+ x.gsub(/\\\\/, "\u007f").gsub(/\\;/, ";").gsub(/\\,/, ",").gsub(/\\[Nn]/, "\n").tr("\u007f", "\\")
322
+ end
323
+
324
+ # Enforce type restrictions on values of particular properties.
325
+ # If successful, return typed interpretation of string
326
+ def typematch(strict, key, params, _component, value, ctx)
327
+ errors = []
328
+ params[:VALUE] = params[:VALUE].downcase if params && params[:VALUE]
329
+ ctx1 = Rsec::ParseContext.new value, "source"
330
+ case key
331
+ when :VERSION
332
+ ret = versionvalue._parse ctx1
333
+ when :SOURCE, :URL, :IMPP, :FBURL, :CALURI, :CALADRURI, :CAPURI
334
+ ret = uri._parse ctx1
335
+ # not imposing filename restrictions on calendar URIs
336
+ when :NAME, :FN, :LABEL, :EMAIL, :MAILER, :TITLE, :ROLE, :NOTE, :PRODID, :SORT_STRING, :UID
337
+ ret = text_t._parse ctx1
338
+ when :CLASS
339
+ ret = classvalue._parse ctx1
340
+ when :CATEGORIES, :NICKNAME
341
+ ret = textlist._parse ctx1
342
+ when :ORG
343
+ ret = org._parse ctx1
344
+ when :PROFILE
345
+ ret = profilevalue._parse ctx1
346
+ when :N
347
+ ret = fivepartname._parse ctx1
348
+ when :PHOTO, :LOGO, :SOUND
349
+ ret = if params && params[:VALUE] == "uri"
350
+ uri._parse ctx1
351
+ else
352
+ binary._parse ctx1
353
+ end
354
+ when :KEY
355
+ ret = if params && params[:ENCODING] == "b"
356
+ binary._parse ctx1
357
+ else
358
+ text_t._parse ctx1
359
+ end
360
+ when :BDAY
361
+ ret = if params && params[:VALUE] == "date-time"
362
+ date_time._parse ctx1
363
+ elsif params && params[:VALUE] == "date"
364
+ date_t._parse ctx1
365
+ else
366
+ # unlike VCARD 4, can have either date || date_time without explicit value switch
367
+ date_or_date_time._parse ctx1
368
+ end
369
+ when :REV
370
+ ret = if params && params[:VALUE] == "date"
371
+ date_t._parse ctx1
372
+ elsif params && params[:VALUE] == "date-time"
373
+ date_time._parse ctx1
374
+ else
375
+ # unlike VCARD 4, can have either date || date_time without explicit value switch
376
+ ret = date_or_date_time._parse ctx1
377
+ end
378
+ when :ADR
379
+ ret = address._parse ctx1
380
+ when :TEL
381
+ ret = phone_number._parse ctx1
382
+ when :TZ
383
+ ret = if params && params[:VALUE] == "text"
384
+ text_t._parse ctx1
385
+ else
386
+ utc_offset._parse ctx1
387
+ end
388
+ when :GEO
389
+ ret = geovalue._parse ctx1
390
+ when :AGENT
391
+ if params && params[:VALUE] == "uri"
392
+ ret = uri._parse ctx1
393
+ else
394
+ # unescape
395
+ value = value.gsub(/\\n/, "\n").gsub(/\\;/, ";").gsub(/\\,/, ",").gsub(/\\:/, ":")
396
+ # spec says that colons need to be escaped, but none of the examples do so
397
+ value = value.gsub(/BEGIN:VCARD\n/, "BEGIN:VCARD\nVERSION:3.0\n") unless value =~ /\nVERSION:3\.0/
398
+ ctx1 = Rsec::ParseContext.new value, "source"
399
+ ret = PropertyValue::Agent.new(Grammar.new(strict).vobject_grammar._parse(ctx1))
400
+ # TODO same strictness as grammar
401
+ end
402
+ else
403
+ ret = text_t._parse ctx1
404
+ end
405
+ if ret.is_a?(Hash) && ret[:error]
406
+ parse_err(strict, errors, "#{ret[:error]} for property #{key}, value #{value}", ctx)
407
+ end
408
+ if Rsec::INVALID[ret]
409
+ parse_err(strict, errors, "Type mismatch for property #{key}, value #{value}", ctx)
410
+ end
411
+ Rsec::Fail.reset
412
+ [ret, errors]
413
+ end
414
+
415
+ private
416
+
417
+ def parse_err(strict, errors, msg, ctx)
418
+ if strict
419
+ raise ctx.report_error msg, "source"
420
+ else
421
+ errors << ctx.report_error(msg, "source")
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,40 @@
1
+ require "vobject/component"
2
+ require "vobject/vcard/v4_0/property"
3
+ require "vobject/vcard/v4_0/grammar"
4
+ require "pp"
5
+
6
+ module Vcard::V4_0
7
+ class Component < Vobject::Component
8
+ class << self
9
+ def parse(vcf, strict)
10
+ hash = Vcard::V4_0::Grammar.new(strict).parse(vcf)
11
+ comp_name = hash.keys.first
12
+ new comp_name, hash[comp_name], hash[:errors]
13
+ end
14
+
15
+ private
16
+
17
+ def raise_invalid_parsing
18
+ raise "vCard parse failed"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def property_base_class
25
+ version_class.const_get(:Property)
26
+ end
27
+
28
+ def component_base_class
29
+ version_class.const_get(:Component)
30
+ end
31
+
32
+ def parameter_base_class
33
+ version_class.const_get(:Parameter)
34
+ end
35
+
36
+ def version_class
37
+ Vcard::V4_0
38
+ end
39
+ end
40
+ end