weary 0.7.2 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +11 -8
- data/Gemfile.lock +49 -53
- data/LICENSE +1 -1
- data/README.md +134 -208
- data/Rakefile +6 -47
- data/lib/weary.rb +4 -66
- data/lib/weary/adapter.rb +23 -0
- data/lib/weary/adapters/net_http.rb +68 -0
- data/lib/weary/client.rb +243 -0
- data/lib/weary/deferred.rb +35 -0
- data/lib/weary/env.rb +32 -0
- data/lib/weary/middleware.rb +9 -0
- data/lib/weary/middleware/basic_auth.rb +17 -0
- data/lib/weary/middleware/content_type.rb +28 -0
- data/lib/weary/middleware/oauth.rb +31 -0
- data/lib/weary/request.rb +137 -124
- data/lib/weary/resource.rb +152 -128
- data/lib/weary/response.rb +48 -99
- data/lib/weary/route.rb +53 -0
- data/lib/weary/version.rb +3 -0
- data/spec/spec_helper.rb +4 -56
- data/spec/support/shared_examples_for_a_rack_app.rb +70 -0
- data/spec/support/shared_examples_for_a_rack_env.rb +14 -0
- data/spec/support/shared_examples_for_a_uri.rb +9 -0
- data/spec/weary/adapter_spec.rb +26 -0
- data/spec/weary/adapters/nethttp_spec.rb +88 -0
- data/spec/weary/client_spec.rb +258 -0
- data/spec/weary/deferred_spec.rb +35 -0
- data/spec/weary/env_spec.rb +12 -0
- data/spec/weary/middleware/basic_auth_spec.rb +23 -0
- data/spec/weary/middleware/content_type_spec.rb +34 -0
- data/spec/weary/middleware/oauth_spec.rb +27 -0
- data/spec/weary/request_spec.rb +227 -315
- data/spec/weary/resource_spec.rb +233 -233
- data/spec/weary/response_spec.rb +82 -159
- data/spec/weary/route_spec.rb +72 -0
- data/spec/weary_spec.rb +3 -56
- data/weary.gemspec +16 -79
- metadata +138 -98
- data/VERSION +0 -1
- data/examples/batch.rb +0 -20
- data/examples/repo.rb +0 -16
- data/examples/status.rb +0 -26
- data/lib/weary/base.rb +0 -124
- data/lib/weary/batch.rb +0 -37
- data/lib/weary/exceptions.rb +0 -15
- data/lib/weary/httpverb.rb +0 -32
- data/spec/fixtures/github.yml +0 -11
- data/spec/fixtures/twitter.xml +0 -763
- data/spec/fixtures/vimeo.json +0 -1
- data/spec/weary/base_spec.rb +0 -320
- data/spec/weary/batch_spec.rb +0 -71
- data/spec/weary/httpverb_spec.rb +0 -25
data/Rakefile
CHANGED
@@ -1,51 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
rescue LoadError
|
5
|
-
# Fall back on doing an unlocked resolve at runtime.
|
6
|
-
require "rubygems"
|
7
|
-
require "bundler"
|
8
|
-
Bundler.setup
|
9
|
-
end
|
10
|
-
|
11
|
-
require 'spec/rake/spectask'
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'yard'
|
12
4
|
|
13
5
|
task :default => :spec
|
14
6
|
|
15
|
-
|
16
|
-
Rake::RDocTask.new do |rdoc|
|
17
|
-
rdoc.rdoc_dir = 'doc'
|
18
|
-
rdoc.title = 'weary'
|
19
|
-
rdoc.main = 'README.md'
|
20
|
-
rdoc.rdoc_files.include('README.*', 'lib/**/*.rb', 'LICENSE')
|
21
|
-
rdoc.options << '--inline-source'
|
22
|
-
end
|
23
|
-
|
24
|
-
begin
|
25
|
-
require 'jeweler'
|
26
|
-
Jeweler::Tasks.new do |gemspec|
|
27
|
-
gemspec.name = "weary"
|
28
|
-
gemspec.rubyforge_project = "weary"
|
29
|
-
gemspec.summary = "A little DSL for consuming RESTful web services"
|
30
|
-
gemspec.email = "mark@markwunsch.com"
|
31
|
-
gemspec.homepage = "http://github.com/mwunsch/weary"
|
32
|
-
gemspec.description = "A tiny DSL that makes the consumption of RESTful web services simple."
|
33
|
-
gemspec.authors = "Mark Wunsch"
|
34
|
-
gemspec.add_dependency 'crack', '>= 0.1.7'
|
35
|
-
gemspec.add_dependency 'oauth', '>= 0.3.5'
|
36
|
-
gemspec.add_development_dependency 'bundler', ">= 0.9.7"
|
37
|
-
end
|
38
|
-
Jeweler::GemcutterTasks.new
|
39
|
-
rescue LoadError
|
40
|
-
puts "Jeweler not available. Install it with: gem install jeweler"
|
41
|
-
end
|
42
|
-
|
43
|
-
desc "Open an irb session preloaded with this library"
|
44
|
-
task :console do
|
45
|
-
sh "irb -rubygems -I lib -r weary.rb"
|
46
|
-
end
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
47
8
|
|
48
|
-
|
49
|
-
|
50
|
-
t.spec_opts = ['--color','--format nested']
|
51
|
-
end
|
9
|
+
# The state of yard-tomdoc isn't so great right now
|
10
|
+
YARD::Rake::YardocTask.new
|
data/lib/weary.rb
CHANGED
@@ -1,34 +1,8 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'net/https'
|
4
|
-
|
5
|
-
require 'crack'
|
6
|
-
require 'oauth'
|
7
|
-
|
8
|
-
autoload :Yaml, 'yaml'
|
9
|
-
|
10
|
-
require 'weary/request'
|
11
|
-
require 'weary/response'
|
12
|
-
require 'weary/resource'
|
13
|
-
require 'weary/batch'
|
14
|
-
require 'weary/exceptions'
|
15
|
-
require 'weary/httpverb'
|
16
|
-
require 'weary/base'
|
1
|
+
require 'weary/version'
|
2
|
+
require 'weary/client'
|
17
3
|
|
18
4
|
module Weary
|
19
|
-
|
20
|
-
# Supported HTTP Verbs
|
21
|
-
Methods = [:get, :post, :put, :delete, :head]
|
22
|
-
|
23
|
-
# Supported Content Types
|
24
|
-
ContentTypes = { :json => [:json, 'json', 'application/json', 'text/json', 'application/javascript', 'text/javascript'],
|
25
|
-
:xml => [:xml, 'xml', 'text/xml', 'application/xml'],
|
26
|
-
:html => [:html, 'html', 'text/html'],
|
27
|
-
:yaml => [:yaml, 'yaml', 'application/x-yaml', 'text/yaml'],
|
28
|
-
:plain => [:plain, 'plain', 'text/plain'] }
|
29
|
-
|
30
|
-
# A collection of User Agent strings that I stole from HURL (http://hurl.it)
|
31
|
-
UserAgents = {
|
5
|
+
USER_AGENTS = {
|
32
6
|
"Firefox 1.5.0.12 - Mac" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12",
|
33
7
|
"Firefox 1.5.0.12 - Windows" => "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12",
|
34
8
|
"Firefox 2.0.0.12 - Mac" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12",
|
@@ -58,40 +32,4 @@ module Weary
|
|
58
32
|
"Safari 4.0.2 - Mac" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19",
|
59
33
|
"Safari 4.0.2 - Windows" => "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1"
|
60
34
|
}
|
61
|
-
|
62
|
-
class << self
|
63
|
-
def get(url,&block)
|
64
|
-
request url, :get, &block
|
65
|
-
end
|
66
|
-
|
67
|
-
def post(url,&block)
|
68
|
-
request url, :post, &block
|
69
|
-
end
|
70
|
-
|
71
|
-
def put(url,&block)
|
72
|
-
request url, :put, &block
|
73
|
-
end
|
74
|
-
|
75
|
-
def delete(url,&block)
|
76
|
-
request url, :delete, &block
|
77
|
-
end
|
78
|
-
|
79
|
-
def head(url,&block)
|
80
|
-
request url, :head, &block
|
81
|
-
end
|
82
|
-
|
83
|
-
# Create a Request for the URL.
|
84
|
-
# Defaults to a GET Request. Use a block to further modify the Request
|
85
|
-
def request(url,via = :get, &block)
|
86
|
-
req = Request.new(url,via)
|
87
|
-
yield req if block_given?
|
88
|
-
req
|
89
|
-
end
|
90
|
-
|
91
|
-
# Create a Batch for a group of Requests
|
92
|
-
def batch(*requests)
|
93
|
-
Batch.new(requests)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'weary/response'
|
2
|
+
require 'weary/adapters/net_http'
|
3
|
+
module Weary
|
4
|
+
# An abstract interface. A subclass should be something that actually opens
|
5
|
+
# a socket to make the request, e.g. Net/HTTP, Curb, etc.
|
6
|
+
module Adapter
|
7
|
+
|
8
|
+
def initialize(app=nil)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
connect(Rack::Request.new(env)).finish
|
14
|
+
end
|
15
|
+
|
16
|
+
# request is a Rack::Request
|
17
|
+
# This computation is performed in a Promise/Future
|
18
|
+
# Returns a Rack::Response
|
19
|
+
def connect(request)
|
20
|
+
Rack::Response.new [""], 501, {"Content-Type" => "text/plain"}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
module Weary
|
5
|
+
module Adapter
|
6
|
+
class NetHttp
|
7
|
+
include Weary::Adapter
|
8
|
+
|
9
|
+
def self.call(env)
|
10
|
+
connect(Rack::Request.new(env)).finish
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.connect(request)
|
14
|
+
connection = socket(request)
|
15
|
+
response = connection.request prepare(request)
|
16
|
+
Rack::Response.new response.body, response.code, normalize_response(response.to_hash)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.prepare(request)
|
20
|
+
req_class = request_class(request.request_method)
|
21
|
+
req = req_class.new(request.fullpath, normalize_request_headers(request.env))
|
22
|
+
if req.request_body_permitted? # What's the best way of passing the body?
|
23
|
+
req.body = request.body.read
|
24
|
+
request.body.rewind
|
25
|
+
end
|
26
|
+
req.content_type = request.content_type if request.content_type
|
27
|
+
req.content_length = request.content_length if request.content_length
|
28
|
+
req
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.normalize_request_headers(env)
|
32
|
+
req_headers = env.reject {|k,v| !k.start_with? "HTTP_" }
|
33
|
+
normalized = req_headers.map do |k, v|
|
34
|
+
new_key = k.sub("HTTP_",'').split('_').map(&:capitalize).join('-')
|
35
|
+
[new_key, v] unless UNWANTED_REQUEST_HEADERS.include? new_key
|
36
|
+
end
|
37
|
+
Hash[normalized]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.normalize_response(headers)
|
41
|
+
headers.reject {|k,v| k.downcase == 'status' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.socket(request)
|
45
|
+
host = request.env['HTTP_HOST'] || request.env['SERVER_NAME']
|
46
|
+
port = request.env['SERVER_PORT'].to_s
|
47
|
+
connection = Net::HTTP.new host, port
|
48
|
+
connection.use_ssl = request.scheme == 'https'
|
49
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if connection.use_ssl?
|
50
|
+
connection
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.request_class(method)
|
54
|
+
capitalized = method.capitalize
|
55
|
+
Net::HTTP.const_get capitalized
|
56
|
+
end
|
57
|
+
|
58
|
+
def call(env)
|
59
|
+
self.class.call(env)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
UNWANTED_REQUEST_HEADERS = []
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/weary/client.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'weary/resource'
|
2
|
+
|
3
|
+
module Weary
|
4
|
+
autoload :Route, 'weary/route'
|
5
|
+
|
6
|
+
# An abstract class used to construct client libraries and the primary
|
7
|
+
# entrance point to use the Weary framework. Client defines a DSL to describe
|
8
|
+
# and construct a set of Resources, which in turn can generate Requests that
|
9
|
+
# can perform HTTP actions.
|
10
|
+
#
|
11
|
+
# Resources are defined and stored by one of the class methods corresponding
|
12
|
+
# to an HTTP request method. Every Resource is declared with a name that
|
13
|
+
# acts as both a key to access the resource, and, when the class is
|
14
|
+
# instantiated, the name of a dynamically generated instance method.
|
15
|
+
#
|
16
|
+
# Examples
|
17
|
+
#
|
18
|
+
# class GiltSales < Weary::Client
|
19
|
+
# domain "https://api.gilt.com/v1"
|
20
|
+
#
|
21
|
+
# required :apikey
|
22
|
+
#
|
23
|
+
# optional :affid
|
24
|
+
#
|
25
|
+
# get :active, "/sales/active.json"
|
26
|
+
#
|
27
|
+
# get :active_in_store, "/sales/:store/active.json"
|
28
|
+
#
|
29
|
+
# get :upcoming, "/sales/upcoming.json"
|
30
|
+
#
|
31
|
+
# get :upcoming_in_store, "/sales/:store/upcoming.json"
|
32
|
+
#
|
33
|
+
# get :detail, "/sales/:store/:sale_key/detail.json"
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# sales = GiltSales.new
|
37
|
+
# sales.active_in_store :store => :women, :apikey => "my-key"
|
38
|
+
# # => A Weary::Request object
|
39
|
+
#
|
40
|
+
class Client
|
41
|
+
|
42
|
+
# Internal: HTTP Request verbs supported by Weary. These translate to class
|
43
|
+
# methods of Client.
|
44
|
+
REQUEST_METHODS = [
|
45
|
+
:copy, :delete, :get, :head, :lock, :mkcol, :move, :options,
|
46
|
+
:patch, :post, :propfind, :proppatch, :put, :trace, :unlock
|
47
|
+
]
|
48
|
+
|
49
|
+
class << self
|
50
|
+
|
51
|
+
REQUEST_METHODS.each do |request_method|
|
52
|
+
# Generate a resource of the specified REQUEST_METHOD. This
|
53
|
+
# method is defined for each of the REQUEST_METHODS.
|
54
|
+
#
|
55
|
+
# name - A Symbol name or descriptor of a resource to dynamically
|
56
|
+
# generate an instance method of the same name.
|
57
|
+
# path - A String path to the resource. If the class's domain is set it
|
58
|
+
# will be prepended to this path to form the resource's uri.
|
59
|
+
# block - An optional block to be used to further customize the resource.
|
60
|
+
#
|
61
|
+
# Returns a Response object describing an HTTP endpoint.
|
62
|
+
#
|
63
|
+
# Signature
|
64
|
+
#
|
65
|
+
# <method>(name, path, &block)
|
66
|
+
#
|
67
|
+
# method - An HTTP request method (and member of REQUEST_METHODS).
|
68
|
+
define_method request_method do |name, path, &block|
|
69
|
+
resource(name, request_method.to_s.upcase, path, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# An accessor to set the domain where the client's resources are
|
74
|
+
# located. This is prepended to the resources' path to form the resource
|
75
|
+
# uri.
|
76
|
+
#
|
77
|
+
# host - An optional String to set the client domain.
|
78
|
+
#
|
79
|
+
# Returns the domain String.
|
80
|
+
def domain(host=nil)
|
81
|
+
@domain = host unless host.nil?
|
82
|
+
@domain ||= ""
|
83
|
+
end
|
84
|
+
|
85
|
+
# An accessor to set optional parameters permitted by all
|
86
|
+
# resources described by the client.
|
87
|
+
#
|
88
|
+
# params - Zero or more Symbol parameters expected by the resources.
|
89
|
+
#
|
90
|
+
# Returns an Array of parameters.
|
91
|
+
def optional(*params)
|
92
|
+
@optional = params unless params.empty?
|
93
|
+
@optional ||= []
|
94
|
+
end
|
95
|
+
|
96
|
+
# An accessor to set parameters required by the all of the
|
97
|
+
# resources of the client.
|
98
|
+
#
|
99
|
+
# params - Zero or more Symbol parameters required by the resources.
|
100
|
+
#
|
101
|
+
# Returns an Array of parameters.
|
102
|
+
def required(*params)
|
103
|
+
@required = params unless params.empty?
|
104
|
+
@required ||= []
|
105
|
+
end
|
106
|
+
|
107
|
+
# An accessor to set default parameters to be used by the all of
|
108
|
+
# the resources described by the client.
|
109
|
+
#
|
110
|
+
# hash - An optional Hash of key/value pairs describing the
|
111
|
+
# default parameters to be sent to the resources
|
112
|
+
#
|
113
|
+
# Returns a Hash of the default parameters sent to all resources.
|
114
|
+
def defaults(hash=nil)
|
115
|
+
@defaults = hash unless hash.nil?
|
116
|
+
@defaults ||= {}
|
117
|
+
end
|
118
|
+
|
119
|
+
# An accessor to set HTTP request headers for all of the client's
|
120
|
+
# resources.
|
121
|
+
#
|
122
|
+
# hash - An optional Hash of key/value pairs that are sent as HTTP
|
123
|
+
# request headers when a resource's request is performed.
|
124
|
+
#
|
125
|
+
# Returns a Hash of the headers.
|
126
|
+
def headers(hash=nil)
|
127
|
+
@headers = hash unless hash.nil?
|
128
|
+
@headers ||= {}
|
129
|
+
end
|
130
|
+
|
131
|
+
# Send a Rack middleware to all of the requests generated by
|
132
|
+
# the client.
|
133
|
+
#
|
134
|
+
# middleware - An object that implements the rack middleware interface.
|
135
|
+
# args - Zero or more optional arguments to send to the middleware.
|
136
|
+
# block - An optional block to send to the middleware.
|
137
|
+
#
|
138
|
+
# Returns an Array of middlewares.
|
139
|
+
def use(middleware, *args, &block)
|
140
|
+
@middlewares ||= []
|
141
|
+
@middlewares << [middleware, args, block]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Internal: Create and build a resource description of a request. The
|
145
|
+
# resource is then stored in an internal hash, generating an instance
|
146
|
+
# method.
|
147
|
+
#
|
148
|
+
# name - A Symbol name/descriptor of the resource.
|
149
|
+
# method - A Symbol or String of the request method used in the request.
|
150
|
+
# path - A String path to the resource that is appended to the domain
|
151
|
+
# to form the uri.
|
152
|
+
#
|
153
|
+
# Yields the Resource for further construction.
|
154
|
+
#
|
155
|
+
# Returns the generated Resource.
|
156
|
+
def resource(name, method, path="")
|
157
|
+
resource = Weary::Resource.new method, "#{domain}#{path}"
|
158
|
+
resource.optional *optional
|
159
|
+
resource.required *required
|
160
|
+
resource.defaults defaults
|
161
|
+
resource.headers headers
|
162
|
+
if !@middlewares.nil? && !@middlewares.empty?
|
163
|
+
@middlewares.each {|middleware| resource.use *middleware }
|
164
|
+
end
|
165
|
+
yield resource if block_given?
|
166
|
+
self[name] = resource
|
167
|
+
end
|
168
|
+
|
169
|
+
# A getter for the stored table of Resources.
|
170
|
+
#
|
171
|
+
# Returns a Hash of Resources stored by their name keys.
|
172
|
+
def resources
|
173
|
+
@resources ||= {}
|
174
|
+
end
|
175
|
+
|
176
|
+
# Store a Resource at the given key. A method is built for the
|
177
|
+
# instances with the same name as the key that calls the request method
|
178
|
+
# of the Resource.
|
179
|
+
#
|
180
|
+
# name - A Symbol name of the Resource and the eventual name of the
|
181
|
+
# method that will build the request.
|
182
|
+
# resource - The Resource object to store. When the named method is
|
183
|
+
# called on the client instance, this resource's request
|
184
|
+
# method will be called.
|
185
|
+
#
|
186
|
+
# Returns the stored Resource.
|
187
|
+
def []=(name,resource)
|
188
|
+
store name, resource
|
189
|
+
end
|
190
|
+
|
191
|
+
# A quick getter to retrieve a Resource from the client's
|
192
|
+
# internal store.
|
193
|
+
#
|
194
|
+
# name - The Symbol name of the Resource.
|
195
|
+
#
|
196
|
+
# Returns the Resource stored at name.
|
197
|
+
def [](name)
|
198
|
+
resources[name]
|
199
|
+
end
|
200
|
+
|
201
|
+
# Internal: Build a Rack router for the client's resources.
|
202
|
+
#
|
203
|
+
# Returns a Route object of the resources at the domain.
|
204
|
+
def route
|
205
|
+
Weary::Route.new resources.values, domain
|
206
|
+
end
|
207
|
+
|
208
|
+
# A Rack middleware interface that uses the internal router to
|
209
|
+
# determine the best Resource available.
|
210
|
+
#
|
211
|
+
# Returns an Array resembling a Rack response tuple.
|
212
|
+
def call(env)
|
213
|
+
route.call(env)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# Private: Store the resource in a hash keyed by the name (as a Symbol).
|
219
|
+
#
|
220
|
+
# Returns the saved Resource.
|
221
|
+
# Raises ArgumentError if you try to store anything but a Resource.
|
222
|
+
def store(name, resource)
|
223
|
+
raise ArgumentError, "Expected a Weary::Resource but got #{resource.inspect}" \
|
224
|
+
unless resource.is_a? Weary::Resource
|
225
|
+
key = name.to_sym
|
226
|
+
build_method(key, resource)
|
227
|
+
resources[key] = resource
|
228
|
+
end
|
229
|
+
|
230
|
+
# Private: Do the metaprogramming necessary for the resource names to
|
231
|
+
# become instance methods.
|
232
|
+
def build_method(key, resource)
|
233
|
+
define_method(key) do |*parameters, &block|
|
234
|
+
parameters = parameters.first || {}
|
235
|
+
@defaults ||= {}
|
236
|
+
request = resource.request(@defaults.merge(parameters), &block)
|
237
|
+
request
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
end
|