cycr 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .*.sw?
3
3
  work
4
4
  doc
5
+ .yardoc
data/README.rdoc CHANGED
@@ -1,35 +1,52 @@
1
1
  = cycr
2
2
 
3
- * http://github.com/apohllo/cycr
3
+ * http://github.com/apohllo/cycr - code
4
+ * http://rubydoc.info/gems/cycr/frames - documentation
4
5
 
5
- = DESCRIPTION
6
+ == DESCRIPTION
6
7
 
7
- 'cycr' is a Ruby client for the (Open)Cyc server http://www.opencyc.org.
8
+ *cycr* is a Ruby client for the (Open)Cyc server http://www.opencyc.org.
8
9
 
9
- = FEATURES/PROBLEMS
10
+ == FEATURES
10
11
 
11
12
  * The text protocol is used to talk with Cyc
12
- * Ruby symbols are converted to Cyc symbols
13
+ * Ruby symbols are converted to Cyc terms
13
14
  * Ruby arrays are converted to SubL arrays
14
15
  * Ruby calls on the client are transparently translated to SubL
15
- * Support for subcalls (like 'with-any-mt')
16
- * Support for NARTs might not be fully functional (but works somehow)
16
+ * Support for subcalls (like +with-any-mt+)
17
+ * connection drivers: regular socket and synchrony based communication
18
+ * auto reconnecting the client after server downtime
19
+ * thread safe client as an option
20
+
21
+ == PROBLEMS
22
+
23
+ * Support for NARTs might not be fully functional (works only with ResearchCyc)
17
24
  * Support for CycL queries not implemented yet!
18
25
 
19
- = SYNOPSIS
26
+ == SYNOPSIS
20
27
 
21
- 'cycr' is a Ruby client for the (Open)Cyc server. It is designed as a
28
+ *cycr* is a Ruby client for the (Open)Cyc server. It is designed as a
22
29
  substitution for the original Java client. It allows for conversation
23
30
  with the ontology with regular Ruby objects (like symbols, arrays)
24
- and also exposes the raw text protocol.
31
+ and also exposes a raw text protocol.
32
+
33
+ If you use this library you might be intereste in:
34
+
35
+ * https://github.com/apohllo/cyc-console - replacement for Cyc console
36
+ * https://github.com/apohllo/navicyc - Rails and ExtJS replacement for Cyc browser
37
+
25
38
 
26
- = REQUIREMENTS
39
+ == REQUIREMENTS
27
40
 
28
41
  (Open)Cyc server with TCP communication enabled.
29
42
 
30
- = INSTALL
43
+ == INSTALL
44
+
45
+ The gem is available at rubygems.org, so you can install it with:
46
+
47
+ $ sudo gem install cycr
31
48
 
32
- You need RubyGems v.1.3.5 at least:
49
+ In case of problems just make sure that you have RubyGems v.1.3.5 at least:
33
50
 
34
51
  $ gem -v
35
52
  1.2.0 # => not OK
@@ -38,61 +55,65 @@ You need RubyGems v.1.3.5 at least:
38
55
  $ gem -v
39
56
  1.3.7 # => OK
40
57
 
41
- The gem is available at rubygems.org, so you can install it with:
42
-
43
- $ sudo gem install cycr
44
58
 
45
59
  == BASIC USAGE
46
60
 
47
61
  Prerequisites:
48
62
 
49
- * Cyc server is running
50
- ** you can download it from http://www.opencyc.org
51
- * Telnet connection is on
52
- ** type (enable-tcp-server :cyc-api 3601) in the cyc console or Cyc Browser
53
- -> Tools -> Interactor
63
+ * running Cyc server - you can download it from http://www.opencyc.org
64
+ * Telnet connection is turned on.
65
+ Type +(enable-tcp-server :cyc-api 3601)+ in the cyc console
66
+ or Cyc Browser -> Tools -> Interactor
67
+
68
+ Then you can start +irb+ session to see it in action.
54
69
 
55
- The you can start 'irb' to see it in action:
70
+ Include the cycr gem first
56
71
 
57
- $ irb
58
- # include the cycr gem
59
72
  require 'cycr'
60
73
 
61
- # create the cyc client object, default host: localhost, port: 3601
74
+ Create a cyc client object, default host: localhost, port: 3601
75
+
62
76
  cyc = Cyc::Client.new
63
77
 
64
- # check if Dog generalizes to Animal
78
+ Check if Dog generalizes to Animal
79
+
65
80
  cyc.genls? :Dog, :Animal # => true
66
81
 
67
- # check if Animal generalizes to Dog
82
+ Check if Animal generalizes to Dog
83
+
68
84
  cyc.genls? :Animal, :Dog # => nil
69
85
 
70
- # check the minimal generalizations of Animal
86
+ Check the minimal generalizations of Animal
87
+
71
88
  genls = cyc.min_genls :Animal
72
89
  # => [:"Organism-Whole", :"PerceptualAgent-Embodied",
73
- :"Agent-NonArtifactual", :SolidTangibleThing, [:CollectionUnionFn,
74
- [:TheSet, :Animal, [:GroupFn, :Animal]]],...
90
+ # :"Agent-NonArtifactual", :SolidTangibleThing, [:CollectionUnionFn,
91
+ # [:TheSet, :Animal, [:GroupFn, :Animal]]],...
92
+
93
+ Check the maximal specializations of the first of the above results
75
94
 
76
- # check the maximal specializations of the first of the above results
77
95
  cyc.max_specs genls.first
78
96
  # => [:Microorganism, :EukaryoticOrganism, :"Unvaccinated-Generic",
79
- :"Vaccinated-Generic", :MulticellularOrganism, :Heterotroph, :Autotroph,
80
- :Lichen, :TerrestrialOrganism, :Animal, :AquaticOrganism, :Mutant,
81
- :Carnivore, :Extraterrestrial, :"Exotic-Foreign",...
97
+ # :"Vaccinated-Generic", :MulticellularOrganism, :Heterotroph, :Autotroph,
98
+ # :Lichen, :TerrestrialOrganism, :Animal, :AquaticOrganism, :Mutant,
99
+ # :Carnivore, :Extraterrestrial, :"Exotic-Foreign",...
100
+
101
+ It works with NARTs (but I didn't tested this functionality extensively,
102
+ so this might cause some problems - beware):
82
103
 
