cora 0.0.4

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,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ - ruby-head
5
+
6
+ notifications:
7
+ irc: "irc.freenode.org#siriproxy"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cora.gemspec
4
+ gemspec
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2, :cli => "--color" do
5
+ watch(%r{^spec/.+_spec\.rb})
6
+ watch(%r{^lib/.+\.rb}) { "spec" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: run specs.'
6
+ task :default => :spec
7
+
8
+ desc "Run specs"
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
11
+ # Put spec opts in a file named .rspec in root
12
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cora/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cora"
7
+ s.version = Cora::VERSION
8
+ s.authors = ["Jack Chen (chendo)"]
9
+ s.email = ["@chendo"]
10
+ s.homepage = ""
11
+ s.summary = "Example summary"
12
+ s.description = "Example description"
13
+
14
+ s.rubyforge_project = "cora"
15
+
16
+ s.add_development_dependency "rspec"
17
+ s.add_development_dependency "guard-rspec"
18
+ s.add_development_dependency "rake"
19
+
20
+ s.add_runtime_dependency "geocoder"
21
+
22
+ s.files = `git ls-files 2> /dev/null`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/* 2> /dev/null`.split("\n")
24
+ s.executables = `git ls-files -- bin/* 2> /dev/null`.split("\n").map{ |f| File.basename(f) }
25
+
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,62 @@
1
+ require "cora/version"
2
+ require "cora/plugin"
3
+ require "cora/location"
4
+
5
+ class Cora
6
+
7
+ attr_reader :location
8
+
9
+ def plugins
10
+ @plugins ||= []
11
+ end
12
+
13
+ def process(text)
14
+ log "Processing '#{text}'"
15
+
16
+ if @callback
17
+ log "Active callback found, resuming"
18
+
19
+ # We must set the active callback to nil first, otherwise
20
+ # multiple callbacks within one listen block won't work
21
+ callback = @callback
22
+ @callback = nil
23
+ callback.call(text)
24
+ return true
25
+ end
26
+
27
+ plugins.each do |plugin|
28
+ log "Processing plugin #{plugin}"
29
+ return true if plugin.process(text)
30
+ end
31
+
32
+ log "No matches for '#{text}'"
33
+ no_matches
34
+ end
35
+
36
+ def respond(text, options = {})
37
+ end
38
+
39
+ def no_matches
40
+ end
41
+
42
+ def set_priority_plugin(plugin)
43
+ plugins.delete(plugin)
44
+ plugins.unshift(plugin)
45
+ end
46
+
47
+ def set_callback(&block)
48
+ @callback = block
49
+ end
50
+
51
+ def set_active_fiber(fiber)
52
+ @fiber = fiber
53
+ end
54
+
55
+ def set_location(latitude, longitude, extra = {})
56
+ @location = Location.new(latitude, longitude, extra)
57
+ end
58
+
59
+ def log(text)
60
+ $stderr.puts(text) if defined?(LOG)
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ require 'geocoder'
2
+
3
+ class Cora::Location
4
+
5
+ extend Forwardable
6
+
7
+ def_delegators :result, :address, :city, :state, :state_code, :country, :country_code, :postal_code
8
+
9
+ attr_reader :latitude, :longitude, :extra
10
+
11
+ def initialize(latitude, longitude, extra = {})
12
+ @latitude = latitude
13
+ @longitude = longitude
14
+ @extra = extra
15
+ end
16
+
17
+ # Returns nil if geolocation failed, or an instance of Geocoder::Result::Google
18
+ # See: http://rubydoc.info/github/alexreisner/geocoder/master/Geocoder/Result/Google
19
+ def result
20
+ results.first
21
+ end
22
+
23
+ # Returns an array of objects of class Geocoder::Result::Google (probably)
24
+ def results
25
+ @results ||= perform_reverse_geocode
26
+ end
27
+
28
+ private
29
+
30
+ def perform_reverse_geocode
31
+ Geocoder.search([latitude, longitude].join(','))
32
+ end
33
+
34
+ end
@@ -0,0 +1,97 @@
1
+ require 'fiber'
2
+ require 'forwardable'
3
+
4
+ class Cora::Plugin
5
+
6
+ # These could use some more work
7
+ CONFIRM_REGEX = /yes|yeah|yep|ok|confirm|affirmative|indeed|engage/i
8
+ DENY_REGEX = /no|nope|nah|cancel|negative/i
9
+
10
+ extend Forwardable
11
+
12
+ def_delegators :manager, :log, :location
13
+
14
+ attr_accessor :manager, :match_data
15
+ attr_reader :current_state
16
+
17
+ class << self
18
+
19
+ def listen_for(regex, options = {}, &block)
20
+ listeners[regex] = {
21
+ block: block,
22
+ within_state: ([options[:within_state]].flatten)
23
+ }
24
+ end
25
+
26
+ def listeners
27
+ @listeners ||= {}
28
+ end
29
+
30
+ end
31
+
32
+ def process(text)
33
+ listeners.each do |regex, entry|
34
+ if match = text.match(regex)
35
+ captures = match.captures
36
+ log "Matches #{regex}"
37
+
38
+ if entry[:within_state]
39
+ log "Applicable states: #{entry[:within_state].join(', ')}"
40
+ log "Current state: #{current_state}"
41
+
42
+ if entry[:within_state].include?(current_state)
43
+ log "Matches, executing block"
44
+
45
+ self.match_data = match
46
+ Fiber.new {
47
+ instance_exec(*captures, &entry[:block])
48
+ }.resume
49
+ return true
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ false
56
+ end
57
+
58
+ def listeners
59
+ self.class.listeners
60
+ end
61
+
62
+ def say(text, options={})
63
+ log "Say: #{text}"
64
+ manager.respond(text, options)
65
+ end
66
+
67
+ def ask(question, options={})
68
+ log "Ask: #{question}"
69
+
70
+ f = Fiber.current
71
+ options[:prompt_for_response] = true
72
+ manager.respond(question, options)
73
+ manager.set_callback do |text|
74
+ f.resume(text)
75
+ end
76
+
77
+ Fiber.yield
78
+ end
79
+
80
+ def confirm(question, options = {unmatched_message: "I'm sorry, I didn't understand that."}, &block)
81
+ while (response = ask(question))
82
+ if response.match(CONFIRM_REGEX)
83
+ return true
84
+ elsif response.match(DENY_REGEX)
85
+ return false
86
+ else
87
+ say options[:unmatched_message]
88
+ end
89
+ end
90
+ end
91
+
92
+ def set_state(state)
93
+ @current_state = state
94
+ manager.set_priority_plugin(self)
95
+ end
96
+
97
+ end
@@ -0,0 +1,3 @@
1
+ class Cora
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,223 @@
1
+ require 'spec_helper'
2
+ require 'support/test_plugin'
3
+
4
+ describe Cora do
5
+ context "integration test" do
6
+ let(:plugin) do
7
+ TestPlugin.new.tap { |plugin| plugin.manager = subject }
8
+ end
9
+
10
+ before do
11
+ subject.plugins << plugin
12
+ end
13
+
14
+ context "single state" do
15
+ it "responds to a simple test hook" do
16
+
17
+ subject.should_receive(:respond).with("test!", {})
18
+ subject.process("this is a test")
19
+
20
+ end
21
+ end
22
+
23
+ context "say" do
24
+ it "responds to a say with options" do
25
+ subject.should_receive(:respond).with("test options", {option: "option"})
26
+ subject.process("options")
27
+ end
28
+ end
29
+
30
+ context "multiple state" do
31
+ it "doesn't respond to listeners that don't have the required state" do
32
+ subject.should_receive(:no_matches)
33
+
34
+ subject.process("bar")
35
+ end
36
+
37
+ it "responds when in the correct state" do
38
+ subject.process("foo")
39
+
40
+ subject.should_receive(:respond).with("bar get", {})
41
+ subject.process("bar")
42
+ end
43
+ end
44
+
45
+ context "multiple plugins" do
46
+ class TestPlugin2 < Cora::Plugin
47
+
48
+ listen_for /test/ do
49
+ say "test2"
50
+ end
51
+
52
+ listen_for /bar/ do
53
+ say "bad bar"
54
+ end
55
+
56
+ end
57
+
58
+ before do
59
+ subject.plugins << TestPlugin2.new.tap { |plugin| plugin.manager = subject}
60
+ end
61
+
62
+ it "processes the plugins in order" do
63
+ subject.should_receive(:respond).with("test!", {})
64
+ subject.process("test")
65
+
66
+ subject.plugins.reverse!
67
+ subject.should_receive(:respond).with("test2", {})
68
+ subject.process("test")
69
+ end
70
+
71
+ it "when state is set, cora should ignore other plugins" do
72
+ subject.plugins.reverse! # So TestPlugin2's bar is checked first
73
+ subject.process("foo")
74
+ subject.should_receive(:respond).with("bar get", {})
75
+ subject.process("bar")
76
+ end
77
+ end
78
+
79
+ context "asking" do
80
+ # Now this is interesting. On further thought, we can't do
81
+ # answer = ask("What is your name?")
82
+ # since we need to relinquish the CPU somehow. Either we use blocks,
83
+ # but since we're on 1.9, we can use Fibers. I think?
84
+
85
+ it "gets input from the user and uses it intelligently" do
86
+ subject.should_receive(:respond).with("Who should I send it to?", prompt_for_response: true)
87
+ subject.process("send message")
88
+ subject.should_receive(:respond).with("Sending message to chendo", {})
89
+ subject.process("chendo")
90
+ end
91
+
92
+ it "can ask multiple questions" do
93
+ subject.should_receive(:respond).with("Question 1", prompt_for_response: true)
94
+ subject.process("ask multiple")
95
+
96
+ subject.should_receive(:respond).with("You said: Answer 1", {})
97
+ subject.should_receive(:respond).with("Question 2", prompt_for_response: true)
98
+ subject.process("Answer 1")
99
+
100
+ subject.should_receive(:respond).with("You said: Answer 2", {})
101
+ subject.should_receive(:respond).with("Question 3", prompt_for_response: true)
102
+ subject.process("Answer 2")
103
+
104
+ subject.should_receive(:respond).with("You said: Answer 3", {})
105
+ subject.process("Answer 3")
106
+ end
107
+
108
+ it "can ask questions with options" do
109
+ subject.should_receive(:respond).with("Question with option", {prompt_for_response: true, option: "option"})
110
+ subject.process("ask option")
111
+ subject.process("plamoni")
112
+
113
+ random_num = rand(1000000)
114
+ subject.should_receive(:respond).with("Question with option", {prompt_for_response: true, option: random_num})
115
+ subject.process("ask option #{random_num}")
116
+ subject.process("plamoni")
117
+ end
118
+ end
119
+
120
+ context "confirming" do
121
+ it "can confirm with affirmative responses" do
122
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
123
+ subject.process("confirm something")
124
+ subject.should_receive(:respond).with("Confirmed", {})
125
+ subject.process("Yes")
126
+
127
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
128
+ subject.process("confirm something")
129
+ subject.should_receive(:respond).with("Confirmed", {})
130
+ subject.process("Yeah")
131
+
132
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
133
+ subject.process("confirm something")
134
+ subject.should_receive(:respond).with("Confirmed", {})
135
+ subject.process("Yep")
136
+
137
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
138
+ subject.process("confirm something")
139
+ subject.should_receive(:respond).with("Confirmed", {})
140
+ subject.process("Yes please")
141
+ end
142
+
143
+ it "can deny with deny responses" do
144
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
145
+ subject.process("confirm something")
146
+ subject.should_receive(:respond).with("Cancelled", {})
147
+ subject.process("No")
148
+
149
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
150
+ subject.process("confirm something")
151
+ subject.should_receive(:respond).with("Cancelled", {})
152
+ subject.process("Nah")
153
+
154
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
155
+ subject.process("confirm something")
156
+ subject.should_receive(:respond).with("Cancelled", {})
157
+ subject.process("Nope")
158
+
159
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
160
+ subject.process("confirm something")
161
+ subject.should_receive(:respond).with("Cancelled", {})
162
+ subject.process("No thanks")
163
+ end
164
+
165
+ it "can call confirm recursively" do
166
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
167
+ subject.process("nested confirm")
168
+ subject.should_receive(:respond).with("Confirmed", {})
169
+ subject.should_receive(:respond).with("What about inside itself?", prompt_for_response: true)
170
+ subject.process("Yes")
171
+ subject.should_receive(:respond).with("Confirmed2", {})
172
+ subject.process("Yes")
173
+ end
174
+
175
+ it "can call confirm sequentially" do
176
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true).ordered
177
+ subject.process("sequential confirm")
178
+ subject.should_receive(:respond).with("Confirmed", {}).ordered
179
+ subject.should_receive(:respond).with("And a second time?", prompt_for_response: true).ordered
180
+ subject.process("Yes")
181
+ subject.should_receive(:respond).with("Cancelled2", {}).ordered
182
+ subject.should_receive(:respond).with("And a third time?", prompt_for_response: true).ordered
183
+ subject.process("No")
184
+ subject.should_receive(:respond).with("Confirmed3", {}).ordered
185
+ subject.process("Yes")
186
+ end
187
+
188
+ it "can reprompt if given invalid answer to confirmation" do
189
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
190
+ subject.process("confirm something")
191
+ subject.should_receive(:respond).with("I'm sorry, I didn't understand that.", {}).ordered
192
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true).ordered
193
+ subject.should_receive(:respond).with("Confirmed", {}).ordered
194
+ # subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
195
+ subject.process("Lamp")
196
+ subject.process("Yes")
197
+ end
198
+
199
+ it "can reprompt with custom message if given invalid answer to confirmation" do
200
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
201
+ subject.process("confirm custom reprompt")
202
+ subject.should_receive(:respond).with("What you say!?", {}).ordered
203
+ subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true).ordered
204
+ subject.should_receive(:respond).with("Confirmed", {}).ordered
205
+ # subject.should_receive(:respond).with("Does confirm work?", prompt_for_response: true)
206
+ subject.process("Lamp")
207
+ subject.process("Yes")
208
+ end
209
+ end
210
+
211
+ context "captures" do
212
+ it "passes the captures to the block" do
213
+ subject.should_receive(:respond).with("Nice to meet you, Jackie Chan", {})
214
+ subject.process("my name is Jackie Chan")
215
+ end
216
+
217
+ it "lets you access the match_data for dynamic capture count" do
218
+ subject.should_receive(:respond).with("only repeat this", {})
219
+ subject.process("only repeat this, not this")
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) + "/../lib"
2
+ require 'cora'
@@ -0,0 +1,96 @@
1
+
2
+ class TestPlugin < Cora::Plugin
3
+
4
+ listen_for /test/ do
5
+ say "test!"
6
+ end
7
+
8
+ listen_for /^options$/ do
9
+ say "test options", {option: "option"}
10
+ end
11
+
12
+ listen_for /^foo$/ do
13
+ say "foo"
14
+ set_state :waiting_for_bar
15
+ end
16
+
17
+ listen_for /send message/ do
18
+ receipent = ask "Who should I send it to?"
19
+ say "Sending message to #{receipent}"
20
+ end
21
+
22
+ listen_for /ask multiple/ do
23
+ answer = ask "Question 1"
24
+ say "You said: #{answer}"
25
+ answer = ask "Question 2"
26
+ say "You said: #{answer}"
27
+ answer = ask "Question 3"
28
+ say "You said: #{answer}"
29
+ end
30
+
31
+ listen_for /ask option[ ]?([0-9]*)/ do |numeric_option|
32
+ opt = "option"
33
+ opt = numeric_option.to_i if not numeric_option.empty?
34
+ answer = ask "Question with option", {option: opt}
35
+ end
36
+
37
+ listen_for /confirm something/ do
38
+ if confirm "Does confirm work?"
39
+ say "Confirmed"
40
+ else
41
+ say "Cancelled"
42
+ end
43
+ end
44
+
45
+ listen_for /confirm custom reprompt/ do
46
+ if confirm "Does confirm work?", unmatched_message: "What you say!?"
47
+ say "Confirmed"
48
+ else
49
+ say "Cancelled"
50
+ end
51
+ end
52
+
53
+
54
+ listen_for /nested confirm/ do
55
+ if confirm "Does confirm work?"
56
+ say "Confirmed"
57
+ if confirm "What about inside itself?"
58
+ say "Confirmed2"
59
+ else
60
+ say "Cancelled2"
61
+ end
62
+ else
63
+ say "Cancelled"
64
+ end
65
+ end
66
+
67
+ listen_for /sequential confirm/ do
68
+ if confirm "Does confirm work?"
69
+ say "Confirmed"
70
+ else
71
+ say "Cancelled"
72
+ end
73
+ if confirm "And a second time?"
74
+ say "Confirmed2"
75
+ else
76
+ say "Cancelled2"
77
+ end
78
+ if confirm "And a third time?"
79
+ say "Confirmed3"
80
+ else
81
+ say "Cancelled3"
82
+ end
83
+ end
84
+
85
+ listen_for /bar/, within_state: :waiting_for_bar do
86
+ say "bar get"
87
+ end
88
+
89
+ listen_for /my name is (\w+) (\w+)/i do |first_name, last_name|
90
+ say "Nice to meet you, #{first_name} #{last_name}"
91
+ end
92
+
93
+ listen_for /only repeat this/i do
94
+ say "#{match_data}"
95
+ end
96
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cora
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jack Chen (chendo)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70356401591140 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70356401591140
25
+ - !ruby/object:Gem::Dependency
26
+ name: guard-rspec
27
+ requirement: &70356401590140 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70356401590140
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70356401588880 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70356401588880
47
+ - !ruby/object:Gem::Dependency
48
+ name: geocoder
49
+ requirement: &70356401588060 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70356401588060
58
+ description: Example description
59
+ email:
60
+ - ! '@chendo'
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - .gitignore
66
+ - .travis.yml
67
+ - Gemfile
68
+ - Guardfile
69
+ - Rakefile
70
+ - cora.gemspec
71
+ - lib/cora.rb
72
+ - lib/cora/location.rb
73
+ - lib/cora/plugin.rb
74
+ - lib/cora/version.rb
75
+ - spec/cora_spec.rb
76
+ - spec/spec_helper.rb
77
+ - spec/support/test_plugin.rb
78
+ homepage: ''
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project: cora
98
+ rubygems_version: 1.8.10
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Example summary
102
+ test_files:
103
+ - spec/cora_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/support/test_plugin.rb