dict_client 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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/COPYING +339 -0
- data/Gemfile +1 -0
- data/README.md +52 -0
- data/Rakefile +7 -0
- data/bin/dict_client +170 -0
- data/dict_client.gemspec +20 -0
- data/lib/dict_client.rb +59 -0
- data/lib/dict_client/client.rb +117 -0
- data/lib/dict_client/readers.rb +85 -0
- data/lib/dict_client/responses.rb +157 -0
- data/spec/client_spec.rb +60 -0
- data/spec/dict_client_spec.rb +21 -0
- data/spec/readers_spec.rb +34 -0
- data/spec/responses_spec.rb +99 -0
- data/spec/spec_helper.rb +83 -0
- metadata +102 -0
data/dict_client.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'dict_client'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.homepage = 'https://github.com/leikind/dict_client'
|
5
|
+
s.date = '2013-07-25'
|
6
|
+
s.summary = 'A simple client side DICT library and executable'
|
7
|
+
s.description = 'The Dictionary Server Protocol (DICT) is a TCP transaction based ' +
|
8
|
+
'query/response protocol that allows a client to access dictionary ' +
|
9
|
+
'definitions from a set of natural language dictionary databases. ' +
|
10
|
+
'See RFC 2229 for details. http://tools.ietf.org/html/rfc2229'
|
11
|
+
s.authors = ['Dave Pearson', 'Yuri Leikind']
|
12
|
+
s.email = 'yuri.leikind@gmail.com'
|
13
|
+
s.files = `git ls-files`.split($/)
|
14
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
|
+
|
17
|
+
s.add_development_dependency 'rake'
|
18
|
+
s.add_development_dependency 'rspec'
|
19
|
+
|
20
|
+
end
|
data/lib/dict_client.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module DictClient
|
5
|
+
|
6
|
+
DEFAULT_HOST = 'dict.mova.org'
|
7
|
+
|
8
|
+
DEFAULT_PORT = 2628
|
9
|
+
|
10
|
+
EOL = "\r\n"
|
11
|
+
|
12
|
+
# End of data marker
|
13
|
+
EOD = '.' + EOL
|
14
|
+
|
15
|
+
# The special database names.
|
16
|
+
DB_FIRST = '!'
|
17
|
+
DB_ALL = '*'
|
18
|
+
|
19
|
+
# Match strategies.
|
20
|
+
MATCH_DEFAULT = '.'
|
21
|
+
MATCH_EXACT = 'exact'
|
22
|
+
MATCH_PREFIX = 'prefix'
|
23
|
+
|
24
|
+
RESPONSE_DATABASES_FOLLOW = 110
|
25
|
+
RESPONSE_STRATEGIES_FOLLOW = 111
|
26
|
+
RESPONSE_INFO_FOLLOWS = 112
|
27
|
+
RESPONSE_HELP_FOLLOWS = 113
|
28
|
+
RESPONSE_SERVER_INFO_FOLLOWS = 114
|
29
|
+
RESPONSE_DEFINITIONS_FOLLOW = 150
|
30
|
+
RESPONSE_DEFINITION_FOLLOWS = 151
|
31
|
+
RESPONSE_MATCHES_FOLLOW = 152
|
32
|
+
RESPONSE_CONNECTED = 220
|
33
|
+
RESPONSE_OK = 250
|
34
|
+
RESPONSE_NO_MATCH = 552
|
35
|
+
RESPONSE_NO_DATABASES = 554
|
36
|
+
RESPONSE_NO_STRATEGIES = 555
|
37
|
+
|
38
|
+
CLIENT_NAME = 'client github.com/leikind/dict_client'
|
39
|
+
|
40
|
+
class DictError < RuntimeError
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.reply_code(text, default = nil)
|
44
|
+
|
45
|
+
if text =~ /^\d{3} /
|
46
|
+
text.to_i
|
47
|
+
elsif default
|
48
|
+
default
|
49
|
+
else
|
50
|
+
raise DictError.new, "Invalid reply from host \"#{text}\"."
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
require 'dict_client/readers.rb'
|
58
|
+
require 'dict_client/responses.rb'
|
59
|
+
require 'dict_client/client.rb'
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module DictClient
|
3
|
+
|
4
|
+
class Client
|
5
|
+
|
6
|
+
def initialize(host = DEFAULT_HOST, port = DEFAULT_PORT)
|
7
|
+
@host, @port = host, port
|
8
|
+
end
|
9
|
+
|
10
|
+
def connected?
|
11
|
+
! @conn.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
|
16
|
+
@conn = tcp_open @host, @port
|
17
|
+
|
18
|
+
@banner = @conn.readline
|
19
|
+
|
20
|
+
unless DictClient.reply_code(@banner) == RESPONSE_CONNECTED
|
21
|
+
raise DictError.new, "Connection refused \"#{@banner}\"."
|
22
|
+
end
|
23
|
+
|
24
|
+
# announce ourselves to the server.
|
25
|
+
send_command CLIENT_NAME
|
26
|
+
|
27
|
+
unless DictClient.reply_code(reply = @conn.readline()) == RESPONSE_OK
|
28
|
+
raise DictError.new, "Client announcement failed \"#{reply}\""
|
29
|
+
end
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
yield self
|
33
|
+
else
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
if connected?
|
41
|
+
send_command 'quit'
|
42
|
+
@conn.close
|
43
|
+
@conn = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def banner
|
48
|
+
check_connection
|
49
|
+
@banner
|
50
|
+
end
|
51
|
+
|
52
|
+
def databases
|
53
|
+
request_response "show db", DictionariesTcpReader.new, Dictionaries
|
54
|
+
end
|
55
|
+
|
56
|
+
def strategies
|
57
|
+
request_response "show strat", StrategiesTcpReader.new, Strategies
|
58
|
+
end
|
59
|
+
|
60
|
+
def server
|
61
|
+
request_response "show server", ServerInfoTcpReader.new, ServerInfo
|
62
|
+
end
|
63
|
+
|
64
|
+
def help
|
65
|
+
request_response "help", ServerHelpTcpReader.new, ServerHelp
|
66
|
+
end
|
67
|
+
|
68
|
+
def info database
|
69
|
+
request_response %!show info "#{database}"!, DictionaryInfoTcpReader.new, DictionaryInfo
|
70
|
+
end
|
71
|
+
|
72
|
+
def match(word, strategy = MATCH_DEFAULT, database = DB_ALL)
|
73
|
+
request_response %!match #{database} #{strategy} "#{word}"!, MatchTcpReader.new, WordMatch
|
74
|
+
end
|
75
|
+
|
76
|
+
def define(word, database = DB_ALL)
|
77
|
+
request_response %!define #{database} "#{word}"!, WordDefinitionsTcpReader.new, WordDefinitions
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def tcp_open host, port
|
84
|
+
TCPSocket.open(host, port)
|
85
|
+
end
|
86
|
+
|
87
|
+
def request_response(command, reader, response_class)
|
88
|
+
|
89
|
+
send_command command
|
90
|
+
|
91
|
+
if DictClient.reply_code(reply = @conn.readline) == reader.good_response_code
|
92
|
+
response_class.new(reader.read_from(@conn))
|
93
|
+
elsif reader.bad_response_code && DictClient.reply_code(reply) == reader.bad_response_code
|
94
|
+
EmptyResponse.new
|
95
|
+
else
|
96
|
+
raise DictError.new, reply
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def check_connection
|
102
|
+
unless connected?
|
103
|
+
raise DictError.new, 'Not connected.'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def send_command command
|
108
|
+
check_connection
|
109
|
+
# STDERR.puts command
|
110
|
+
@conn.write command + EOL
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module DictClient
|
3
|
+
|
4
|
+
class SimpleTcpReader
|
5
|
+
|
6
|
+
def read_from socket
|
7
|
+
|
8
|
+
[].tap do |lines|
|
9
|
+
while DictClient.reply_code(reply = socket.readline(), 0) != RESPONSE_OK
|
10
|
+
lines.push reply.force_encoding('UTF-8') unless reply == EOD
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def good_response_code
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def bad_response_code
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class DictionariesTcpReader < SimpleTcpReader
|
26
|
+
def good_response_code
|
27
|
+
::DictClient::RESPONSE_DATABASES_FOLLOW
|
28
|
+
end
|
29
|
+
|
30
|
+
def bad_response_code
|
31
|
+
::DictClient::RESPONSE_NO_DATABASES
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class StrategiesTcpReader < SimpleTcpReader
|
36
|
+
def good_response_code
|
37
|
+
::DictClient::RESPONSE_STRATEGIES_FOLLOW
|
38
|
+
end
|
39
|
+
|
40
|
+
def bad_response_code
|
41
|
+
::DictClient::RESPONSE_NO_STRATEGIES
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ServerHelpTcpReader < SimpleTcpReader
|
46
|
+
def good_response_code
|
47
|
+
::DictClient::RESPONSE_HELP_FOLLOWS
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class ServerInfoTcpReader < SimpleTcpReader
|
52
|
+
def good_response_code
|
53
|
+
::DictClient::RESPONSE_SERVER_INFO_FOLLOWS
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class DictionaryInfoTcpReader < SimpleTcpReader
|
58
|
+
def good_response_code
|
59
|
+
::DictClient::RESPONSE_INFO_FOLLOWS
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class MatchTcpReader < SimpleTcpReader
|
64
|
+
def good_response_code
|
65
|
+
::DictClient::RESPONSE_MATCHES_FOLLOW
|
66
|
+
end
|
67
|
+
|
68
|
+
def bad_response_code
|
69
|
+
::DictClient::RESPONSE_NO_MATCH
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
class WordDefinitionsTcpReader < SimpleTcpReader
|
75
|
+
|
76
|
+
def good_response_code
|
77
|
+
::DictClient::RESPONSE_DEFINITIONS_FOLLOW
|
78
|
+
end
|
79
|
+
|
80
|
+
def bad_response_code
|
81
|
+
::DictClient::RESPONSE_NO_MATCH
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module DictClient
|
3
|
+
|
4
|
+
class EmptyResponse
|
5
|
+
def to_s
|
6
|
+
'none'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SimpleResponse
|
11
|
+
|
12
|
+
def initialize lines
|
13
|
+
lines.each do |line|
|
14
|
+
process_line line
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
@response
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def process_line line
|
25
|
+
@response ||= ''
|
26
|
+
|
27
|
+
@response << line
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
ServerInfo = SimpleResponse
|
32
|
+
ServerHelp = SimpleResponse
|
33
|
+
DictionaryInfo = SimpleResponse
|
34
|
+
|
35
|
+
module Formattable
|
36
|
+
def longest list
|
37
|
+
list.max{|a,b| a.length <=> b.length }.length
|
38
|
+
end
|
39
|
+
|
40
|
+
def print_formatted list, max_key, max_value
|
41
|
+
list.to_a.map do |k, v|
|
42
|
+
sprintf "%#{max_key}s %-#{max_value}s", k, v
|
43
|
+
end.join("\n")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class KeyValueResponse < SimpleResponse
|
48
|
+
|
49
|
+
include Formattable
|
50
|
+
|
51
|
+
def to_h
|
52
|
+
@table
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
max_value = longest @table.values
|
57
|
+
max_key = longest @table.keys
|
58
|
+
|
59
|
+
print_formatted @table, max_key, max_value
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def process_line line
|
66
|
+
@table ||= {}
|
67
|
+
|
68
|
+
if line =~ /^([^\s]+)\s+"(.+?)"/
|
69
|
+
@table[$1] = $2
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
Dictionaries = KeyValueResponse
|
77
|
+
|
78
|
+
Strategies = KeyValueResponse
|
79
|
+
|
80
|
+
class WordMatch < SimpleResponse
|
81
|
+
|
82
|
+
include Formattable
|
83
|
+
|
84
|
+
attr_reader :matches
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
max_key = longest @matches.map{|tuple| tuple[0] }
|
88
|
+
max_value = longest @matches.map{|tuple| tuple[1] }
|
89
|
+
|
90
|
+
print_formatted @matches, max_key, max_value
|
91
|
+
end
|
92
|
+
|
93
|
+
def count
|
94
|
+
@matches.size
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def process_line line
|
100
|
+
@matches ||= []
|
101
|
+
|
102
|
+
if line =~ /^([^\s]+)\s+"([^"]+)"/
|
103
|
+
@matches << [$1, $2]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
class WordDefinitions < SimpleResponse
|
110
|
+
|
111
|
+
attr_reader :definitions
|
112
|
+
|
113
|
+
def initialize lines
|
114
|
+
@definitions = []
|
115
|
+
super(lines)
|
116
|
+
end
|
117
|
+
|
118
|
+
class WordDefinition < Struct.new(:word, :dictionary_name, :dictionary_description, :definition)
|
119
|
+
|
120
|
+
BAR = ('-' * 76) + "\n"
|
121
|
+
|
122
|
+
def to_s(n = nil)
|
123
|
+
(n.nil? ? '' : "#{n}) ") +
|
124
|
+
"#{dictionary_name} (#{dictionary_description}): #{word}\n" +
|
125
|
+
BAR + definition + BAR
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
@definitions.each_with_index.to_a.map{|definition, idx| definition.to_s(idx+1)}.join
|
132
|
+
end
|
133
|
+
|
134
|
+
def count
|
135
|
+
@definitions.size
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def process_line line
|
141
|
+
if line =~ /^#{RESPONSE_DEFINITION_FOLLOWS}\s+"([^\s+]+)"\s+([^\s+]+)\s+"(.+?)"/
|
142
|
+
word, dictionary_name, dictionary_description = $1, $2, $3
|
143
|
+
|
144
|
+
@current_definition = WordDefinition.new(word, dictionary_name, dictionary_description, '')
|
145
|
+
@definitions << @current_definition
|
146
|
+
|
147
|
+
else
|
148
|
+
if @current_definition
|
149
|
+
@current_definition.definition << line
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper.rb'
|
3
|
+
|
4
|
+
describe DictClient::Client do
|
5
|
+
|
6
|
+
let!(:dictd){}
|
7
|
+
|
8
|
+
subject do
|
9
|
+
DictClient::Client.new.tap do |client|
|
10
|
+
def client.tcp_open(h,p)
|
11
|
+
MockedDictdServerSocket.new.tap{|m| @mock = m}
|
12
|
+
end
|
13
|
+
|
14
|
+
def client.mock
|
15
|
+
@mock
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'connects' do
|
21
|
+
subject.connect.should == subject
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
it 'client command has been sent' do
|
26
|
+
subject.connect
|
27
|
+
subject.mock.incoming_commands[0].should match(/^client /)
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'disconnected' do
|
31
|
+
|
32
|
+
before do
|
33
|
+
subject.connect
|
34
|
+
subject.disconnect
|
35
|
+
end
|
36
|
+
|
37
|
+
its(:connected?){ should be_false }
|
38
|
+
|
39
|
+
it 'client command has been sent' do
|
40
|
+
subject.mock.incoming_commands[1].should match(/^quit\r\n/)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
context 'inside a session' do
|
47
|
+
before do
|
48
|
+
subject.connect
|
49
|
+
end
|
50
|
+
|
51
|
+
after do
|
52
|
+
subject.disconnect
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
its(:connected?){ should be_true }
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|