83
- # It works with NARTs (but I didn't tested this functionality extensively,
84
- # so this might cause some problems - beware):
85
104
  genls[4]
86
105
  # => [:CollectionUnionFn, [:TheSet, :Animal, [:GroupFn, :Animal]]]
87
106
 
88
107
  cyc.max_specs genls[4]
89
108
  # => [:Animal, [:GroupFn, :Animal]]
90
109
 
91
- # What is more, you might even build complex subcalls, such as:
110
+ What is more, you might even build complex subcalls, such as:
111
+
92
112
  cyc.genls? :Person, :HomoSapiens # => nil
93
113
  cyc.with_any_mt{|cyc| cyc.genls? :Person, :HomoSapiens} # => true
94
114
 
95
- # The assertions are parsed, as well as Cyc symbols and variables
115
+ The assertions are parsed, as well as Cyc symbols and variables
116
+
96
117
  keys = cyc.key_predicate_rule_index :isa
97
118
  # => [:POS, :NEG]
98
119
  keys[0].class
@@ -106,8 +127,8 @@ The you can start 'irb' to see it in action:
106
127
  # => [:BACKWARD]
107
128
  rules = cyc.gather_predicate_rule_index :isa, keys[0], keys1[2], keys2[0]
108
129
  # => [[:implies, [:and, [:objectOfPossessionTransfer, :TheBuying, ?OBJ],
109
- [:activityObjectType, :TheSelectingAProduct, ?PREFERED]],
110
- [:isa, ?OBJ, ?PREFERED]] : BuyingMt]
130
+ # [:activityObjectType, :TheSelectingAProduct, ?PREFERED]],
131
+ # [:isa, ?OBJ, ?PREFERED]] : BuyingMt]
111
132
  cyc.assertion_p rules[0]
112
133
  # => true
113
134
 
@@ -115,28 +136,62 @@ The you can start 'irb' to see it in action:
115
136
  # => ?OBJ
116
137
  rules[0].formula[1][1][2].class
117
138
  # => Cyc::Variable
118
- # the variable cannot be checked for type, since it won't be bound
139
+
140
+ The variable cannot be checked for type, since it won't be bound.
119
141
 
120
142
 
121
- # If you want to see the query which is send to Cyc, just turn on
122
- # debugging:
143
+ If you want to see the query which is send to Cyc, just turn on
144
+ debugging:
145
+
123
146
  cyc.debug = true
124
- cyc.genls? :Dog, :Anial
147
+ cyc.genls? :Dog, :Animal
125
148
  # Send: (genls? #$Dog #$Animal)
126
149
  # Recv: 200 T
127
150
  # => true
128
151
 
129
- # The same way, you can turn it off:
152
+ The same way, you can turn it off:
153
+
130
154
  cyc.debug = false
131
155
 
132
- # Remember to close the client on exit
156
+ Remember to close the client on exit
157
+
133
158
  cyc.close
134
159
 
160
+ By default a Cyc client uses regular TCP socket communication and is not
161
+ thread safe. To turn-on thread-safety pass a +thread_safe+ option to the
162
+ constructor:
163
+
164
+ cyc = Cyc::Client.new(:thread_safe => true)
165
+
166
+ Alternatively you can use a client with fibers and Event Machine synchrony
167
+ To set-up event machine driver before requiring +cycr+:
168
+
169
+ require 'cyc/connection/synchrony'
170
+ require 'cycr'
171
+
172
+ Make sure that you have 'em-synchrony' gem installed. A sample code looks as
173
+ follows:
174
+
175
+ EM.synchrony do
176
+ cyc = EM::Synchrony::ConnectionPool.new(size => 5) do
177
+ Cyc::Client.new :url => 'cyc://localhost:3601', :debug => true
178
+ end
179
+ Fiber.new do
180
+ puts "Ani", cyc.fi_complete("Ani").inspect
181
+ end.resume
182
+ puts "Mi", cyc.talk('(fi-complete "Mi")').inspect
183
+ EM.stop
184
+ end
185
+
186
+ `Mi` will arrive before `Ani`
187
+
188
+ Warning: always use +EM::Synchrony::ConnectionPool+ to handle Fiber concurrency race conditions.
189
+
135
190
  == LICENSE:
136
191
 
137
192
  (The MIT/X11 License)
138
193
 
139
- Copyright (c) 2008-2012 Aleksander Pohl
194
+ Copyright (c) 2008-2012 Aleksander Pohl, Rafal Michalski
140
195
 
141
196
  Permission is hereby granted, free of charge, to any person obtaining
142
197
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -1,3 +1,6 @@
1
+ $:.unshift "lib"
2
+ require 'cyc/version'
3
+
1
4
  task :default => [:test]
2
5
 
3
6
  $gem_name = "cycr"
@@ -29,3 +32,8 @@ desc "Clean"
29
32
  task :clean do
30
33
  sh "rm #$gem_name*.gem"
31
34
  end
35
+
36
+ desc "Show changelog from the last release"
37
+ task :changelog do
38
+ sh "git log v#{Cyc::VERSION}.. --pretty=%s | tee"
39
+ end
data/changelog.txt CHANGED
@@ -1,3 +1,18 @@
1
+ 0.2.0
2
+ - Rake task for changelog
3
+ - Support for Ruby 1.8 dropped
4
+ - Remove support for Client.new(host,port) syntax (use options instead)
5
+ - Documentation updates.
6
+ - Fix README formatting
7
+ - :host & :port options added to client constructor
8
+ - Fixed multiprocess tests
9
+ - Removal of unused code
10
+ - Early detect broken connection to allow conn.write again; connected? return value
11
+ - Client reuses driver connection while reconnecting
12
+ - Client#check_parenthesis much faster
13
+ - Parser: include NIL in results - missing NILs may brake partial results
14
+ - Thread safety support
15
+ - Cyc::Client rework to allow multiple connection drivers
1
16
  0.1.0
