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,225 @@
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 "vobject/component"
9
+ require "vobject/vcard/v4_0/paramcheck"
10
+ require "vobject/vcard/v4_0/typegrammars"
11
+ require_relative "../../../c"
12
+ require_relative "../../../error"
13
+
14
+ module Vcard::V4_0
15
+ class Grammar
16
+ attr_accessor :strict, :errors
17
+
18
+ class << self
19
+ def unfold(str)
20
+ str.gsub(/[\n\r]+[ \t]/, "")
21
+ end
22
+ end
23
+
24
+ # RFC 6868
25
+ def rfc6868decode(x)
26
+ x.gsub(/\^n/, "\n").gsub(/\^\^/, "^").gsub(/\^'/, '"')
27
+ end
28
+
29
+ def vobject_grammar
30
+ # properties with value cardinality 1
31
+ @cardinality1 = {}
32
+ @cardinality1[:PARAM] = Set.new [:VALUE]
33
+ @cardinality1[:PROP] = Set.new [:KIND, :N, :BDAY, :ANNIVERSARY, :GENDER, :PRODID, :REV, :UID, :BIRTHPLACE, :DEATHPLACE, :DEATHDATE]
34
+
35
+ group = C::IANATOKEN
36
+ linegroup = group << "."
37
+ beginend = /BEGIN/i.r | /END/i.r
38
+
39
+ # parameters && parameter types
40
+ paramname = /LANGUAGE/i.r | /VALUE/i.r | /PREF/i.r | /ALTID/i.r | /PID/i.r |
41
+ /TYPE/i.r | /MEDIATYPE/i.r | /CALSCALE/i.r | /SORT-AS/i.r |
42
+ /GEO/i.r | /TZ/i.r | /LABEL/i.r | /INDEX/i.r | /LEVEL/i.r
43
+ otherparamname = C::NAME_VCARD ^ paramname
44
+ paramvalue = C::QUOTEDSTRING_VCARD.map { |s| rfc6868decode s } | C::PTEXT_VCARD.map { |s| rfc6868decode(s).upcase }
45
+ # tzidvalue = seq("/".r._?, C::PTEXT_VCARD).map { |_, val| val }
46
+ calscalevalue = /GREGORIAN/i.r | C::IANATOKEN | C::XNAME_VCARD
47
+ prefvalue = /[0-9]{1,2}/i.r | "100".r
48
+ pidvalue = /[0-9]+(\.[0-9]+)?/.r
49
+ pidvaluelist = seq(pidvalue, ",", lazy { pidvaluelist }) do |a, _, b|
50
+ [a, b].flatten
51
+ end | (pidvalue ^ ",".r).map { |z| [z] }
52
+ typeparamtel1 = /TEXT/i.r | /VOICE/i.r | /FAX/i.r | /CELL/i.r | /VIDEO/i.r |
53
+ /PAGER/i.r | /TEXTPHONE/i.r
54
+ typeparamtel = typeparamtel1 | C::IANATOKEN | C::XNAME_VCARD
55
+ typeparamrelated = /CONTACT/i.r | /ACQUAINTANCE/i.r | /FRIEND/i.r | /MET/i.r |
56
+ /CO-WORKER/i.r | /COLLEAGUE/i.r | /CO-RESIDENT/i.r | /NEIGHBOR/i.r |
57
+ /CHILD/i.r | /PARENT/i.r | /SIBLING/i.r | /SPOUSE/i.r | /KIN/i.r |
58
+ /MUSE/i.r | /CRUSH/i.r | /DATE/i.r | /SWEETHEART/i.r | /ME/i.r |
59
+ /AGENT/i.r | /EMERGENCY/i.r
60
+ typevalue = /WORK/i.r | /HOME/i.r | typeparamtel1 | typeparamrelated | C::IANATOKEN | C::XNAME_VCARD
61
+ typevaluelist = seq(typevalue << ",".r, lazy { typevaluelist }) do |a, b|
62
+ [a.upcase, b].flatten
63
+ end | typevalue.map { |t| [t.upcase] }
64
+ typeparamtel1list = seq(typeparamtel << ",".r, lazy { typeparamtel1list }) do |a, b|
65
+ [a.upcase, b].flatten
66
+ end | typeparamtel.map { |t| [t.upcase] }
67
+ geourlvalue = seq('"'.r >> C::TEXT4 << '"'.r) do |s|
68
+ parse_err("geo value not a URI") unless s =~ URI::DEFAULT_PARSER.make_regexp
69
+ s
70
+ end
71
+ tzvalue = paramvalue | geourlvalue
72
+ valuetype = /TEXT/i.r | /URI/i.r | /TIMESTAMP/i.r | /TIME/i.r | /DATE-TIME/i.r | /DATE/i.r |
73
+ /DATE-AND-OR-TIME/i.r | /BOOLEAN/i.r | /INTEGER/i.r | /FLOAT/i.r | /UTC-OFFSET/i.r |
74
+ /LANGUAGE-TAG/i.r | C::IANATOKEN | C::XNAME_VCARD
75
+ mediaattr = /[!\"#$%&'*+.^A-Z0-9a-z_`i{}|~-]+/.r
76
+ mediavalue = mediaattr | C::QUOTEDSTRING_VCARD
77
+ mediatail = seq(";".r >> mediaattr << "=".r, mediavalue).map do |a, v|
78
+ ";#{a}=#{v}"
79
+ end
80
+ rfc4288regname = /[A-Za-z0-9!#$&.+^+-]{1,127}/.r
81
+ rfc4288typename = rfc4288regname
82
+ rfc4288subtypename = rfc4288regname
83
+ mediavalue = seq(rfc4288typename << "/".r, rfc4288subtypename, mediatail.star).map do |t, s, tail|
84
+ ret = "#{t}/#{s}"
85
+ ret = ret . tail[0] unless tail.empty?
86
+ ret
87
+ end
88
+ pvalue_list = (seq(paramvalue << ",".r, lazy { pvalue_list }) & /[;:]/.r).map do |e, list|
89
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
90
+ end | (paramvalue & /[;:]/.r).map do |e|
91
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
92
+ end
93
+ quoted_string_list = (seq(C::QUOTEDSTRING_VCARD << ",".r, lazy { quoted_string_list }) & /[;:]/.r).map do |e, list|
94
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
95
+ end | (C::QUOTEDSTRING_VCARD & /[;:]/.r).map do |e|
96
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
97
+ end
98
+
99
+ # fmttypevalue = seq(rfc4288typename, "/", rfc4288subtypename).map(&:join)
100
+ levelvalue = /beginner/i.r | /average/i.r | /expert/i.r | /high/i.r | /medium/i.r | /low/i.r
101
+
102
+ param = seq(/ALTID/i.r, "=", paramvalue) do |name, _, val|
103
+ { name.upcase.tr("-", "_").to_sym => val }
104
+ end | seq(/LANGUAGE/i.r, "=", C::RFC5646LANGVALUE) do |name, _, val|
105
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
106
+ end | seq(/PREF/i.r, "=", prefvalue) do |name, _, val|
107
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
108
+ end | seq(/TYPE/i.r, "=", "\"".r >> typevaluelist << "\"".r) do |name, _, val|
109
+ # not in spec but in examples. Errata ID 3488, "Held for Document Update": acknwoledged as error requiring an updated spec. With this included, TYPE="x,y,z" is a list of values; the proper ABNF behaviour is that "x,y,z" is interpreted as a single value
110
+ { name.upcase.tr("-", "_").to_sym => val }
111
+ end | seq(/TYPE/i.r, "=", typevaluelist) do |name, _, val|
112
+ { name.upcase.tr("-", "_").to_sym => val }
113
+ end | seq(/MEDIATYPE/i.r, "=", mediavalue) do |name, _, val|
114
+ { name.upcase.tr("-", "_").to_sym => val }
115
+ end | seq(/CALSCALE/i.r, "=", calscalevalue) do |name, _, val|
116
+ { name.upcase.tr("-", "_").to_sym => val }
117
+ end | seq(/SORT-AS/i.r, "=", pvalue_list) do |name, _, val|
118
+ { name.upcase.tr("-", "_").to_sym => val }
119
+ end | seq(/TZ/i.r, "=", tzvalue) do |name, _, val|
120
+ { name.upcase.tr("-", "_").to_sym => val }
121
+ end | seq(/GEO/i.r, "=", geourlvalue) do |name, _, val|
122
+ { name.upcase.tr("-", "_").to_sym => val }
123
+ end | seq(/VALUE/i.r, "=", valuetype) do |name, _, val|
124
+ { name.upcase.tr("-", "_").to_sym => val }
125
+ end | seq(/PID/i.r, "=", pidvaluelist) do |name, _, val|
126
+ { name.upcase.tr("-", "_").to_sym => val }
127
+ end | seq(/INDEX/i.r, "=", prim(:int32)) do |name, _, val|
128
+ { name.upcase.tr("-", "_").to_sym => val }
129
+ end | seq(/LEVEL/i.r, "=", levelvalue) do |name, _, val|
130
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
131
+ end | seq(otherparamname, "=", pvalue_list) do |name, _, val|
132
+ val = val[0] if val.length == 1
133
+ { name.upcase.tr("-", "_").to_sym => val }
134
+ end | seq(paramname, "=", pvalue_list) do |name, _, val|
135
+ parse_err("Violated format of parameter value #{name} = #{val}")
136
+ end
137
+
138
+ params = seq(";".r >> param, lazy { params }) do |p, ps|
139
+ p.merge(ps) do |key, old, new|
140
+ if @cardinality1[:PARAM].include?(key)
141
+ parse_err("Violated cardinality of parameter #{key}")
142
+ end
143
+ [old, new].flatten
144
+ # deal with duplicate properties
145
+ end
146
+ end | seq(";".r >> param ^ ";".r).map { |e| e[0] }
147
+
148
+ contentline = seq(linegroup._?, C::NAME_VCARD, params._? << ":".r,
149
+ C::VALUE, /[\r\n]/) do |l, name, p, value, _|
150
+ key = name.upcase.tr("-", "_").to_sym
151
+ hash = { key => {} }
152
+ errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
153
+ hash[key][:value], errors1 = Typegrammars.typematch(strict, key, p[0], :GENERIC, value)
154
+ errors << errors1
155
+ hash[key][:group] = l[0] unless l.empty?
156
+ hash[key][:params] = p[0] unless p.empty?
157
+ hash
158
+ end
159
+ props = seq(contentline, lazy { props }) do |c, rest|
160
+ c.merge(rest) do |key, old, new|
161
+ if @cardinality1[:PROP].include?(key.upcase) &&
162
+ !(new.is_a?(Array) &&
163
+ new[0].key?(:params) && new[0][:params].key?(:ALTID) &&
164
+ old.key?(:params) && old[:params].key?(:ALTID) &&
165
+ old[:params][:ALTID] == new[0][:params][:ALTID]) &&
166
+ !(new.is_a?(Hash) &&
167
+ old.key?(:params) && old[:params].key?(:ALTID) &&
168
+ new.key?(:params) && new[:params].key?(:ALTID) &&
169
+ old[:params][:ALTID] == new[:params][:ALTID])
170
+ parse_err("Violated cardinality of property #{key}")
171
+ end
172
+ [old, new].flatten
173
+ # deal with duplicate properties
174
+ end
175
+ end | ("".r & beginend).map { {} }
176
+
177
+ calpropname = /VERSION/i.r
178
+ calprop = seq(calpropname << ":".r, C::VALUE, /[\r\n]/.r) do |key, value|
179
+ key = key.upcase.tr("-", "_").to_sym
180
+ hash = { key => {} }
181
+ hash[key][:value], errors1 = Typegrammars.typematch(strict, key, nil, :VCARD, value)
182
+ errors << errors1
183
+ hash
184
+ end
185
+ vobject = seq(/BEGIN:VCARD[\r\n]/i.r >> calprop, props << /END:VCARD[\r\n]/i.r) do |v, rest|
186
+ parse_err("Missing VERSION attribute") unless v.has_key?(:VERSION)
187
+ parse_err("Missing FN attribute") unless rest.has_key?(:FN)
188
+ rest.delete(:END)
189
+ { VCARD: v.merge(rest), errors: errors.flatten }
190
+ end
191
+ vobject.eof
192
+ end
193
+
194
+ def initialize(strict)
195
+ self.strict = strict
196
+ self.errors = []
197
+ end
198
+
199
+ def parse(vobject)
200
+ @ctx = Rsec::ParseContext.new self.class.unfold(vobject), "source"
201
+ ret = vobject_grammar._parse @ctx
202
+ if !ret || Rsec::INVALID[ret]
203
+ if strict
204
+ raise @ctx.generate_error "source"
205
+ else
206
+ errors << @ctx.generate_error("source")
207
+ ret = { VCARD: nil, errors: errors.flatten }
208
+ end
209
+
210
+ end
211
+ Rsec::Fail.reset
212
+ ret
213
+ end
214
+
215
+ private
216
+
217
+ def parse_err(msg)
218
+ if strict
219
+ raise @ctx.report_error msg, "source"
220
+ else
221
+ errors << @ctx.report_error(msg, "source")
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,270 @@
1
+ require "set"
2
+ require "uri"
3
+ require "date"
4
+ include Rsec::Helpers
5
+ require "vobject/vcard/version"
6
+ require "vobject"
7
+
8
+ module Vcard::V4_0
9
+ class Paramcheck
10
+ class << self
11
+ def paramcheck(strict, prop, params, ctx)
12
+ errors = []
13
+ if params && params[:TYPE]
14
+ case prop
15
+ when :FN, :NICKNAME, :PHOTO, :ADR, :TEL, :EMAIL, :IMPP, :LANG, :TZ,
16
+ :GEO, :TITLE, :ROLE, :LOGO, :ORG, :RELATED, :CATEGORIES, :NOTE,
17
+ :SOUND, :URL, :KEY, :FBURL, :CALADRURI, :CALURI, :EXPERTISE,
18
+ :HOBBY, :INTEREST, :ORG_DIRECTORY
19
+ # no-op
20
+ when /^x/i
21
+ # no-op
22
+ else
23
+ parse_err(strict, errors, ":TYPE parameter given for #{prop}", ctx)
24
+ end
25
+ end
26
+ if params && params[:MEDIATYPE]
27
+ case prop
28
+ when :SOURCE, :PHOTO, :IMPP, :GEO, :LOGO, :MEMBER, :SOUND, :URL,
29
+ :FBURL, :CALADRURI, :CALURI, :UID, :TZ
30
+ # no-op
31
+ when :TEL, :KEY
32
+ if params[:VALUE] == "uri"
33
+ else
34
+ parse_err(strict, errors, ":MEDIATYPE parameter given for #{prop} with :VALUE of text", ctx)
35
+ end
36
+ when :RELATED
37
+ if params[:VALUE] == "text"
38
+ parse_err(strict, errors, ":MEDIATYPE parameter given for #{prop} with :VALUE of text", ctx)
39
+ end
40
+ when /^x/i
41
+ else
42
+ parse_err(strict, errors, ":MEDIATYPE parameter given for #{prop}", ctx)
43
+ end
44
+ end
45
+ if params && params[:CALSCALE]
46
+ case prop
47
+ when :BDAY, :ANNIVERSARY
48
+ # no-op
49
+ when :DEATHDATE
50
+ if params[:VALUE] == "text"
51
+ parse_err(strict, errors, ":CALSCALE parameter given for #{prop} with :VALUE of text", ctx)
52
+ end
53
+ when /^x/i
54
+ # no-op
55
+ else
56
+ parse_err(strict, errors, ":CALSCALE parameter given for #{prop}", ctx)
57
+ end
58
+ end
59
+ if params && params[:GEO]
60
+ case prop
61
+ when :ADR
62
+ # no-op
63
+ when /^x/i
64
+ # no-op
65
+ else
66
+ parse_err(strict, errors, ":GEO parameter given for #{prop}", ctx)
67
+ end
68
+ end
69
+ if params && params[:TZ]
70
+ case prop
71
+ when :ADR
72
+ # no-op
73
+ when /^x/i
74
+ # no-op
75
+ else
76
+ parse_err(strict, errors, ":TZ parameter given for #{prop}", ctx)
77
+ end
78
+ end
79
+ if params && params[:LANGUAGE]
80
+ case prop
81
+ when :FN, :N, :NICKNAME, :ADR, :TITLE, :ROLE, :LOGO, :ORG, :NOTE,
82
+ :SOUND, :BIRTHPLACE, :DEATHPLACE, :EXPERTISE, :HOBBY, :INTEREST, :ORG_DIRECTORY
83
+ # no-op
84
+ when :BDAY, :ANNIVERSARY, :DEATHDATE
85
+ # added :ANNIVERSARY per errata
86
+ if params[:VALUE] == "text"
87
+ else
88
+ parse_err(strict, errors, ":LANGUAGE parameter given for #{prop} with :VALUE of date/time", ctx)
89
+ end
90
+ when :RELATED
91
+ if params[:VALUE] == "text"
92
+ else
93
+ parse_err(strict, errors, ":LANGUAGE parameter given for #{prop} with :VALUE of uri", ctx)
94
+ end
95
+ when /^x/i
96
+ # no-op
97
+ else
98
+ parse_err(strict, errors, ":LANGUAGE parameter given for #{prop}", ctx)
99
+ end
100
+ end
101
+ if params && params[:VALUE]
102
+ case prop
103
+ when :SOURCE, :KIND, :XML, :FN, :N, :NICKNAME, :PHOTO, :GENDER, :ADR,
104
+ :TEL, :EMAIL, :IMPP, :LANG, :TZ, :GEO, :TITLE, :ROLE, :LOGO, :ORG,
105
+ :MEMBER, :RELATED, :CATEGORIES, :NOTE, :PRODID, :REV, :SOUND, :URL, :VERSION,
106
+ :KEY, :FBURL, :CALADRURI, :CALURI, :BDAY, :ANNIVERSARY, :BIRTHPLACE,
107
+ :DEATHPLACE, :DEATHDATE
108
+ # no-op
109
+ when /^x/i
110
+ # no-op
111
+ else
112
+ parse_err(strict, errors, ":VALUE parameter given for #{prop}", ctx)
113
+ end
114
+ end
115
+ if params && params[:PREF]
116
+ case prop
117
+ when :SOURCE, :FN, :NICKNAME, :PHOTO, :ADR, :TEL, :EMAIL, :IMPP, :LANG,
118
+ :TZ, :GEO, :TITLE, :ROLE, :LOGO, :ORG, :MEMBER, :RELATED, :CATEGORIES,
119
+ :NOTE, :SOUND, :URL, :KEY, :FBURL, :CALADRURI, :CALURI, :EXPERTISE,
120
+ :HOBBY, :INTEREST, :ORG_DIRECTORY, :ORG_DIRECTORY
121
+ # no-op
122
+ when /^x/i
123
+ # no-op
124
+ else
125
+ parse_err(strict, errors, ":PREF parameter given for #{prop}", ctx)
126
+ end
127
+ end
128
+ if params && params[:PID]
129
+ case prop
130
+ when :SOURCE, :FN, :NICKNAME, :PHOTO, :ADR, :TEL, :EMAIL, :IMPP, :LANG,
131
+ :TZ, :GEO, :TITLE, :ROLE, :LOGO, :ORG, :MEMBER, :RELATED, :CATEGORIES,
132
+ :NOTE, :SOUND, :URL, :KEY, :FBURL, :CALADRURI, :CALURI, :ORG_DIRECTORY
133
+ # no-op
134
+ when /^x/i
135
+ # no-op
136
+ else
137
+ parse_err(strict, errors, ":PID parameter given for #{prop}", ctx)
138
+ end
139
+ end
140
+ if params && params[:SORT_AS]
141
+ case prop
142
+ when :N, :ORG
143
+ # no-op
144
+ when /^x/i
145
+ # no-op
146
+ else
147
+ parse_err(strict, errors, ":SORT_AS parameter given for #{prop}", ctx)
148
+ end
149
+ end
150
+ if params && params[:ALTID]
151
+ case prop
152
+ when :SOURCE, :XML, :FN, :N, :NICKNAME, :PHOTO, :BDAY, :ANNIVERSARY, :ADR,
153
+ :TEL, :EMAIL, :IMPP, :LANG, :TZ, :GEO, :TITLE, :ROLE, :LOGO, :ORG, :MEMBER,
154
+ :RELATED, :CATEGORIES, :NOTE, :SOUND, :URL, :KEY, :FBURL, :CALADRURI, :CALURI,
155
+ :BIRTHPLACE, :DEATHPLACE, :DEATHDATE, :EXPERTISE, :HOBBY, :INTEREST, :ORG_DIRECTORY
156
+ # no-op
157
+ when /^x/i
158
+ # no-op
159
+ else
160
+ parse_err(strict, errors, ":SOURCE parameter given for #{prop}", ctx)
161
+ end
162
+ end
163
+ if params && params[:LABEL]
164
+ case prop
165
+ when :ADR
166
+ # no-op
167
+ when /^x/i
168
+ # no-op
169
+ else
170
+ parse_err(strict, errors, ":LABEL parameter given for #{prop}", ctx)
171
+ end
172
+ end
173
+ if params && params[:LEVEL]
174
+ case prop
175
+ when :EXPERTISE, :HOBBY, :INTEREST
176
+ # no-op
177
+ when /^x/i
178
+ # no-op
179
+ else
180
+ parse_err(strict, errors, ":LEVEL parameter given for #{prop}", ctx)
181
+ end
182
+ end
183
+ if params && params[:INDEX]
184
+ case prop
185
+ when :EXPERTISE, :HOBBY, :INTEREST, :ORG_DIRECTORY
186
+ # no-op
187
+ when /^x/i
188
+ # no-op
189
+ else
190
+ parse_err(strict, errors, ":INDEX parameter given for #{prop}", ctx)
191
+ end
192
+ end
193
+ params.each do |p|
194
+ case p
195
+ when :LANGUAGE, :VALUE, :PREF, :PID, :TYPE, :GEO, :TZ, :SORT_AS, :CALSCALE,
196
+ :LABEL, :ALTID
197
+ # no-op
198
+ when /^x/i
199
+ # xname parameters are always allowed
200
+ else
201
+ # any-param
202
+ case prop
203
+ when :SOURCE, :KIND, :FN, :N, :NICKNAME, :PHOTO, :BDAY, :ANNIVERSARY, :GENDER,
204
+ :ADR, :TEL, :EMAIL, :IMPP, :LANG, :TZ, :GEO, :TITLE, :ROLE, :LOGO, :ORG, :MEMBER,
205
+ :RELATED, :CATEGORIES, :NOTE, :PRODID, :REV, :SOUND, :UID, :CLIENTPIDMAP, :URL,
206
+ :VERSION, :KEY, :FBURL, :CALADRURI, :CALURI, :BIRTHPLACE, :DEATHPLACE, :DEATHDATE,
207
+ :EXPERTISE, :HOBBY, :INTEREST, :ORG_DIRECTORY
208
+ # no-op
209
+ when /^x/i
210
+ # no-op
211
+ else
212
+ parse_err(strict, errors, "#{p} parameter given for #{prop}", ctx)
213
+ end
214
+ end
215
+ end
216
+ case prop
217
+ when :SOURCE, :PHOTO, :IMPP, :GEO, :LOGO, :MEMBER, :SOUND, :URL, :FBURL,
218
+ :CALADRURI, :CALURI
219
+ params.each do |key, val|
220
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "uri"
221
+ end
222
+ when :LANG
223
+ params.each do |key, val|
224
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "language-tag"
225
+ end
226
+ when :REV
227
+ params.each do |key, val|
228
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "timestamp"
229
+ end
230
+ when :KIND, :XML, :FN, :N, :NICKNAME, :GENDER, :ADR, :EMAIL, :TITLE, :ROLE, :ORG,
231
+ :CATEGORIES, :NOTE, :PRODID, :VERSION
232
+ params.each do |key, val|
233
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "text"
234
+ end
235
+ when :BDAY, :ANNIVERSARY, :DEATHDATE
236
+ params.each do |key, val|
237
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "date-and-or-time" && val != "text"
238
+ end
239
+ when :TEL, :RELATED, :UID, :KEY, :BIRTHPLACE, :DEATHPLACE
240
+ params.each do |key, val|
241
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "uri" && val != "text"
242
+ end
243
+ when :TZ
244
+ params.each do |key, val|
245
+ parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "uri" && val != "text" && val != "utc-offset"
246
+ end
247
+ when :EXPERTISE
248
+ if params && params[:LEVEL]
249
+ parse_err(strict, errors, "illegal value #{params[:LEVEL]} given for parameter :LEVEL of #{prop}", ctx) unless params[:LEVEL] =~ /^(beginner|average|expert)$/i
250
+ end
251
+ when :HOBBY, :INTEREST
252
+ if params && params[:LEVEL]
253
+ parse_err(strict, errors, "illegal value #{params[:LEVEL]} given for parameter :LEVEL of #{prop}", ctx) unless params[:LEVEL] =~ /^(high|medium|low)$/i
254
+ end
255
+ end
256
+ errors
257
+ end
258
+
259
+ private
260
+
261
+ def parse_err(strict, errors, msg, ctx)
262
+ if strict
263
+ raise ctx.report_error msg, "source"
264
+ else
265
+ errors << ctx.report_error(msg, "source")
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end