ruby_protobuf 0.1.0 → 0.2.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/History.txt +5 -3
- data/Manifest.txt +20 -11
- data/README.txt +1 -1
- data/lib/protobuf/{wire_type.rb → common/wire_type.rb} +0 -0
- data/lib/protobuf/{compiler.rb → compiler/compiler.rb} +13 -13
- data/lib/protobuf/{parser.y → compiler/parser.y} +0 -0
- data/lib/protobuf/descriptor/descriptor.proto +286 -0
- data/lib/protobuf/descriptor/descriptor.rb +54 -0
- data/lib/protobuf/descriptor/descriptor_builder.rb +144 -0
- data/lib/protobuf/descriptor/descriptor_proto.rb +119 -0
- data/lib/protobuf/descriptor/enum_descriptor.rb +33 -0
- data/lib/protobuf/descriptor/field_descriptor.rb +50 -0
- data/lib/protobuf/descriptor/file_descriptor.rb +38 -0
- data/lib/protobuf/{decoder.rb → message/decoder.rb} +1 -1
- data/lib/protobuf/{encoder.rb → message/encoder.rb} +12 -11
- data/lib/protobuf/message/enum.rb +21 -0
- data/lib/protobuf/{extend.rb → message/extend.rb} +1 -1
- data/lib/protobuf/{field.rb → message/field.rb} +82 -53
- data/lib/protobuf/{message.rb → message/message.rb} +12 -3
- data/lib/protobuf/message/service.rb +9 -0
- data/lib/ruby_protobuf.rb +1 -1
- data/test/addressbook.rb +22 -4
- data/test/check_unbuild.rb +30 -0
- data/test/data/data2.bin +3 -0
- data/test/test_addressbook.rb +2 -2
- data/test/test_compiler.rb +9 -9
- data/test/test_descriptor.rb +122 -0
- data/test/test_message.rb +2 -2
- data/test/types.rb +1 -1
- metadata +23 -14
- data/bin/ruby_protobuf +0 -0
- data/lib/protobuf/enum.rb +0 -13
- data/lib/protobuf/service.rb +0 -7
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'protobuf/descriptor/file_descriptor'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
module Descriptor
|
5
|
+
def self.id2type(type_id)
|
6
|
+
require 'protobuf/descriptor/descriptor_proto'
|
7
|
+
case type_id
|
8
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_DOUBLE
|
9
|
+
:double
|
10
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FLOAT
|
11
|
+
:float
|
12
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT64
|
13
|
+
:int64
|
14
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT64
|
15
|
+
:unit64
|
16
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT32
|
17
|
+
:int64
|
18
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED64
|
19
|
+
:fixed64
|
20
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED32
|
21
|
+
:fixed32
|
22
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_BOOL
|
23
|
+
:bool
|
24
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING
|
25
|
+
:string
|
26
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_GROUP
|
27
|
+
:group
|
28
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE
|
29
|
+
:message
|
30
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_BYTES
|
31
|
+
:bytes
|
32
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32
|
33
|
+
:uint32
|
34
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM
|
35
|
+
:enum
|
36
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED32
|
37
|
+
:sfixed32
|
38
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED64
|
39
|
+
:sfixed64
|
40
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT32
|
41
|
+
:sint32
|
42
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT64
|
43
|
+
:sint64
|
44
|
+
else
|
45
|
+
raise ArgumentError.new("Invalid type: #{proto.type}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.type2id(type)
|
50
|
+
require 'protobuf/descriptor/descriptor_proto'
|
51
|
+
case type
|
52
|
+
when :double
|
53
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_DOUBLE
|
54
|
+
when :float
|
55
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_FLOAT
|
56
|
+
when :int64
|
57
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT64
|
58
|
+
when :unit64
|
59
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT64
|
60
|
+
when :int64
|
61
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT32
|
62
|
+
when :fixed64
|
63
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED64
|
64
|
+
when :fixed32
|
65
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED32
|
66
|
+
when Google::Protobuf::FieldDescriptorProto::Type::TYPE_BOOL
|
67
|
+
:bool
|
68
|
+
when :string
|
69
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING
|
70
|
+
when :group
|
71
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_GROUP
|
72
|
+
when :message
|
73
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE
|
74
|
+
when :bytes
|
75
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_BYTES
|
76
|
+
when :uint32
|
77
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32
|
78
|
+
when :enum
|
79
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM
|
80
|
+
when :sfixed32
|
81
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED32
|
82
|
+
when :sfixed64
|
83
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED64
|
84
|
+
when :sint32
|
85
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT32
|
86
|
+
when :sint64
|
87
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT64
|
88
|
+
else
|
89
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.id2label(label_id)
|
94
|
+
require 'protobuf/descriptor/descriptor_proto'
|
95
|
+
case label_id
|
96
|
+
when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED
|
97
|
+
:required
|
98
|
+
when Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL
|
99
|
+
:optional
|
100
|
+
when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED
|
101
|
+
:repeated
|
102
|
+
else
|
103
|
+
raise ArgumentError.new("Invalid label: #{proto.label}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.label2id(label)
|
108
|
+
require 'protobuf/descriptor/descriptor_proto'
|
109
|
+
case label
|
110
|
+
when :required
|
111
|
+
Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED
|
112
|
+
when :optional
|
113
|
+
Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL
|
114
|
+
when :repeated
|
115
|
+
Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED
|
116
|
+
else
|
117
|
+
raise ArgumentError.new("Invalid label: #{label}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class DescriptorBuilder
|
122
|
+
class <<self
|
123
|
+
|
124
|
+
def proto_type
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def build(proto, opt={})
|
129
|
+
acceptable_descriptor(proto).build proto
|
130
|
+
end
|
131
|
+
|
132
|
+
def acceptable_descriptor(proto)
|
133
|
+
Protobuf::Descriptor.constants.each do |class_name|
|
134
|
+
descriptor_class = Protobuf::Descriptor.const_get class_name
|
135
|
+
if descriptor_class.respond_to?(:proto_type) and
|
136
|
+
descriptor_class.proto_type == proto.class.name
|
137
|
+
return descriptor_class
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'protobuf/message/message'
|
2
|
+
require 'protobuf/message/enum'
|
3
|
+
require 'protobuf/message/service'
|
4
|
+
require 'protobuf/message/extend'
|
5
|
+
module Google
|
6
|
+
module Protobuf
|
7
|
+
::Protobuf::OPTIONS[:java_package] = :"com.google.protobuf"
|
8
|
+
::Protobuf::OPTIONS[:java_outer_classname] = :"DescriptorProtos"
|
9
|
+
::Protobuf::OPTIONS[:optimize_for] = :SPEED
|
10
|
+
class FileDescriptorProto < ::Protobuf::Message
|
11
|
+
optional :string, :name, 1
|
12
|
+
optional :string, :package, 2
|
13
|
+
repeated :string, :dependency, 3
|
14
|
+
repeated :DescriptorProto, :message_type, 4
|
15
|
+
repeated :EnumDescriptorProto, :enum_type, 5
|
16
|
+
repeated :ServiceDescriptorProto, :service, 6
|
17
|
+
repeated :FieldDescriptorProto, :extension, 7
|
18
|
+
optional :FileOptions, :options, 8
|
19
|
+
end
|
20
|
+
class DescriptorProto < ::Protobuf::Message
|
21
|
+
optional :string, :name, 1
|
22
|
+
repeated :FieldDescriptorProto, :field, 2
|
23
|
+
repeated :FieldDescriptorProto, :extension, 6
|
24
|
+
repeated :DescriptorProto, :nested_type, 3
|
25
|
+
repeated :EnumDescriptorProto, :enum_type, 4
|
26
|
+
class ExtensionRange < ::Protobuf::Message
|
27
|
+
optional :int32, :start, 1
|
28
|
+
optional :int32, :end, 2
|
29
|
+
end
|
30
|
+
repeated :ExtensionRange, :extension_range, 5
|
31
|
+
optional :MessageOptions, :options, 7
|
32
|
+
end
|
33
|
+
class FieldDescriptorProto < ::Protobuf::Message
|
34
|
+
class Type < ::Protobuf::Enum
|
35
|
+
TYPE_DOUBLE = 1
|
36
|
+
TYPE_FLOAT = 2
|
37
|
+
TYPE_INT64 = 3
|
38
|
+
TYPE_UINT64 = 4
|
39
|
+
TYPE_INT32 = 5
|
40
|
+
TYPE_FIXED64 = 6
|
41
|
+
TYPE_FIXED32 = 7
|
42
|
+
TYPE_BOOL = 8
|
43
|
+
TYPE_STRING = 9
|
44
|
+
TYPE_GROUP = 10
|
45
|
+
TYPE_MESSAGE = 11
|
46
|
+
TYPE_BYTES = 12
|
47
|
+
TYPE_UINT32 = 13
|
48
|
+
TYPE_ENUM = 14
|
49
|
+
TYPE_SFIXED32 = 15
|
50
|
+
TYPE_SFIXED64 = 16
|
51
|
+
TYPE_SINT32 = 17
|
52
|
+
TYPE_SINT64 = 18
|
53
|
+
end
|
54
|
+
class Label < ::Protobuf::Enum
|
55
|
+
LABEL_OPTIONAL = 1
|
56
|
+
LABEL_REQUIRED = 2
|
57
|
+
LABEL_REPEATED = 3
|
58
|
+
end
|
59
|
+
optional :string, :name, 1
|
60
|
+
optional :int32, :number, 3
|
61
|
+
optional :Label, :label, 4
|
62
|
+
optional :Type, :type, 5
|
63
|
+
optional :string, :type_name, 6
|
64
|
+
optional :string, :extendee, 2
|
65
|
+
optional :string, :default_value, 7
|
66
|
+
optional :FieldOptions, :options, 8
|
67
|
+
end
|
68
|
+
class EnumDescriptorProto < ::Protobuf::Message
|
69
|
+
optional :string, :name, 1
|
70
|
+
repeated :EnumValueDescriptorProto, :value, 2
|
71
|
+
optional :EnumOptions, :options, 3
|
72
|
+
end
|
73
|
+
class EnumValueDescriptorProto < ::Protobuf::Message
|
74
|
+
optional :string, :name, 1
|
75
|
+
optional :int32, :number, 2
|
76
|
+
optional :EnumValueOptions, :options, 3
|
77
|
+
end
|
78
|
+
class ServiceDescriptorProto < ::Protobuf::Message
|
79
|
+
optional :string, :name, 1
|
80
|
+
repeated :MethodDescriptorProto, :method, 2
|
81
|
+
optional :ServiceOptions, :options, 3
|
82
|
+
end
|
83
|
+
class MethodDescriptorProto < ::Protobuf::Message
|
84
|
+
optional :string, :name, 1
|
85
|
+
optional :string, :input_type, 2
|
86
|
+
optional :string, :output_type, 3
|
87
|
+
optional :MethodOptions, :options, 4
|
88
|
+
end
|
89
|
+
class FileOptions < ::Protobuf::Message
|
90
|
+
optional :string, :java_package, 1
|
91
|
+
optional :string, :java_outer_classname, 8
|
92
|
+
optional :bool, :java_multiple_files, 10, {:default => :false}
|
93
|
+
class OptimizeMode < ::Protobuf::Enum
|
94
|
+
SPEED = 1
|
95
|
+
CODE_SIZE = 2
|
96
|
+
end
|
97
|
+
optional :OptimizeMode, :optimize_for, 9, {:default => :CODE_SIZE}
|
98
|
+
end
|
99
|
+
class MessageOptions < ::Protobuf::Message
|
100
|
+
optional :bool, :message_set_wire_format, 1, {:default => :false}
|
101
|
+
end
|
102
|
+
class FieldOptions < ::Protobuf::Message
|
103
|
+
optional :CType, :ctype, 1
|
104
|
+
class CType < ::Protobuf::Enum
|
105
|
+
CORD = 1
|
106
|
+
STRING_PIECE = 2
|
107
|
+
end
|
108
|
+
optional :string, :experimental_map_key, 9
|
109
|
+
end
|
110
|
+
class EnumOptions < ::Protobuf::Message
|
111
|
+
end
|
112
|
+
class EnumValueOptions < ::Protobuf::Message
|
113
|
+
end
|
114
|
+
class ServiceOptions < ::Protobuf::Message
|
115
|
+
end
|
116
|
+
class MethodOptions < ::Protobuf::Message
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Protobuf
|
2
|
+
module Descriptor
|
3
|
+
class EnumDescriptor
|
4
|
+
def initialize(enum_class)
|
5
|
+
@enum_class = enum_class
|
6
|
+
end
|
7
|
+
|
8
|
+
def proto_type
|
9
|
+
Google::Protobuf::EnumDescriptorProto
|
10
|
+
end
|
11
|
+
|
12
|
+
def build(proto, opt)
|
13
|
+
mod = opt[:module]
|
14
|
+
cls = mod.const_set proto.name, Class.new(Protobuf::Enum)
|
15
|
+
proto.value.each do |value_proto|
|
16
|
+
cls.const_set value_proto.name, value_proto.number
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def unbuild(parent_proto)
|
21
|
+
enum_proto = Google::Protobuf::EnumDescriptorProto.new
|
22
|
+
enum_proto.name = @enum_class.name.split('::').last
|
23
|
+
@enum_class.constants.each do |const|
|
24
|
+
enum_value_proto = Google::Protobuf::EnumValueDescriptorProto.new
|
25
|
+
enum_value_proto.name = const
|
26
|
+
enum_value_proto.number = @enum_class.const_get const
|
27
|
+
enum_proto.value << enum_value_proto
|
28
|
+
end
|
29
|
+
parent_proto.enum_type << enum_proto
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Protobuf
|
2
|
+
module Descriptor
|
3
|
+
class FieldDescriptor
|
4
|
+
def initialize(field_instance=nil)
|
5
|
+
@field_instance = field_instance
|
6
|
+
end
|
7
|
+
|
8
|
+
def proto_type
|
9
|
+
'Google::Protobuf::FieldDescriptorProto'
|
10
|
+
end
|
11
|
+
|
12
|
+
def build(proto, opt={})
|
13
|
+
cls = opt[:class]
|
14
|
+
rule = Protobuf::Descriptor.id2label proto.label
|
15
|
+
type = Protobuf::Descriptor.id2type proto.type
|
16
|
+
type = proto.type_name.to_sym if [:message, :enum].include? type
|
17
|
+
opts = {}
|
18
|
+
opts[:default] = proto.default_value if proto.default_value
|
19
|
+
cls.define_field rule, type, proto.name, proto.number, opts
|
20
|
+
end
|
21
|
+
|
22
|
+
def unbuild(parent_proto, extension=false)
|
23
|
+
field_proto = Google::Protobuf::FieldDescriptorProto.new
|
24
|
+
field_proto.name = @field_instance.name.to_s
|
25
|
+
field_proto.number = @field_instance.tag
|
26
|
+
field_proto.label = Protobuf::Descriptor.label2id @field_instance.rule
|
27
|
+
field_proto.type = Protobuf::Descriptor.type2id @field_instance.type
|
28
|
+
if [Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE,
|
29
|
+
Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM].include? field_proto.type
|
30
|
+
field_proto.type_name = @field_instance.type.to_s.split('::').last
|
31
|
+
end
|
32
|
+
field_proto.default_value = @field_instance.default.to_s if @field_instance.default
|
33
|
+
|
34
|
+
case parent_proto
|
35
|
+
when Google::Protobuf::FileDescriptorProto
|
36
|
+
parent_proto.extension << field_proto
|
37
|
+
when Google::Protobuf::DescriptorProto
|
38
|
+
if extension
|
39
|
+
parent_proto.extension << field_proto
|
40
|
+
else
|
41
|
+
parent_proto.field << field_proto
|
42
|
+
end
|
43
|
+
else
|
44
|
+
raise TypeError.new(parent_proto.class.name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Protobuf
|
2
|
+
module Descriptor
|
3
|
+
class FileDescriptor
|
4
|
+
class <<self
|
5
|
+
def proto_type
|
6
|
+
'Google::Protobuf::FileDescriptorProto'
|
7
|
+
end
|
8
|
+
|
9
|
+
def build(proto, opt={})
|
10
|
+
mod = Object
|
11
|
+
if package = proto.package and not package.empty?
|
12
|
+
module_names = package.split '::'
|
13
|
+
module_names.each do |module_name|
|
14
|
+
mod = mod.const_set module_name, Module.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
proto.message_type.each do |message_proto|
|
18
|
+
Protobuf::Message.descriptor.build message_proto, :module => mod
|
19
|
+
end
|
20
|
+
proto.enum_type.each do |enum_proto|
|
21
|
+
Protobuf::Enum.descriptor.build enum_proto, :module => mod
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def unbuild(messages)
|
26
|
+
messages = [messages] unless messages.is_a? Array
|
27
|
+
proto = Google::Protobuf::FileDescriptorProto.new
|
28
|
+
proto.package = messages.first.to_s.split('::')[0..-2].join('::') if messages.first.to_s =~ /::/
|
29
|
+
messages.each do |message|
|
30
|
+
message.descriptor.unbuild proto
|
31
|
+
end
|
32
|
+
proto
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'protobuf/wire_type'
|
1
|
+
require 'protobuf/common/wire_type'
|
2
2
|
|
3
3
|
module Protobuf
|
4
4
|
class Encoder
|
@@ -14,23 +14,24 @@ module Protobuf
|
|
14
14
|
|
15
15
|
def encode(stream=@stream, message=@message)
|
16
16
|
message.each_field do |field, value|
|
17
|
-
|
18
|
-
key_bytes = Protobuf::Field::Int.get_bytes key
|
19
|
-
#stream.write key_bytes.pack('C*')
|
20
|
-
stream.write key_bytes
|
17
|
+
next unless value # TODO
|
21
18
|
|
22
19
|
if field.repeated?
|
23
20
|
value.each do |val|
|
24
|
-
|
25
|
-
#stream.write bytes.pack('C*')
|
26
|
-
stream.write bytes
|
21
|
+
write_pair field, val, stream
|
27
22
|
end
|
28
23
|
else
|
29
|
-
|
30
|
-
#stream.write bytes.pack('C*')
|
31
|
-
stream.write bytes
|
24
|
+
write_pair field, value, stream
|
32
25
|
end
|
33
26
|
end
|
34
27
|
end
|
28
|
+
|
29
|
+
def write_pair(field, value, stream)
|
30
|
+
key = (field.tag << 3) | field.wire_type
|
31
|
+
key_bytes = Protobuf::Field::VarintField.get_bytes key
|
32
|
+
stream.write key_bytes
|
33
|
+
bytes = field.get value
|
34
|
+
stream.write bytes
|
35
|
+
end
|
35
36
|
end
|
36
37
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'protobuf/descriptor/enum_descriptor'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
class Enum
|
5
|
+
class <<self
|
6
|
+
def get_name_by_tag(tag)
|
7
|
+
constants.find do |name|
|
8
|
+
class_eval(name) == tag
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid_tag?(tag)
|
13
|
+
not get_name_by_tag(tag).nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def descriptor
|
17
|
+
@descriptor ||= Protobuf::Descriptor::EnumDescriptor.new(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|