2
17
  - Add license info to some files
3
18
  - Add tests to Rakefile
data/cycr.gemspec CHANGED
@@ -1,18 +1,22 @@
1
+ $:.unshift "lib"
2
+ require 'cyc/version'
3
+
1
4
  Gem::Specification.new do |s|
2
5
  s.name = "cycr"
3
- s.version = "0.1.0"
6
+ s.version = Cyc::VERSION.to_s
7
+ s.required_ruby_version = '>= 1.9.2'
4
8
  s.date = "#{Time.now.strftime("%Y-%m-%d")}"
5
9
  s.summary = "Ruby client for the (Open)Cyc server"
6
10
  s.email = "apohllo@o2.pl"
7
11
  s.homepage = "http://github.com/apohllo/cycr"
8
12
  s.require_path = "lib"
9
13
  s.description = "Ruby wrapper for (Open)Cyc server and ontology"
10
- s.has_rdoc = false
11
- s.authors = ['Aleksander Pohl']
14
+ s.authors = ['Aleksander Pohl', 'Rafal Michalski']
12
15
  s.files = `git ls-files`.split("\n")
13
16
  s.test_files = Dir.glob("spec/**/*") + Dir.glob("integration/**/*")
14
17
  s.rdoc_options = ["--main", "README.rdoc"]
15
18
  s.has_rdoc = true
16
19
  s.extra_rdoc_files = ["README.rdoc"]
17
- s.add_development_dependency("rspec", [">= 1.2.9"])
20
+ s.add_development_dependency("rspec", ["~> 2.8.0"])
21
+ s.add_development_dependency("em-synchrony", ["~> 1.0.0"])
18
22
  end
@@ -1,16 +1,8 @@
1
1
  $:.unshift "lib"
2
2
  require 'cycr'
3
+ require 'cyc/connection/synchrony' if defined? Fiber
3
4
 
4
- describe Cyc::Client do
5
- before(:each) do
6
- @client = Cyc::Client.new()
7
- # @client.debug = true
8
- end
9
-
10
- after(:each) do
11
- @client.close
12
- end
13
-
5
+ shared_examples Cyc::Client do
14
6
  it "should allow to talk to the server" do
15
7
  @client.talk("(constant-count)").should_not == nil
16
8
  end
@@ -53,15 +45,141 @@ describe Cyc::Client do
53
45
  @client.talk('(gather-predicate-extent-index #$minimizeExtent)').size.should > 100
54
46
  end
55
47
 
56
- it "should allow multiple processes to use the client" do
57
- parent_pid = Process.pid
58
- if fork
59
- @client.find_constant("Cat")
60
- else
61
- @client.find_constant("Dog")
48
+ end
49
+
50
+ describe Cyc::Client do
51
+ include_examples Cyc::Client
52
+
53
+ it "should have socket driver" do
54
+ @client.driver.type.should == :socket
55
+ end
56
+
57
+ before(:all) do
58
+ Cyc::Connection.driver = Cyc::Connection::SocketDriver
59
+ @client = Cyc::Client.new()
60
+ # @client.debug = true
61
+ end
62
+
63
+ after(:each) do
64
+ @client.close
65
+ end
66
+
67
+ end
68
+
69
+ if defined? Cyc::Connection::SynchronyDriver
70
+ describe Cyc::Connection::SynchronyDriver do
71
+ include_examples Cyc::Client
72
+
73
+ it "should have synchrony driver" do
74
+ @client.driver.type.should == :synchrony
75
+ end
76
+
77
+ around(:each) do |test_case|
78
+ EM.synchrony do
79
+ @client = Cyc::Client.new(:driver => Cyc::Connection::SynchronyDriver)
80
+ # @client.debug = true
81
+ test_case.call
82
+ @client.close
83
+ EM.stop
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ describe "synchrony fiber concurrency" do
90
+ around(:each) do |test_case|
91
+ EM.synchrony do
92
+ @client = EM::Synchrony::ConnectionPool.new(size: 1) do
93
+ Cyc::Client.new(:driver => Cyc::Connection::SynchronyDriver, :debug => false)
94
+ end
95
+ test_case.call
96
+ @client.close
97
+ EM.stop
98
+ end
99
+ end
100
+
101
+ # this is a little bit loooong test
102
+ # but tests aync nature of Fibers and composite results (subseq x)
103
+ it "should have consistent results running long query in separate fibers" do
104
+ @fiber = Fiber.current
105
+ done = 0
106
+ size = ('A'..'Z').to_a.each do |char|
107
+ Fiber.new do
108
+ result_size = @client.fi_complete(char).each do |value|
109
+ value.to_s[0].upcase.should == char
110
+ end.length
111
+ result_size.should > 0
112
+ done += 1
113
+ EM.next_tick { @fiber.resume }
114
+ end.resume
115
+ end.size
116
+ while done < size
117
+ @fiber = Fiber.current
118
+ Fiber.yield
119
+ end
62
120
  end
63
- if Process.pid == parent_pid
64
- Process.waitall
121
+ end
122
+ end
123
+
124
+ describe "client thread concurrency" do
125
+
126
+ before(:all) do
127
+ Cyc::Connection.driver = Cyc::Connection::SocketDriver
128
+ @client = Cyc::Client.new :thread_safe => true
129
+ # @client.debug = true
130
+ end
131
+
132
+ it "should have socket driver" do
133
+ @client.driver.type.should == :socket
134
+ end
135
+
136
+ it "should have thread_safe? flag set" do
137
+ @client.thread_safe?.should == true
138
+ end
139
+
140
+ it "should have consistent results running long query in separate threads" do
141
+ results = {}
142
+ mutex = Mutex.new
143
+ ('A'..'Z').map do |char|
144
+ Thread.new do
145
+ Thread.pass
146
+ result = @client.fi_complete char
147
+ @client.close
148
+ mutex.synchronize { results[char] = result }
149
+ end
150
+ end.each {|t| t.join }
151
+ results.each_pair do |char, result|
152
+ result.should_not == nil
153
+ size = result.each do |value|
154
+ value.to_s[0].upcase.should == char
155
+ end.length
156
+ size.should > 0
65
157
  end
