cycr 0.1.0 → 0.2.0

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 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: