typhoeus 0.4.2 → 0.5.0.alpha
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/CHANGELOG.md +86 -28
- data/Gemfile +17 -1
- data/README.md +20 -422
- data/Rakefile +21 -12
- data/lib/typhoeus.rb +58 -41
- data/lib/typhoeus/config.rb +14 -0
- data/lib/typhoeus/errors.rb +9 -0
- data/lib/typhoeus/errors/no_stub.rb +12 -0
- data/lib/typhoeus/errors/typhoeus_error.rb +8 -0
- data/lib/typhoeus/expectation.rb +126 -0
- data/lib/typhoeus/hydra.rb +31 -236
- data/lib/typhoeus/hydras/block_connection.rb +33 -0
- data/lib/typhoeus/hydras/easy_factory.rb +67 -0
- data/lib/typhoeus/hydras/easy_pool.rb +40 -0
- data/lib/typhoeus/hydras/memoizable.rb +53 -0
- data/lib/typhoeus/hydras/queueable.rb +46 -0
- data/lib/typhoeus/hydras/runnable.rb +18 -0
- data/lib/typhoeus/hydras/stubbable.rb +27 -0
- data/lib/typhoeus/request.rb +68 -243
- data/lib/typhoeus/requests/actions.rb +101 -0
- data/lib/typhoeus/requests/block_connection.rb +31 -0
- data/lib/typhoeus/requests/callbacks.rb +82 -0
- data/lib/typhoeus/requests/marshal.rb +21 -0
- data/lib/typhoeus/requests/memoizable.rb +36 -0
- data/lib/typhoeus/requests/operations.rb +52 -0
- data/lib/typhoeus/requests/responseable.rb +29 -0
- data/lib/typhoeus/requests/stubbable.rb +29 -0
- data/lib/typhoeus/response.rb +24 -118
- data/lib/typhoeus/responses/header.rb +50 -0
- data/lib/typhoeus/responses/informations.rb +43 -0
- data/lib/typhoeus/responses/legacy.rb +27 -0
- data/lib/typhoeus/responses/status.rb +78 -0
- data/lib/typhoeus/version.rb +3 -1
- metadata +34 -141
- data/lib/typhoeus/curl.rb +0 -453
- data/lib/typhoeus/easy.rb +0 -115
- data/lib/typhoeus/easy/auth.rb +0 -14
- data/lib/typhoeus/easy/callbacks.rb +0 -33
- data/lib/typhoeus/easy/ffi_helper.rb +0 -61
- data/lib/typhoeus/easy/infos.rb +0 -90
- data/lib/typhoeus/easy/options.rb +0 -115
- data/lib/typhoeus/easy/proxy.rb +0 -20
- data/lib/typhoeus/easy/ssl.rb +0 -82
- data/lib/typhoeus/filter.rb +0 -28
- data/lib/typhoeus/form.rb +0 -61
- data/lib/typhoeus/header.rb +0 -54
- data/lib/typhoeus/hydra/callbacks.rb +0 -24
- data/lib/typhoeus/hydra/connect_options.rb +0 -61
- data/lib/typhoeus/hydra/stubbing.rb +0 -68
- data/lib/typhoeus/hydra_mock.rb +0 -131
- data/lib/typhoeus/multi.rb +0 -146
- data/lib/typhoeus/param_processor.rb +0 -43
- data/lib/typhoeus/remote.rb +0 -306
- data/lib/typhoeus/remote_method.rb +0 -108
- data/lib/typhoeus/remote_proxy_object.rb +0 -50
- data/lib/typhoeus/utils.rb +0 -50
@@ -0,0 +1,101 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests # :nodoc:
|
3
|
+
|
4
|
+
# Module containing logic about shortcuts to
|
5
|
+
# http methods. Like
|
6
|
+
# Typhoeus.get("www.example.com")
|
7
|
+
module Actions
|
8
|
+
|
9
|
+
# Make a get request.
|
10
|
+
#
|
11
|
+
# @example Make get request.
|
12
|
+
# Typhoeus.get("www.example.com")
|
13
|
+
#
|
14
|
+
# @param [ String ] url The url to request.
|
15
|
+
# @param [ options ] options The options.
|
16
|
+
#
|
17
|
+
# @return [ Response ] The response.
|
18
|
+
def get(url, options = {})
|
19
|
+
Request.run(url, options.merge(:method => :get))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Make a post request.
|
23
|
+
#
|
24
|
+
# @example Make post request.
|
25
|
+
# Typhoeus.post("www.example.com")
|
26
|
+
#
|
27
|
+
# @param [ String ] url The url to request.
|
28
|
+
# @param [ options ] options The options.
|
29
|
+
#
|
30
|
+
# @return [ Response ] The response.
|
31
|
+
def post(url, options = {})
|
32
|
+
Request.run(url, options.merge(:method => :post))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Make a put request.
|
36
|
+
#
|
37
|
+
# @example Make put request.
|
38
|
+
# Typhoeus.put("www.example.com")
|
39
|
+
#
|
40
|
+
# @param [ String ] url The url to request.
|
41
|
+
# @param [ options ] options The options.
|
42
|
+
#
|
43
|
+
# @return [ Response ] The response.
|
44
|
+
def put(url, options = {})
|
45
|
+
Request.run(url, options.merge(:method => :put))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Make a delete request.
|
49
|
+
#
|
50
|
+
# @example Make delete request.
|
51
|
+
# Typhoeus.delete("www.example.com")
|
52
|
+
#
|
53
|
+
# @param [ String ] url The url to request.
|
54
|
+
# @param [ options ] options The options.
|
55
|
+
#
|
56
|
+
# @return [ Response ] The response.
|
57
|
+
def delete(url, options = {})
|
58
|
+
Request.run(url, options.merge(:method => :delete))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Make a head request.
|
62
|
+
#
|
63
|
+
# @example Make head request.
|
64
|
+
# Typhoeus.head("www.example.com")
|
65
|
+
#
|
66
|
+
# @param [ String ] url The url to request.
|
67
|
+
# @param [ options ] options The options.
|
68
|
+
#
|
69
|
+
# @return [ Response ] The response.
|
70
|
+
def head(url, options = {})
|
71
|
+
Request.run(url, options.merge(:method => :head))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Make a patch request.
|
75
|
+
#
|
76
|
+
# @example Make patch request.
|
77
|
+
# Typhoeus.patch("www.example.com")
|
78
|
+
#
|
79
|
+
# @param [ String ] url The url to request.
|
80
|
+
# @param [ options ] options The options.
|
81
|
+
#
|
82
|
+
# @return [ Response ] The response.
|
83
|
+
def patch(url, options = {})
|
84
|
+
Request.run(url, options.merge(:method => :patch))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make a options request.
|
88
|
+
#
|
89
|
+
# @example Make options request.
|
90
|
+
# Typhoeus.options("www.example.com")
|
91
|
+
#
|
92
|
+
# @param [ String ] url The url to request.
|
93
|
+
# @param [ options ] options The options.
|
94
|
+
#
|
95
|
+
# @return [ Response ] The response.
|
96
|
+
def options(url, options = {})
|
97
|
+
Request.run(url, options.merge(:method => :options))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module handles the blocked connection request mode on
|
5
|
+
# the request side, where only stubbed requests
|
6
|
+
# are allowed.
|
7
|
+
# Connection blocking needs to be turned on:
|
8
|
+
# Typhoeus.configure do |config|
|
9
|
+
# config.block_connection = true
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# When trying to do real requests a NoStub error
|
13
|
+
# is raised.
|
14
|
+
module BlockConnection
|
15
|
+
|
16
|
+
# Overrides run in order to check before if block connection
|
17
|
+
# is turned on. If thats the case a NoStub error is
|
18
|
+
# raised.
|
19
|
+
#
|
20
|
+
# @example Run request.
|
21
|
+
# request.run
|
22
|
+
def run
|
23
|
+
if Typhoeus::Config.block_connection
|
24
|
+
raise Typhoeus::Errors::NoStub.new(self)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module contains the logic for the response callbacks.
|
5
|
+
# The on_complete callback is the only one at the moment.
|
6
|
+
#
|
7
|
+
# You can set multiple callbacks, which are then executed
|
8
|
+
# in the same order.
|
9
|
+
#
|
10
|
+
# request.on_complete { p 1 }
|
11
|
+
# request.on_complete { p 2 }
|
12
|
+
# request.execute_callbacks
|
13
|
+
# #=> 1
|
14
|
+
# #=> 2
|
15
|
+
#
|
16
|
+
# You can clear the callbacks:
|
17
|
+
#
|
18
|
+
# request.on_complete { p 1 }
|
19
|
+
# request.on_complete { p 2 }
|
20
|
+
# request.on_complete.clear
|
21
|
+
# request.execute_callbacks
|
22
|
+
# #=> []
|
23
|
+
module Callbacks
|
24
|
+
|
25
|
+
module Types
|
26
|
+
# Set on_complete callback.
|
27
|
+
#
|
28
|
+
# @example Set on_complete.
|
29
|
+
# request.on_complete { p "yay" }
|
30
|
+
#
|
31
|
+
# @param [ Block ] block The block to execute.
|
32
|
+
def on_complete(&block)
|
33
|
+
@on_complete ||= []
|
34
|
+
@on_complete << block if block_given?
|
35
|
+
@on_complete
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set on_success callback.
|
39
|
+
#
|
40
|
+
# @example Set on_success.
|
41
|
+
# request.on_success { p "yay" }
|
42
|
+
#
|
43
|
+
# @param [ Block ] block The block to execute.
|
44
|
+
def on_success(&block)
|
45
|
+
@on_success ||= []
|
46
|
+
@on_success << block if block_given?
|
47
|
+
@on_success
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set on_failure callback.
|
51
|
+
#
|
52
|
+
# @example Set on_failure.
|
53
|
+
# request.on_failure { p "yay" }
|
54
|
+
#
|
55
|
+
# @param [ Block ] block The block to execute.
|
56
|
+
def on_failure(&block)
|
57
|
+
@on_failure ||= []
|
58
|
+
@on_failure << block if block_given?
|
59
|
+
@on_failure
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Execute nessecary callback and yields response. This
|
64
|
+
# include in every case on_complete, on_success if
|
65
|
+
# successful and on_failure if not.
|
66
|
+
#
|
67
|
+
# @example Execute callbacks.
|
68
|
+
# request.execute_callbacks
|
69
|
+
def execute_callbacks
|
70
|
+
callbacks = Typhoeus.on_complete + on_complete
|
71
|
+
|
72
|
+
if response && response.success?
|
73
|
+
callbacks += Typhoeus.on_success + on_success
|
74
|
+
elsif response
|
75
|
+
callbacks += Typhoeus.on_failure + on_failure
|
76
|
+
end
|
77
|
+
|
78
|
+
callbacks.map{ |callback| callback.call(self.response) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module contains custom serializer.
|
5
|
+
module Marshal
|
6
|
+
|
7
|
+
# Return the important data needed to serialize this Request, except the
|
8
|
+
# `on_complete` handler, since they cannot be marshalled.
|
9
|
+
def marshal_dump
|
10
|
+
(instance_variables - ['@on_complete', :@on_complete]).map do |name|
|
11
|
+
[name, instance_variable_get(name)]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load.
|
16
|
+
def marshal_load(attributes)
|
17
|
+
attributes.each { |name, value| instance_variable_set(name, value) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module handles the GET request memoization
|
5
|
+
# on the request side. Memoization needs to be turned
|
6
|
+
# on:
|
7
|
+
# Typhoeus.configure do |config|
|
8
|
+
# config.memoize = true
|
9
|
+
# end
|
10
|
+
module Memoizable
|
11
|
+
|
12
|
+
# Override response setter and memoizes response
|
13
|
+
# if the request is memoizable.
|
14
|
+
#
|
15
|
+
# @param [ Response ] response The response to set.
|
16
|
+
#
|
17
|
+
# @example Set response.
|
18
|
+
# request.response = response
|
19
|
+
def response=(response)
|
20
|
+
hydra.memory[self] = response if memoizable?
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return whether a request is memoizable.
|
25
|
+
#
|
26
|
+
# @example Is request memoizable?
|
27
|
+
# request.memoizable?
|
28
|
+
#
|
29
|
+
# @return [ Boolean ] Return true if memoizable, false else.
|
30
|
+
def memoizable?
|
31
|
+
Typhoeus::Config.memoize &&
|
32
|
+
(options[:method].nil? || options[:method] == :get)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module contains everything what is necessary
|
5
|
+
# to make a single request.
|
6
|
+
module Operations
|
7
|
+
|
8
|
+
# :nodoc:
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods # :nodoc:
|
14
|
+
|
15
|
+
# Shortcut to perform a single request.
|
16
|
+
#
|
17
|
+
# @example Perform request.
|
18
|
+
# Request.run("www.example.com")
|
19
|
+
#
|
20
|
+
# @param [ String ] url The url to request.
|
21
|
+
# @param [ Hash ] options The options hash.
|
22
|
+
#
|
23
|
+
# @return [ Response ] The response.
|
24
|
+
def run(url, options = {})
|
25
|
+
new(url, options).run
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Run a request.
|
30
|
+
#
|
31
|
+
# @example Run a request.
|
32
|
+
# request.run
|
33
|
+
#
|
34
|
+
# @return [ Response ] The response.
|
35
|
+
def run
|
36
|
+
easy = Typhoeus.get_easy
|
37
|
+
easy.http_request(
|
38
|
+
url,
|
39
|
+
options.fetch(:method, :get),
|
40
|
+
options.reject{|k,_| k==:method}
|
41
|
+
)
|
42
|
+
easy.prepare
|
43
|
+
easy.perform
|
44
|
+
@response = Response.new(easy.to_hash)
|
45
|
+
@response.request = self
|
46
|
+
Typhoeus.release_easy(easy)
|
47
|
+
execute_callbacks
|
48
|
+
@response
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module contains logic for having a reponse
|
5
|
+
# getter and setter.
|
6
|
+
module Responseable
|
7
|
+
|
8
|
+
# Set the response.
|
9
|
+
#
|
10
|
+
# @example Set response.
|
11
|
+
# request.response = response
|
12
|
+
#
|
13
|
+
# @param [ Response ] value The response to set.
|
14
|
+
def response=(value)
|
15
|
+
@response = value
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return the response.
|
19
|
+
#
|
20
|
+
# @example Return response.
|
21
|
+
# request.response
|
22
|
+
#
|
23
|
+
# @return [ Response ] The response.
|
24
|
+
def response
|
25
|
+
@response ||= nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Requests
|
3
|
+
|
4
|
+
# This module handles stubbing on the request side.
|
5
|
+
# It plays well with the block_connection configuration,
|
6
|
+
# which raises when you make a request which is not stubbed.
|
7
|
+
module Stubbable
|
8
|
+
|
9
|
+
# Override run in order to check for matching expecations.
|
10
|
+
# When an expecation is found, super is not called. Instead a
|
11
|
+
# canned response is assigned to the request.
|
12
|
+
#
|
13
|
+
# @example Run the request.
|
14
|
+
# request.run
|
15
|
+
#
|
16
|
+
# @return [ Response ] The response.
|
17
|
+
def run
|
18
|
+
if expectation = Expectation.find_by(self)
|
19
|
+
@response = expectation.response
|
20
|
+
@response.mock = true
|
21
|
+
execute_callbacks
|
22
|
+
@response
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/typhoeus/response.rb
CHANGED
@@ -1,123 +1,29 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
:requested_url, :requested_remote_method,
|
6
|
-
:requested_http_method, :start_time,
|
7
|
-
:effective_url, :start_transfer_time,
|
8
|
-
:app_connect_time, :pretransfer_time,
|
9
|
-
:connect_time, :name_lookup_time,
|
10
|
-
:curl_return_code, :curl_error_message,
|
11
|
-
:primary_ip, :redirect_count
|
12
|
-
|
13
|
-
attr_writer :headers_hash
|
14
|
-
|
15
|
-
def initialize(params = {})
|
16
|
-
@code = params[:code]
|
17
|
-
@curl_return_code = params[:curl_return_code]
|
18
|
-
@curl_error_message = params[:curl_error_message]
|
19
|
-
@status_message = params[:status_message]
|
20
|
-
@http_version = params[:http_version]
|
21
|
-
@headers = params[:headers]
|
22
|
-
@body = params[:body]
|
23
|
-
@time = params[:time]
|
24
|
-
@requested_url = params[:requested_url]
|
25
|
-
@requested_http_method = params[:requested_http_method]
|
26
|
-
@start_time = params[:start_time]
|
27
|
-
@start_transfer_time = params[:start_transfer_time]
|
28
|
-
@app_connect_time = params[:app_connect_time]
|
29
|
-
@pretransfer_time = params[:pretransfer_time]
|
30
|
-
@connect_time = params[:connect_time]
|
31
|
-
@name_lookup_time = params[:name_lookup_time]
|
32
|
-
@request = params[:request]
|
33
|
-
@effective_url = params[:effective_url]
|
34
|
-
@primary_ip = params[:primary_ip]
|
35
|
-
@redirect_count = params[:redirect_count]
|
36
|
-
@mock = params[:mock] || false # default
|
37
|
-
@headers_hash = Header.new(params[:headers_hash]) if params[:headers_hash]
|
38
|
-
end
|
39
|
-
|
40
|
-
# Returns true if this is a mock response.
|
41
|
-
def mock?
|
42
|
-
@mock
|
43
|
-
end
|
44
|
-
|
45
|
-
def headers
|
46
|
-
@headers ||= @headers_hash ? construct_header_string : ''
|
47
|
-
end
|
48
|
-
|
49
|
-
def headers_hash
|
50
|
-
@headers_hash ||= begin
|
51
|
-
headers.split("\n").map {|o| o.strip}.inject(Typhoeus::Header.new) do |hash, o|
|
52
|
-
if o.empty? || o =~ /^HTTP\/[\d\.]+/
|
53
|
-
hash
|
54
|
-
else
|
55
|
-
i = o.index(":") || o.size
|
56
|
-
key = o.slice(0, i)
|
57
|
-
value = o.slice(i + 1, o.size)
|
58
|
-
value = value.strip unless value.nil?
|
59
|
-
if hash.key? key
|
60
|
-
hash[key] = [hash[key], value].flatten
|
61
|
-
else
|
62
|
-
hash[key] = value
|
63
|
-
end
|
64
|
-
|
65
|
-
hash
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def status_message
|
72
|
-
return @status_message if @status_message != nil
|
73
|
-
|
74
|
-
# HTTP servers can choose not to include the explanation to HTTP codes. The RFC
|
75
|
-
# states this (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4):
|
76
|
-
# Except when responding to a HEAD request, the server SHOULD include an entity containing
|
77
|
-
# an explanation of the error situation [...]
|
78
|
-
# This means 'HTTP/1.1 404' is as valid as 'HTTP/1.1 404 Not Found' and we have to handle it.
|
1
|
+
require 'typhoeus/responses/header'
|
2
|
+
require 'typhoeus/responses/informations'
|
3
|
+
require 'typhoeus/responses/legacy'
|
4
|
+
require 'typhoeus/responses/status'
|
79
5
|
|
80
|
-
|
81
|
-
if first_header_line != nil and first_header_line[/\d{3} (.*)$/, 1] != nil
|
82
|
-
@status_message = first_header_line[/\d{3} (.*)$/, 1].chomp
|
83
|
-
else
|
84
|
-
@status_message = nil
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def http_version
|
89
|
-
@http_version ||= first_header_line ? first_header_line[/HTTP\/(\S+)/, 1] : nil
|
90
|
-
end
|
91
|
-
|
92
|
-
def success?
|
93
|
-
@code >= 200 && @code < 300
|
94
|
-
end
|
95
|
-
|
96
|
-
def modified?
|
97
|
-
@code != 304
|
98
|
-
end
|
6
|
+
module Typhoeus
|
99
7
|
|
100
|
-
|
101
|
-
|
8
|
+
# This class respresents the response.
|
9
|
+
class Response
|
10
|
+
include Responses::Informations
|
11
|
+
include Responses::Legacy
|
12
|
+
include Responses::Status
|
13
|
+
|
14
|
+
attr_accessor :request, :options, :mock
|
15
|
+
|
16
|
+
# Create a new response.
|
17
|
+
#
|
18
|
+
# @example Create a response.
|
19
|
+
# Response.new
|
20
|
+
#
|
21
|
+
# @param [ Hash ] options The options hash.
|
22
|
+
#
|
23
|
+
# @return [ Response ] The new response.
|
24
|
+
def initialize(options = {})
|
25
|
+
@options = options
|
26
|
+
@header = options[:header]
|
102
27
|
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def first_header_line
|
107
|
-
@first_header_line ||= @headers.to_s.split("\n").first
|
108
|
-
end
|
109
|
-
|
110
|
-
def construct_header_string
|
111
|
-
lines = ["HTTP/#{http_version} #{code} #{status_message}"]
|
112
|
-
|
113
|
-
@headers_hash.each do |key, values|
|
114
|
-
[values].flatten.each do |value|
|
115
|
-
lines << "#{key}: #{value}"
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
lines << '' << ''
|
120
|
-
lines.join("\r\n")
|
121
|
-
end
|
122
28
|
end
|
123
29
|
end
|