ruby-vobject 0.1.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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