mutils 0.2.35 → 1.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.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +3 -3
- data/.github/dependabot.yml +10 -0
- data/.gitignore +1 -0
- data/.huskyrc +7 -0
- data/.rspec +0 -1
- data/.rubocop.yml +2 -7
- data/.rubocop_todo.yml +10 -2
- data/.travis.yml +26 -4
- data/CHANGELOG.md +86 -45
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -71
- data/README.md +135 -47
- data/SECURITY.md +13 -0
- data/Version +1 -0
- data/commitlint.config.js +1 -0
- data/gemdeploy.sh +10 -0
- data/lib/mutils.rb +3 -3
- data/lib/mutils/lib/helper.rb +42 -0
- data/lib/mutils/lib/result_hash.rb +25 -0
- data/lib/mutils/serialization/base_serializer.rb +0 -8
- data/lib/mutils/serialization/methods/attributes.rb +52 -0
- data/lib/mutils/serialization/methods/main.rb +27 -0
- data/lib/mutils/serialization/methods/relations.rb +40 -0
- data/lib/mutils/serialization/results/attributes.rb +24 -0
- data/lib/mutils/serialization/results/main.rb +59 -0
- data/lib/mutils/serialization/results/relations.rb +23 -0
- data/lib/mutils/serialization/serialization_includes.rb +9 -7
- data/lib/mutils/serialization/serialization_methods.rb +7 -69
- data/lib/mutils/serialization/serialization_results.rb +6 -71
- data/lib/mutils/version.rb +1 -1
- data/mutils.gemspec +5 -8
- data/package-lock.json +7157 -0
- data/package.json +94 -0
- metadata +25 -71
- data/.github/workflows/gempush.yml +0 -28
- data/.mergify.yml +0 -8
- data/.ruby-version +0 -1
- data/BENCHMARK-SERIALIZER-JSON.md +0 -82
- data/Makefile +0 -8
- data/benchmark/benchmark-serializer-json.rb +0 -252
data/README.md
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
[](https://codeclimate.com/github/code-vedas/mutils/maintainability)
|
|
2
2
|

|
|
3
3
|
[](https://badge.fury.io/rb/mutils)
|
|
4
|
-
[](https://coveralls.io/github/Code-Vedas/mutils?branch=master)
|
|
5
|
+
[](https://travis-ci.com/code-vedas/mutils)
|
|
6
6
|
# Mutils
|
|
7
7
|
## Introduction
|
|
8
8
|
`mutils` is collection of useful modules for `ruby on rails` which is tested and benchmarked against high load.
|
|
9
9
|
|
|
10
10
|
These collection of modules are built by developer for developers :-)
|
|
11
|
+
# Table of Contents
|
|
12
|
+
|
|
13
|
+
* [Features](#features)
|
|
14
|
+
* [Installation](#installation)
|
|
15
|
+
* [Usage](#usage)
|
|
16
|
+
* [Rails Generator](#rails-generator)
|
|
17
|
+
* [Attributes](#attributes)
|
|
18
|
+
* [Relations](#relations)
|
|
19
|
+
* [Conditional Attributes](#conditional-attributes)
|
|
20
|
+
* [Conditional Relations](#conditional-relations)
|
|
21
|
+
* [Attributes Block](#attributes-blocks)
|
|
22
|
+
* [Attributes Block with Params](#attributes-blocks-with-params)
|
|
23
|
+
* [Custom Methods](#custom-methods)
|
|
24
|
+
* [Name Tag](#name-tag)
|
|
25
|
+
* [Sample Usage](#sample-usage)
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
* Simple declaration syntax similar to Active Model Serializer
|
|
29
|
+
* Realtionships support `belongs_to`, `has_many`, `has_one`
|
|
30
|
+
* Block style attributes with params
|
|
31
|
+
|
|
11
32
|
## Installation
|
|
12
33
|
|
|
13
34
|
Add this line to your application's Gemfile:
|
|
@@ -22,17 +43,9 @@ Or install it yourself as:
|
|
|
22
43
|
|
|
23
44
|
$ gem install mutils
|
|
24
45
|
|
|
25
|
-
##
|
|
26
|
-
| Sno | Name | Status |
|
|
27
|
-
|:---: |:-----------------: |:------: |
|
|
28
|
-
| 1 | Serializer - JSON | Done |
|
|
29
|
-
| 2 | Serializer - XML | Done |
|
|
46
|
+
## Usage
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
### Serializer - JSON
|
|
33
|
-
JSON Serializer for Active Models
|
|
34
|
-
|
|
35
|
-
#### Generate Serializer by command
|
|
48
|
+
### Rails Generator
|
|
36
49
|
```shell script
|
|
37
50
|
rails g mutils:serializer User id first_name last_name email
|
|
38
51
|
|
|
@@ -50,13 +63,7 @@ class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
|
50
63
|
end
|
|
51
64
|
```
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
1. Attributes
|
|
55
|
-
2. Custom Methods
|
|
56
|
-
3. Relations
|
|
57
|
-
3. name_tag
|
|
58
|
-
|
|
59
|
-
##### Attributes
|
|
66
|
+
### Attributes
|
|
60
67
|
Attributes are fields in the model itself. You can reference them by below example
|
|
61
68
|
```ruby
|
|
62
69
|
# frozen_string_literal: true
|
|
@@ -68,54 +75,131 @@ class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
|
68
75
|
attribute :email, {always_include: true} ## this will allow to selectively include email
|
|
69
76
|
end
|
|
70
77
|
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
### Relations
|
|
79
|
+
Relations such as `has_many`, `belongs_to`, `has_one` can be used as follows
|
|
80
|
+
1. Every relation must be provided with their own serializer
|
|
81
|
+
2. `always_include` option can be used to instruct `Serializer` to always include this relation
|
|
82
|
+
3. `always_include` by default is disabled, relations which are not `always_include` can be included while using the serializer. Refer to next section for this usage
|
|
83
|
+
4. `label` option can be used to override model class name while serializing
|
|
75
84
|
```ruby
|
|
76
85
|
# frozen_string_literal: true
|
|
77
86
|
|
|
78
87
|
# User Serializer
|
|
79
88
|
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
80
89
|
attributes :id, :first_name, :last_name, :email
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
|
|
91
|
+
belongs_to :company, serializer: CompanySerializer, always_include: true
|
|
83
92
|
## OR
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
belongs_to :company, serializer: CompanySerializer, always_include: true, label: 'organization' ##<== important to give singular name
|
|
94
|
+
|
|
95
|
+
has_many :comments, serializer: CommentSerializer
|
|
96
|
+
has_one :account, serializer: AccountSerializer
|
|
86
97
|
|
|
87
98
|
def full_name
|
|
88
99
|
"#{scope.first_name} #{scope.last_name}"
|
|
89
100
|
end
|
|
90
101
|
end
|
|
91
102
|
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
### Conditional Attributes
|
|
104
|
+
Serializer can have conditional attributes with `if: Proc`
|
|
105
|
+
`if: Proc` block can receive `scope` and `params` as arguments
|
|
106
|
+
|
|
107
|
+
- __in proc {|scope|}__, scope is object which is being serialized
|
|
108
|
+
- __in proc {|scope,params|}__, scope is object which is being serialized and params is hash given to Serializer as second arguments in {params:anything}
|
|
109
|
+
```ruby
|
|
110
|
+
# frozen_string_literal: true
|
|
111
|
+
|
|
112
|
+
# User Serializer
|
|
113
|
+
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
114
|
+
attributes :id, :first_name, :last_name
|
|
115
|
+
attribute :email, if: proc { |scope| scope.name == 'mutils' } ## Email will only serialize if user's name is 'mutils'
|
|
116
|
+
# OR with Params
|
|
117
|
+
attribute :email, if: proc { |scope,params| params && params[:show_email] == true } ## Email will only serialize if params[:show_email] is true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
UserSerializer.new(user) # Without params
|
|
121
|
+
UserSerializer.new(user,{params:{show_email:true}}) # With params
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Conditional Relations
|
|
125
|
+
Serializer can have conditional relations with `if: Proc`
|
|
126
|
+
`if: Proc` block can receive `scope` and `params` as arguments
|
|
127
|
+
|
|
128
|
+
- __in proc {|scope|}__, scope is object which is being serialized
|
|
129
|
+
- __in proc {|scope,params|}__, scope is object which is being serialized and params is hash given to Serializer as second arguments in {params:anything}
|
|
130
|
+
```ruby
|
|
131
|
+
# frozen_string_literal: true
|
|
132
|
+
|
|
133
|
+
# User Serializer
|
|
134
|
+
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
135
|
+
attributes :id, :first_name, :last_name
|
|
136
|
+
has_many :comments, serializer: CommentSerializer, if: proc { |scope| scope.name == 'mutils' } ## comments will only serialize if user's name is 'mutils'
|
|
137
|
+
belongs_to :account, serializer: AccountSerializer, if: proc { |scope| scope.name != 'mutils' } ## account will only serialize if user's name is not 'mutils'
|
|
138
|
+
# OR with Params
|
|
139
|
+
belongs_to :account, serializer: AccountSerializer, if: proc { |scope,params| params && params[:show_account] == true } ## account will only serialize if params[:show_account] is true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
UserSerializer.new(user) # Without params
|
|
143
|
+
UserSerializer.new(user,{params:{show_account:true}}) # With params
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Attributes Blocks
|
|
148
|
+
While writting attribute a block can be provided for useful transformations like `full_name` as shown below
|
|
98
149
|
```ruby
|
|
99
150
|
# frozen_string_literal: true
|
|
100
151
|
|
|
101
152
|
# User Serializer
|
|
102
153
|
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
103
154
|
attributes :id, :first_name, :last_name, :email
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
155
|
+
attribute :full_name do |object|
|
|
156
|
+
"#{object.first_name} #{object.last_name}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
### Attributes Blocks with Params
|
|
161
|
+
While writting attribute a block can be provided for useful transformations like `full_name` as shown below
|
|
162
|
+
```ruby
|
|
163
|
+
# frozen_string_literal: true
|
|
164
|
+
|
|
165
|
+
# User Serializer
|
|
166
|
+
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
167
|
+
attributes :id, :first_name, :last_name, :email
|
|
168
|
+
attribute :is_owner do |object,params|
|
|
169
|
+
params[:owner].id == object.id ? true:false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
```ruby
|
|
174
|
+
# in controller
|
|
175
|
+
|
|
176
|
+
user = current_user
|
|
177
|
+
owner = owner_user
|
|
178
|
+
render json: UserSerializer.new(user,{params:{owner:owner}})
|
|
179
|
+
```
|
|
180
|
+
### Custom Methods
|
|
181
|
+
Custom methods used in Serializer can be useful for cases as below.
|
|
182
|
+
`scope` will be available to reference object in Serializer in below case its `user`
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# frozen_string_literal: true
|
|
186
|
+
|
|
187
|
+
# User Serializer
|
|
188
|
+
class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
189
|
+
attributes :id, :first_name, :last_name, :email
|
|
190
|
+
###
|
|
191
|
+
custom_methods :full_name
|
|
192
|
+
## OR
|
|
193
|
+
custom_method :full_name, {always_include: true} ## this will allow to selectively include full_name
|
|
194
|
+
###
|
|
111
195
|
|
|
112
196
|
def full_name
|
|
113
197
|
"#{scope.first_name} #{scope.last_name}"
|
|
114
198
|
end
|
|
115
199
|
end
|
|
116
200
|
```
|
|
117
|
-
|
|
118
|
-
name_tag is used to provide custom name to serializer output keys for json
|
|
201
|
+
### Name Tag
|
|
202
|
+
name_tag is used to provide custom name to serializer output keys for json
|
|
119
203
|
|
|
120
204
|
**Options**
|
|
121
205
|
- ``name_tag 'Person', true`` # Include Person or People in JSON serialization as root, true|false this only implies to root serializer
|
|
@@ -137,20 +221,20 @@ class UserSerializer < Mutils::Serialization::BaseSerializer
|
|
|
137
221
|
end
|
|
138
222
|
```
|
|
139
223
|
|
|
140
|
-
|
|
224
|
+
### Sample Usage
|
|
141
225
|
|
|
142
226
|
```ruby
|
|
143
227
|
user = User.first
|
|
144
228
|
options = {includes: [:comments,:account]}
|
|
145
229
|
UserSerializer.new(user,options).to_h
|
|
146
230
|
```
|
|
147
|
-
###or
|
|
231
|
+
### or
|
|
148
232
|
```ruby
|
|
149
233
|
users = User.all
|
|
150
234
|
options = {includes: [:account]}
|
|
151
235
|
UserSerializer.new(users,options).to_json
|
|
152
236
|
```
|
|
153
|
-
###or in controllers
|
|
237
|
+
### or in controllers
|
|
154
238
|
```ruby
|
|
155
239
|
users = User.all
|
|
156
240
|
options = {includes: [:account]}
|
|
@@ -168,4 +252,8 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
168
252
|
|
|
169
253
|
## Code of Conduct
|
|
170
254
|
|
|
171
|
-
Everyone interacting in the Mutils project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
255
|
+
Everyone interacting in the Mutils project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/code-vedas/mutils/blob/master/CODE_OF_CONDUCT.md).
|
|
256
|
+
|
|
257
|
+
## Security
|
|
258
|
+
|
|
259
|
+
For security refer to [security](https://github.com/Code-Vedas/mutils/blob/master/SECURITY.md) document.
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Versions currently being supported with security updates.
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------- | ------------------ |
|
|
9
|
+
| 1.x.x | :white_check_mark: |
|
|
10
|
+
|
|
11
|
+
## Reporting a Vulnerability
|
|
12
|
+
|
|
13
|
+
Create an issue or email at security@codevedas.com
|
data/Version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v1.1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {extends: ['@commitlint/config-angular']};
|
data/gemdeploy.sh
ADDED
data/lib/mutils.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'dry/inflector'
|
|
3
4
|
require_relative 'mutils/version'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require 'active_support/core_ext/hash'
|
|
5
|
+
require_relative 'mutils/lib/helper'
|
|
6
|
+
require_relative 'mutils/lib/result_hash'
|
|
7
7
|
require_relative 'mutils/serialization/serialization_results'
|
|
8
8
|
require_relative 'mutils/serialization/serialization_includes'
|
|
9
9
|
require_relative 'mutils/serialization/serialization_methods'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
# module Mutils
|
|
5
|
+
module Mutils
|
|
6
|
+
module Lib
|
|
7
|
+
# Helper: caching expensive repetitive operations
|
|
8
|
+
class Helper
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
self.inflector_object = Dry::Inflector.new
|
|
13
|
+
self.pluralize_cache = {}
|
|
14
|
+
self.underscore_cache = {}
|
|
15
|
+
self.constantize_cache = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def underscore(string)
|
|
19
|
+
underscore_cache[string] = inflector_object.underscore string unless underscore_cache[string]
|
|
20
|
+
underscore_cache[string]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pluralize(string)
|
|
24
|
+
pluralize_cache[string] = inflector_object.pluralize string unless pluralize_cache[string]
|
|
25
|
+
pluralize_cache[string]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def constantize(string)
|
|
29
|
+
constantize_cache[string] = Object.const_get string unless constantize_cache[string]
|
|
30
|
+
constantize_cache[string]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def collection?(object)
|
|
34
|
+
object.respond_to?(:size) && !object.respond_to?(:each_pair)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_accessor :inflector_object, :pluralize_cache, :underscore_cache, :constantize_cache
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# module Mutils
|
|
4
|
+
module Mutils
|
|
5
|
+
module Lib
|
|
6
|
+
# ResultHash: Store result using this class.
|
|
7
|
+
class ResultHash
|
|
8
|
+
def initialize
|
|
9
|
+
self._hash = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def []=(key, value)
|
|
13
|
+
_hash[key] = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def hash
|
|
17
|
+
_hash
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_accessor :_hash
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -33,14 +33,6 @@ module Mutils
|
|
|
33
33
|
JSON.generate(as_json, options)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def to_xml(_options = {})
|
|
37
|
-
to_h.to_xml(root: class_name, skip_instruct: true, indent: 2)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def as_xml(_options = {})
|
|
41
|
-
to_xml
|
|
42
|
-
end
|
|
43
|
-
|
|
44
36
|
private
|
|
45
37
|
|
|
46
38
|
attr_writer :scope
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module Mutils
|
|
4
|
+
module Mutils
|
|
5
|
+
# Module SerializationCore
|
|
6
|
+
module Serialization
|
|
7
|
+
# Module Methods
|
|
8
|
+
module Methods
|
|
9
|
+
# Module Attributes
|
|
10
|
+
module Attributes
|
|
11
|
+
def attributes(*attributes_list)
|
|
12
|
+
parse_attributes_methods(attributes_list, false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def custom_methods(*attributes_list)
|
|
16
|
+
parse_attributes_methods(attributes_list, true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_attributes_methods(list, is_method)
|
|
20
|
+
self.attributes_to_serialize = {} if attributes_to_serialize.nil?
|
|
21
|
+
list&.each do |attr|
|
|
22
|
+
value = { method: is_method, always_include: true }
|
|
23
|
+
attributes_to_serialize[attr] = value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def attribute(method_name, options = {}, &proc)
|
|
28
|
+
raise "if: should be a Proc object for attribute #{method_name}" if options[:if] && !options[:if].instance_of?(Proc)
|
|
29
|
+
|
|
30
|
+
if proc.instance_of? Proc
|
|
31
|
+
self.attributes_to_serialize_blocks = {} if attributes_to_serialize_blocks.nil?
|
|
32
|
+
options[:block] = proc
|
|
33
|
+
attributes_to_serialize_blocks[method_name] = options
|
|
34
|
+
else
|
|
35
|
+
add_single_attribute(method_name, options, false)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def custom_method(method_name, options = {})
|
|
40
|
+
add_single_attribute(method_name, options, true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_single_attribute(method_name, options, is_method)
|
|
44
|
+
self.attributes_to_serialize = {} if attributes_to_serialize.nil?
|
|
45
|
+
always_include = options[:always_include].nil? ? false : options[:always_include]
|
|
46
|
+
value = { method: is_method, always_include: always_include, if: options[:if] }
|
|
47
|
+
attributes_to_serialize[method_name] = value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module Mutils
|
|
4
|
+
module Mutils
|
|
5
|
+
# Module SerializationCore
|
|
6
|
+
module Serialization
|
|
7
|
+
# Module Methods
|
|
8
|
+
module Methods
|
|
9
|
+
# Module Main
|
|
10
|
+
module Main
|
|
11
|
+
def name_tag(name_tag, root = nil)
|
|
12
|
+
self.serializer_name = name_tag
|
|
13
|
+
self.include_root = root
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def class_exists?(class_name)
|
|
17
|
+
klass = begin
|
|
18
|
+
Mutils::Lib::Helper.instance.constantize(class_name.to_s)
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
klass && defined?(klass) && klass.is_a?(Class)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|