vobject 0.1.0 → 1.0.2

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 (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