66
158
  end
159
+
160
+ end
161
+
162
+ describe "client multiple processes" do
163
+
164
+ before(:all) do
165
+ Cyc::Connection.driver = Cyc::Connection::SocketDriver
166
+ @client = Cyc::Client.new()
167
+ # @client.debug = true
168
+ end
169
+
170
+ after(:each) do
171
+ @client.close
172
+ end
173
+
174
+ it "should have socket driver" do
175
+ @client.driver.type.should == :socket
176
+ end
177
+
178
+ it "should allow multiple processes to use the client" do
179
+ fork { @client.find_constant("Cat").should == :Cat }
180
+ fork { @client.find_constant("Dog").should == :Dog }
181
+ @client.find_constant("Animal").should == :Animal
182
+ Process.waitall
183
+ end
184
+
67
185
  end
data/lib/cyc/assertion.rb CHANGED
@@ -2,7 +2,7 @@ module Cyc
2
2
  # Author:: Aleksander Pohl (mailto:apohllo@o2.pl)
3
3
  # License:: MIT/X11 License
4
4
  #
5
- # This class represent the Cyc assertions.
5
+ # This class represent a Cyc assertion.
6
6
  class Assertion
7
7
  # The logical formula of the assertion.
8
8
  attr_reader :formula
data/lib/cyc/builder.rb CHANGED
@@ -2,9 +2,9 @@ module Cyc
2
2
  # This class is used to capture calls to the Cyc client, to allow
3
3
  # nested calls, like
4
4
  #
5
- # cyc.with_any_mt do |cyc|
6
- # cyc.comment :Collection
7
- # end
5
+ # cyc.with_any_mt do |cyc|
6
+ # cyc.comment :Collection
7
+ # end
8
8
  class Builder
9
9
  def initialize
10
10
  reset
data/lib/cyc/client.rb CHANGED
@@ -1,99 +1,169 @@
1
- require 'net/telnet'
1
+ require 'uri'
2
+ require 'cyc/connection'
2
3
  require 'cyc/exception'
3
4
 
4
- module Cyc
5
+ module Cyc #:nodoc:
5
6
  # Author:: Aleksander Pohl (mailto:apohllo@o2.pl)
6
7
  # License:: MIT/X11 License
7
8
  #
8
- # This class is the implementation of the Cyc server client.
9
+ # This class is the implementation of a Cyc client.
10
+ #
11
+ # Example:
12
+ #
13
+ # cyc = Cyc::Client.new
14
+ # cyc.genls? :Dog, :Animal # checks if Dog generalizes to Animal
15
+ # #=> true
16
+ # cyc.genls? :Animal, :Dog # checks if Animal generalizes to Dog
17
+ # #=> nil
9
18
  class Client
10
19
  # If set to true, all communication with the server is logged
11
20
  # to standard output
12
21
  attr_accessor :debug
13
- attr_reader :host, :port
22
+
23
+ # The +host+ the client connects to.
24
+ attr_reader :host
25
+
26
+ # The +port+ the client connects to.
27
+ attr_reader :port
28
+
29
+ # The +driver+ the client uses to connect to the server.
30
+ attr_reader :driver
31
+
32
+ # +true+ if the client is thread safe.
33
+ attr_reader :thread_safe
34
+ alias_method :thread_safe?, :thread_safe
35
+
36
+ # The +connection+ object - direct usage is discouraged.
37
+ # Use connection() call instead.
38
+ attr_accessor :conn
39
+ protected :conn
14
40
 
15
41
  # Creates new Client.
16
- def initialize(host="localhost",port="3601",debug=false)
17
- @debug = debug
18
- @host = host
19
- @port = port
42
+ # Usage:
43
+ # Cyc::Client.new [options = {}]
44
+ #
45
+ # options:
46
+ # - +:host+ = +localhost+ server address
47
+ # - +:port+ = +3601+ server port
48
+ # - +:debug+ = +false+ initial debug flag
49
+ # - +:timeout+ = +0.2+ connection timeout in seconds
50
+ # - +:url+ (String): +cyc://host:port+ overrides +:host+, +:port+
51
+ # - +:driver+ (Class) = Cyc::Connection::Socket client connection driver class
52
+ # - +:thread_safe+ = +true+ set to +true+ if you want to share client between
53
+ # threads
54
+ #
55
+ # Example:
56
+ # Cyc::Client.new
57
+ # Cyc::Client.new :host => 'cyc.example', :port => 3661, :debug => true
58
+ # Cyc::Client.new :debug => true, :url => 'cyc://localhost/3661',
59
+ # :timeout => 1.5, :driver => Cyc::Connection::SynchronyDriver
60
+ #
61
+ # Thread safe client:
62
+ # Cyc::Client.new :thread_safe => true
63
+ def initialize(options={})
20
64
  @pid = Process.pid
21
- @parser = Parser.new
22
- @mts_cache = {}
23
- @builder = Builder.new
65
+ unless Hash === options
66
+ raise ArgumentError.new("The Client.new(host,port) API is no longer supported.")
67
+ end
68
+ @host = options[:host] || "localhost"
69
+ @port = (options[:port] || 3601).to_i
70
+ if url = options[:url]
71
+ url = URI.parse(url)
72
+ @host = url.host || @host
73
+ @port = url.port || @port
74
+ end
75
+ @timeout = (options[:timeout] || 0.2).to_f
76
+ @driver = options[:driver] || Connection.driver
77
+ @debug = !!options[:debug]
78
+ @thread_safe = !!options[:thread_safe]
79
+
80
+ if @thread_safe
81
+ self.extend ThreadSafeClientExtension
82
+ else
83
+ @conn = nil
84
+ end
85
+ end
86
+
87
+ # Returns +true+ if the client is connected with the server.
88
+ def connected?
89
+ (conn=self.conn) && conn.connected? && @pid == Process.pid || false
24
90
  end
25
91
 
26
92
  # (Re)connects to the cyc server.
