syc-ontact 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +23 -0
- data/README.md +190 -0
- data/bin/sycontact +140 -0
- data/lib/sycontact/address_book.rb +64 -0
- data/lib/sycontact/address_book_library.rb +57 -0
- data/spec/sycontact/address_book_library_spec.rb +19 -0
- data/spec/sycontact/address_book_spec.rb +107 -0
- data/spec/sycontact/files/address_source.rb +86 -0
- data/spec/sycontact/files/test-contacts/amanda_sugar.contact +14 -0
- data/spec/sycontact/files/test-contacts/pierre_sugar.contact +14 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fe93582cead2b2ccc48d60fcaa54a8d602baec10
|
4
|
+
data.tar.gz: 501cacb2bc96c5fb42e65226012da13859a0ce97
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4fb3c6fb9ed8d190de1a2f8e4525d99b5573b633abd5d4fe88c2ad44ebeda43036c587aa1c249fd7e964b14b021acd01cb9dec4ad42f15fd31b16f43ec21aa80
|
7
|
+
data.tar.gz: 1409a116127b05967202549f873093b370994d20d297b97d5041c5dfc4dd1a764a6e674f896619a5ab1488230c6f013a807da37005b691a5a4050f9ab6b95221
|
data/LICENSE.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*syc-ontact* is published under the [MIT-License](http://opensource.org/licenses/MIT)
|
2
|
+
|
3
|
+
The MIT License (MIT)
|
4
|
+
|
5
|
+
Copyright (c) 2014 Sugar Your Coffee
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
15
|
+
all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
sycontact
|
2
|
+
=========
|
3
|
+
`syc-ontact` is a command line interface for looking up contacts from any source that is providing contact information.
|
4
|
+
|
5
|
+
Installation
|
6
|
+
============
|
7
|
+
`gem install sycontact`
|
8
|
+
|
9
|
+
Setup
|
10
|
+
=====
|
11
|
+
To use `sycontact` a source-file has to be provided. The source-file is Ruby script retrieving the data from the source. A source can be anything that can be read from the Ruby script, e.g. a web site, an LDAP server, a file with contact data.
|
12
|
+
|
13
|
+
**Note:** Without a user defined Ruby script file (a Ruby module) `sycontact` will do nothing but
|
14
|
+
creating a configuration file, a source directory and displaying the help page.
|
15
|
+
|
16
|
+
To get `sycontact` to life you have to follow these setup steps:
|
17
|
+
|
18
|
+
1. start `sycontact` once. This will create the configuration file and the working directory
|
19
|
+
2. provide a Ruby module describing how to retrieve the data from the source. The module name has
|
20
|
+
to be `some_name_source.rb`.
|
21
|
+
|
22
|
+
The Ruby source code below describes a source-file that is retrieving contact data from a contact directory with the name *test-contacts* below the module's directory.
|
23
|
+
|
24
|
+
|
25
|
+
```
|
26
|
+
module AddressSource
|
27
|
+
|
28
|
+
# Where to find the contact files
|
29
|
+
URL = File.join(File.dirname(__FILE__), "test-contacts")
|
30
|
+
|
31
|
+
# Regex to extract contact data from the contact files
|
32
|
+
REGEX = { cn: /(?<=<common_name>)[\w -]*(?=<\common_name>)/,
|
33
|
+
sn: /(?<=<surname>)[\w -]*(?=<\/surname>)/,
|
34
|
+
gn: /(?<=<given_name>)[\w -]*(?=<\/given_name>)/,
|
35
|
+
c: /(?<=<country>)[\w]*(?=<\/country>)/,
|
36
|
+
l: /(?<=<location>)[\w]*(?=<\/location>)/,
|
37
|
+
st: /(?<=<state>)[\w]*(?=<\/state>)/,
|
38
|
+
street: /(?<=<street>)[\w .]*(?=<\/street>)/,
|
39
|
+
o: /(?<=<organization>)[\w -]*(?=<\/organization>)/,
|
40
|
+
ou: /(?<=<department>)[\w -]*(?=<\/department>)/,
|
41
|
+
title: /(?<=<title>)[\w .-]*(?=<\/title>)/,
|
42
|
+
description: /(?<=<description>)[\w -+]*(?=<\/description>)/,
|
43
|
+
telephone: /(?<=<telephone>)[\w +()-]*(?=<\/telephone>)/,
|
44
|
+
mobile: /(?<=<mobile>)[\w +()-]*(?=<\/mobile>)/,
|
45
|
+
mail: /(?<=<email>)[\w @.-]*(?=<\/email>)/
|
46
|
+
}
|
47
|
+
|
48
|
+
# Mandatory method! Will be invoked by `sycontact`.
|
49
|
+
# Will lookup the contact based on the pattern provided
|
50
|
+
def lookup(pattern = {})
|
51
|
+
contacts = []
|
52
|
+
create_source_files(pattern).each do |source_file|
|
53
|
+
|
54
|
+
next unless File.exist? source_file
|
55
|
+
|
56
|
+
source = File.read(source_file)
|
57
|
+
|
58
|
+
next if source.empty?
|
59
|
+
|
60
|
+
values = {}
|
61
|
+
|
62
|
+
REGEX.each do |key, regex|
|
63
|
+
value = source.scan(regex)[0]
|
64
|
+
values[key] = value if value
|
65
|
+
end
|
66
|
+
|
67
|
+
contacts << values
|
68
|
+
end
|
69
|
+
|
70
|
+
contacts.keep_if do |contact|
|
71
|
+
pattern.each.reduce(true) do |match, pattern|
|
72
|
+
contact_does_not_have_key = contact[pattern[0]].nil?
|
73
|
+
regex = Regexp.new(pattern[1].strip.downcase)
|
74
|
+
pos = regex =~ contact[pattern[0]].strip.downcase unless contact_does_not_have_key
|
75
|
+
match and (not pos.nil? or contact_does_not_have_key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Creates the contact file name. In this case the contact files have to be in the form
|
83
|
+
# firstname_lastname.contact or lastname_firstname.contact. If neather is given all *_*.contact
|
84
|
+
# files are retrieved
|
85
|
+
def create_source_files(pattern)
|
86
|
+
source_files = []
|
87
|
+
if pattern[:cn]
|
88
|
+
names = pattern[:cn].scan(/(^[a-zA-Z]*)[^a-zA-Z]*([a-zA-Z]*)/).flatten
|
89
|
+
names[0] = '*' if names[0].empty?
|
90
|
+
names[1] = '*' if names[1].empty?
|
91
|
+
names.permutation do |names|
|
92
|
+
file = File.join(URL, "#{names.join('_').downcase}.contact")
|
93
|
+
Dir.glob(file).each { |file| source_files << file }
|
94
|
+
end
|
95
|
+
elsif pattern[:sn] or pattern[:gn]
|
96
|
+
sn = pattern[:sn] ? pattern[:sn].strip.downcase : '*'
|
97
|
+
gn = pattern[:gn] ? pattern[:gn].strip.downcase : '*'
|
98
|
+
Dir.glob(File.join(URL, "#{gn}_#{sn}.contact")).each do |file|
|
99
|
+
source_files << file
|
100
|
+
end
|
101
|
+
else
|
102
|
+
Dir.glob(File.join(URL, "*_*.contact")).each do |file|
|
103
|
+
source_files << file
|
104
|
+
end
|
105
|
+
end
|
106
|
+
source_files
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
**Listing 1: Source-file to provide information about how to retrieve contact information**
|
112
|
+
|
113
|
+
Below a contact file that can be read by the above source module.
|
114
|
+
|
115
|
+
```
|
116
|
+
<common_name>Sugar Pierre</common_name>
|
117
|
+
<given_name>Pierre</given_name>
|
118
|
+
<surname>Sugar</surname>
|
119
|
+
<country>CA</country>
|
120
|
+
<location>Vancouver</location>
|
121
|
+
<state>BC</state>
|
122
|
+
<street>Robson Street</street>
|
123
|
+
<organization>SugarYourCoffee</organization>
|
124
|
+
<department>DevOps</department>
|
125
|
+
<telephone>+001 (123) 4567</telephone>
|
126
|
+
<mobile>+001 (765) 4321</mobile>
|
127
|
+
<email>pierre@sugaryourcoffee.de</email>
|
128
|
+
```
|
129
|
+
|
130
|
+
**Listing 2: Contact file that can be read by the `AddressSource` module**
|
131
|
+
|
132
|
+
Usage
|
133
|
+
=====
|
134
|
+
|
135
|
+
Get help for sycontact
|
136
|
+
|
137
|
+
$ sycontact -h
|
138
|
+
|
139
|
+
```
|
140
|
+
Usage: sycontact [options]
|
141
|
+
-p, --print RAW|SUMMARY|ALL Print contact attributes
|
142
|
+
SUMMARY (default)
|
143
|
+
--cn COMMON_NAME Common name e.g. 'Jane Doe' or 'Doe, Jane'
|
144
|
+
--sn SURNAME Surname e.g. 'Doe'
|
145
|
+
--gn GIVEN_NAME Given name e.g. 'Jane'
|
146
|
+
--uid USER_ID User ID
|
147
|
+
-c COUNTRY Country in ISO 3166 e.g. 'CA' for Canada
|
148
|
+
-l LOCATION City e.g. 'Vancouver'
|
149
|
+
--st STATE State e.g. 'British Columbia'
|
150
|
+
--street STREET Street e.g. 'Robson Street'
|
151
|
+
-o ORGANIZATION Organization e.g. 'Northstar'
|
152
|
+
--ou ORGANIZATIONAL_UNIT Department e.g. 'R&D'
|
153
|
+
--title TITLE Title e.g. 'Dr.'
|
154
|
+
--description DESCRIPTION Description e.g. 'Head of R&D'
|
155
|
+
--telephone TELEPHONE Telephone number e.g. '+001 (252) 4354'
|
156
|
+
--mobile MOBILE_PHONE Mobile number e.g. '+001 (252) 4345'
|
157
|
+
--mail E-MAIL E-Mail address e.g. 'jane@northstart.ca'
|
158
|
+
-h, --help Show his message
|
159
|
+
```
|
160
|
+
|
161
|
+
Lookup a contact with summary output
|
162
|
+
|
163
|
+
$ sycontact --cn sugar
|
164
|
+
$ sycontact --cn "sugar, pierre"
|
165
|
+
$ sycontact --cn "pierre sugar"
|
166
|
+
|
167
|
+
Any of the above commands result in the following output
|
168
|
+
|
169
|
+
```
|
170
|
+
AddressSource
|
171
|
+
|
172
|
+
CN..................Sugar Pierre
|
173
|
+
C...................DE
|
174
|
+
L...................Vancouver
|
175
|
+
O...................SugarYourCoffee
|
176
|
+
OU..................DevOps
|
177
|
+
TELEPHONE...........+001 (123) 4567
|
178
|
+
MOBILE..............+001 (765) 4321
|
179
|
+
MAIL................pierre@sugaryourcoffee.de
|
180
|
+
```
|
181
|
+
Sources
|
182
|
+
=======
|
183
|
+
Home page: <http://syc.dyndns.org/drupal/wiki/sycontact-lookup-contacts-any-source>
|
184
|
+
|
185
|
+
Source: <https://github.com/sugaryourcoffee/syc-ontact>
|
186
|
+
|
187
|
+
Contact
|
188
|
+
=======
|
189
|
+
|
190
|
+
<pierre@sugaryourcoffee.de>
|
data/bin/sycontact
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Main program of sycontact
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'yaml'
|
7
|
+
require_relative '../lib/sycontact/address_book_library'
|
8
|
+
|
9
|
+
# Directory holding the confifuration file and the sources
|
10
|
+
sycontact_directory = File.expand_path("~/.syc/sycontact")
|
11
|
+
# sycontact configuration file
|
12
|
+
sycontact_file = File.join(sycontact_directory, "sycontact.rc")
|
13
|
+
# Directory where address books are saved to, that is the modules holding the script to retrieve
|
14
|
+
# contacts from the contact source file (e.g. Internet, LDAP server, file)
|
15
|
+
address_books_directory = File.join(sycontact_directory, "address_books/")
|
16
|
+
|
17
|
+
unless File.exist? sycontact_directory
|
18
|
+
Dir.mkdir sycontact_directory
|
19
|
+
end
|
20
|
+
|
21
|
+
unless File.exists? address_books_directory
|
22
|
+
Dir.mkdir address_books_directory
|
23
|
+
end
|
24
|
+
|
25
|
+
unless File.exists? sycontact_file
|
26
|
+
puts "\nsycontact"
|
27
|
+
puts "========="
|
28
|
+
puts "You have to add the address source file directory to #{sycontact_file}"
|
29
|
+
puts "Look at the README.md to see how to configure sycontact\n"
|
30
|
+
config = { address_books: [ ] }
|
31
|
+
File.open(sycontact_file, 'w') { |f| YAML.dump(config, f) }
|
32
|
+
ARGV << "-h"
|
33
|
+
else
|
34
|
+
config = YAML.load_file(sycontact_file)
|
35
|
+
address_books = config[:address_books]
|
36
|
+
if address_books.empty?
|
37
|
+
puts "\nsycontact"
|
38
|
+
puts "========="
|
39
|
+
puts "You have to add the address source file directory to #{sycontact_file}\n\n"
|
40
|
+
ARGV << "-h"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
options = { p: "summary" }
|
45
|
+
|
46
|
+
option_parser = OptionParser.new do |opts|
|
47
|
+
|
48
|
+
opts.on("-p", "--print RAW|SUMMARY|ALL", "Print contact attributes",
|
49
|
+
"SUMMARY (default)") do |print|
|
50
|
+
options[:p] = print
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("--cn COMMON_NAME", "Common name e.g. 'Jane Doe' or 'Doe, Jane'") do |common_name|
|
54
|
+
options[:cn] = common_name
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on("--sn SURNAME", "Surname e.g. 'Doe'") do |surname|
|
58
|
+
options[:sn] = surname
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on("--gn GIVEN_NAME", "Given name e.g. 'Jane'") do |given_name|
|
62
|
+
options[:gn] = given_name
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on("--uid USER_ID", "User ID") do |user_id|
|
66
|
+
options[:uid] = user_id
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on("-c COUNTRY", "Country in ISO 3166 e.g. 'CA' for Canada") do |country|
|
70
|
+
options[:c] = country
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("-l LOCATION", "City e.g. 'Vancouver'") do |location|
|
74
|
+
options[:l] = location
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on("--st STATE", "State e.g. 'British Columbia'") do |state|
|
78
|
+
options[:st] = state
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on("--street STREET", "Street e.g. 'Robson Street'") do |street|
|
82
|
+
options[:street] = street
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.on("-o ORGANIZATION", "Organization e.g. 'Northstar'") do |organization|
|
86
|
+
options[:o] = organization
|
87
|
+
end
|
88
|
+
|
89
|
+
opts.on("--ou ORGANIZATIONAL_UNIT", "Department e.g. 'R&D'") do |organizational_unit|
|
90
|
+
options[:ou] = organizational_unit
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("--title TITLE", "Title e.g. 'Dr.'") do |title|
|
94
|
+
options[:title] = title
|
95
|
+
end
|
96
|
+
|
97
|
+
opts.on("--description DESCRIPTION", "Description e.g. 'Head of R&D'") do |description|
|
98
|
+
options[:descripton] = description
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on("--telephone TELEPHONE", "Telephone number e.g. '+001 (252) 4354'") do |telephone|
|
102
|
+
options[:telephone] = telephone
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on("--mobile MOBILE_PHONE", "Mobile number e.g. '+001 (252) 4345'") do |mobile_phone|
|
106
|
+
options[:mobile] = mobile_phone
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.on("--mail E-MAIL", "E-Mail address e.g. 'jane@northstart.ca'") do |email|
|
110
|
+
options[:mail] = email
|
111
|
+
end
|
112
|
+
|
113
|
+
opts.on("-h", "--help", "Show his message") do
|
114
|
+
puts opts
|
115
|
+
exit(0)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
option_parser.parse!
|
121
|
+
rescue OptionParser::ParseError => e
|
122
|
+
STDERR.puts e.message, "\n", options
|
123
|
+
exit(1)
|
124
|
+
end
|
125
|
+
|
126
|
+
library = Sycontact::AddressBookLibrary.new(address_books[0])
|
127
|
+
|
128
|
+
case options[:p].downcase
|
129
|
+
when "raw"
|
130
|
+
options.delete(:p)
|
131
|
+
library.lookup(options).each do |c|
|
132
|
+
puts c
|
133
|
+
end
|
134
|
+
when "all"
|
135
|
+
options.delete(:p)
|
136
|
+
library.print_all(options)
|
137
|
+
else
|
138
|
+
options.delete(:p)
|
139
|
+
library.print_summary(options)
|
140
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# sycontac module providing functions to lookup contacts
|
2
|
+
module Sycontact
|
3
|
+
|
4
|
+
# AddressBook is a wrapper for source modules that contain a script for retrieving contact data
|
5
|
+
# from a source as Internet, LDAP server or a file
|
6
|
+
class AddressBook
|
7
|
+
|
8
|
+
# Holds the values that are used when printing the summary of a contact
|
9
|
+
SUMMARY = [ :cn, :c, :l, :o, :ou, :telephone, :mobile, :mail ]
|
10
|
+
|
11
|
+
# Creates a new AddressBook. It requires the source module file and extends AddressBook with
|
12
|
+
# the source module
|
13
|
+
def initialize(source)
|
14
|
+
source = source.sub('.rb', '')
|
15
|
+
require source
|
16
|
+
@module_name = pascalize(File.basename(source))
|
17
|
+
extend self.class.module_eval(@module_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
# The source module can override the address source title, print_summary and print_all methods.
|
21
|
+
# If the methods are not overridden the default methods are invoked. The source module has to
|
22
|
+
# provide a lookup method. If the lookup method is not available "method missing" will be
|
23
|
+
# thrown
|
24
|
+
def method_missing(method, *args, &block)
|
25
|
+
case method
|
26
|
+
when :title
|
27
|
+
@module_name
|
28
|
+
when :print_summary
|
29
|
+
args.each do |contact|
|
30
|
+
puts "\n"
|
31
|
+
contact.each do |k,v|
|
32
|
+
if block_given?
|
33
|
+
yield(k, v)
|
34
|
+
else
|
35
|
+
unless SUMMARY.index(k).nil?
|
36
|
+
puts "#{k.to_s.upcase.ljust(20, '.')}#{v}\n" unless v.nil? or v.empty?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
when :print_all
|
42
|
+
args.each do |contact|
|
43
|
+
puts "\n"
|
44
|
+
contact.each do |k,v|
|
45
|
+
if block_given?
|
46
|
+
yield(k, v)
|
47
|
+
else
|
48
|
+
puts "#{k.to_s.upcase.ljust(20, '.')}#{v}\n" unless v.nil? or v.empty?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Pascalizes/camalizes a string as address_source to AddressSource
|
60
|
+
def pascalize(string)
|
61
|
+
(string.split('_').map { |word| word.capitalize }).join
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'address_book'
|
2
|
+
|
3
|
+
# sycontac module providing functions to lookup contacts
|
4
|
+
module Sycontact
|
5
|
+
|
6
|
+
# AddressBookLibrary creates AddressBook objects and forwards all messages invoked on
|
7
|
+
# AddressBookLibrary to all AddressBooks.
|
8
|
+
class AddressBookLibrary
|
9
|
+
|
10
|
+
# The contacts from the last lookup invocation
|
11
|
+
attr_reader :contacts
|
12
|
+
|
13
|
+
# Creates AddressBook objects based on the address book source files contained in the
|
14
|
+
# address_book_directory
|
15
|
+
def initialize(address_book_directory)
|
16
|
+
@address_books = []
|
17
|
+
Dir.glob(File.join(address_book_directory, "*_source.rb")).each do |address_book|
|
18
|
+
@address_books << AddressBook.new(address_book)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Looks up a contact based on the pattern and returns the contact data as a hash. The contact
|
23
|
+
# data can subsequentially retrievied with :contacts
|
24
|
+
def lookup(pattern = {})
|
25
|
+
@contacts = {}
|
26
|
+
@address_books.each do |address_book|
|
27
|
+
puts "\n#{address_book.title}\n"
|
28
|
+
@contacts[address_book.title] = address_book.lookup(pattern)
|
29
|
+
end
|
30
|
+
@contacts
|
31
|
+
end
|
32
|
+
|
33
|
+
# Invokes a lookup on all AddressBook objects and prints the result to the console with all
|
34
|
+
# attributes found in the contact source
|
35
|
+
def print_all(pattern = {})
|
36
|
+
@address_books.each do |address_book|
|
37
|
+
puts "\n#{address_book.title}"
|
38
|
+
address_book.lookup(pattern).each do |contact|
|
39
|
+
address_book.print_all(contact)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Invokes a lookup on all AddressBook objects and prints a subset of the result to the console.
|
45
|
+
# The attributes that are selected for print are defined in AddressBook::SUMMARY
|
46
|
+
def print_summary(pattern = {})
|
47
|
+
@address_books.each do |address_book|
|
48
|
+
puts "\n#{address_book.title}"
|
49
|
+
address_book.lookup(pattern).each do |contact|
|
50
|
+
address_book.print_summary(contact)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sycontact/address_book_library'
|
2
|
+
|
3
|
+
# sycontac module providing functions to lookup contacts
|
4
|
+
module Sycontact
|
5
|
+
|
6
|
+
describe AddressBookLibrary do
|
7
|
+
|
8
|
+
before do
|
9
|
+
address_book_directory = File.join(File.dirname(__FILE__), "files")
|
10
|
+
@library = AddressBookLibrary.new(address_book_directory)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should look up contact" do
|
14
|
+
@library.lookup(sn: "sugar").should_not be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'sycontact/address_book'
|
2
|
+
|
3
|
+
# sycontac module providing functions to lookup contacts
|
4
|
+
module Sycontact
|
5
|
+
describe AddressBook do
|
6
|
+
|
7
|
+
before do
|
8
|
+
source_file = File.join(File.dirname(__FILE__), "files/address_source.rb")
|
9
|
+
@address_book = AddressBook.new(source_file)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "finds contact with sn and gn set" do
|
13
|
+
@address_book.lookup(sn: "Sugar",
|
14
|
+
gn: "Pierre").should_not be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it "finds contact with leading and training white spaces in sn and gn" do
|
18
|
+
@address_book.lookup(sn: " Sugar ",
|
19
|
+
gn: " Pierre ").should_not be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
it "ignores capitalized values in sn and gn" do
|
23
|
+
@address_book.lookup(sn: " sugar",
|
24
|
+
gn: " pierre \n").should_not be_empty
|
25
|
+
end
|
26
|
+
|
27
|
+
it "finds contact if only sn is set" do
|
28
|
+
@address_book.lookup(sn: "sugar").should_not be_empty
|
29
|
+
end
|
30
|
+
|
31
|
+
it "finds contact with cn = 'Pierre Sugar' set" do
|
32
|
+
@address_book.lookup(cn: "Pierre Sugar").should_not be_empty
|
33
|
+
end
|
34
|
+
|
35
|
+
it "finds contact with cn = 'Sugar, Pierre' set" do
|
36
|
+
@address_book.lookup(cn: "Sugar, Pierre").should_not be_empty
|
37
|
+
end
|
38
|
+
|
39
|
+
it "ignores capitalized values in find cn" do
|
40
|
+
@address_book.lookup(cn: "pierre Sugar").should_not be_empty
|
41
|
+
@address_book.lookup(cn: "sugar, pierre").should_not be_empty
|
42
|
+
end
|
43
|
+
|
44
|
+
it "finds contact with cn when only one part of the name is set" do
|
45
|
+
@address_book.lookup(cn: "Pierre").should_not be_empty
|
46
|
+
@address_book.lookup(cn: "Sugar").size.should eq 2
|
47
|
+
end
|
48
|
+
|
49
|
+
it "finds multiple contacts based on attributes other than cn, gn and sn" do
|
50
|
+
@address_book.lookup(mail: "amanda@sugar.com").should_not be_empty
|
51
|
+
end
|
52
|
+
|
53
|
+
it "find should return nil when no attribute matches" do
|
54
|
+
@address_book.lookup(mail: "user@example.com").should be_empty
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return title" do
|
58
|
+
@address_book.title.should eq "AddressSource"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should return print summary of result" do
|
62
|
+
output = []
|
63
|
+
@address_book.lookup(cn: "Pierre Sugar").each do |contact|
|
64
|
+
@address_book.print_summary(contact) do |k, v|
|
65
|
+
output << "#{k} has the value of #{v}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
output.should eq [
|
69
|
+
"sn has the value of Sugar",
|
70
|
+
"gn has the value of Pierre",
|
71
|
+
"c has the value of DE",
|
72
|
+
"l has the value of Vancouver",
|
73
|
+
"st has the value of BC",
|
74
|
+
"street has the value of Robson Street",
|
75
|
+
"o has the value of SugarYourCoffee",
|
76
|
+
"ou has the value of DevOps",
|
77
|
+
"title has the value of No Title",
|
78
|
+
"description has the value of Development and Operations",
|
79
|
+
"telephone has the value of +001 (123) 4567",
|
80
|
+
"mobile has the value of +001 (765) 4321",
|
81
|
+
"mail has the value of pierre@sugaryourcoffee.de"
|
82
|
+
]
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should print all of result" do
|
86
|
+
output = []
|
87
|
+
@address_book.lookup(cn: "Pierre Sugar").each do |contact|
|
88
|
+
output << @address_book.print_all(contact)
|
89
|
+
end
|
90
|
+
output.should eq [
|
91
|
+
[{:sn=>"Sugar",
|
92
|
+
:gn=>"Pierre",
|
93
|
+
:c=>"DE",
|
94
|
+
:l=>"Vancouver",
|
95
|
+
:st=>"BC",
|
96
|
+
:street=>"Robson Street",
|
97
|
+
:o=>"SugarYourCoffee",
|
98
|
+
:ou=>"DevOps",
|
99
|
+
:title=>"No Title",
|
100
|
+
:description=>"Development and Operations",
|
101
|
+
:telephone=>"+001 (123) 4567",
|
102
|
+
:mobile=>"+001 (765) 4321",
|
103
|
+
:mail=>"pierre@sugaryourcoffee.de"}]
|
104
|
+
]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Test module for rspec providing functions to lookup contacts in the test-contacts directory
|
2
|
+
module AddressSource
|
3
|
+
|
4
|
+
# URL where the contact source files can be found
|
5
|
+
URL = File.join(File.dirname(__FILE__), "test-contacts")
|
6
|
+
|
7
|
+
# REGEX to extract contact data from the contact source file
|
8
|
+
REGEX = { cn: /(?<=<common_name>)[\w -]*(?=<\common_name>)/,
|
9
|
+
sn: /(?<=<surname>)[\w -]*(?=<\/surname>)/,
|
10
|
+
gn: /(?<=<given_name>)[\w -]*(?=<\/given_name>)/,
|
11
|
+
c: /(?<=<country>)[\w]*(?=<\/country>)/,
|
12
|
+
l: /(?<=<location>)[\w]*(?=<\/location>)/,
|
13
|
+
st: /(?<=<state>)[\w]*(?=<\/state>)/,
|
14
|
+
street: /(?<=<street>)[\w .]*(?=<\/street>)/,
|
15
|
+
o: /(?<=<organization>)[\w -]*(?=<\/organization>)/,
|
16
|
+
ou: /(?<=<department>)[\w -]*(?=<\/department>)/,
|
17
|
+
title: /(?<=<title>)[\w .-]*(?=<\/title>)/,
|
18
|
+
description: /(?<=<description>)[\w -+]*(?=<\/description>)/,
|
19
|
+
telephone: /(?<=<telephone>)[\w +()-]*(?=<\/telephone>)/,
|
20
|
+
mobile: /(?<=<mobile>)[\w +()-]*(?=<\/mobile>)/,
|
21
|
+
mail: /(?<=<email>)[\w @.-]*(?=<\/email>)/
|
22
|
+
}
|
23
|
+
|
24
|
+
# Looks up a contact based on the pattern
|
25
|
+
def lookup(pattern = {})
|
26
|
+
contacts = []
|
27
|
+
create_source_files(pattern).each do |source_file|
|
28
|
+
|
29
|
+
next unless File.exist? source_file
|
30
|
+
|
31
|
+
source = File.read(source_file)
|
32
|
+
|
33
|
+
next if source.empty?
|
34
|
+
|
35
|
+
values = {}
|
36
|
+
|
37
|
+
REGEX.each do |key, regex|
|
38
|
+
value = source.scan(regex)[0]
|
39
|
+
values[key] = value if value
|
40
|
+
end
|
41
|
+
|
42
|
+
contacts << values
|
43
|
+
end
|
44
|
+
|
45
|
+
contacts.keep_if do |contact|
|
46
|
+
pattern.each.reduce(true) do |match, pattern|
|
47
|
+
contact_does_not_have_key = contact[pattern[0]].nil?
|
48
|
+
regex = Regexp.new(pattern[1].strip.downcase)
|
49
|
+
pos = regex =~ contact[pattern[0]].strip.downcase unless contact_does_not_have_key
|
50
|
+
match and (not pos.nil? or contact_does_not_have_key)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Overrides the default title method in AddressBook
|
56
|
+
def title
|
57
|
+
"AddressSource"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Creates a source file name based on the pattern
|
63
|
+
def create_source_files(pattern)
|
64
|
+
source_files = []
|
65
|
+
if pattern[:cn]
|
66
|
+
names = pattern[:cn].scan(/(^[a-zA-Z]*)[^a-zA-Z]*([a-zA-Z]*)/).flatten
|
67
|
+
names[0] = '*' if names[0].empty?
|
68
|
+
names[1] = '*' if names[1].empty?
|
69
|
+
names.permutation do |names|
|
70
|
+
file = File.join(URL, "#{names.join('_').downcase}.contact")
|
71
|
+
Dir.glob(file).each { |file| source_files << file }
|
72
|
+
end
|
73
|
+
elsif pattern[:sn] or pattern[:gn]
|
74
|
+
sn = pattern[:sn] ? pattern[:sn].strip.downcase : '*'
|
75
|
+
gn = pattern[:gn] ? pattern[:gn].strip.downcase : '*'
|
76
|
+
Dir.glob(File.join(URL, "#{gn}_#{sn}.contact")).each do |file|
|
77
|
+
source_files << file
|
78
|
+
end
|
79
|
+
else
|
80
|
+
Dir.glob(File.join(URL, "*_*.contact")).each do |file|
|
81
|
+
source_files << file
|
82
|
+
end
|
83
|
+
end
|
84
|
+
source_files
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Test file for rspec
|
2
|
+
<given_name>Amanda</given_name>
|
3
|
+
<surname>Sugar</surname>
|
4
|
+
<country>DE</country>
|
5
|
+
<location>City</location>
|
6
|
+
<state>State</state>
|
7
|
+
<street>Street</street>
|
8
|
+
<organization>Company</organization>
|
9
|
+
<department>Department</department>
|
10
|
+
<title>Dr.</title>
|
11
|
+
<description>Description of Amanda Sugar</description>
|
12
|
+
<telephone>+49 (123) 4567</telephone>
|
13
|
+
<mobile>+49 (765) 4321</mobile>
|
14
|
+
<email>amanda@sugar.com</email>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Test file for rspec
|
2
|
+
<given_name>Pierre</given_name>
|
3
|
+
<surname>Sugar</surname>
|
4
|
+
<country>DE</country>
|
5
|
+
<location>Vancouver</location>
|
6
|
+
<state>BC</state>
|
7
|
+
<street>Robson Street</street>
|
8
|
+
<organization>SugarYourCoffee</organization>
|
9
|
+
<department>DevOps</department>
|
10
|
+
<title>No Title</title>
|
11
|
+
<description>Development and Operations</description>
|
12
|
+
<telephone>+001 (123) 4567</telephone>
|
13
|
+
<mobile>+001 (765) 4321</mobile>
|
14
|
+
<email>pierre@sugaryourcoffee.de</email>
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syc-ontact
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pierre Sugar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: "sycontact\n=========\n`syc-ontact` is a command line interface for looking
|
28
|
+
up contacts from any source that is providing contact information.\n\nInstallation\n============\n`gem
|
29
|
+
install sycontact`\n\nSetup\n=====\nTo use `sycontact` a source-file has to be provided.
|
30
|
+
The source-file is Ruby script retrieving the data from the source. A source can
|
31
|
+
be anything that can be read from the Ruby script, e.g. a web site, an LDAP server,
|
32
|
+
a file with contact data.\n\n**Note:** Without a user defined Ruby script file (a
|
33
|
+
Ruby module) `sycontact` will do nothing but \n creating a configuration
|
34
|
+
file, a source directory and displaying the help page.\n\nTo get `sycontact` to
|
35
|
+
life you have to follow these setup steps:\n\n1. start `sycontact` once. This will
|
36
|
+
create the configuration file and the working directory\n2. provide a Ruby module
|
37
|
+
describing how to retrieve the data from the source. The module name has\n to
|
38
|
+
be `some_name_source.rb`.\n\nThe Ruby source code below describes a source-file
|
39
|
+
that is retrieving contact data from a contact directory with the name *test-contacts*
|
40
|
+
below the module's directory.\n\n\n```\nmodule AddressSource\n\n # Where to find
|
41
|
+
the contact files\n URL = File.join(File.dirname(__FILE__), \"test-contacts\")\n\n
|
42
|
+
\ # Regex to extract contact data from the contact files\n REGEX = { cn: /(?<=<common_name>)[\\w
|
43
|
+
-]*(?=<\\common_name>)/,\n sn: /(?<=<surname>)[\\w -]*(?=<\\/surname>)/,\n
|
44
|
+
\ gn: /(?<=<given_name>)[\\w -]*(?=<\\/given_name>)/,\n c:
|
45
|
+
/(?<=<country>)[\\w]*(?=<\\/country>)/,\n l: /(?<=<location>)[\\w]*(?=<\\/location>)/,\n
|
46
|
+
\ st: /(?<=<state>)[\\w]*(?=<\\/state>)/,\n street: /(?<=<street>)[\\w
|
47
|
+
.]*(?=<\\/street>)/,\n o: /(?<=<organization>)[\\w -]*(?=<\\/organization>)/,\n
|
48
|
+
\ ou: /(?<=<department>)[\\w -]*(?=<\\/department>)/,\n title:
|
49
|
+
/(?<=<title>)[\\w .-]*(?=<\\/title>)/,\n description: /(?<=<description>)[\\w
|
50
|
+
-+]*(?=<\\/description>)/,\n telephone: /(?<=<telephone>)[\\w +()-]*(?=<\\/telephone>)/,\n
|
51
|
+
\ mobile: /(?<=<mobile>)[\\w +()-]*(?=<\\/mobile>)/,\n mail:
|
52
|
+
/(?<=<email>)[\\w @.-]*(?=<\\/email>)/\n }\n\n # Mandatory method! Will
|
53
|
+
be invoked by `sycontact`.\n # Will lookup the contact based on the pattern provided\n
|
54
|
+
\ def lookup(pattern = {})\n contacts = []\n create_source_files(pattern).each
|
55
|
+
do |source_file|\n\n next unless File.exist? source_file\n\n source =
|
56
|
+
File.read(source_file)\n\n next if source.empty?\n\n values = {}\n\n REGEX.each
|
57
|
+
do |key, regex|\n value = source.scan(regex)[0]\n values[key] = value
|
58
|
+
if value\n end\n\n contacts << values\n end\n\n contacts.keep_if
|
59
|
+
do |contact|\n pattern.each.reduce(true) do |match, pattern| \n contact_does_not_have_key
|
60
|
+
= contact[pattern[0]].nil?\n regex = Regexp.new(pattern[1].strip.downcase)\n
|
61
|
+
\ pos = regex =~ contact[pattern[0]].strip.downcase unless contact_does_not_have_key\n
|
62
|
+
\ match and (not pos.nil? or contact_does_not_have_key)\n end\n end\n
|
63
|
+
\ end\n\n private\n\n # Creates the contact file name. In this case the contact
|
64
|
+
files have to be in the form\n # firstname_lastname.contact or lastname_firstname.contact.
|
65
|
+
If neather is given all *_*.contact\n # files are retrieved\n def create_source_files(pattern)\n
|
66
|
+
\ source_files = []\n if pattern[:cn]\n names = pattern[:cn].scan(/(^[a-zA-Z]*)[^a-zA-Z]*([a-zA-Z]*)/).flatten\n
|
67
|
+
\ names[0] = '*' if names[0].empty?\n names[1] = '*' if names[1].empty?\n
|
68
|
+
\ names.permutation do |names|\n file = File.join(URL, \"#{names.join('_').downcase}.contact\")\n
|
69
|
+
\ Dir.glob(file).each { |file| source_files << file }\n end\n elsif
|
70
|
+
pattern[:sn] or pattern[:gn]\n sn = pattern[:sn] ? pattern[:sn].strip.downcase
|
71
|
+
: '*'\n gn = pattern[:gn] ? pattern[:gn].strip.downcase : '*'\n Dir.glob(File.join(URL,
|
72
|
+
\"#{gn}_#{sn}.contact\")).each do |file|\n source_files << file\n end\n
|
73
|
+
\ else\n Dir.glob(File.join(URL, \"*_*.contact\")).each do |file|\n source_files
|
74
|
+
<< file\n end\n end\n source_files\n end\nend\n```\n\n**Listing
|
75
|
+
1: Source-file to provide information about how to retrieve contact information**\n\nBelow
|
76
|
+
a contact file that can be read by the above source module.\n\n```\n<common_name>Sugar
|
77
|
+
Pierre</common_name>\n<given_name>Pierre</given_name>\n<surname>Sugar</surname>\n<country>CA</country>\n<location>Vancouver</location>\n<state>BC</state>\n<street>Robson
|
78
|
+
Street</street>\n<organization>SugarYourCoffee</organization>\n<department>DevOps</department>\n<telephone>+001
|
79
|
+
(123) 4567</telephone>\n<mobile>+001 (765) 4321</mobile>\n<email>pierre@sugaryourcoffee.de</email>\n```\n\n**Listing
|
80
|
+
2: Contact file that can be read by the `AddressSource` module**\n\nUsage\n=====\n\nGet
|
81
|
+
help for sycontact\n\n $ sycontact -h\n\n```\nUsage: sycontact [options]\n -p,
|
82
|
+
--print RAW|SUMMARY|ALL Print contact attributes\n SUMMARY
|
83
|
+
(default)\n --cn COMMON_NAME Common name e.g. 'Jane Doe' or 'Doe,
|
84
|
+
Jane'\n --sn SURNAME Surname e.g. 'Doe'\n --gn GIVEN_NAME
|
85
|
+
\ Given name e.g. 'Jane'\n --uid USER_ID User
|
86
|
+
ID\n -c COUNTRY Country in ISO 3166 e.g. 'CA' for Canada\n
|
87
|
+
\ -l LOCATION City e.g. 'Vancouver'\n --st STATE State
|
88
|
+
e.g. 'British Columbia'\n --street STREET Street e.g. 'Robson
|
89
|
+
Street'\n -o ORGANIZATION Organization e.g. 'Northstar'\n --ou
|
90
|
+
ORGANIZATIONAL_UNIT Department e.g. 'R&D'\n --title TITLE Title
|
91
|
+
e.g. 'Dr.'\n --description DESCRIPTION Description e.g. 'Head of R&D'\n
|
92
|
+
\ --telephone TELEPHONE Telephone number e.g. '+001 (252) 4354'\n --mobile
|
93
|
+
MOBILE_PHONE Mobile number e.g. '+001 (252) 4345'\n --mail E-MAIL
|
94
|
+
\ E-Mail address e.g. 'jane@northstart.ca'\n -h, --help Show
|
95
|
+
his message\n```\n\nLookup a contact with summary output\n\n $ sycontact --cn
|
96
|
+
sugar\n $ sycontact --cn \"sugar, pierre\"\n $ sycontact --cn \"pierre sugar\"\n\nAny
|
97
|
+
of the above commands result in the following output\n\n```\nAddressSource\n\nCN..................Sugar
|
98
|
+
Pierre\nC...................DE\nL...................Vancouver\nO...................SugarYourCoffee\nOU..................DevOps\nTELEPHONE...........+001
|
99
|
+
(123) 4567\nMOBILE..............+001 (765) 4321\nMAIL................pierre@sugaryourcoffee.de\n```\nSources\n=======\nHome
|
100
|
+
page: <http://syc.dyndns.org/drupal/wiki/sycontact-lookup-contacts-any-source>\n\nSource:
|
101
|
+
<https://github.com/sugaryourcoffee/syc-ontact>\n\nContact\n=======\n\n<pierre@sugaryourcoffee.de>\n"
|
102
|
+
email: pierre@sugaryourcoffee.de
|
103
|
+
executables:
|
104
|
+
- sycontact
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- LICENSE.md
|
109
|
+
- README.md
|
110
|
+
- bin/sycontact
|
111
|
+
- lib/sycontact/address_book.rb
|
112
|
+
- lib/sycontact/address_book_library.rb
|
113
|
+
- spec/sycontact/address_book_library_spec.rb
|
114
|
+
- spec/sycontact/address_book_spec.rb
|
115
|
+
- spec/sycontact/files/address_source.rb
|
116
|
+
- spec/sycontact/files/test-contacts/amanda_sugar.contact
|
117
|
+
- spec/sycontact/files/test-contacts/pierre_sugar.contact
|
118
|
+
homepage: https://github.com/sugaryourcoffee/syc-ontact
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '1.9'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.2.0
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Lookup contacts from any source by providing customized source files
|
142
|
+
test_files:
|
143
|
+
- spec/sycontact/files/test-contacts/pierre_sugar.contact
|
144
|
+
- spec/sycontact/files/test-contacts/amanda_sugar.contact
|
145
|
+
- spec/sycontact/files/address_source.rb
|
146
|
+
- spec/sycontact/address_book_library_spec.rb
|
147
|
+
- spec/sycontact/address_book_spec.rb
|