virginity 0.3.31

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/virginity.rb +6 -0
  3. data/lib/virginity/api_extensions.rb +87 -0
  4. data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
  5. data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
  6. data/lib/virginity/bnf.rb +84 -0
  7. data/lib/virginity/dir_info.rb +93 -0
  8. data/lib/virginity/dir_info/content_line.rb +146 -0
  9. data/lib/virginity/dir_info/line_folding.rb +60 -0
  10. data/lib/virginity/dir_info/param.rb +208 -0
  11. data/lib/virginity/dir_info/query.rb +144 -0
  12. data/lib/virginity/encoding_decoding.rb +177 -0
  13. data/lib/virginity/encodings.rb +36 -0
  14. data/lib/virginity/fixes.rb +230 -0
  15. data/lib/virginity/vcard.rb +244 -0
  16. data/lib/virginity/vcard/base_field.rb +126 -0
  17. data/lib/virginity/vcard/categories.rb +57 -0
  18. data/lib/virginity/vcard/cleaning.rb +364 -0
  19. data/lib/virginity/vcard/field.rb +22 -0
  20. data/lib/virginity/vcard/field/params.rb +93 -0
  21. data/lib/virginity/vcard/field_values.rb +10 -0
  22. data/lib/virginity/vcard/field_values/binary.rb +22 -0
  23. data/lib/virginity/vcard/field_values/boolean.rb +14 -0
  24. data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
  25. data/lib/virginity/vcard/field_values/date.rb +16 -0
  26. data/lib/virginity/vcard/field_values/integer.rb +15 -0
  27. data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
  28. data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
  29. data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
  30. data/lib/virginity/vcard/field_values/text.rb +23 -0
  31. data/lib/virginity/vcard/field_values/uri.rb +15 -0
  32. data/lib/virginity/vcard/fields.rb +284 -0
  33. data/lib/virginity/vcard/fields_osx.rb +95 -0
  34. data/lib/virginity/vcard/fields_soocial.rb +45 -0
  35. data/lib/virginity/vcard/name_handler.rb +151 -0
  36. data/lib/virginity/vcard/patching.rb +262 -0
  37. data/lib/virginity/vcard21.rb +2 -0
  38. data/lib/virginity/vcard21/base.rb +30 -0
  39. data/lib/virginity/vcard21/parser.rb +359 -0
  40. data/lib/virginity/vcard21/reader.rb +103 -0
  41. data/lib/virginity/vcard21/writer.rb +139 -0
  42. metadata +111 -0