27
93
  def reconnect
94
+ # reuse existing connection driver
95
+ # to prevent race condition between fibers
96
+ conn = (self.conn||= @driver.new)
97
+ conn.disconnect if connected?
28
98
  @pid = Process.pid
29
- @conn = Net::Telnet.new("Port" => @port, "Telnetmode" => false,
30
- "Timeout" => 600, "Host" => @host)
99
+ puts "connecting: #@host:#@port $$#@pid" if @debug
100
+ conn.connect(@host, @port, @timeout)
101
+ self
31
102
  end
32
103
 
104
+ # Usually the connection will be established on first use
105
+ # however connect() allows to force early connection:
106
+ # Cyc::Client.new().connect
107
+ # Usefull in fiber concurrent environment to setup connection early.
108
+ alias_method :connect, :reconnect
33
109
  # Returns the connection object. Ensures that the pid of current
34
110
  # process is the same as the pid, the connection was initialized with.
35
111
  #
36
112
  # If the block is given, the command is guarded by assertion, that
37
113
  # it will be performed, even if the connection was reset.
38
114
  def connection
39
- if @conn.nil? or @pid != Process.pid
40
- reconnect
41
- end
115
+ reconnect unless connected?
42
116
  if block_given?
43
117
  begin
44
- yield @conn
118
+ yield conn
45
119
  rescue Errno::ECONNRESET
46
120
  reconnect
47
- yield @conn
121
+ yield conn
48
122
  end
49
123
  else
50
- @conn
124
+ conn
51
125
  end
52
126
  end
53
127
 
54
128
  protected :connection, :reconnect
55
129
 
56
- # Clears the microtheory cache.
57
- def clear_cache
58
- @mts_cache = {}
59
- end
60
-
61
- # Closes connection with the server
130
+ # Closes connection with the server.
62
131
  def close
63
- connection{|c| c.puts("(api-quit)")}
64
- @conn = nil
132
+ conn.write("(api-quit)") if connected?
133
+ rescue Errno::ECONNRESET
134
+ ensure
135
+ self.conn = nil
65
136
  end
66
137
 
67
- # Sends message +msg+ to the Cyc server and returns a parsed answer.
68
- def talk(msg, options={})
69
- send_message(msg)
138
+ # Sends the +messsage+ to the Cyc server and returns a parsed answer.
139
+ def talk(message, options={})
140
+ send_message(message)
70
141
  receive_answer(options)
71
142
  end
72
143
 
73
- # Sends message +msg+ to the Cyc server and
144
+ # Sends the +message+ to the Cyc server and
74
145
  # returns the raw answer (i.e. not parsed).
75
- def raw_talk(msg, options={})
76
- send_message(msg)
146
+ def raw_talk(message, options={})
147
+ send_message(message)
77
148
  receive_raw_answer(options)
78
149
  end
79
150
 
80
- # Scans the :message: to find out if the parenthesis are matched.
81
- # Raises UnbalancedClosingParenthesis exception if there is not matched closing
151
+ # Scans the +message+ to find out if the parenthesis are matched.
152
+ # Raises UnbalancedClosingParenthesis exception if there is a not matched closing
82
153
  # parenthesis. The message of the exception contains the string with the
83
154
  # unmatched parenthesis highlighted.
84
- # Raises UnbalancedOpeningParenthesis exception if there is not matched opening
155
+ # Raises UnbalancedOpeningParenthesis exception if there is a not matched opening
85
156
  # parenthesis.
86
157
  def check_parenthesis(message)
87
158
  count = 0
88
- position = 0
89
- message.scan(/./) do |char|
90
- position += 1
91
- next if char !~ /\(|\)/
159
+ message.scan(/[()]/) do |char|
92
160
  count += (char == "(" ? 1 : -1)
93
161
  if count < 0
162
+ # this *is* thread safe
163
+ position = $~.offset(0)[0]
94
164
  raise UnbalancedClosingParenthesis.
95
- new((position > 1 ? message[0..position-2] : "") +
96
- "<error>)</error>" + message[position..-1])
165
+ new((position > 1 ? message[0...position] : "") +
166
+ "<error>)</error>" + message[position+1..-1])
97
167
  end
98
168
  end
99
169
  raise UnbalancedOpeningParenthesis.new(count) if count > 0
@@ -101,74 +171,44 @@ module Cyc
101
171
 
102
172
  # Sends a raw message to the Cyc server. The user is
103
173
  # responsible for receiving the answer by calling
104
- # +receive_answer+ or +receive_raw_answer+.
174
+ # receive_answer or receive_raw_answer.
105
175
  def send_message(message)
106
- position = 0
107
176
  check_parenthesis(message)
108
- @last_message = message
109
177
  puts "Send: #{message}" if @debug
110
- connection{|c| c.puts(message)}
178
+ connection{|c| c.write(message)}
111
179
  end
112
180
 
113
181
  # Receives and parses an answer for a message from the Cyc server.
114
182
  def receive_answer(options={})
115
- receive_raw_answer do |answer|
183
+ receive_raw_answer do |answer, last_message|
116
184
  begin
117
- result = @parser.parse(answer,options[:stack])
185
+ result = Parser.new.parse answer, options[:stack]
118
186
  rescue ContinueParsing => ex
119
- result = ex.stack
120
- current_result = result
121
- last_message = @last_message
187
+ current_result = result = ex.stack
122
188
  while current_result.size == 100 do
123
189
  send_message("(subseq #{last_message} #{result.size} " +
124
190
  "#{result.size + 100})")
125
191
  current_result = receive_answer(options) || []
126
192
  result.concat(current_result)
127
193
  end
128
- rescue CycError => ex
129
- puts ex.to_s
130
- return nil
131
194
  end
132
195
  return result
133
196
  end
134
197
  end
135
198
 
136
- # Receives raw answer from server. If a +block+ is given
199
+ # Receives raw answer from server. If a block is given
137
200
  # the answer is yield to the block, otherwise the answer is returned.
138
201
  def receive_raw_answer(options={})
