zero 0.1.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 +2 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +59 -0
- data/Guardfile +15 -0
- data/README.md +60 -0
- data/Thorfile +6 -0
- data/lib/zero.rb +7 -0
- data/lib/zero/controller.rb +46 -0
- data/lib/zero/rack_request.rb +44 -0
- data/lib/zero/renderer.rb +139 -0
- data/lib/zero/request.rb +100 -0
- data/lib/zero/request/accept.rb +28 -0
- data/lib/zero/request/accept_type.rb +58 -0
- data/lib/zero/request/client.rb +31 -0
- data/lib/zero/request/parameter.rb +101 -0
- data/lib/zero/request/server.rb +41 -0
- data/lib/zero/response.rb +80 -0
- data/lib/zero/router.rb +63 -0
- data/lib/zero/version.rb +3 -0
- data/spec/fixtures/templates/index.html.erb +1 -0
- data/spec/fixtures/templates/index.json.erb +1 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/unit/controller/call_spec.rb +22 -0
- data/spec/unit/controller/renderer_spec.rb +11 -0
- data/spec/unit/renderer/read_template_path_spec.rb +53 -0
- data/spec/unit/renderer/render_spec.rb +50 -0
- data/spec/unit/renderer/template_path.rb +8 -0
- data/spec/unit/renderer/type_map_spec.rb +9 -0
- data/spec/unit/request/accept/encoding_spec.rb +9 -0
- data/spec/unit/request/accept/language_spec.rb +9 -0
- data/spec/unit/request/accept/types_spec.rb +9 -0
- data/spec/unit/request/accept_spec.rb +7 -0
- data/spec/unit/request/accepttype/each_spec.rb +10 -0
- data/spec/unit/request/accepttype/preferred_spec.rb +35 -0
- data/spec/unit/request/client/address_spec.rb +9 -0
- data/spec/unit/request/client/hostname_spec.rb +9 -0
- data/spec/unit/request/client/user_agent_spec.rb +9 -0
- data/spec/unit/request/client_spec.rb +8 -0
- data/spec/unit/request/content_type_spec.rb +16 -0
- data/spec/unit/request/create_spec.rb +21 -0
- data/spec/unit/request/delete_spec.rb +15 -0
- data/spec/unit/request/get_spec.rb +15 -0
- data/spec/unit/request/head_spec.rb +15 -0
- data/spec/unit/request/method_spec.rb +8 -0
- data/spec/unit/request/parameter/[]_spec.rb +56 -0
- data/spec/unit/request/parameter/custom_spec.rb +18 -0
- data/spec/unit/request/parameter/initialize_spec.rb +12 -0
- data/spec/unit/request/parameter/payload_spec.rb +33 -0
- data/spec/unit/request/parameter/query_spec.rb +25 -0
- data/spec/unit/request/params_spec.rb +8 -0
- data/spec/unit/request/patch_spec.rb +15 -0
- data/spec/unit/request/path_spec.rb +9 -0
- data/spec/unit/request/post_spec.rb +15 -0
- data/spec/unit/request/put_spec.rb +15 -0
- data/spec/unit/request/server/hostname_spec.rb +9 -0
- data/spec/unit/request/server/port_spec.rb +7 -0
- data/spec/unit/request/server/protocol_spec.rb +9 -0
- data/spec/unit/request/server/software_spec.rb +9 -0
- data/spec/unit/request/server_spec.rb +8 -0
- data/spec/unit/response/response_spec.rb +146 -0
- data/spec/unit/router/call_spec.rb +55 -0
- data/zero.gemspec +27 -0
- metadata +345 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
zero (0.0.1)
|
5
|
+
tilt
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
coderay (1.0.8)
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
guard (1.5.4)
|
13
|
+
listen (>= 0.4.2)
|
14
|
+
lumberjack (>= 1.0.2)
|
15
|
+
pry (>= 0.9.10)
|
16
|
+
thor (>= 0.14.6)
|
17
|
+
guard-bundler (1.0.0)
|
18
|
+
bundler (~> 1.0)
|
19
|
+
guard (~> 1.1)
|
20
|
+
guard-rspec (2.3.0)
|
21
|
+
guard (>= 1.1)
|
22
|
+
rspec (~> 2.11)
|
23
|
+
listen (0.6.0)
|
24
|
+
lumberjack (1.0.2)
|
25
|
+
method_source (0.8.1)
|
26
|
+
multi_json (1.3.7)
|
27
|
+
pry (0.9.10)
|
28
|
+
coderay (~> 1.0.5)
|
29
|
+
method_source (~> 0.8)
|
30
|
+
slop (~> 3.3.1)
|
31
|
+
rack (1.4.1)
|
32
|
+
rspec (2.12.0)
|
33
|
+
rspec-core (~> 2.12.0)
|
34
|
+
rspec-expectations (~> 2.12.0)
|
35
|
+
rspec-mocks (~> 2.12.0)
|
36
|
+
rspec-core (2.12.0)
|
37
|
+
rspec-expectations (2.12.0)
|
38
|
+
diff-lcs (~> 1.1.3)
|
39
|
+
rspec-mocks (2.12.0)
|
40
|
+
simplecov (0.7.1)
|
41
|
+
multi_json (~> 1.0)
|
42
|
+
simplecov-html (~> 0.7.1)
|
43
|
+
simplecov-html (0.7.1)
|
44
|
+
slop (3.3.3)
|
45
|
+
thor (0.16.0)
|
46
|
+
tilt (1.3.3)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
ruby
|
50
|
+
|
51
|
+
DEPENDENCIES
|
52
|
+
guard
|
53
|
+
guard-bundler
|
54
|
+
guard-rspec
|
55
|
+
rack
|
56
|
+
rspec
|
57
|
+
simplecov
|
58
|
+
thor
|
59
|
+
zero!
|
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
# Uncomment next line if Gemfile contain `gemspec' command
|
7
|
+
watch(/^.+\.gemspec/)
|
8
|
+
end
|
9
|
+
|
10
|
+
guard 'rspec' do
|
11
|
+
watch(%r{^spec/.+_spec\.rb$})
|
12
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
13
|
+
watch('spec/spec_helper.rb') { "spec" }
|
14
|
+
end
|
15
|
+
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
zero - a toolkit for web services
|
2
|
+
=================================
|
3
|
+
|
4
|
+
The focus of this project is to provide small helpers for building web services
|
5
|
+
without the need of learning a complete new web stack from the bottom to the
|
6
|
+
top.
|
7
|
+
|
8
|
+
As this project is still at the beginning, some things may change or may not be
|
9
|
+
as good as other parts. But we are working on these parts to make them easier
|
10
|
+
to use and stable enough.
|
11
|
+
|
12
|
+
The following is a small list of things, the project already provides and at the
|
13
|
+
end you can find a small sample on what is already possible.
|
14
|
+
|
15
|
+
parts of the toolkit
|
16
|
+
--------------------
|
17
|
+
|
18
|
+
### Request
|
19
|
+
|
20
|
+
A new request implementation is provided to make it easier for accessing all the
|
21
|
+
various elements of a request. It provides an interface for all access headers
|
22
|
+
through the method `#access`. It also provides a method `#params`, which returns
|
23
|
+
an object with parameters seperated between *query* and *payload* parameters.
|
24
|
+
|
25
|
+
### Response
|
26
|
+
|
27
|
+
A new response is also provided. which is just some lines of code at the moment,
|
28
|
+
but will grow in functionality. We aim at a response which will take care of all
|
29
|
+
the http specific stuff itself, to make it easier for you.
|
30
|
+
|
31
|
+
### Router
|
32
|
+
|
33
|
+
A small router with variables is also provided. It can take static urls, but
|
34
|
+
also dynamic routes and push them to the appropiate controller. It is very
|
35
|
+
lightweight and can therefore be used in other projects, where a Rack::Router
|
36
|
+
is needed but can't do as much and other implementations are just to big.
|
37
|
+
|
38
|
+
### Renderer
|
39
|
+
|
40
|
+
This part should do the work for you to decide, which template to use. You can
|
41
|
+
define a template directory and a mapping of simple shortcuts of types to all
|
42
|
+
types the template should be rendered with. With these two information the
|
43
|
+
renderer will take care of selecting the correct template, so that you can
|
44
|
+
concentrate on the hard parts.
|
45
|
+
|
46
|
+
### Controller
|
47
|
+
|
48
|
+
This is in a very rough state at the moment, but already shows the potential in
|
49
|
+
which direction this hole project should go. It defines a very simple interface
|
50
|
+
for your own controllers, in that it only wants `#render` to be implemented, but
|
51
|
+
also calls `#process` to seperate the processing and rendering. It uses the
|
52
|
+
Zero tools at the moment, but later should be able to use other libs too.
|
53
|
+
|
54
|
+
example
|
55
|
+
-------
|
56
|
+
|
57
|
+
To give you an impression on what these parts can already do for you, you can
|
58
|
+
take a look at the sample application
|
59
|
+
[here](https://github.com/Gibheer/zero-examples) and try it out. It does not
|
60
|
+
take much.
|
data/Thorfile
ADDED
data/lib/zero.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Zero
|
2
|
+
# abstract class to make creation of controllers easier
|
3
|
+
#
|
4
|
+
# This abstract class creates an interface to make it easier to write
|
5
|
+
# rack compatible controllers. It catches #call and creates a new instance
|
6
|
+
# with the environment and calls #render on it.
|
7
|
+
class Controller
|
8
|
+
# initialize a new instance of the controller and call response on it
|
9
|
+
def self.call(env)
|
10
|
+
new(Zero::Request.create(env)).response
|
11
|
+
end
|
12
|
+
|
13
|
+
# set the renderer to use in the controller
|
14
|
+
def self.renderer=(renderer)
|
15
|
+
@@renderer = renderer
|
16
|
+
end
|
17
|
+
|
18
|
+
# get the renderer set in the controller
|
19
|
+
def self.renderer
|
20
|
+
@@renderer
|
21
|
+
end
|
22
|
+
|
23
|
+
# a small helper to get the actual renderer
|
24
|
+
def renderer
|
25
|
+
self.class.renderer
|
26
|
+
end
|
27
|
+
|
28
|
+
# initialize the controller
|
29
|
+
# @param request [Request] a request object
|
30
|
+
def initialize(request)
|
31
|
+
@request = request
|
32
|
+
@response = Zero::Response.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# build the response and return it
|
36
|
+
#
|
37
|
+
# This method calls #process if it was defined so make it easier to process
|
38
|
+
# the request before rendering stuff.
|
39
|
+
# @return Response a rack conform response
|
40
|
+
def response
|
41
|
+
process if respond_to?(:process)
|
42
|
+
render
|
43
|
+
@response.to_a
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# as we need #update_param we patch it it into the request
|
2
|
+
# this is directly from thr request.rb of the rack repository, so this
|
3
|
+
# can be deleted, when rack was released
|
4
|
+
r = Rack::Request.new(Rack::MockRequest.env_for('foo'))
|
5
|
+
if not r.respond_to?('update_param') then
|
6
|
+
class Rack::Request
|
7
|
+
# Destructively update a parameter, whether it's in GET and/or POST.
|
8
|
+
# Returns nil.
|
9
|
+
#
|
10
|
+
# The parameter is updated wherever it was previous defined, so
|
11
|
+
# GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
|
12
|
+
#
|
13
|
+
# env['rack.input'] is not touched.
|
14
|
+
def update_param(k, v)
|
15
|
+
found = false
|
16
|
+
if self.GET.has_key?(k)
|
17
|
+
found = true
|
18
|
+
self.GET[k] = v
|
19
|
+
end
|
20
|
+
if self.POST.has_key?(k)
|
21
|
+
found = true
|
22
|
+
self.POST[k] = v
|
23
|
+
end
|
24
|
+
unless found
|
25
|
+
self.GET[k] = v
|
26
|
+
end
|
27
|
+
@params = nil
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Destructively delete a parameter, whether it's in GET or POST.
|
32
|
+
# Returns the value of the deleted parameter.
|
33
|
+
#
|
34
|
+
# If the parameter is in both GET and POST, the POST value takes
|
35
|
+
# precedence since that's how #params works.
|
36
|
+
#
|
37
|
+
# env['rack.input'] is not touched.
|
38
|
+
def delete_param(k)
|
39
|
+
v = [ self.POST.delete(k), self.GET.delete(k) ].compact.first
|
40
|
+
@params = nil
|
41
|
+
v
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Zero
|
2
|
+
# the base renderer for getting render containers
|
3
|
+
#
|
4
|
+
# This class handles templates and render coontainers, which can be used for
|
5
|
+
# the actual rendering.
|
6
|
+
#
|
7
|
+
# To use this renderer you have to give it a template path and optionally
|
8
|
+
# a map of shorthand type descriptions to fully types. This will then be used
|
9
|
+
# to extend the internal map of templates to possible formats in a way, that
|
10
|
+
# you will be able to answer xhtml and html requests with the same template.
|
11
|
+
#
|
12
|
+
# When the object is initialized and you are sure, everything is loaded, call
|
13
|
+
# #read_template_path! and the template tree will be built. Without this step,
|
14
|
+
# you will probably don't get any output.
|
15
|
+
#
|
16
|
+
# After the setup, the renderer can be used to build render containers, which
|
17
|
+
# then can be used to actually render something.
|
18
|
+
class Renderer
|
19
|
+
# initializes a new Renderer
|
20
|
+
#
|
21
|
+
# This method takes a path to the base template directory and a type map.
|
22
|
+
# This type map is used to extend the possible renderings for different
|
23
|
+
# types, which the clients sends.
|
24
|
+
#
|
25
|
+
# @example create a simple renderer
|
26
|
+
# Renderer.new('app/templates')
|
27
|
+
#
|
28
|
+
# @example create a renderer with a small map
|
29
|
+
# Renderer.new('app', {
|
30
|
+
# 'html' => ['text/html', 'application/html+xml'],
|
31
|
+
# 'json' => ['application/json', 'application/aweomse+json']
|
32
|
+
# })
|
33
|
+
#
|
34
|
+
# @param template_path [String] a string to templates
|
35
|
+
# @param type_map [Hash] a map of simple types to complex ones
|
36
|
+
def initialize(template_path, type_map = {})
|
37
|
+
@template_path = template_path + '/'
|
38
|
+
@type_map = type_map
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns the hash of type conversions
|
42
|
+
# @return [Hash] type conversion
|
43
|
+
attr_reader :type_map
|
44
|
+
# get the path to the templates
|
45
|
+
# @return [String] the base template path
|
46
|
+
attr_reader :template_path
|
47
|
+
# get the tree of templates
|
48
|
+
# @api private
|
49
|
+
# @return [Hash] the template tree
|
50
|
+
attr_reader :templates
|
51
|
+
|
52
|
+
# load the template tree
|
53
|
+
#
|
54
|
+
# This method gets all templates in the `template_path` and builds an
|
55
|
+
# internal tree structure, where templates and types direct the request to
|
56
|
+
# the wanted template.
|
57
|
+
# @return [Self] returns the object
|
58
|
+
def read_template_path!
|
59
|
+
# TODO clean up later
|
60
|
+
@templates = {}
|
61
|
+
search_files.each do |file|
|
62
|
+
parts = file.gsub(/#{template_path}/, '').split('.')
|
63
|
+
@templates[parts[0]] ||= {}
|
64
|
+
|
65
|
+
# Set default value
|
66
|
+
types = 'default'
|
67
|
+
# Overwrite default value, if it's set in template path
|
68
|
+
if parts.count > 2 then
|
69
|
+
types = parts[1]
|
70
|
+
end
|
71
|
+
|
72
|
+
read_type(types).each do |type|
|
73
|
+
@templates[parts[0]][type] = file
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# render a template
|
79
|
+
#
|
80
|
+
# This method will render the given template, based on the type in the given
|
81
|
+
# context.
|
82
|
+
# @param name [String] the name of the template
|
83
|
+
# @param type [Array] a list of accept types used to find the template
|
84
|
+
# @param context [Object] the context in which to evaluate the template
|
85
|
+
# @return [String] the rendered content
|
86
|
+
def render(name, type, context)
|
87
|
+
template(name, type).render(context)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# search in `template_path` for templates beginning with `template_name`
|
93
|
+
# @api private
|
94
|
+
# @param template_name [String] the name of the template
|
95
|
+
# @return [#each] a list of all templates found
|
96
|
+
def search_files
|
97
|
+
Dir[template_path + '**/*.*']
|
98
|
+
end
|
99
|
+
|
100
|
+
# gets the type information from a file and converts it to an array of
|
101
|
+
# possible matching types
|
102
|
+
# @api private
|
103
|
+
# @param short_notation [String] a short notation of a type, like `html`
|
104
|
+
# @return [Array] a list of matching types, like `text/html`
|
105
|
+
def read_type(short_notation)
|
106
|
+
to_type_list(type_map[short_notation] || short_notation)
|
107
|
+
end
|
108
|
+
|
109
|
+
# convert a map to an array if it is not one
|
110
|
+
# @api private
|
111
|
+
# @param original_map [Object] the type(s) to convert
|
112
|
+
# @return [Array] a list of objects
|
113
|
+
def to_type_list(original_map)
|
114
|
+
return original_map if original_map.respond_to?(:each)
|
115
|
+
[original_map]
|
116
|
+
end
|
117
|
+
|
118
|
+
# get the prepared template for the name and type
|
119
|
+
# @api private
|
120
|
+
# @param name [String] the name of the template
|
121
|
+
# @param types [Array] the types for the template
|
122
|
+
# @return [Tilt::Template] a prepared tilt template
|
123
|
+
def template(name, types)
|
124
|
+
if templates.has_key? name
|
125
|
+
types.each do |type|
|
126
|
+
template = templates[name][type]
|
127
|
+
unless template.nil?
|
128
|
+
return template if template.kind_of?(Tilt::Template)
|
129
|
+
return Tilt.new(template)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
raise ArgumentError.new(
|
133
|
+
"No template found for any of this types #{types.join ', '}!"
|
134
|
+
)
|
135
|
+
end
|
136
|
+
raise ArgumentError.new "No template found for '#{name}'!"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/zero/request.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require_relative 'request/accept'
|
2
|
+
require_relative 'request/client'
|
3
|
+
require_relative 'request/parameter'
|
4
|
+
require_relative 'request/server'
|
5
|
+
|
6
|
+
module Zero
|
7
|
+
# This class wraps around a rack environment for easier access to all data.
|
8
|
+
class Request
|
9
|
+
def self.create(environment)
|
10
|
+
return environment['zero.request'] if environment.has_key?('zero.request')
|
11
|
+
new(environment)
|
12
|
+
end
|
13
|
+
|
14
|
+
# create a new request object
|
15
|
+
def initialize(env)
|
16
|
+
@env = env
|
17
|
+
@env['zero.request'] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
# get the environment
|
21
|
+
# @return [Hash] the environment hash
|
22
|
+
attr_reader :env
|
23
|
+
|
24
|
+
# get the requested path
|
25
|
+
# @return [String] returns the requested path
|
26
|
+
def path
|
27
|
+
@path ||= @env[CONST_PATH_INFO]
|
28
|
+
end
|
29
|
+
|
30
|
+
# return all information about the client
|
31
|
+
# @return [Client] an information object about the client
|
32
|
+
def client
|
33
|
+
@client ||= Request::Client.new(@env)
|
34
|
+
end
|
35
|
+
|
36
|
+
# get the information on the server
|
37
|
+
# @return [Server] information on the running server
|
38
|
+
def server
|
39
|
+
@server ||= Request::Server.new(@env)
|
40
|
+
end
|
41
|
+
|
42
|
+
# get an object representing the parameters of the request
|
43
|
+
# @return [Parameter] object having all parameters
|
44
|
+
def params
|
45
|
+
@params ||= Request::Parameter.new(@env)
|
46
|
+
end
|
47
|
+
|
48
|
+
# return the content type of the request
|
49
|
+
# TODO change into its own object?
|
50
|
+
# @return [String] returns the content type of the request
|
51
|
+
def content_type
|
52
|
+
@env[CONST_CONTENT_TYPE] if @env.has_key?(CONST_CONTENT_TYPE)
|
53
|
+
end
|
54
|
+
|
55
|
+
# get the media types
|
56
|
+
# @return [Accept] on Accept object managing all types and their order
|
57
|
+
def accept
|
58
|
+
@accept ||= Request::Accept.new(@env)
|
59
|
+
end
|
60
|
+
|
61
|
+
# get the method of the request
|
62
|
+
# @return [Symbol] the symbol representation of the method
|
63
|
+
def method
|
64
|
+
@method ||= @env[CONST_REQUEST_METHOD].downcase.to_sym
|
65
|
+
end
|
66
|
+
# is the method 'GET'?
|
67
|
+
# @return [Boolean] true if this is a get request
|
68
|
+
def get?; method == :get; end
|
69
|
+
# is the method 'POST'?
|
70
|
+
# @return [Boolean] true if this is a post request
|
71
|
+
def post?; method == :post; end
|
72
|
+
# is the method 'PUT'?
|
73
|
+
# @return [Boolean] true if this is a put request
|
74
|
+
def put?; method == :put; end
|
75
|
+
# is the method 'DELETE'?
|
76
|
+
# @return [Boolean] true if this is a delete request
|
77
|
+
def delete?; method == :delete; end
|
78
|
+
# is the method 'HEAD'?
|
79
|
+
# @return [Boolean] true if this is a head request
|
80
|
+
def head?; method == :head; end
|
81
|
+
# is the method 'PATCH'?
|
82
|
+
# @return [Boolean] true if this is a patch request
|
83
|
+
def patch?; method == :patch; end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# constant for the content type key
|
88
|
+
# @api private
|
89
|
+
CONST_CONTENT_TYPE = 'CONTENT_TYPE'
|
90
|
+
# constant for the http accept key
|
91
|
+
# @api private
|
92
|
+
CONST_HTTP_ACCEPT = 'HTTP_ACCEPT'
|
93
|
+
# constant for the path info key
|
94
|
+
# @api private
|
95
|
+
CONST_PATH_INFO = 'PATH_INFO'
|
96
|
+
# constant for the request method key
|
97
|
+
# @api private
|
98
|
+
CONST_REQUEST_METHOD = 'REQUEST_METHOD'
|
99
|
+
end
|
100
|
+
end
|