wase_endpoint 0.0.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 +1 -0
- data/README.rdoc +53 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/example/init.rb +6 -0
- data/example/my_endpoint.rb +11 -0
- data/example/server.rb +16 -0
- data/lib/wase_endpoint.rb +62 -0
- data/lib/wase_endpoint/message.rb +43 -0
- data/lib/wase_endpoint/twitterer.rb +37 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/wase_endpoint_message_spec.rb +120 -0
- data/spec/wase_endpoint_spec.rb +12 -0
- data/spec/wase_endpoint_twitterer_spec.rb +83 -0
- metadata +71 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
data/README.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= WaseEndpoint
|
2
|
+
|
3
|
+
WaseEndpoint is a library for building daemons that act as WASE Endpoints for the EngineYard Wase competition: http://bit.ly/3qRMbv
|
4
|
+
|
5
|
+
== Install
|
6
|
+
|
7
|
+
WaseEndpoint is hosted by http://gemcutter.com. Please make sure you have added them to your gem sources.
|
8
|
+
|
9
|
+
$ sudo gem install WaseEndpoint
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
The following are all in the example directory.
|
14
|
+
|
15
|
+
Your endpoint logic:
|
16
|
+
|
17
|
+
# example/my_endpoint.rb
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'wase_endpoint'
|
21
|
+
|
22
|
+
class MyEndpoint < WaseEndpoint
|
23
|
+
|
24
|
+
# Just return our json as it came in.
|
25
|
+
def secret_sauce(raw_json)
|
26
|
+
raw_json
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
The init file:
|
32
|
+
|
33
|
+
# example/init.rb
|
34
|
+
|
35
|
+
require 'my_endpoint'
|
36
|
+
|
37
|
+
MyEndpoint.new( :username => 'twitter_username',
|
38
|
+
:password => 'twitter_password',
|
39
|
+
:logfile => 'my_endpoint.log',
|
40
|
+
:sleep_period => 60 )
|
41
|
+
|
42
|
+
That's it! I also included a basic sinatra server in 'server.rb' that can be used as an input/output/program-listing node.
|
43
|
+
|
44
|
+
== Problems, Comments, Suggestions?
|
45
|
+
|
46
|
+
Issues can be tracked on github:
|
47
|
+
|
48
|
+
All of the above are most welcome. mailto:dougal.s@gmail.com
|
49
|
+
|
50
|
+
|
51
|
+
== Credits
|
52
|
+
|
53
|
+
Douglas F Shearer - http://douglasfshearer.com
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
|
4
|
+
task :default => :spec
|
5
|
+
|
6
|
+
desc "Run all specs"
|
7
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
8
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gemspec|
|
14
|
+
gemspec.name = "wase_endpoint"
|
15
|
+
gemspec.summary = "WaseEndpoint is a library for building daemons that act as WASE Endpoints for http://bit.ly/3qRMbv"
|
16
|
+
gemspec.description = gemspec.summary
|
17
|
+
gemspec.email = "dougal.s@gmail.com"
|
18
|
+
gemspec.homepage = "http://github.com/dougal/wase_endpoint"
|
19
|
+
gemspec.authors = ["Douglas F Shearer"]
|
20
|
+
end
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
24
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/example/init.rb
ADDED
data/example/server.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'twitter'
|
3
|
+
require 'json'
|
4
|
+
require 'rest_client'
|
5
|
+
require 'robustthread'
|
6
|
+
|
7
|
+
require 'wase_endpoint/message'
|
8
|
+
require 'wase_endpoint/twitterer'
|
9
|
+
|
10
|
+
class WaseEndpoint
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
@logger = Logger.new(options[:logfile])
|
14
|
+
@twitterer = Twitterer.new(options[:username], options[:password])
|
15
|
+
@sleep_period = options[:sleep_period] || 60
|
16
|
+
|
17
|
+
start
|
18
|
+
end
|
19
|
+
|
20
|
+
# Override this.
|
21
|
+
# Argument is a json encoded string.
|
22
|
+
# Should return a json encoded string.
|
23
|
+
def secret_sauce(raw_json)
|
24
|
+
raise Exception, 'You need to override secret_sauce'
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def logger
|
30
|
+
@logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def start
|
34
|
+
RobustThread.logger = @logger
|
35
|
+
pid = fork do
|
36
|
+
RobustThread.loop(:seconds => @sleep_period, :label => 'Checking for new messages and processing them') do
|
37
|
+
|
38
|
+
# Grab any new messages.
|
39
|
+
messages = @twitterer.fetch
|
40
|
+
@logger.info "Processing #{messages.size} messages"
|
41
|
+
|
42
|
+
messages.each do |message|
|
43
|
+
program_listing = message.fetch_program_listing
|
44
|
+
if message.program_counter >= program_listing.size
|
45
|
+
throw Exception 'Program counter has gone too far'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Here's where your magic happens.
|
49
|
+
message.send_input(secret_sauce(message.fetch_output))
|
50
|
+
|
51
|
+
# Tell the next endpoint.
|
52
|
+
@twitterer.send(program_listing[message.program_counter + 1], message.program_counter + 1, message.program_listing_uri, message.output_uri)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
sleep
|
57
|
+
end
|
58
|
+
puts pid
|
59
|
+
Process.detach pid
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class WaseEndpoint
|
2
|
+
|
3
|
+
class Message
|
4
|
+
|
5
|
+
attr_reader :program_counter, :program_listing_uri, :timestamp, :output_uri, :input_uri, :input_uri_1, :id
|
6
|
+
|
7
|
+
def initialize(id, raw_message)
|
8
|
+
myname_and_hashtag, program_counter, program_listing_uri, unix_timestamp, output_uri, input_uri, input_uri_1 = raw_message.split(/,/)
|
9
|
+
|
10
|
+
@id = id
|
11
|
+
@program_counter = program_counter.to_i
|
12
|
+
@program_listing_uri = program_listing_uri.strip
|
13
|
+
@timestamp = unix_timestamp.to_i
|
14
|
+
@output_uri = output_uri.strip
|
15
|
+
@input_uri = input_uri.strip if input_uri
|
16
|
+
@input_uri_1 = input_uri_1.strip if input_uri_1
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
@id == other.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_program_listing
|
24
|
+
JSON.parse(RestClient.get('http://' + @program_listing_uri))
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_output
|
28
|
+
RestClient.get('http://' + @output_uri)
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_input(input)
|
32
|
+
input_uri = 'http://' + (@input_uri || @output_uri)
|
33
|
+
|
34
|
+
# RestClient can't follow a redirect for put, so we'll expand it.
|
35
|
+
input_uri[/bit\.ly(\/\w+)/]
|
36
|
+
expanded_input_uri = Net::HTTP.new('bit.ly').head($1)['Location']
|
37
|
+
|
38
|
+
RestClient.put(expanded_input_uri, input)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class WaseEndpoint
|
2
|
+
class Twitterer
|
3
|
+
|
4
|
+
def initialize(username, password)
|
5
|
+
twitter_http_auth = Twitter::HTTPAuth.new(username, password)
|
6
|
+
@twitter_client = Twitter::Base.new(twitter_http_auth)
|
7
|
+
|
8
|
+
# The oldest message ID could be stored here, but since twitter IDs
|
9
|
+
# aren't always time-linear, comparing the messages is safer.
|
10
|
+
@all_messages = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Fetches the timeline and returns any new messages.
|
14
|
+
def fetch
|
15
|
+
new_messages = []
|
16
|
+
|
17
|
+
# Ignore any messages that don't have the hashtag.
|
18
|
+
# Use twitter search to do this instead?
|
19
|
+
@twitter_client.replies.reject{|m| !m.text[/#wase/]}.each do |reply|
|
20
|
+
message = Message.new(reply.id, reply.text)
|
21
|
+
|
22
|
+
# Skip if we've already processed this message.
|
23
|
+
unless @all_messages.include?(message)
|
24
|
+
@all_messages << message
|
25
|
+
new_messages << message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
new_messages
|
29
|
+
end
|
30
|
+
|
31
|
+
def send(recipient, program_counter, program_list_uri, output_uri, input_uri=nil, input_uri_1=nil)
|
32
|
+
text = ["#{recipient} #wase", program_counter, program_list_uri, Time.now.utc.to_i, output_uri, input_uri, input_uri_1].compact.join(', ')
|
33
|
+
@twitter_client.update(text)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe WaseEndpoint::Message do
|
5
|
+
|
6
|
+
describe "with no input URI" do
|
7
|
+
|
8
|
+
before(:all) do
|
9
|
+
raw_message = "@ey-sort #wase, 0, bit.ly/7yQK6, 1256850843, bit.ly/2uhGcl"
|
10
|
+
@message = WaseEndpoint::Message.new(123, raw_message)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return the id" do
|
14
|
+
@message.id.should == 123
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return the program counter as an integer" do
|
18
|
+
@message.program_counter.should == 0
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return the program listing url without whitespace" do
|
22
|
+
@message.program_listing_uri.should == 'bit.ly/7yQK6'
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return the timestamp as an integer" do
|
26
|
+
@message.timestamp.should == 1256850843
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return the output uri without whitespace" do
|
30
|
+
@message.output_uri.should == 'bit.ly/2uhGcl'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return the input uri as nil" do
|
34
|
+
@message.input_uri.should be_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return the second input uri as nil" do
|
38
|
+
@message.input_uri_1.should be_nil
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should fetch the program listing as an array" do
|
42
|
+
RestClient.should_receive(:get).with('http://bit.ly/7yQK6').and_return('["@ey-sort", "@ey-firsthalf", "@ey-firsthalf", "@engineyard"]')
|
43
|
+
|
44
|
+
@message.fetch_program_listing.should == ["@ey-sort", "@ey-firsthalf", "@ey-firsthalf", "@engineyard"]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should fetch the output data" do
|
48
|
+
RestClient.should_receive(:get).with('http://bit.ly/2uhGcl').and_return('[58, 92, 12, 18, 76]')
|
49
|
+
|
50
|
+
@message.fetch_output.should == '[58, 92, 12, 18, 76]'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should send the input data using the expanded output URI" do
|
54
|
+
mock_net_http = mock(Net::HTTP)
|
55
|
+
mock_net_http.should_receive(:head).with('/2uhGcl').and_return({'Location' => 'http://example.com/'})
|
56
|
+
Net::HTTP.should_receive(:new).with('bit.ly').and_return(mock_net_http)
|
57
|
+
RestClient.should_receive(:put).with('http://example.com/', '[1, 2, 3, 4]')
|
58
|
+
|
59
|
+
@message.send_input('[1, 2, 3, 4]')
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "with one input URI" do
|
65
|
+
|
66
|
+
before(:all) do
|
67
|
+
raw_message = "@ey-sort #wase, 0, bit.ly/7yQK6, 1256850843, bit.ly/2uhGcl, bit.ly/3kl0xs"
|
68
|
+
@message = WaseEndpoint::Message.new(123, raw_message)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should return the input uri without whitespace" do
|
72
|
+
@message.input_uri.should == 'bit.ly/3kl0xs'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should return the second input uri as nil" do
|
76
|
+
@message.input_uri_1.should be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should send the input data using the expanded output URI" do
|
80
|
+
mock_net_http = mock(Net::HTTP)
|
81
|
+
mock_net_http.should_receive(:head).with('/3kl0xs').and_return({'Location' => 'http://example.com/'})
|
82
|
+
Net::HTTP.should_receive(:new).with('bit.ly').and_return(mock_net_http)
|
83
|
+
RestClient.should_receive(:put).with('http://example.com/', '[1, 2, 3, 4]')
|
84
|
+
|
85
|
+
@message.send_input('[1, 2, 3, 4]')
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "with two input URIs" do
|
91
|
+
|
92
|
+
before(:all) do
|
93
|
+
raw_message = "@ey-sort #wase, 0, bit.ly/7yQK6, 1256850843, bit.ly/2uhGcl, bit.ly/3kl0xs, bit.ly/3kl0xt"
|
94
|
+
@message = WaseEndpoint::Message.new(123, raw_message)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should return the second input uri without whitespace" do
|
98
|
+
@message.input_uri_1.should == 'bit.ly/3kl0xt'
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "comparison via ID" do
|
104
|
+
|
105
|
+
before(:all) do
|
106
|
+
@raw_message = "@ey-sort #wase, 0, bit.ly/7yQK6, 1256850843, bit.ly/2uhGcl"
|
107
|
+
@message_1 = WaseEndpoint::Message.new(123, @raw_message)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should equal" do
|
111
|
+
@message_1.should == WaseEndpoint::Message.new(123, @raw_message)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should not be equal" do
|
115
|
+
@message_1.should_not == WaseEndpoint::Message.new(456, @raw_message)
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe WaseEndpoint::Twitterer do
|
5
|
+
|
6
|
+
it "should return a twitter client object" do
|
7
|
+
username ='foo'
|
8
|
+
password = 'bar'
|
9
|
+
Twitter::HTTPAuth.should_receive(:new).with(username, password).and_return(mock_twitter_http_auth)
|
10
|
+
Twitter::Base.should_receive(:new).with(mock_twitter_http_auth).and_return(mock_twitter_base)
|
11
|
+
|
12
|
+
WaseEndpoint::Twitterer.new(username, password)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "fetching messages" do
|
16
|
+
|
17
|
+
before(:all) do
|
18
|
+
username ='foo'
|
19
|
+
password = 'bar'
|
20
|
+
Twitter::HTTPAuth.stub(:new)
|
21
|
+
Twitter::Base.stub(:new).and_return(mock_twitter_base)
|
22
|
+
|
23
|
+
@twitterer = WaseEndpoint::Twitterer.new(username, password)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should return the latest message" do
|
27
|
+
mock_twitter_base.should_receive(:replies).and_return([mock_mash(123)])
|
28
|
+
messages = @twitterer.fetch
|
29
|
+
messages.size.should == 1
|
30
|
+
messages.first.class.should == WaseEndpoint::Message
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return an empty messages array" do
|
34
|
+
mock_twitter_base.should_receive(:replies).and_return([mock_mash(123)])
|
35
|
+
@twitterer.fetch.should == []
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "sending messages" do
|
41
|
+
|
42
|
+
before(:all) do
|
43
|
+
username ='foo'
|
44
|
+
password = 'bar'
|
45
|
+
Twitter::HTTPAuth.stub(:new)
|
46
|
+
Twitter::Base.stub(:new).and_return(mock_twitter_base)
|
47
|
+
|
48
|
+
@time = Time.now
|
49
|
+
Time.stub(:now).and_return(@time)
|
50
|
+
|
51
|
+
@twitterer = WaseEndpoint::Twitterer.new(username, password)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should send a message with all inputs" do
|
55
|
+
mock_twitter_base.should_receive(:update).with("@a #wase, 10, pl_uri, #{@time.utc.to_i}, o_uri, i_uri, i_1_uri")
|
56
|
+
@twitterer.send('@a', 10, 'pl_uri', 'o_uri', 'i_uri', 'i_1_uri')
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should send a message with only one input" do
|
60
|
+
mock_twitter_base.should_receive(:update).with("@a #wase, 10, pl_uri, #{@time.utc.to_i}, o_uri, i_uri")
|
61
|
+
@twitterer.send('@a', 10, 'pl_uri', 'o_uri', 'i_uri')
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should send a message with no inputs" do
|
65
|
+
mock_twitter_base.should_receive(:update).with("@a #wase, 10, pl_uri, #{@time.utc.to_i}, o_uri")
|
66
|
+
@twitterer.send('@a', 10, 'pl_uri', 'o_uri')
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def mock_twitter_http_auth
|
72
|
+
@auth_mock ||= mock(Twitter::HTTPAuth)
|
73
|
+
end
|
74
|
+
|
75
|
+
def mock_twitter_base
|
76
|
+
@base_mock ||= mock(Twitter::Base)
|
77
|
+
end
|
78
|
+
|
79
|
+
def mock_mash(message_id)
|
80
|
+
mock(Mash, :id => message_id, :text => '@ey-sort #wase, 0, bit.ly/7yQK6, 1256850843, bit.ly/2uhGcl, bit.ly/3kl0xs')
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wase_endpoint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Douglas F Shearer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-05 00:00:00 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: WaseEndpoint is a library for building daemons that act as WASE Endpoints for http://bit.ly/3qRMbv
|
17
|
+
email: dougal.s@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- README.rdoc
|
27
|
+
- Rakefile
|
28
|
+
- VERSION
|
29
|
+
- example/init.rb
|
30
|
+
- example/my_endpoint.rb
|
31
|
+
- example/server.rb
|
32
|
+
- lib/wase_endpoint.rb
|
33
|
+
- lib/wase_endpoint/message.rb
|
34
|
+
- lib/wase_endpoint/twitterer.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
- spec/wase_endpoint_message_spec.rb
|
37
|
+
- spec/wase_endpoint_spec.rb
|
38
|
+
- spec/wase_endpoint_twitterer_spec.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/dougal/wase_endpoint
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --charset=UTF-8
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: WaseEndpoint is a library for building daemons that act as WASE Endpoints for http://bit.ly/3qRMbv
|
67
|
+
test_files:
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
- spec/wase_endpoint_message_spec.rb
|
70
|
+
- spec/wase_endpoint_spec.rb
|
71
|
+
- spec/wase_endpoint_twitterer_spec.rb
|