java_bin 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/CHANGELOG +6 -0
- data/README.rdoc +45 -23
- data/VERSION +1 -1
- data/ext/java_bin/ext/extconf.rb +13 -13
- data/ext/java_bin/ext/parser.c +30 -31
- data/ext/java_bin/ext/parser.h +30 -18
- data/java_bin.gemspec +2 -2
- data/test/test_java_bin_parser.rb +16 -13
- metadata +2 -2
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
data/README.rdoc
CHANGED
@@ -1,31 +1,42 @@
|
|
1
1
|
= java_bin
|
2
2
|
|
3
|
-
This is
|
3
|
+
This is an Apache Solr JavaBin format (binary format) implementation for Ruby.
|
4
4
|
|
5
5
|
== Features
|
6
6
|
|
7
7
|
* fast parse, and less network traffic.
|
8
8
|
MRI 1.8.7
|
9
9
|
[data1]
|
10
|
-
ruby eval parse. 5000 times. elapsed time 1.
|
11
|
-
javabin parse. 5000 times. elapsed time 0.
|
12
|
-
2.
|
10
|
+
ruby eval parse. 5000 times. elapsed time 1.282744
|
11
|
+
javabin parse. 5000 times. elapsed time 0.4634
|
12
|
+
2.76811394044022 times
|
13
13
|
|
14
14
|
[data2]
|
15
|
-
ruby2 eval parse. 5000 times. elapsed time 6.
|
16
|
-
javabin2 parse. 5000 times. elapsed time 2.
|
17
|
-
2.
|
15
|
+
ruby2 eval parse. 5000 times. elapsed time 6.568942
|
16
|
+
javabin2 parse. 5000 times. elapsed time 2.267351
|
17
|
+
2.89718795193157 times
|
18
|
+
|
19
|
+
REE 1.8.7
|
20
|
+
[data1]
|
21
|
+
ruby eval parse. 5000 times. elapsed time 0.940934
|
22
|
+
javabin parse. 5000 times. elapsed time 0.310581
|
23
|
+
3.02959292422911 times
|
24
|
+
|
25
|
+
[data2]
|
26
|
+
ruby2 eval parse. 5000 times. elapsed time 4.708985
|
27
|
+
javabin2 parse. 5000 times. elapsed time 1.482276
|
28
|
+
3.176861124379 times
|
18
29
|
|
19
30
|
YARV 1.9.2
|
20
31
|
[data1]
|
21
|
-
ruby eval parse. 5000 times. elapsed time 1.
|
22
|
-
javabin parse. 5000 times. elapsed time 0.
|
23
|
-
3.
|
32
|
+
ruby eval parse. 5000 times. elapsed time 1.348342119
|
33
|
+
javabin parse. 5000 times. elapsed time 0.367371147
|
34
|
+
3.6702450097421506 times
|
24
35
|
|
25
36
|
[data2]
|
26
|
-
ruby2 eval parse. 5000 times. elapsed time 6.
|
27
|
-
javabin2 parse. 5000 times. elapsed time 1.
|
28
|
-
3.
|
37
|
+
ruby2 eval parse. 5000 times. elapsed time 6.486995777
|
38
|
+
javabin2 parse. 5000 times. elapsed time 1.73077786
|
39
|
+
3.748023317677521 times
|
29
40
|
|
30
41
|
* rsolr/solr-ruby support.
|
31
42
|
* pure and c extension code.
|
@@ -34,38 +45,49 @@ This is a Apache Solr JavaBin format (binary format) implementation for Ruby.
|
|
34
45
|
== Requirements
|
35
46
|
|
36
47
|
* Ruby1.8.7 or later (include 1.9.x)
|
37
|
-
* (recommended) C compiler: you can
|
38
|
-
* JavaBin has been tested with MRI 1.8.7, REE 1.8.7, YARV 1.9.2 pre1 on Ubuntu Linux 9.10 (32bit), and Apache Solr 1.4
|
48
|
+
* (recommended) C compiler: you can also use java_bin without c extension, but 'pure' is 30 times slower than 'ext'.
|
49
|
+
* JavaBin has been tested with MRI 1.8.7, REE 1.8.7, YARV 1.9.2 pre1 on Ubuntu Linux 9.10 (32bit) and MRI 1.8.6 on Windows Vista Business (32bit), and Apache Solr 1.4
|
39
50
|
|
40
51
|
== Install
|
41
52
|
|
53
|
+
(Linux)
|
42
54
|
gem sources -a http://gemcutter.org
|
43
55
|
gem install java_bin
|
44
56
|
|
57
|
+
(Windows)
|
58
|
+
start => visual studio c++ 200x xxxx edition => visual studio tools => visual studio 200x command prompt
|
59
|
+
gem sources -a http://gemcutter.org
|
60
|
+
gem install java_bin
|
61
|
+
or
|
62
|
+
cd (path to)\ext\java_bin\ext
|
63
|
+
ruby extconf.rb
|
64
|
+
nmake
|
65
|
+
|
45
66
|
== Simple Usage
|
46
67
|
|
47
68
|
require 'rubygems'
|
48
69
|
require 'java_bin'
|
49
70
|
|
50
71
|
@parser = JavaBin.parser.new
|
51
|
-
result = @parser.parse(
|
72
|
+
result = @parser.parse( javabin format string )
|
52
73
|
|
53
74
|
=== With RSolr/Solr-Ruby
|
54
75
|
|
55
76
|
* By RSolr/Solr-Ruby support, a parser automatically uses javabin format instead of ruby format.
|
56
77
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
78
|
+
require 'rubygems'
|
79
|
+
require 'rsolr'
|
80
|
+
require 'java_bin'
|
81
|
+
|
82
|
+
solr = RSolr.connect
|
83
|
+
solr.select :q => '*:*'
|
63
84
|
|
64
85
|
== TODO
|
65
86
|
|
66
87
|
* more parse speed
|
67
88
|
* license
|
68
|
-
* windows build
|
89
|
+
* windows build(1.9.x)
|
90
|
+
* 64bit build
|
69
91
|
* builder(writer)作成
|
70
92
|
* JRuby support
|
71
93
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/ext/java_bin/ext/extconf.rb
CHANGED
@@ -2,24 +2,24 @@
|
|
2
2
|
require 'mkmf'
|
3
3
|
require 'rbconfig'
|
4
4
|
|
5
|
-
unless
|
6
|
-
$CFLAGS
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
unless RUBY_PLATFORM =~ /mswin32/ # Linux
|
6
|
+
unless $CFLAGS.gsub!(/ -O[\dsz]?/, ' -O3')
|
7
|
+
$CFLAGS << ' -O3'
|
8
|
+
end
|
9
|
+
if CONFIG['CC'] =~ /gcc/
|
10
|
+
$CFLAGS << ' -Wall'
|
11
|
+
end
|
12
|
+
else # Windows
|
13
|
+
$CFLAGS.gsub!(/-O2b2xg-/, '/O2b2x')
|
14
|
+
$CFLAGS.gsub!(/-MD/, ' /MT')
|
15
|
+
$CFLAGS.gsub!(/ -G6/, '')
|
16
|
+
$CFLAGS << ' /wd4819' # VC++はUTF-8 BOM無しは駄目なのでここで抑制. gccは逆にBOM付は駄目
|
13
17
|
end
|
14
18
|
|
15
|
-
if RUBY_VERSION >= '1.9'
|
16
|
-
$CFLAGS << ' -DRUBY_19'
|
17
|
-
end
|
19
|
+
$CFLAGS << ' -DRUBY_19' if RUBY_VERSION >= '1.9'
|
18
20
|
|
19
|
-
have_header("endian.h")
|
20
21
|
have_header("byteswap.h")
|
21
22
|
have_header("sys/types.h")
|
22
|
-
|
23
23
|
have_header("ruby.h")
|
24
24
|
have_header("ruby/encoding.h")
|
25
25
|
|
data/ext/java_bin/ext/parser.c
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
/* utf-8 */
|
2
1
|
#include "parser.h"
|
3
2
|
|
4
3
|
/*
|
@@ -94,52 +93,73 @@ static VALUE JavaBinParser_read_string(JAVA_BIN_PARSER* ptr) {
|
|
94
93
|
|
95
94
|
static VALUE JavaBinParser_read_byte(JAVA_BIN_PARSER* ptr) {
|
96
95
|
int8_t c;
|
97
|
-
|
98
|
-
return
|
96
|
+
_readnumeric(ptr, c);
|
97
|
+
return INT2FIX(*((int8_t*)&c));
|
99
98
|
}
|
100
99
|
|
101
100
|
static VALUE JavaBinParser_read_short(JAVA_BIN_PARSER* ptr) {
|
102
101
|
u_int16_t c;
|
103
|
-
|
102
|
+
_readnumeric(ptr, c);
|
104
103
|
c = _swap_16(c);
|
105
|
-
return
|
104
|
+
return INT2FIX(*((int16_t*)&c));
|
106
105
|
}
|
107
106
|
|
108
107
|
static VALUE JavaBinParser_read_int(JAVA_BIN_PARSER* ptr) {
|
109
108
|
u_int32_t c;
|
110
|
-
|
109
|
+
_readnumeric(ptr, c);
|
111
110
|
c = _swap_32(c);
|
112
111
|
return INT2NUM(*((int32_t*)&c));
|
113
112
|
}
|
114
113
|
|
115
114
|
static VALUE JavaBinParser_read_long(JAVA_BIN_PARSER* ptr) {
|
116
115
|
u_int64_t c;
|
117
|
-
|
116
|
+
_readnumeric(ptr, c);
|
118
117
|
c = _swap_64(c);
|
119
118
|
return LL2NUM(*((int64_t*)&c));
|
120
119
|
}
|
121
120
|
|
122
121
|
static VALUE JavaBinParser_read_date(JAVA_BIN_PARSER* ptr) {
|
123
122
|
u_int64_t c;
|
124
|
-
|
123
|
+
_readnumeric(ptr, c);
|
125
124
|
c = _swap_64(c);
|
126
125
|
return rb_funcall(rb_cTime, i_At, 1, ULL2NUM(*((int64_t*)&c) / 1000));
|
127
126
|
}
|
128
127
|
|
129
128
|
static VALUE JavaBinParser_read_float(JAVA_BIN_PARSER* ptr) {
|
130
129
|
u_int32_t c;
|
131
|
-
|
130
|
+
_readnumeric(ptr, c);
|
132
131
|
c = _swap_32(c);
|
133
132
|
return rb_float_new((double)*((float*)&c));
|
134
133
|
}
|
135
134
|
|
136
135
|
static VALUE JavaBinParser_read_double(JAVA_BIN_PARSER* ptr) {
|
137
136
|
u_int64_t c;
|
138
|
-
|
137
|
+
_readnumeric(ptr, c);
|
139
138
|
c = _swap_64(c);
|
140
139
|
return rb_float_new(*((double*)&c));
|
141
140
|
}
|
142
141
|
|
142
|
+
static void JavaBinParser_extend_cache(JAVA_BIN_PARSER* ptr) {
|
143
|
+
_EXTERN_STRING_INFO* newP;
|
144
|
+
int next_size;
|
145
|
+
if (ptr->cache == NULL) {
|
146
|
+
next_size = 64;
|
147
|
+
} else {
|
148
|
+
next_size = ptr->cache_size * 2;
|
149
|
+
}
|
150
|
+
|
151
|
+
newP = (_EXTERN_STRING_INFO*) malloc(next_size * sizeof(_EXTERN_STRING_INFO));
|
152
|
+
if (!newP) {
|
153
|
+
rb_raise(rb_eRuntimeError, "JavaBinParser_extend_cache - allocate error");
|
154
|
+
}
|
155
|
+
|
156
|
+
if (ptr->cache) {
|
157
|
+
memcpy(newP, ptr->cache, sizeof(_EXTERN_STRING_INFO) * ptr->cache_size);
|
158
|
+
}
|
159
|
+
ptr->cache = newP;
|
160
|
+
ptr->cache_size = next_size;
|
161
|
+
}
|
162
|
+
|
143
163
|
static VALUE JavaBinParser_read_val(JAVA_BIN_PARSER* ptr) {
|
144
164
|
int size;
|
145
165
|
int i;
|
@@ -272,27 +292,6 @@ static VALUE JavaBinParser_alloc(VALUE klass) {
|
|
272
292
|
return Data_Wrap_Struct(klass, 0, JavaBinParser_free, NULL);
|
273
293
|
}
|
274
294
|
|
275
|
-
static void JavaBinParser_extend_cache(JAVA_BIN_PARSER* ptr) {
|
276
|
-
_EXTERN_STRING_INFO* newP;
|
277
|
-
int next_size;
|
278
|
-
if (ptr->cache == NULL) {
|
279
|
-
next_size = 64;
|
280
|
-
} else {
|
281
|
-
next_size = ptr->cache_size * 2;
|
282
|
-
}
|
283
|
-
|
284
|
-
newP = (_EXTERN_STRING_INFO*) malloc(next_size * sizeof(_EXTERN_STRING_INFO));
|
285
|
-
if (!newP) {
|
286
|
-
rb_raise(rb_eRuntimeError, "JavaBinParser_extend_cache - allocate error");
|
287
|
-
}
|
288
|
-
|
289
|
-
if (ptr->cache) {
|
290
|
-
memcpy(newP, ptr->cache, sizeof(_EXTERN_STRING_INFO) * ptr->cache_size);
|
291
|
-
}
|
292
|
-
ptr->cache = newP;
|
293
|
-
ptr->cache_size = next_size;
|
294
|
-
}
|
295
|
-
|
296
295
|
/*
|
297
296
|
* rubyメソッド
|
298
297
|
*/
|
data/ext/java_bin/ext/parser.h
CHANGED
@@ -1,23 +1,18 @@
|
|
1
|
-
/* utf-8 */
|
2
1
|
#ifndef _PARSER_H_
|
3
2
|
#define _PARSER_H_
|
4
3
|
|
5
4
|
#include "ruby.h"
|
6
5
|
|
7
|
-
#if HAVE_ENDIAN_H
|
8
|
-
#include <endian.h>
|
9
|
-
#endif /* HAVE_ENDIAN_H */
|
10
|
-
|
11
6
|
#if HAVE_BYTESWAP_H
|
12
|
-
#include <byteswap.h>
|
7
|
+
#include <byteswap.h>
|
13
8
|
#endif /* HAVE_BYTESWAP_H */
|
14
9
|
|
15
10
|
#if HAVE_SYS_TYPES_H
|
16
|
-
#include <sys/types.h>
|
11
|
+
#include <sys/types.h>
|
17
12
|
#endif /* HAVE_SYS_TYPES_H */
|
18
13
|
|
19
14
|
#ifdef HAVE_RUBY_ENCODING_H
|
20
|
-
#include <ruby/encoding.h>
|
15
|
+
#include <ruby/encoding.h>
|
21
16
|
#endif /* HAVE_RUBY_ENCODING_H */
|
22
17
|
|
23
18
|
/*
|
@@ -86,30 +81,47 @@ typedef struct java_bin_parser {
|
|
86
81
|
} JAVA_BIN_PARSER;
|
87
82
|
|
88
83
|
#ifdef HAVE_RUBY_ENCODING_H
|
89
|
-
#define _utf8_string(str, len) (rb_enc_str_new(str, len, rb_encUtf8))
|
84
|
+
#define _utf8_string(str, len) (rb_enc_str_new(str, len, rb_encUtf8))
|
90
85
|
#else
|
91
|
-
#define _utf8_string(str, len) (rb_str_new(str, len))
|
86
|
+
#define _utf8_string(str, len) (rb_str_new(str, len))
|
92
87
|
#endif
|
93
88
|
|
94
89
|
#define _getbyte(ptr) (ptr->data[ptr->current++])
|
95
90
|
#define _skipbytes(ptr, x) ((ptr->current) += (x))
|
96
91
|
#define _readnumeric(ptr, c) \
|
97
|
-
|
92
|
+
{ u_int8_t* p; \
|
98
93
|
p = (void*)&c; \
|
99
94
|
memcpy(p, &ptr->data[ptr->current], sizeof(c)); \
|
100
|
-
_skipbytes(ptr, sizeof(c));
|
101
|
-
|
102
|
-
|
103
|
-
#
|
104
|
-
#
|
105
|
-
|
106
|
-
|
95
|
+
_skipbytes(ptr, sizeof(c)); \
|
96
|
+
}
|
97
|
+
|
98
|
+
#ifndef WORDS_BIGENDIAN
|
99
|
+
#ifdef _WIN32
|
100
|
+
#define _swap_16(o) _byteswap_ushort(o)
|
101
|
+
#define _swap_32(o) _byteswap_ulong(o)
|
102
|
+
#define _swap_64(o) _byteswap_uint64(o)
|
103
|
+
#else
|
104
|
+
#define _swap_16(o) bswap_16(o)
|
105
|
+
#define _swap_32(o) bswap_32(o)
|
106
|
+
#define _swap_64(o) bswap_64(o)
|
107
|
+
#endif
|
107
108
|
#else
|
108
109
|
#define _swap_16(o) (o)
|
109
110
|
#define _swap_32(o) (o)
|
110
111
|
#define _swap_64(o) (o)
|
111
112
|
#endif
|
112
113
|
|
114
|
+
#ifdef _WIN32
|
115
|
+
typedef signed char int8_t;
|
116
|
+
typedef signed short int16_t;
|
117
|
+
typedef signed int int32_t;
|
118
|
+
typedef signed __int64 int64_t;
|
119
|
+
typedef unsigned char u_int8_t;
|
120
|
+
typedef unsigned short u_int16_t;
|
121
|
+
typedef unsigned int u_int32_t;
|
122
|
+
typedef unsigned __int64 u_int64_t;
|
123
|
+
#endif
|
124
|
+
|
113
125
|
static int32_t JavaBinParser_read_v_int(JAVA_BIN_PARSER* ptr);
|
114
126
|
static int64_t JavaBinParser_read_v_long(JAVA_BIN_PARSER* ptr);
|
115
127
|
static int JavaBinParser_read_size(JAVA_BIN_PARSER* ptr);
|
data/java_bin.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{java_bin}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["kennyj"]
|
12
|
-
s.date = %q{2010-01-
|
12
|
+
s.date = %q{2010-01-20}
|
13
13
|
s.description = %q{Apache Solr JavaBin format (binary format) implementation for Ruby.}
|
14
14
|
s.email = %q{kennyj@gmail.com}
|
15
15
|
s.extensions = ["ext/java_bin/ext/extconf.rb"]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# vim:fileencoding=utf-8
|
2
|
+
|
2
3
|
require 'stringio'
|
3
4
|
require 'helper'
|
4
5
|
|
@@ -12,13 +13,15 @@ end
|
|
12
13
|
|
13
14
|
class TestJavaBinParser < Test::Unit::TestCase
|
14
15
|
|
16
|
+
READ_FLAG = (RUBY_VERSION >= '1.9' ? 'r:utf-8' : 'rb')
|
17
|
+
|
15
18
|
private
|
16
19
|
def write_v_int(i, output)
|
17
|
-
while ((i & ~0x7F) != 0)
|
20
|
+
while ((i & ~0x7F) != 0)
|
18
21
|
output.putc(((i & 0x7f) | 0x80))
|
19
22
|
# i >>>= 7
|
20
23
|
i = (i >> 7) # TODO 論理シフト
|
21
|
-
end
|
24
|
+
end
|
22
25
|
output.putc(i)
|
23
26
|
end
|
24
27
|
|
@@ -27,9 +30,9 @@ class TestJavaBinParser < Test::Unit::TestCase
|
|
27
30
|
if (size < 0x1f)
|
28
31
|
output.putc(tag | size)
|
29
32
|
else
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
+
output.putc(tag | 0x1f)
|
34
|
+
write_v_int(size - 0x1f, output)
|
35
|
+
end
|
33
36
|
else
|
34
37
|
output.putc(tag)
|
35
38
|
write_v_int(size, output)
|
@@ -60,22 +63,22 @@ class TestJavaBinParser < Test::Unit::TestCase
|
|
60
63
|
end
|
61
64
|
|
62
65
|
def test_javabin_dat
|
63
|
-
result = @parser.parse(open("fixtures/javabin.dat",
|
66
|
+
result = @parser.parse(open("fixtures/javabin.dat", READ_FLAG).read)
|
64
67
|
assert result['response']['docs'][0]['features'].include?('eaiou with umlauts: ëäïöü')
|
65
68
|
assert_equal result['response']['docs'][1]['incubationdate_dt'], Time.local(2006, 1, 17, 9, 0, 0)
|
66
|
-
assert_equal result['response']['docs'][1]['score'], 0.5030758380889893
|
69
|
+
assert_equal result['response']['docs'][1]['score'], 0.5030758380889893
|
67
70
|
end
|
68
71
|
|
69
72
|
def test_javabin2_dat
|
70
|
-
result = @parser.parse(open("fixtures/javabin2.dat",
|
73
|
+
result = @parser.parse(open("fixtures/javabin2.dat", READ_FLAG).read)
|
71
74
|
assert_equal 19, result['response']['docs'].size
|
72
75
|
end
|
73
76
|
|
74
77
|
|
75
78
|
TIMES = 5000
|
76
79
|
def test_javabin_parse_and_ruby_eval
|
77
|
-
r = open("fixtures/ruby.dat",
|
78
|
-
jb = open("fixtures/javabin.dat",
|
80
|
+
r = open("fixtures/ruby.dat", READ_FLAG).read
|
81
|
+
jb = open("fixtures/javabin.dat", READ_FLAG).read
|
79
82
|
puts ""
|
80
83
|
r_et = elapsed_time("ruby eval parse. ", TIMES) { eval(r) }
|
81
84
|
jb_et = elapsed_time("javabin parse. ", TIMES) { @parser.parse(jb) }
|
@@ -84,8 +87,8 @@ class TestJavaBinParser < Test::Unit::TestCase
|
|
84
87
|
end
|
85
88
|
|
86
89
|
def test_javabin2_parse_and_ruby2_eval
|
87
|
-
r = open("fixtures/ruby2.dat",
|
88
|
-
jb = open("fixtures/javabin2.dat",
|
90
|
+
r = open("fixtures/ruby2.dat", READ_FLAG).read
|
91
|
+
jb = open("fixtures/javabin2.dat", READ_FLAG).read
|
89
92
|
puts ""
|
90
93
|
r_et = elapsed_time("ruby2 eval parse. ", TIMES) { eval(r) }
|
91
94
|
jb_et = elapsed_time("javabin2 parse. ", TIMES) { @parser.parse(jb) }
|
@@ -185,7 +188,7 @@ class TestJavaBinParser < Test::Unit::TestCase
|
|
185
188
|
write_v_int(array.size, sio)
|
186
189
|
array.each { |e| sio.putc e }
|
187
190
|
sio.pos = 0
|
188
|
-
assert_equal array, @parser.parse(sio.read)
|
191
|
+
assert_equal array, @parser.parse(sio.read)
|
189
192
|
end
|
190
193
|
|
191
194
|
def test_iterator
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: java_bin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- kennyj
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-01-
|
12
|
+
date: 2010-01-20 00:00:00 +09:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|