vcard 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
7
+ *.gemspec
data/LICENSE ADDED
@@ -0,0 +1,58 @@
1
+ vPim is copyrighted free software by Sam Roberts <sroberts@uniserve.com>.
2
+
3
+ You can redistribute it and/or modify it under either the terms of the GPL (see
4
+ the file GPL), or the conditions below:
5
+
6
+ 1. You may make and give away verbatim copies of the source form of the
7
+ software without restriction, provided that you duplicate all of the
8
+ original copyright notices and associated disclaimers.
9
+
10
+ 2. You may modify your copy of the software in any way, provided that
11
+ you do at least ONE of the following:
12
+
13
+ a) place your modifications in the Public Domain or otherwise make them
14
+ Freely Available, such as by posting said modifications to Usenet or an
15
+ equivalent medium, or by allowing the author to include your
16
+ modifications in the software.
17
+
18
+ b) use the modified software only within your corporation or
19
+ organization.
20
+
21
+ c) give non-standard binaries non-standard names, with instructions on
22
+ where to get the original software distribution.
23
+
24
+ d) make other distribution arrangements with the author.
25
+
26
+ 3. You may distribute the software in object code or binary form,
27
+ provided that you do at least ONE of the following:
28
+
29
+ a) distribute the binaries and library files of the software, together
30
+ with instructions (in the manual page or equivalent) on where to get the
31
+ original distribution.
32
+
33
+ b) accompany the distribution with the machine-readable source of the
34
+ software.
35
+
36
+ c) give non-standard binaries non-standard names, with instructions on
37
+ where to get the original software distribution.
38
+
39
+ d) make other distribution arrangements with the author.
40
+
41
+ 4. You may modify and include the part of the software into any other
42
+ software (possibly commercial). But some files in the distribution
43
+ are not written by the author, so that they are not under these terms.
44
+
45
+ For the list of those files and their copying conditions, see the
46
+ file LEGAL.
47
+
48
+ 5. The scripts and library files supplied as input to or produced as
49
+ output from the software do not automatically fall under the
50
+ copyright of the software, but belong to whomever generated them,
51
+ and may be sold commercially, and may be aggregated with this
52
+ software.
53
+
54
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
55
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
56
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
57
+ PURPOSE.
58
+
@@ -0,0 +1,7 @@
1
+ = vcard
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Jakub Kuźma. See LICENSE for details.
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "vcard"
10
+ gem.summary = %Q{Vcard support extracted from Vpim (Ruby 1.9.1 compatible)}
11
+ gem.email = "qoobaa@gmail.com"
12
+ gem.homepage = "http://github.com/qoobaa/vcard"
13
+ gem.authors = ["Jakub Kuźma"]
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/*_test.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION.yml')
47
+ config = YAML.load(File.read('VERSION.yml'))
48
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
49
+ else
50
+ version = ""
51
+ end
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "vcard #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
58
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,34 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ #:main:README
10
+ #:title:vPim - vCard and iCalendar support for Ruby
11
+ module Vpim
12
+ # Exception used to indicate that data being decoded is invalid, the message
13
+ # should describe what is invalid.
14
+ class InvalidEncodingError < StandardError; end
15
+
16
+ # Exception used to indicate that data being decoded is unsupported, the message
17
+ # should describe what is unsupported.
18
+ #
19
+ # If its unsupported, its likely because I didn't anticipate it being useful
20
+ # to support this, and it likely it could be supported on request.
21
+ class UnsupportedError < StandardError; end
22
+
23
+ # Exception used to indicate that encoding failed, probably because the
24
+ # object would not result in validly encoded data. The message should
25
+ # describe what is unsupported.
26
+ class Unencodeable < StandardError; end
27
+ end
28
+
29
+ require "vcard/attachment"
30
+ require "vcard/dirinfo"
31
+ require "vcard/enumerator"
32
+ require "vcard/field"
33
+ require "vcard/rfc2425"
34
+ require "vcard/vcard"
@@ -0,0 +1,100 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Vpim
10
+
11
+ # Attachments are used by both iCalendar and vCard. They are either a URI or
12
+ # inline data, and their decoded value will be either a Uri or a Inline, as
13
+ # appropriate.
14
+ #
15
+ # Besides the methods specific to their class, both kinds of object implement
16
+ # a set of common methods, allowing them to be treated uniformly:
17
+ # - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
18
+ # - Uri#to_s, Inline#to_s: return the value as a String.
19
+ # - Uri#format, Inline#format: the format of the value. This is supposed to
20
+ # be an "iana defined" identifier (like "image/jpeg"), but could be almost
21
+ # anything (or nothing) in practice. Since the parameter is optional, it may
22
+ # be "".
23
+ #
24
+ # The objects can also be distinguished by their class, if necessary.
25
+ module Attachment
26
+
27
+ # TODO - It might be possible to autodetect the format from the first few
28
+ # bytes of the value, and return the appropriate MIME type when format
29
+ # isn't defined.
30
+ #
31
+ # iCalendar and vCard put the format in different parameters, and the
32
+ # default kind of value is different.
33
+ def Attachment.decode(field, defkind, fmtparam) #:nodoc:
34
+ format = field.pvalue(fmtparam) || ''
35
+ kind = field.kind || defkind
36
+ case kind
37
+ when 'text'
38
+ Inline.new(Vpim.decode_text(field.value), format)
39
+ when 'uri'
40
+ Uri.new(field.value_raw, format)
41
+ when 'binary'
42
+ Inline.new(field.value, format)
43
+ else
44
+ raise InvalidEncodingError, "Attachment of type #{kind} is not allowed"
45
+ end
46
+ end
47
+
48
+ # Extends a String to support some of the same methods as Uri.
49
+ class Inline < String
50
+ def initialize(s, format) #:nodoc:
51
+ @format = format
52
+ super(s)
53
+ end
54
+
55
+ # Return an IO object for the inline data. See +stringio+ for more
56
+ # information.
57
+ def to_io
58
+ StringIO.new(self)
59
+ end
60
+
61
+ # The format of the inline data.
62
+ # See Attachment.
63
+ attr_reader :format
64
+ end
65
+
66
+ # Encapsulates a URI and implements some methods of String.
67
+ class Uri
68
+ def initialize(uri, format) #:nodoc:
69
+ @uri = uri
70
+ @format = format
71
+ end
72
+
73
+ # The URI value.
74
+ attr_reader :uri
75
+
76
+ # The format of the data referred to by the URI.
77
+ # See Attachment.
78
+ attr_reader :format
79
+
80
+ # Return an IO object from opening the URI. See +open-uri+ for more
81
+ # information.
82
+ def to_io
83
+ open(@uri)
84
+ end
85
+
86
+ # Return the String from reading the IO object to end-of-data.
87
+ def to_s
88
+ to_io.read(nil)
89
+ end
90
+
91
+ def inspect #:nodoc:
92
+ s = "<#{self.class.to_s}: #{uri.inspect}>"
93
+ s << ", #{@format.inspect}" if @format
94
+ s
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+
@@ -0,0 +1,272 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Vpim
10
+ # An RFC 2425 directory info object.
11
+ #
12
+ # A directory information object is a sequence of fields. The basic
13
+ # structure of the object, and the way in which it is broken into fields
14
+ # is common to all profiles of the directory info type.
15
+ #
16
+ # A vCard, for example, is a specialization of a directory info object.
17
+ #
18
+ # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
19
+ #
20
+ # Here's an example of encoding a simple vCard using the low-level APIs:
21
+ #
22
+ # card = Vpim::Vcard.create
23
+ # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' )
24
+ # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' )
25
+ # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' )
26
+ # puts card.to_s
27
+ #
28
+ # Don't do it like that, use Vpim::Vcard::Maker.
29
+ class DirectoryInfo
30
+ include Enumerable
31
+
32
+ private_class_method :new
33
+
34
+ # Initialize a DirectoryInfo object from +fields+. If +profile+ is
35
+ # specified, check the BEGIN/END fields.
36
+ def initialize(fields, profile = nil) #:nodoc:
37
+ if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field }
38
+ raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects'
39
+ end
40
+
41
+ @string = nil # this is used as a flag to indicate that recoding will be necessary
42
+ @fields = fields
43
+
44
+ check_begin_end(profile) if profile
45
+ end
46
+
47
+ # Decode +card+ into a DirectoryInfo object.
48
+ #
49
+ # +card+ may either be a something that is convertible to a string using
50
+ # #to_str or an Array of objects that can be joined into a string using
51
+ # #join("\n"), or an IO object (which will be read to end-of-file).
52
+ #
53
+ # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
54
+ #
55
+ # A DirectoryInfo is mutable, you can add new fields to it, see
56
+ # Vpim::DirectoryInfo::Field#create() for how to create a new Field.
57
+ #
58
+ # TODO: I don't believe this is ever used, maybe I can remove it.
59
+ def DirectoryInfo.decode(card) #:nodoc:
60
+ if card.respond_to? :to_str
61
+ string = card.to_str
62
+ elsif card.kind_of? Array
63
+ string = card.join("\n")
64
+ elsif card.kind_of? IO
65
+ string = card.read(nil)
66
+ else
67
+ raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
68
+ end
69
+
70
+ fields = Vpim.decode(string)
71
+
72
+ new(fields)
73
+ end
74
+
75
+ # Create a new DirectoryInfo object. The +fields+ are an optional array of
76
+ # DirectoryInfo::Field objects to add to the new object, between the
77
+ # BEGIN/END. If the +profile+ string is not nil, then it is the name of
78
+ # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
79
+ # will be added.
80
+ #
81
+ # A DirectoryInfo is mutable, you can add new fields to it using #push(),
82
+ # and see Field#create().
83
+ def DirectoryInfo.create(fields = [], profile = nil)
84
+
85
+ if profile
86
+ p = profile.to_str
87
+ f = [ Field.create('BEGIN', p) ]
88
+ f.concat fields
89
+ f.push Field.create('END', p)
90
+ fields = f
91
+ end
92
+
93
+ new(fields, profile)
94
+ end
95
+
96
+ # The first field named +name+, or nil if no
97
+ # match is found.
98
+ def field(name)
99
+ enum_by_name(name).each { |f| return f }
100
+ nil
101
+ end
102
+
103
+ # The value of the first field named +name+, or nil if no
104
+ # match is found.
105
+ def [](name)
106
+ enum_by_name(name).each { |f| return f.value if f.value != ''}
107
+ enum_by_name(name).each { |f| return f.value }
108
+ nil
109
+ end
110
+
111
+ # An array of all the values of fields named +name+, converted to text
112
+ # (using Field#to_text()).
113
+ #
114
+ # TODO - call this #texts(), as in the plural?
115
+ def text(name)
116
+ accum = []
117
+ each do |f|
118
+ if f.name? name
119
+ accum << f.to_text
120
+ end
121
+ end
122
+ accum
123
+ end
124
+
125
+ # Array of all the Field#group()s.
126
+ def groups
127
+ @fields.collect { |f| f.group } .compact.uniq
128
+ end
129
+
130
+ # All fields, frozen.
131
+ def fields #:nodoc:
132
+ @fields.dup.freeze
133
+ end
134
+
135
+ # Yields for each Field for which +cond+.call(field) is true. The
136
+ # (default) +cond+ of nil is considered true for all fields, so
137
+ # this acts like a normal #each() when called with no arguments.
138
+ def each(cond = nil) # :yields: Field
139
+ @fields.each do |field|
140
+ if(cond == nil || cond.call(field))
141
+ yield field
142
+ end
143
+ end
144
+ self
145
+ end
146
+
147
+ # Returns an Enumerator for each Field for which #name?(+name+) is true.
148
+ #
149
+ # An Enumerator supports all the methods of Enumerable, so it allows iteration,
150
+ # collection, mapping, etc.
151
+ #
152
+ # Examples:
153
+ #
154
+ # Print all the nicknames in a card:
155
+ #
156
+ # card.enum_by_name('NICKNAME') { |f| puts f.value }
157
+ #
158
+ # Print an Array of the preferred email addresses in the card:
159
+ #
160
+ # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? }
161
+ def enum_by_name(name)
162
+ Enumerator.new(self, Proc.new { |field| field.name?(name) })
163
+ end
164
+
165
+ # Returns an Enumerator for each Field for which #group?(+group+) is true.
166
+ #
167
+ # For example, to print all the fields, sorted by group, you could do:
168
+ #
169
+ # card.groups.sort.each do |group|
170
+ # card.enum_by_group(group).each do |field|
171
+ # puts "#{group} -> #{field.name}"
172
+ # end
173
+ # end
174
+ #
175
+ # or to get an array of all the fields in group 'AGROUP', you could do:
176
+ #
177
+ # card.enum_by_group('AGROUP').to_a
178
+ def enum_by_group(group)
179
+ Enumerator.new(self, Proc.new { |field| field.group?(group) })
180
+ end
181
+
182
+ # Returns an Enumerator for each Field for which +cond+.call(field) is true.
183
+ def enum_by_cond(cond)
184
+ Enumerator.new(self, cond )
185
+ end
186
+
187
+ # Force card to be reencoded from the fields.
188
+ def dirty #:nodoc:
189
+ #string = nil
190
+ end
191
+
192
+ # Append +field+ to the fields. Note that it won't be literally appended
193
+ # to the fields, it will be inserted before the closing END field.
194
+ def push(field)
195
+ dirty
196
+ @fields[-1,0] = field
197
+ self
198
+ end
199
+
200
+ alias << push
201
+
202
+ # Push +field+ onto the fields, unless there is already a field
203
+ # with this name.
204
+ def push_unique(field)
205
+ push(field) unless @fields.detect { |f| f.name? field.name }
206
+ self
207
+ end
208
+
209
+ # Append +field+ to the end of all the fields. This isn't usually what you
210
+ # want to do, usually a DirectoryInfo's first and last fields are a
211
+ # BEGIN/END pair, see #push().
212
+ def push_end(field)
213
+ @fields << field
214
+ self
215
+ end
216
+
217
+ # Delete +field+.
218
+ #
219
+ # Warning: You can't delete BEGIN: or END: fields, but other
220
+ # profile-specific fields can be deleted, including mandatory ones. For
221
+ # vCards in particular, in order to avoid destroying them, I suggest
222
+ # creating a new Vcard, and copying over all the fields that you still
223
+ # want, rather than using #delete. This is easy with Vcard::Maker#copy, see
224
+ # the Vcard::Maker examples.
225
+ def delete(field)
226
+ case
227
+ when field.name?('BEGIN'), field.name?('END')
228
+ raise ArgumentError, 'Cannot delete BEGIN or END fields.'
229
+ else
230
+ @fields.delete field
231
+ end
232
+
233
+ self
234
+ end
235
+
236
+ # The string encoding of the DirectoryInfo. See Field#encode for information
237
+ # about the width parameter.
238
+ def encode(width=nil)
239
+ unless @string
240
+ @string = @fields.collect { |f| f.encode(width) } . join ""
241
+ end
242
+ @string
243
+ end
244
+
245
+ alias to_s encode
246
+
247
+ # Check that the DirectoryInfo object is correctly delimited by a BEGIN
248
+ # and END, that their profile values match, and if +profile+ is specified, that
249
+ # they are the specified profile.
250
+ def check_begin_end(profile=nil) #:nodoc:
251
+ unless @fields.first
252
+ raise "No fields to check"
253
+ end
254
+ unless @fields.first.name? 'BEGIN'
255
+ raise "Needs BEGIN, found: #{@fields.first.encode nil}"
256
+ end
257
+ unless @fields.last.name? 'END'
258
+ raise "Needs END, found: #{@fields.last.encode nil}"
259
+ end
260
+ unless @fields.last.value? @fields.first.value
261
+ raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
262
+ end
263
+ if profile
264
+ if ! @fields.first.value? profile
265
+ raise "Mismatched profile"
266
+ end
267
+ end
268
+ true
269
+ end
270
+ end
271
+ end
272
+