syc-ontact 0.0.1
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.
- 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
|