139
- answer = connection{|c| c.waitfor(/./)}
140
- puts "Recv: #{answer}" if @debug
141
- if answer.nil?
142
- raise CycError.new("Unknwon error occured. " +
143
- "Check the submitted query in detail:\n" +
144
- @last_message)
145
- end
146
- while not answer =~ /\n/ do
147
- next_answer = connection{|c| c.waitfor(/./)}
148
- puts "Recv: #{next_answer}" if @debug
149
- if answer.nil?
150
- answer = next_answer
151
- else
152
- answer += next_answer
153
- end
154
- end
155
- # XXX ignore some potential asynchronous answers
156
- # XXX check if everything works ok
157
- #answer = answer.split("\n")[-1]
158
- answer = answer.sub(/(\d\d\d) (.*)/,"\\2")
159
- if($1.to_i == 200)
202
+ status, answer, last_message = connection{|c| c.read}
203
+ puts "Recv: #{last_message} -> #{status} #{answer}" if @debug
204
+ if status == 200
160
205
  if block_given?
161
- yield answer
206
+ yield answer, last_message
162
207
  else
163
208
  return answer
164
209
  end
165
210
  else
166
- unless $2.nil?
167
- raise CycError.new($2.sub(/^"/,"").sub(/"$/,"") + "\n" + @last_message)
168
- else
169
- raise CycError.new("Unknown error! #{answer}")
170
- end
171
- nil
211
+ raise CycError.new(answer.sub(/^"/,"").sub(/"$/,"") + "\n" + last_message)
172
212
  end
173
213
  end
174
214
 
@@ -176,11 +216,12 @@ module Cyc
176
216
  # are translated into corresponding calls for Cyc server.
177
217
  #
178
218
  # E.g. if users initializes the client and calls some Ruby method
219
+ #
179
220
  # cyc = Cyc::Client.new
180
221
  # cyc.genls? :Dog, :Animal
181
222
  #
182
223
  # He/She returns a parsed answer from the server:
183
- # => "T"
224
+ # => true
184
225
  #
185
226
  # Since dashes are not allowed in Ruby method names they are replaced
186
227
  # with underscores:
@@ -193,7 +234,7 @@ module Cyc
193
234
  #
194
235
  # As you see the Ruby symbols are translated into Cyc terms (not Cyc symbols!).
195
236
  #
196
- # It is also possible to nest the calls to build more complex functions:
237
+ # It is also possible to nest the calls to build more complex calls:
197
238
  #
198
239
  # cyc.with_any_mt do |cyc|
199
240
  # cyc.min_genls :Dog
@@ -204,9 +245,22 @@ module Cyc
204
245
  # (with-any-mt (min-genls #$Dog))
205
246
  #
206
247
  def method_missing(name,*args,&block)
207
- @builder.reset
208
- @builder.send(name,*args,&block)
209
- talk(@builder.to_cyc)
248
+ builder = Builder.new
249
+ builder.send(name,*args,&block)
250
+ talk(builder.to_cyc)
251
+ end
252
+ end
253
+
254
+ module ThreadSafeClientExtension #:nodoc:
255
+ THR_VAR_TEMLPATE='_cyc_client_$%s_%s'
256
+
257
+ def self.extend_object(obj)
258
+ obj.instance_variable_set "@thrconn", (THR_VAR_TEMLPATE%[obj.object_id, 'conn']).intern
259
+ super
210
260
  end
261
+
262
+ protected
263
+ def conn; Thread.current[@thrconn]; end
264
+ def conn=(conn); Thread.current[@thrconn] = conn; end
211
265
  end
212
266
  end
