=begin Copyright (C) 2006 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/enumerator' require 'vpim/field' require 'vpim/rfc2425' require 'vpim/vpim' module Vpim # An RFC 2425 directory info object. # # A directory information object is a sequence of fields. The basic # structure of the object, and the way in which it is broken into fields # is common to all profiles of the directory info type. # # A vCard, for example, is a specialization of a directory info object. # # [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt) class DirectoryInfo include Enumerable private_class_method :new # Initialize a DirectoryInfo object from +fields+. If +profile+ is # specified, check the BEGIN/END fields. def initialize(fields, profile = nil) #:nodoc: if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field } raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects' end @string = nil # this is used as a flag to indicate that recoding will be necessary @fields = fields check_begin_end(profile) if profile end # Decode +card+ into a DirectoryInfo object. # # +card+ may either be a something that is convertible to a string using # #to_str or an array of objects that can be joined into a string using # #join("\n"), or an IO object (which will be read to end-of-file). # # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions. # # A DirectoryInfo is mutable, you can add new fields to it, see # Vpim::DirectoryInfo::Field#create() for how to create a new Field. # # TODO: I don't believe this is ever used, maybe I can remove it. def DirectoryInfo.decode(card) #:nodoc: if card.respond_to? :to_str string = card.to_str elsif card.kind_of? Array string = card.join("\n") elsif card.kind_of? IO string = card.read(nil) else raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}" end fields = Vpim.decode(string) new(fields) end # Create a new DirectoryInfo object. The +fields+ are an optional array of # DirectoryInfo::Field objects to add to the new object, between the # BEGIN/END. If the +profile+ string is not nil, then it is the name of # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields # will be added. # # A DirectoryInfo is mutable, you can add new fields to it using #push(), # and see Field#create(). def DirectoryInfo.create(fields = [], profile = nil) if profile p = profile.to_str f = [ Field.create('BEGIN', p) ] f.concat fields f.push Field.create('END', p) fields = f end new(fields, profile) end # The first field named +name+, or nil if no # match is found. def field(name) enum_by_name(name).each { |f| return f } nil end # The value of the first field named +name+, or nil if no # match is found. def [](name) enum_by_name(name).each { |f| return f.value if f.value != ''} enum_by_name(name).each { |f| return f.value } nil end # An array of all the values of fields named +name+, converted to text # (using Field#to_text()). # # TODO - call this #texts(), as in the plural? def text(name) enum_by_name(name).map { |f| f.to_text } end # Array of all the Field#group()s. def groups @fields.collect { |f| f.group } .compact.uniq end # All fields, frozen. def fields #:nodoc: @fields.dup.freeze end # Yields for each Field for which +cond+.call(field) is true. The # (default) +cond+ of nil is considered true for all fields, so # this acts like a normal #each() when called with no arguments. def each(cond = nil) # :yields: Field @fields.each do |field| if(cond == nil || cond.call(field)) yield field end end self end # Returns an Enumerator for each Field for which #name?(+name+) is true. # # An Enumerator supports all the methods of Enumerable, so it allows iteration, # collection, mapping, etc. # # Examples: # # Print all the nicknames in a card: # # card.enum_by_name('NICKNAME') { |f| puts f.value } # # Print an Array of the preferred email addresses in the card: # # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? } def enum_by_name(name) Enumerator.new(self, Proc.new { |field| field.name?(name) }) end # Returns an Enumerator for each Field for which #group?(+group+) is true. # # For example, to print all the fields, sorted by group, you could do: # # card.groups.sort.each do |group| # card.enum_by_group(group).each do |field| # puts "#{group} -> #{field.name}" # end # end # # or to get an array of all the fields in group 'agroup', you could do: # # card.enum_by_group('agroup').to_a def enum_by_group(group) Enumerator.new(self, Proc.new { |field| field.group?(group) }) end # Returns an Enumerator for each Field for which +cond+.call(field) is true. def enum_by_cond(cond) Enumerator.new(self, cond ) end # Force card to be reencoded from the fields. def dirty #:nodoc: #string = nil end # Append +field+ to the fields. Note that it won't be literally appended # to the fields, it will be inserted before the closing END field. def push(field) dirty @fields[-1,0] = field self end alias << push # Push +field+ onto the fields, unless there is already a field # with this name. def push_unique(field) push(field) unless @fields.detect { |f| f.name? field.name } self end # Append +field+ to the end of all the fields. This isn't usually what you # want to do, usually a DirectoryInfo's first and last fields are a # BEGIN/END pair, see #push(). def push_end(field) @fields << field self end # Delete +field+. # # Warning: You can't delete BEGIN: or END: fields, but other # profile-specific fields can be deleted, including mandatory ones. For # vCards in particular, in order to avoid destroying them, I suggest # creating a new Vcard, and copying over all the fields that you still # want, rather than using #delete. This is easy with Maker::Vcard#copy, see # the Maker::Vcard examples. def delete(field) case when field.name?('BEGIN'), field.name?('END') raise ArgumentError, 'Cannot delete BEGIN or END fields.' else @fields.delete field end self end # The string encoding of the DirectoryInfo. See Field#encode for information # about the width parameter. def encode(width=nil) unless @string @string = @fields.collect { |f| f.encode(width) } . join "" end @string end alias to_s encode # Check that the DirectoryInfo object is correctly delimited by a BEGIN # and END, that their profile values match, and if +profile+ is specified, that # they are the specified profile. def check_begin_end(profile=nil) #:nodoc: unless @fields.first raise "No fields to check" end unless @fields.first.name? 'BEGIN' raise "Needs BEGIN, found: #{@fields.first.encode nil}" end unless @fields.last.name? 'END' raise "Needs END, found: #{@fields.last.encode nil}" end unless @fields.last.value? @fields.first.value raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}" end if profile if ! @fields.first.value? profile raise "Mismatched profile" end end true end end end