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.
@@ -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
@@ -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
@@ -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