@@ -0,0 +1,69 @@
1
+ require "cyc/exception"
2
+
3
+ module Cyc #:nodoc:
4
+ module Connection #:nodoc:
5
+ # Author:: Rafal Michalski (mailto:royaltm75@gmail.com)
6
+ # Licence:: MIT/X11 License
7
+ #
8
+ # DataBuffer is chunky text data to server answer assembly class.
9
+ #
10
+ # Usage:
11
+ #
12
+ # b = DataBuffer.new
13
+ # b << "200 Some response\n" << "300 Some other" << " response\n" << "additional data\n"
14
+ # b.next_result
15
+ # => [200, "Some response"]
16
+ # b.next_result
17
+ # => [300, "Some other response\nadditional data"]
18
+ # b.next_result
19
+ # => nil
20
+ # b << "Invalid response\n"
21
+ # b.next_result("meta data")
22
+ # Cyc::ProtocolError:
23
+ # Unexpected data from server: "Invalid response", check the submitted query in detail:
24
+ # "meta data"
25
+ #
26
+ class DataBuffer
27
+ EOL = "\n"
28
+ RESULT_MATCH = /^(\d\d\d) (.*)$/m
29
+
30
+ # Initializes an empty buffer.
31
+ def initialize
32
+ @buffer = ""
33
+ end
34
+
35
+ # Appends +data+ to the buffer.
36
+ def <<(data)
37
+ @buffer << data
38
+ end
39
+
40
+ # Returns the next complete result stored in the buffer.
41
+ def next_result(org_message=nil)
42
+ res_end = 0
43
+ size = @buffer.length
44
+ while res_end = @buffer.index(EOL, res_end)
45
+ res_end += 1
46
+ if res_end == size || RESULT_MATCH === @buffer[res_end..-1]
47
+ result = @buffer.slice!(0, res_end).chomp EOL
48
+ if RESULT_MATCH === result
49
+ return [$1.to_i, $2]
50
+ else
51
+ raise ProtocolError.new("Unexpected data from server: #{result.inspect}, " +
52
+ "check the submitted query in detail:\n#{org_message.inspect}")
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # String representation of the buffer.
59
+ def to_s
60
+ @buffer
61
+ end
62
+
63
+ # Clears the buffer contents.
64
+ def discard!
65
+ @buffer.clear
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ module Cyc #:nodoc:
2
+ # Use Cyc::Connection.driver = Cyc::Connection::SomeDriver
3
+ # to set default connection driver.
4
+ module Connection
5
+ EOL = "\n"
6
+ class << self
7
+ # The driver used to connect to the Cyc server.
8
+ attr_accessor :driver
9
+ end
10
+ end
11
+ end
12
+ Cyc::Connection.driver = nil
@@ -0,0 +1,95 @@
1
+ require "socket"
2
+ require "cyc/connection/driver"
3
+ require "cyc/exception"
4
+ require "cyc/connection/buffer"
5
+
6
+ module Cyc #:nodoc:
7
+ module Connection #:nodoc:
8
+ # TCPSocket Cyc::Client driver
9
+ # Author:: Rafal Michalski (mailto:royaltm75@gmail.com)
10
+ # Licence:: MIT/X11 License
11
+ #
12
+ # Default driver for Cyc::Client.
13
+ #
14
+ class SocketDriver
15
+ # The type of the driver, i.e. +:socket+.
16
+ def self.type; :socket; end
17
+
18
+ # Initialize a new driver.
19
+ def initialize
20
+ @sock = nil
21
+ @buffer = DataBuffer.new
22
+ @last_message = nil
23
+ end
24
+
25
+ # Returns true if the driver is connected to the server.
26
+ def connected?
27
+ !! @sock
28
+ end
29
+
30
+ # Connects to the server on +host+ and +port+ with given
31
+ # connection +timeout+.
32
+ def connect(host, port, timeout=0.2)
33
+ with_timeout(timeout.to_f) do
34
+ @sock = TCPSocket.new(host, port)
35
+ @sock.sync = true
36
+ @sock.binmode
37
+ #@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
38
+ end
39
+ end
40
+
41
+ # Disconnects the driver from the server.
42
+ def disconnect
43
+ @sock.close if @sock
44
+ rescue
45
+ # This should go to log #2.
46
+ ensure
47
+ @sock = nil
48
+ @buffer.discard!
49
+ end
50
+
51
+ # Send a message to the server.
52
+ def write(rawmsg)
53
+ @last_message = rawmsg
54
+ @sock.write(rawmsg + EOL)
55
+ # ensure that the connection is still with a server
56
+ # and wait for an answer at the same time
57
+ if @sock.eof?
58
+ disconnect
59
+ raise Errno::ECONNRESET
60
+ end
61
+ end
62
+
63
+ # Read next message from the server.
64
+ def read
65
+ begin
66
+ @buffer << @sock.readpartial(4096)
67
+ end until result = @buffer.next_result(@last_message)
68
+ result << @last_message
69
+ rescue IOError, EOFError, Errno::ECONNRESET
70
+ disconnect
71
+ raise Errno::ECONNRESET
72
+ end
73
+
74
+ protected
75
+
76
+ # borrowed from redis-rb
77
+ begin
78
+ require "system_timer"
79
+
80
+ def with_timeout(seconds, &block)
81
+ SystemTimer.timeout_after(seconds, &block)
82
+ end
83
+
84
+ rescue LoadError
85
+ require "timeout"
86
+
87
+ def with_timeout(seconds, &block)
88
+ Timeout.timeout(seconds, &block)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ Cyc::Connection.driver = Cyc::Connection::SocketDriver
@@ -0,0 +1,167 @@
1
+ require "em-synchrony"
2
+ require "cyc/connection/driver"
3
+ require "cyc/exception"
4
+ require "cyc/connection/buffer"
5
+ module Cyc #:nodoc:
6
+ module Connection #:nodoc:
7
+ class ConnectionClient < EventMachine::Connection
8
+ include EventMachine::Deferrable
9
+
10
+ def post_init
11
+ @req = []
12
+ @connected = false
13
+ @buffer = DataBuffer.new
14
+ end
15
+
16
+ def connection_completed
17
+ @connected = true
18
+ succeed
19
+ end
20
+
21
+ def connected?
22
+ @connected
23
+ end
24
+
25
+ def receive_data(data)
26
+ @buffer << data
27
+
28
+ begin
29
+ while (result = @buffer.next_result(@req.first))
30
+ unless @req.empty?
31
+ msg, req = @req.shift(2)
32
+ req.succeed(result << msg)
33
+ end
34
+ end
35
+ rescue RuntimeError => err
36
+ @req.each_slice(2) {|_, r| r.fail [:error, err]}
37
+ @req.clear
38
+ close_connection
39
+ end
40
+ end
41
+
42
+ def read
43
+ EventMachine::Synchrony.sync @req.last unless @req.empty?
44
+ end
45
+
46
+ def send(data)
47
+ @req << data << EventMachine::DefaultDeferrable.new
48
+ callback { send_data data + EOL }
49
+ @req.last
50
+ end
51
+
52
+ def unbind
53
+ @connected = false
54
+ @buffer.discard!
55
+ unless @req.empty?
56
+ @req.each_slice(2) {|_, r| r.fail [:error, Errno::ECONNRESET]}
57
+ @req.clear
58
+ end
59
+ fail
60
+ end
61
+ end
62
+
63
+ # EM-Synchrony Cyc::Client driver
64
+ # Author:: Rafal Michalski (mailto:royaltm75@gmail.com)
65
+ # Licence:: MIT/X11 License
66
+ #
67
+ # Requires: igrigorik/em-synchrony
68
+ #
69
+ # To use this driver simply require this file
70
+ # e.g.
71
+ # require 'cyc/connection/synchrony'
72
+ # beside 'cycr'
73
+ #
74
+ # If required before 'cycr' then the SocketDriver will not be loaded
75
+ # (however you can still load it by hand: require 'cyc/connection/socket'
76
+ # but be carefull though it will override default Cyc::Connection.driver).
77
+ #
78
+ # If required after 'cycr' then the SocketDriver will be preserved but
79
+ # the default driver will be set to SynchronyDriver.
80
+ #
81
+ # Async fiber example:
82
+ #
83
+ # require 'cyc/connection/synchrony'
84
+ # require 'cycr'
85
+ # EM.synchrony do
86
+ # cyc = EM::Synchrony::ConnectionPool.new(size: 5) do
87
+ # Cyc::Client.new :url => 'cyc://localhost:3601', :debug => true
88
+ # end
89
+ # puts cyc.driver, cyc.driver.type.inspect
90
+ # Fiber.new do
91
+ # puts "Ani", cyc.fi_complete("Ani").inspect
92
+ # end.resume
93
+ # puts "Mi", cyc.talk('(fi-complete "Mi")').inspect
94
+ # EM.stop
95
+ # end
96
+ #
97
+ # `Mi` will arrive before `Ani`
98
+ #
99
+ # Warning: always use EM::Synchrony::ConnectionPool to handle Fiber
100
+ # concurrency race conditions.
101
+ class SynchronyDriver
102
+ # The type of the driver, i.e. +:synchrony+.
103
+ def self.type; :synchrony; end
104
+
105
+ # Initialize a new driver.
106
+ def initialize
107
+ @connection = nil
108
+ end
109
+
110
+ # Returns true if the driver is connected to the server.
111
+ def connected?
112
+ @connection && @connection.connected?
113
+ end
114
+
115
+ # Connects to the server on +host+ and +port+ with given
116
+ # connection time-out.
117
+ def connect(host, port, timeout)
118
+ conn = EventMachine.connect(host, port, ConnectionClient) do |c|
119
+ c.pending_connect_timeout = [Float(timeout), 0.1].max
120
+ end
121
+
122
+ setup_connect_callbacks(conn, Fiber.current)
123
+ end
124
+
125
+ # Disconnects the driver from the server.
126
+ def disconnect
127
+ @connection.close_connection
128
+ @connection = nil
129
+ end
130
+
131
+ # Send a message to the server.
132
+ def write(rawmsg)
133
+ @connection.send(rawmsg)
134
+ end
135
+
136
+ # Read next message from the server.
137
+ def read
138
+ status, answer, msg = @connection.read
139
+ if status == :error
140
+ raise answer
141
+ else
142
+ return [status, answer, msg]
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def setup_connect_callbacks(conn, f)
149
+ conn.callback do
150
+ @connection = conn
151
+ f.resume conn
152
+ end
153
+
154
+ conn.errback do
155
+ @connection = conn
156
+ f.resume :refused
157
+ end
158
+
159
+ r = Fiber.yield
160
+ raise Errno::ECONNREFUSED if r == :refused
161
+ r
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ Cyc::Connection.driver = Cyc::Connection::SynchronyDriver
@@ -0,0 +1,2 @@
1
+ require "cyc/connection/driver"
2
+ require "cyc/connection/socket" if Cyc::Connection.driver.nil?
data/lib/cyc/exception.rb CHANGED
@@ -22,6 +22,9 @@ module Cyc
22
22
 
