vobject 0.1.0 → 1.0.2

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