@@ -0,0 +1,95 @@
1
+ module Virginity
2
+
3
+ class RelatedNames < BaseField
4
+ include Params::Type
5
+ include FieldValues::Text
6
+ register_for "X-ABRELATEDNAMES"
7
+ end
8
+
9
+
10
+ class XAbLabel < BaseField
11
+ ABLABEL = "X-ABLabel"
12
+ register_for ABLABEL
13
+ SAFE_LABELS = %w(HOME WORK FAX CELL PREF MAIN PAGER INTERNET VOICE)
14
+ STRANGELABEL_MATCHER = /_\$!<(.*)>!\$_/
15
+ STRANGE_LABELS = {}
16
+ %w(HomePage Other Assistant Father Mother Parent Brother Sister Child Friend Spouse Partner Manager Anniversary).each do |v|
17
+ STRANGE_LABELS[v.upcase] = v
18
+ end
19
+ STRANGE_LABELS.freeze
20
+ #SAFECHARS = /^(.[^\"\;\:\,])*$/
21
+ #ALREADYQOUTED = /^\"(.*?)\"$/
22
+
23
+ def self.from_param(param, options = {})
24
+ raise TypeError, "expected a Param with key == \"TYPE\"" unless param.key.upcase == "TYPE"
25
+ from_text(param.value, options)
26
+ end
27
+
28
+ def to_param
29
+ Param.new("TYPE", text)
30
+ end
31
+
32
+ def self.from_text(t, options = {})
33
+ new(ABLABEL).tap do |label|
34
+ label.text = t
35
+ label.group = options[:group]
36
+ end
37
+ end
38
+
39
+ def self.types_to_convert_to_xablabel(field)
40
+ field.params('TYPE').reject do |type|
41
+ SAFE_LABELS.include?(type.value)
42
+ end
43
+ end
44
+
45
+ # returns an array of XAbLables
46
+ def self.from_field(field)
47
+ types_to_convert_to_xablabel(field).map { |t| from_param(t) }
48
+ end
49
+
50
+ def text
51
+ if match = STRANGELABEL_MATCHER.match(@value)
52
+ match[1].upcase
53
+ else
54
+ @value
55
+ end
56
+ end
57
+
58
+ def text=(text)
59
+ if x = STRANGE_LABELS[text.upcase]
60
+ @value = "_$!<#{x}>!$_"
61
+ else
62
+ @value = text
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ class XAbDate < BaseField
69
+ include FieldValues::Text
70
+ include FieldValues::DateValue
71
+ include Params::Type
72
+ register_for "X-ABDATE"
73
+ end
74
+
75
+
76
+ class XAbAdr < BaseField
77
+ include FieldValues::Text
78
+ ABADR = "X-ABADR"
79
+ register_for ABADR
80
+
81
+ def self.from_param(param)
82
+ raise TypeError unless param.is_a?(Param) and param.key.downcase == "x-format"
83
+ from_text(param.value)
84
+ end
85
+
86
+ def self.from_text(t)
87
+ new(ABADR, EncodingDecoding::encode_text(t))
88
+ end
89
+
90
+ def to_param
91
+ Param.new("x-format", text)
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,45 @@
1
+ module Virginity
2
+
3
+ class XSoocialCustom < BaseField
4
+ PARTS = %w(key_name value)
5
+ include FieldValues::StructuredText.define(PARTS)
6
+ register_for "X-SOOCIAL-CUSTOM"
7
+
8
+ def value
9
+ rewrite_old_kv!
10
+ super
11
+ end
12
+
13
+ def key_name
14
+ rewrite_old_kv!
15
+ super
16
+ end
17
+
18
+ def text
19
+ value
20
+ end
21
+
22
+ def text=(txt)
23
+ self.value = txt
24
+ end
25
+
26
+ def to_s
27
+ rewrite_old_kv!
28
+ super
29
+ end
30
+
31
+ def rewrite_old_kv!
32
+ if name = params('NAME')[0]
33
+ self.key_name, self.value = name.value, EncodingDecoding.decode_text(raw_value)
34
+ params.delete_if { |p| p.key == "NAME" }
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ class XSoocialRemovedCategory < BaseField
41
+ include FieldValues::Text
42
+ register_for "X-SOOCIAL-REMOVED-CATEGORY"
43
+ end
44
+
45
+ end
@@ -0,0 +1,151 @@
1
+ require "virginity/vcard/fields"
2
+
3
+ module Virginity
4
+
5
+ class Vcard < DirectoryInformation
6
+ # A Vcard-wrapper that deals with the the fields N, FN and NICKNAME.
7
+ #
8
+ # You will probably use it like this:
9
+ # v = Vcard.new
10
+ # v.name.given = "Bert"
11
+ # puts v
12
+ #
13
+ # There are undocumented methods for getting and setting: prefix, given, additional, family, and suffix.
14
+ class NameHandler
15
+ # takes a Vcard object or a String
16
+ def initialize(vcard)
17
+ if vcard.is_a? Vcard
18
+ @vcard = vcard
19
+ else
20
+ @vcard = Vcard.from_vcard(vcard.to_s)
21
+ end
22
+ end
23
+
24
+ # formatted name
25
+ def to_s
26
+ fn.text
27
+ end
28
+ alias_method :formatted, :to_s
29
+
30
+ # regenerate the formatted name (that is the FN field)
31
+ def reset_formatted!
32
+ @vcard.delete(*@vcard.lines_with_name("FN"))
33
+ fn.text
34
+ end
35
+
36
+ # generate a FN field using the following fields
37
+ # n > nickname > org > email > impp > tel
38
+ def generate_fn(options = {})
39
+ nfield = n
40
+ g = nfield.given.empty? ? nil : nfield.given
41
+ f = nfield.family.empty? ? nil : nfield.family
42
+ unless [g, f].compact.empty?
43
+ if options[:include_nickname]
44
+ nick = @vcard.nicknames.empty? ? nil : "\"#{@vcard.nicknames.first.values.first}\""
45
+ [g, nick, f].compact.join(" ")
46
+ elsif options[:complete_name]
47
+ prefix = nfield.prefix.empty? ? nil : nfield.prefix
48
+ additional = nfield.additional.empty? ? nil : nfield.additional
49
+ suffix = nfield.suffix.empty? ? nil : nfield.suffix
50
+ [prefix, g, additional, f, suffix].compact.join(" ")
51
+ else
52
+ [g, f].compact.join(" ")
53
+ end
54
+ else
55
+ if not @vcard.nicknames.empty?
56
+ nicknames.first
57
+ elsif @vcard.organisations.first
58
+ @vcard.organisations.first.values.first
59
+ elsif @vcard.emails.first
60
+ @vcard.emails.first.address
61
+ elsif @vcard.impps.first
62
+ @vcard.impps.first.address
63
+ elsif @vcard.telephones.first
64
+ @vcard.telephones.first.number
65
+ else
66
+ ""
67
+ end
68
+ end
69
+ end
70
+
71
+ # generate the fn using the complete name including prefix, additional parts and suffix
72
+ def complete
73
+ generate_fn(:complete_name => true)
74
+ end
75
+
76
+ # add a fn if it's not there (since it is required by the vCard specs) and return it
77
+ def fn
78
+ @vcard.lines_with_name("FN").first || @vcard.add_field("FN:#{EncodingDecoding::encode_text(generate_fn)}")
79
+ end
80
+
81
+ # add a n if it's not there (since it is required by the vCard specs) and return it
82
+ def n
83
+ @vcard.lines_with_name("N").first || @vcard.add_field("N:;;;;")
84
+ end
85
+
86
+ Name::PARTS.each do |part|
87
+ class_eval <<-end_class_eval
88
+ def #{part}
89
+ n.#{part}
90
+ end
91
+
92
+ def #{part}=(value)
93
+ return value if n.#{part} == value
94
+ n.#{part} = value
95
+ reset_formatted!
96
+ value
97
+ end
98
+ end_class_eval
99
+ end
100
+
101
+ # are all parts of the N field empty? ("N:;;;;")
102
+ def empty?
103
+ Name::PARTS.all? { |part| send(part).empty? }
104
+ end
105
+
106
+ # an array with all nicknames
107
+ def nicknames
108
+ @vcard.nicknames.map {|n| n.values.to_a }.flatten
109
+ end
110
+
111
+ def add_nickname(nick)
112
+ @vcard << SeparatedField.new("NICKNAME", EncodingDecoding::encode_text_list([nick]))
113
+ reset_formatted!
114
+ nick
115
+ end
116
+
117
+ def remove_nickname(nick)
118
+ @vcard.nicknames.each do |nickname|
119
+ nickname.values.delete(nick)
120
+ @vcard.delete nickname if nickname.raw_value.empty? # the singular 'value' is meant here, don't change it to values!
121
+ end
122
+ reset_formatted!
123
+ nick
124
+ end
125
+
126
+ def has_nickname?(nick)
127
+ @vcard.nicknames.any? { |nickname| nickname.values.include?(nick) }
128
+ end
129
+
130
+ # merge this name with other_name; conflicting parts will raise a MergeError
131
+ #
132
+ # if the option :simple_name_resolving is true we choose the value in this name instead of raising an error. Parts that are not present in self will be filled in with the value from other_name
133
+ def merge_with!(other_name, options = {})
134
+ Name::PARTS.each do |part|
135
+ own, his = send(part).rstrip, other_name.send(part).rstrip
136
+ if own.empty?
137
+ send "#{part}=", his
138
+ elsif his.empty? or own == his
139
+ # then nothing needs to be done
140
+ else
141
+ # :simple_name_resolving means keep our own name, don't take over his. iow: do nothing
142
+ unless options[:simple_name_resolving]
143
+ raise MergeError, "#{part} name is different: '#{own}' and '#{his}'"
144
+ end
145
+ end
146
+ end
147
+ self
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,262 @@
1
+ require 'forwardable'
2
+ module Virginity
3
+ class Vcard < DirectoryInformation
4
+ # before: TEL;TYPE=HOME:1234
5
+ # after: TEL;TYPE=HOME:1233
6
+ # --- clearly fixing a typo so: ---
7
+ # update("TEL:1234", :value => 1233)
8
+
9
+ # before: TEL;TYPE=HOME:1234
10
+ # after: TEL;TYPE=HOME,WORK:1234
11
+ # --- params changed ---
12
+ # update("TEL:1234", :add_param => "TYPE=WORK")
13
+
14
+ # before: TEL;TYPE=HOME:1234
15
+ # after: TEL;TYPE=WORK:1234
16
+ # --- params changed ---
17
+ # update("TEL:1234", :remove_param => "TYPE=HOME", :add_param => "TYPE=WORK")
18
+
19
+ # before: TEL;TYPE=HOME:1234
20
+ # after: TEL;TYPE=WORK:1233
21
+ # --- someone removed the home phone and added a new number for work ---
22
+ # remove("TEL:1234")
23
+ # add("TEL;TYPE=WORK:1233")
24
+ module Patching
25
+ class IllegalPatch < Error; end
26
+
27
+ def patch!(diff)
28
+ diff.apply(self)
29
+ self
30
+ end
31
+
32
+ def self.diff(before_card, after_card)
33
+ Diff.diff(before_card, after_card)
34
+ end
35
+
36
+
37
+ # the base class
38
+ class Patch
39
+ def self.query_for_line(line)
40
+ line.name + ":" + line.raw_value
41
+ end
42
+
43
+ def self.query_from_string(query_string)
44
+ q = Field.parse(query_string)
45
+ # at this moment all our queries are like "name : raw_value"
46
+ query = { :name => q.name }
47
+ if q.respond_to? :values
48
+ query[:values] = q.values.to_a
49
+ elsif q.respond_to? :text
50
+ query[:text] = q.text
51
+ else
52
+ query[:raw_value] = q.raw_value
53
+ end
54
+ query
55
+ end
56
+
57
+ def field_from_query(query)
58
+ value = query.reject { |k, v| k == :name }
59
+ Field.named(query[:name]) do |f|
60
+ value.each do |k,v|
61
+ f.send(k+'=', v)
62
+ end
63
+ end
64
+ end
65
+
66
+ def apply(vcard)
67
+ raise "responsibility of subclass"
68
+ end
69
+
70
+ def self.normalize_vcard!(vcard)
71
+ vcard.clean_orgs! # orgs are of variable length, but ordered, they need normalizing before we can compare them
72
+ # normalize categories and nicknames (actually every unpackable field)
73
+ vcard.fields.each do |field|
74
+ vcard.unpack_field!(field) if field.respond_to?(:unpacked)
75
+ field.clean!
76
+ field.params.sort!
77
+ end
78
+ # remove double lines
79
+ vcard.clean_same_value_fields!
80
+ vcard.add("N") unless vcard.lines_with_name("N").any?
81
+ vcard.clean_name!
82
+ vcard
83
+ end
84
+ end
85
+
86
+
87
+ # a Diff is a collection of changes, I think we can even nest them which might be a cool way of combining them
88
+ class Diff < Patch
89
+ attr_reader :changes
90
+ extend Forwardable
91
+ def_delegators :@changes, :push, :<<, :empty?, :size
92
+
93
+ def initialize(*changes)
94
+ @changes = changes
95
+ end
96
+
97
+ def self.diff(before_card, after_card)
98
+ patch = Diff.new
99
+ before, after = before_card.deep_copy, after_card.deep_copy
100
+ normalize_vcard!(before)
101
+ normalize_vcard!(after)
102
+ before.lines.each do |line|
103
+ # if the exact same line is in after then we can stop processing this line
104
+ # this will of course always happen for begin:vcard, end:vcard
105
+ next unless after.delete(line).empty?
106
+ q = query_from_string(line)
107
+ if x = after.find_first(q)
108
+ patch << Update.diff_lines(line, x, :query => q)
109
+ after.delete(x)
110
+ #else if "there is a line with just one or 2 characters difference in the value" or "only insignificant characters changed, like dashes in telephone numbers"
111
+ # patch << Update.diff_lines(line, x, :query => q)
112
+ # after.delete(x)
113
+ else
114
+ patch << Remove.new(q)
115
+ end
116
+ end
117
+ # what is left in after should be added
118
+ after.lines.each { |line| patch << Add.new(line.to_s) }
119
+ patch
120
+ end
121
+
122
+ def apply(vcard)
123
+ Patch::normalize_vcard!(vcard)
124
+ @changes.each { |change| change.apply(vcard) }
125
+ end
126
+
127
+ def to_s
128
+ @changes.join("\n")
129
+ end
130
+
131
+ def pretty_print(q)
132
+ @changes.each do |change|
133
+ pp change
134
+ end
135
+ end
136
+ end
137
+
138
+
139
+ # to add a field
140
+ class Add < Patch
141
+ attr_accessor :field
142
+ def initialize(line)
143
+ @field = Field.parse(line)
144
+ end
145
+
146
+ def apply(vcard)
147
+ vcard.add_field(@field)
148
+ end
149
+
150
+ def to_s
151
+ "Add(#{@field.inspect})"
152
+ end
153
+
154
+ def pretty_print(q)
155
+ q.text "Add #{@field}"
156
+ end
157
+ end
158
+
159
+
160
+ # to remove fields matching a query
161
+ class Remove < Patch
162
+ attr_accessor :query
163
+ def initialize(query)
164
+ @query = query
165
+ end
166
+
167
+ def apply(vcard)
168
+ vcard.delete(*vcard.where(@query))
169
+ end
170
+
171
+ def to_s
172
+ "Remove(#{@query.inspect})"
173
+ end
174
+
175
+ def pretty_print(q)
176
+ q.text "Remove #{@query}"
177
+ end
178
+ end
179
+
180
+
181
+ # to update field matching the query
182
+ # for automatically generated diffs this will usually only entail updates to params
183
+ # there is an alternative action that will be executed if there is no field to update
184
+ class Update < Patch
185
+ attr_accessor :query, :updates, :alternative
186
+ def initialize(query, updates, alternative = nil)
187
+ @query = query
188
+ @updates = updates
189
+ @alternative = alternative
190
+ end
191
+
192
+ def self.diff_lines(before, after, options = {})
193
+ Update.new(
194
+ options[:query] || Patch::query_from_string(before),
195
+ line_diff(before, after),
196
+ Add.new(after)
197
+ )
198
+ end
199
+
200
+ COLON = ":"
201
+ SEMICOLON = ";"
202
+ def update_field!(field)
203
+ @updates.each_pair do |key, value|
204
+ case key
205
+ when :value
206
+ field.raw_value = value
207
+ when :add_param
208
+ # params_from_string returns an array of params
209
+ field.params += Param::params_from_string(value + COLON)
210
+ when :remove_param
211
+ Param::params_from_string(value + COLON).each do |param_to_delete|
212
+ field.params.delete_if { |p| p == param_to_delete }
213
+ end
214
+ else
215
+ raise IllegalPatch, "#{key}, #{value}"
216
+ end
217
+ end
218
+ end
219
+
220
+ def apply(vcard)
221
+ to_update = vcard.where(@query)
222
+ if to_update.empty?
223
+ # if the field has been deleted in the meantime on the server, the patch adds it again
224
+ # to_update << vcard.add_field(field_from_query(@query)) if to_update.empty?
225
+ @alternative.apply(vcard) unless @alternative.nil?
226
+ else
227
+ to_update.each { |field| update_field!(field) }
228
+ end
229
+ end
230
+
231
+ def to_s
232
+ s = "Update(#{@query.inspect}, #{@updates.map {|k,v| "#{k}(#{v.inspect})" }.join(", ")})"
233
+ if @alternative
234
+ s << " else { #{alternative} }"
235
+ end
236
+ end
237
+
238
+ def pretty_print(q)
239
+ q.text "Replace #{query} with #{@updates.map {|k,v| "#{k}(#{v.inspect})" }.join(", ")})"
240
+ if @alternative
241
+ q.text " else { #{alternative} }"
242
+ end
243
+ end
244
+
245
+ protected
246
+ # before and after are content lines
247
+ def self.line_diff(before, after)
248
+ patch = {}
249
+ patch[:value] = after.raw_value unless before.raw_value == after.raw_value
250
+ unless (to_remove = before.params - after.params).empty?
251
+ patch[:remove_param] = Param.params_to_s(to_remove)
252
+ end
253
+ unless (to_add = after.params - before.params).empty?
254
+ patch[:add_param] = Param.params_to_s(to_add)
255
+ end
256
+ patch
257
+ end
258
+ end
259
+
260
+ end
261
+ end
262
+ end