23
23
  # Error raised if the message sent to the server has
24
24
  # more closing parentheses than opening parentheses.
25
+ # The exception message should indicate where is the
26
+ # first unbalanced closing parenthesis using 'error'
27
+ # tag.
25
28
  class UnbalancedClosingParenthesis < CycError
26
29
  end
27
30
 
@@ -40,4 +43,9 @@ module Cyc
40
43
  end
41
44
  end
42
45
 
46
+ # Exception raised by the parser if data received from server
47
+ # is not in expected format.
48
+ class ProtocolError < CycError
49
+ end
50
+
43
51
  end
data/lib/cyc/parser.rb CHANGED
@@ -50,6 +50,8 @@ module Cyc
50
50
  top = stack.pop
51
51
  stack[-1].push top
52
52
  raise ContinueParsing.new(stack[0][0])
53
+ when :nil
54
+ stack[-1] << nil
53
55
  when :assertion_sep
54
56
  # ignore
55
57
  end
@@ -0,0 +1,3 @@
1
+ module Cyc
2
+ VERSION = "0.2.0"
3
+ end
data/lib/cycr.rb CHANGED
@@ -8,3 +8,4 @@ require 'cyc/parser'
8
8
  require 'cyc/sexpr.rex'
9
9
  require 'cyc/symbol'
10
10
  require 'cyc/variable'
11
+ require 'cyc/version'
metadata CHANGED
@@ -2,15 +2,16 @@
2
2
  name: cycr
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Aleksander Pohl
9
+ - Rafal Michalski
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
13
 
13
- date: 2012-02-13 00:00:00 Z
14
+ date: 2012-03-16 00:00:00 Z
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: rspec
@@ -18,11 +19,22 @@ dependencies:
18
19
  requirement: &id001 !ruby/object:Gem::Requirement
19
20
  none: false
20
21
  requirements:
21
- - - ">="
22
+ - - ~>
22
23
  - !ruby/object:Gem::Version
23
- version: 1.2.9
24
+ version: 2.8.0
24
25
  type: :development
25
26
  version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: em-synchrony
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 1.0.0
36
+ type: :development
37
+ version_requirements: *id002
26
38
  description: Ruby wrapper for (Open)Cyc server and ontology
27
39
  email: apohllo@o2.pl
28
40
  executables: []
@@ -43,6 +55,11 @@ files:
43
55
  - lib/cyc/builder.rb
44
56
  - lib/cyc/client.rb
45
57
  - lib/cyc/collection.rb
58
+ - lib/cyc/connection.rb
59
+ - lib/cyc/connection/buffer.rb
60
+ - lib/cyc/connection/driver.rb
61
+ - lib/cyc/connection/socket.rb
62
+ - lib/cyc/connection/synchrony.rb
46
63
  - lib/cyc/exception.rb
47
64
  - lib/cyc/extensions.rb
48
65
  - lib/cyc/parser.rb
@@ -50,6 +67,7 @@ files:
50
67
  - lib/cyc/sexpr.rex.rb
51
68
  - lib/cyc/symbol.rb
52
69
  - lib/cyc/variable.rb
70
+ - lib/cyc/version.rb
53
71
  - lib/cycr.rb
54
72
  - spec/parser.rb
55
73
  homepage: http://github.com/apohllo/cycr
@@ -66,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
84
  requirements:
67
85
  - - ">="
68
86
  - !ruby/object:Gem::Version
69
- version: "0"
87
+ version: 1.9.2
70
88
  required_rubygems_version: !ruby/object:Gem::Requirement
71
89
  none: false
72
90
  requirements: