livechat_client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/README.md +327 -0
- data/Rakefile +13 -0
- data/conf/cacert.pem +3376 -0
- data/lib/livechat.rb +23 -0
- data/lib/livechat/rest/agents.rb +19 -0
- data/lib/livechat/rest/canned_responses.rb +9 -0
- data/lib/livechat/rest/chats.rb +18 -0
- data/lib/livechat/rest/client.rb +142 -0
- data/lib/livechat/rest/errors.rb +14 -0
- data/lib/livechat/rest/goals.rb +14 -0
- data/lib/livechat/rest/groups.rb +9 -0
- data/lib/livechat/rest/instance_resource.rb +115 -0
- data/lib/livechat/rest/list_resource.rb +115 -0
- data/lib/livechat/rest/reports.rb +116 -0
- data/lib/livechat/rest/status.rb +13 -0
- data/lib/livechat/rest/utils.rb +25 -0
- data/lib/livechat/rest/visitors.rb +17 -0
- data/lib/livechat/util.rb +7 -0
- data/lib/livechat/version.rb +3 -0
- data/livechat_client.gemspec +33 -0
- data/spec/fixtures/agent.json +30 -0
- data/spec/fixtures/agents.json +16 -0
- data/spec/fixtures/canned_response.json +12 -0
- data/spec/fixtures/canned_responses.json +37 -0
- data/spec/fixtures/chat.json +129 -0
- data/spec/fixtures/chats.json +135 -0
- data/spec/fixtures/goal.json +8 -0
- data/spec/fixtures/goals.json +12 -0
- data/spec/fixtures/group.json +10 -0
- data/spec/fixtures/groups.json +32 -0
- data/spec/fixtures/visitors.json +91 -0
- data/spec/livechat/rest/agents_spec.rb +56 -0
- data/spec/livechat/rest/canned_responses_spec.rb +43 -0
- data/spec/livechat/rest/chats_spec.rb +37 -0
- data/spec/livechat/rest/goals_spec.rb +50 -0
- data/spec/livechat/rest/groups_spec.rb +45 -0
- data/spec/livechat/rest/reports_spec.rb +67 -0
- data/spec/livechat/rest/status_spec.rb +22 -0
- data/spec/livechat/rest/visitors_spec.rb +44 -0
- data/spec/spec_helper.rb +38 -0
- metadata +185 -0
data/lib/livechat.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'cgi'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
|
9
|
+
require "livechat/version"
|
10
|
+
require 'livechat/util'
|
11
|
+
require 'livechat/rest/errors'
|
12
|
+
require 'livechat/rest/utils'
|
13
|
+
require 'livechat/rest/list_resource'
|
14
|
+
require 'livechat/rest/instance_resource'
|
15
|
+
require 'livechat/rest/agents'
|
16
|
+
require 'livechat/rest/canned_responses'
|
17
|
+
require 'livechat/rest/chats'
|
18
|
+
require 'livechat/rest/goals'
|
19
|
+
require 'livechat/rest/groups'
|
20
|
+
require 'livechat/rest/reports'
|
21
|
+
require 'livechat/rest/status'
|
22
|
+
require 'livechat/rest/visitors'
|
23
|
+
require 'livechat/rest/client'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LiveChat
|
2
|
+
module REST
|
3
|
+
class Agents < ListResource
|
4
|
+
def initialize(path, client)
|
5
|
+
super
|
6
|
+
#hard-coded keys since agents are special
|
7
|
+
@instance_id_key = 'login'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Agent < InstanceResource
|
12
|
+
def reset_api_key
|
13
|
+
raise "Can't execute without a REST Client" unless @client
|
14
|
+
set_up_properties_from(@client.put("#{@path}/reset_api_key", {}))
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module LiveChat
|
2
|
+
module REST
|
3
|
+
class Chats < ListResource
|
4
|
+
def initialize(path, client)
|
5
|
+
super
|
6
|
+
#chats is different than the other resources
|
7
|
+
@list_key = 'chats'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Chat < InstanceResource
|
12
|
+
def send_transcript(*args)
|
13
|
+
@client.post "#{@path}/send_transcript", Hash[*args]
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module LiveChat
|
2
|
+
module REST
|
3
|
+
class Client
|
4
|
+
include LiveChat::Util
|
5
|
+
include LiveChat::REST::Utils
|
6
|
+
|
7
|
+
API_VERSION = '2'
|
8
|
+
|
9
|
+
HTTP_HEADERS = {
|
10
|
+
'Accept' => 'application/json',
|
11
|
+
'Accept-Charset' => 'utf-8',
|
12
|
+
'User-Agent' => "livechat-ruby/#{LiveChat::VERSION}",
|
13
|
+
'X-API-Version' => API_VERSION
|
14
|
+
}
|
15
|
+
|
16
|
+
DEFAULTS = {
|
17
|
+
:host => 'api.livechatinc.com',
|
18
|
+
:port => 443,
|
19
|
+
:use_ssl => true,
|
20
|
+
:ssl_verify_peer => true,
|
21
|
+
:ssl_ca_file => File.dirname(__FILE__) + '/../../../conf/cacert.pem',
|
22
|
+
:timeout => 30,
|
23
|
+
:proxy_addr => nil,
|
24
|
+
:proxy_port => nil,
|
25
|
+
:proxy_user => nil,
|
26
|
+
:proxy_pass => nil,
|
27
|
+
:retry_limit => 1,
|
28
|
+
}
|
29
|
+
|
30
|
+
attr_reader :login, :last_request, :last_response
|
31
|
+
|
32
|
+
%w(agents canned_responses chats goals groups reports status visitors).each do |r|
|
33
|
+
define_method(r.to_sym) do |*args|
|
34
|
+
klass = LiveChat::REST.const_get restify(r.capitalize)
|
35
|
+
n = klass.new("/#{r}", self)
|
36
|
+
if args.length > 0
|
37
|
+
n.get(args[0])
|
38
|
+
else
|
39
|
+
n
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Instantiate a new HTTP client to talk to LiveChat. The parameters
|
46
|
+
# +login+ and +api_key+ are required and used to generate the
|
47
|
+
# HTTP basic auth header in each request.
|
48
|
+
#
|
49
|
+
def initialize(options={})
|
50
|
+
yield options if block_given?
|
51
|
+
@config = DEFAULTS.merge! options
|
52
|
+
@login = @config[:login].strip
|
53
|
+
@api_key = @config[:api_key].strip
|
54
|
+
raise ArgumentError, "Login and API key are required!" unless @login and @api_key
|
55
|
+
set_up_connection
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect # :nodoc:
|
59
|
+
"<LiveChat::REST::Client @login=#{@login}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Define #get, #put, #post and #delete helper methods for sending HTTP
|
64
|
+
# requests to LiveChat. You shouldn't need to use these methods directly,
|
65
|
+
# but they can be useful for debugging. Each method returns a hash
|
66
|
+
# obtained from parsing the JSON object in the response body.
|
67
|
+
[:get, :put, :post, :delete].each do |method|
|
68
|
+
method_class = Net::HTTP.const_get method.to_s.capitalize
|
69
|
+
define_method method do |path, *args|
|
70
|
+
params ||= {}
|
71
|
+
params = args[0] if args[0]
|
72
|
+
unless args[1] # build the full path unless already given
|
73
|
+
path = "#{path}"
|
74
|
+
if method == :get
|
75
|
+
path << "?#{url_encode(params)}" if method == :get && !params.empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
request = method_class.new path, HTTP_HEADERS
|
79
|
+
request.basic_auth @login, @api_key
|
80
|
+
request.form_data = params if [:post, :put].include? method
|
81
|
+
connect_and_send request
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
##
|
88
|
+
# Set up and cache a Net::HTTP object to use when making requests. This is
|
89
|
+
# a private method documented for completeness.
|
90
|
+
def set_up_connection # :doc:
|
91
|
+
connection_class = Net::HTTP::Proxy @config[:proxy_addr],
|
92
|
+
@config[:proxy_port], @config[:proxy_user], @config[:proxy_pass]
|
93
|
+
@connection = connection_class.new @config[:host], @config[:port]
|
94
|
+
set_up_ssl
|
95
|
+
@connection.open_timeout = @config[:timeout]
|
96
|
+
@connection.read_timeout = @config[:timeout]
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Set up the ssl properties of the <tt>@connection</tt> Net::HTTP object.
|
101
|
+
# This is a private method documented for completeness.
|
102
|
+
def set_up_ssl # :doc:
|
103
|
+
@connection.use_ssl = @config[:use_ssl]
|
104
|
+
if @config[:ssl_verify_peer]
|
105
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
106
|
+
@connection.ca_file = @config[:ssl_ca_file]
|
107
|
+
else
|
108
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
##
|
114
|
+
# Send an HTTP request using the cached <tt>@connection</tt> object and
|
115
|
+
# return the JSON response body parsed into a hash. Also save the raw
|
116
|
+
# Net::HTTP::Request and Net::HTTP::Response objects as
|
117
|
+
# <tt>@last_request</tt> and <tt>@last_response</tt> to allow for
|
118
|
+
# inspection later.
|
119
|
+
def connect_and_send(request) # :doc:
|
120
|
+
@last_request = request
|
121
|
+
retries_left = @config[:retry_limit]
|
122
|
+
begin
|
123
|
+
response = @connection.request request
|
124
|
+
@last_response = response
|
125
|
+
if response.kind_of? Net::HTTPServerError
|
126
|
+
raise LiveChat::REST::ServerError
|
127
|
+
end
|
128
|
+
rescue Exception
|
129
|
+
raise if request.class == Net::HTTP::Post
|
130
|
+
if retries_left > 0 then retries_left -= 1; retry else raise end
|
131
|
+
end
|
132
|
+
if response.body and !response.body.empty?
|
133
|
+
object = MultiJson.load response.body
|
134
|
+
end
|
135
|
+
if response.kind_of? Net::HTTPClientError
|
136
|
+
raise LiveChat::REST::RequestError.new "#{object['message']}: #{response.body}", object['code']
|
137
|
+
end
|
138
|
+
object
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module LiveChat
|
2
|
+
module REST
|
3
|
+
class Goals < ListResource
|
4
|
+
end
|
5
|
+
|
6
|
+
class Goal < InstanceResource
|
7
|
+
def mark_as_successful(*args)
|
8
|
+
raise "Can't execute without a REST Client" unless @client
|
9
|
+
@client.post "#{@path}/mark_as_successful", Hash[*args]
|
10
|
+
self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#This file is based on code from https://github.com/twilio/twilio-ruby
|
2
|
+
#
|
3
|
+
#Copyright (c) 2010 Andrew Benton.
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
10
|
+
#furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
#all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
#THE SOFTWARE.
|
22
|
+
|
23
|
+
|
24
|
+
module LiveChat
|
25
|
+
module REST
|
26
|
+
##
|
27
|
+
# A class to wrap an instance resource (like a call or application) within
|
28
|
+
# the LiveChat API.
|
29
|
+
class InstanceResource
|
30
|
+
include Utils
|
31
|
+
|
32
|
+
##
|
33
|
+
# Instantiate a new instance resource object. You must pass the +path+ of
|
34
|
+
# the instance (e.g. /chats/MH022RD0K5) as well as a
|
35
|
+
# +client+ object that responds to #get #post and #delete. This client
|
36
|
+
# is meant to be an instance of LiveChat::REST::Client but could just as
|
37
|
+
# well be a mock object if you want to test the interface. The optional
|
38
|
+
# +params+ hash will be converted into attributes on the instantiated
|
39
|
+
# object.
|
40
|
+
def initialize(path, client, params = {})
|
41
|
+
@path, @client = path, client
|
42
|
+
set_up_properties_from params
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect # :nodoc:
|
46
|
+
"<#{self.class} @path=#{@path}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Update the properties of this instance resource using the key/value
|
51
|
+
# pairs in +params+. This makes an HTTP POST request to <tt>@path</tt>
|
52
|
+
# to handle the update.
|
53
|
+
#
|
54
|
+
# After returning, the object will contain the most recent state of the
|
55
|
+
# instance resource, including the newly updated properties.
|
56
|
+
def update(params = {})
|
57
|
+
raise "Can't update a resource without a REST Client" unless @client
|
58
|
+
yield params if block_given?
|
59
|
+
set_up_properties_from(@client.put(@path, params))
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Refresh the attributes of this instance resource object by fetching it
|
65
|
+
# from LiveChat. Calling this makes an HTTP GET request to <tt>@path</tt>.
|
66
|
+
def refresh
|
67
|
+
raise "Can't refresh a resource without a REST Client" unless @client
|
68
|
+
@updated = false
|
69
|
+
set_up_properties_from(@client.get(@path))
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Delete an instance resource from LiveChat. This operation isn't always
|
75
|
+
# supported. For instance, you can't delete an SMS. Calling this method
|
76
|
+
# makes an HTTP DELETE request to <tt>@path</tt>.
|
77
|
+
def delete
|
78
|
+
raise "Can't delete a resource without a REST Client" unless @client
|
79
|
+
@client.delete @path
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Lazily load attributes of the instance resource by waiting to fetch it
|
84
|
+
# until an attempt is made to access an unknown attribute.
|
85
|
+
def method_missing(method, *args)
|
86
|
+
super if @updated
|
87
|
+
set_up_properties_from(@client.get(@path))
|
88
|
+
self.send method, *args
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def set_up_properties_from(hash)
|
94
|
+
eigenclass = class << self; self; end
|
95
|
+
hash.each do |p,v|
|
96
|
+
property = unrestify p
|
97
|
+
eigenclass.send :define_method, property.to_sym, &lambda {v}
|
98
|
+
end
|
99
|
+
@updated = !hash.keys.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
def resource(*resources)
|
103
|
+
resources.each do |r|
|
104
|
+
resource = restify r
|
105
|
+
relative_path = resource
|
106
|
+
path = "#{@path}/#{relative_path}"
|
107
|
+
resource_class = LiveChat::REST.const_get resource
|
108
|
+
instance_variable_set("@#{r}", resource_class.new(path, @client))
|
109
|
+
end
|
110
|
+
self.class.instance_eval {attr_reader *resources}
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#This file is based on code from https://github.com/twilio/twilio-ruby
|
2
|
+
#
|
3
|
+
#Copyright (c) 2010 Andrew Benton.
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
10
|
+
#furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
#all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
#THE SOFTWARE.
|
22
|
+
|
23
|
+
module LiveChat
|
24
|
+
module REST
|
25
|
+
class ListResource
|
26
|
+
include Utils
|
27
|
+
|
28
|
+
def initialize(path, client)
|
29
|
+
@path, @client = path, client
|
30
|
+
resource_name = self.class.name.split('::')[-1]
|
31
|
+
@instance_class = LiveChat::REST.const_get resource_name.chop
|
32
|
+
@instance_id_key = 'id'
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect # :nodoc:
|
36
|
+
"<#{self.class} @path=#{@path}>"
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Grab a list of this kind of resource and return it as an array. The
|
41
|
+
# array includes a special attribute named +total+ which will return the
|
42
|
+
# total number of items in the list on LiveChat's server. This may differ
|
43
|
+
# from the +size+ and +length+ attributes of the returned array since
|
44
|
+
# by default LiveChat will only return 50 resources, and the maximum number
|
45
|
+
# of resources you can request is 1000.
|
46
|
+
#
|
47
|
+
# The optional +params+ hash allows you to filter the list returned. The
|
48
|
+
# filters for each list resource type are defined by LiveChat.
|
49
|
+
def list(params={}, full_path=false)
|
50
|
+
raise "Can't get a resource list without a REST Client" unless @client
|
51
|
+
response = @client.get @path, params, full_path
|
52
|
+
resources = @list_key ? response[@list_key]: response
|
53
|
+
path = full_path ? @path.split('.')[0] : @path
|
54
|
+
resource_list = resources.map do |resource|
|
55
|
+
@instance_class.new "#{path}/#{resource[@instance_id_key]}", @client,
|
56
|
+
resource
|
57
|
+
end
|
58
|
+
# set the +total+ and +next_page+ properties on the array
|
59
|
+
#client, list_class = @client, self.class
|
60
|
+
#resource_list.instance_eval do
|
61
|
+
#eigenclass = class << self; self; end
|
62
|
+
#eigenclass.send :define_method, :total, &lambda {response['total']}
|
63
|
+
#eigenclass.send :define_method, :next_page, &lambda {
|
64
|
+
# if response['next_page_uri']
|
65
|
+
# list_class.new(response['next_page_uri'], client).list({}, true)
|
66
|
+
# else
|
67
|
+
# []
|
68
|
+
# end
|
69
|
+
#}
|
70
|
+
#end
|
71
|
+
resource_list
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Ask LiveChat for the total number of items in the list.
|
76
|
+
# Calling this method makes an HTTP GET request to <tt>@path</tt> with a
|
77
|
+
# page size parameter of 1 to minimize data over the wire while still
|
78
|
+
# obtaining the total. Don't use this if you are planning to
|
79
|
+
# call #list anyway, since the array returned from #list will have a
|
80
|
+
# +total+ attribute as well.
|
81
|
+
#def total
|
82
|
+
# raise "Can't get a resource total without a REST Client" unless @client
|
83
|
+
# @client.get(@path, :page_size => 1)['total']
|
84
|
+
#end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Return an empty instance resource object with the proper path. Note that
|
88
|
+
# this will never raise a LiveChat::REST::RequestError on 404 since no HTTP
|
89
|
+
# request is made. The HTTP request is made when attempting to access an
|
90
|
+
# attribute of the returned instance resource object, such as
|
91
|
+
# its #date_created or #voice_url attributes.
|
92
|
+
def get(id)
|
93
|
+
@instance_class.new "#{@path}/#{id}", @client
|
94
|
+
end
|
95
|
+
alias :find :get # for the ActiveRecord lovers
|
96
|
+
|
97
|
+
##
|
98
|
+
# Return a newly created resource. Some +params+ may be required. Consult
|
99
|
+
# the LiveChat REST API documentation related to the kind of resource you
|
100
|
+
# are attempting to create for details. Calling this method makes an HTTP
|
101
|
+
# POST request to <tt>@path</tt> with the given params
|
102
|
+
def create(params={})
|
103
|
+
raise "Can't create a resource without a REST Client" unless @client
|
104
|
+
yield params if block_given?
|
105
|
+
response = @client.post @path, params
|
106
|
+
@instance_class.new "#{@path}/#{response[@instance_id_key]}", @client, response
|
107
|
+
end
|
108
|
+
|
109
|
+
def each
|
110
|
+
list.each { |result| yield result }
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|