ffi-radix_tree 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c2547ed89d9cbedb9bd340a86785aa16f0becd3c
4
- data.tar.gz: 737b4483c593146c8c9fef84330f9ed5eac149ee
3
+ metadata.gz: b44935a7f81323aa80f656cab493c5637d3029a0
4
+ data.tar.gz: f2854b861c6c3507110328ce5e291f533938f97c
5
5
  SHA512:
6
- metadata.gz: 79474879d50035f089b2a04b60db2dee547e9aabf4aeffa43c33b430156bac7004e55da050e35bb71cda97eede7bcda077c9ca4a53dbc66809d040a06c04d277
7
- data.tar.gz: becb72bae362661101c75aed79d53c42f9d698c19116593b0180971026ba8e59eebcb712d8c65ea7e4e5ffbd71a9de6ef6dcf46b2a22141cc2e64288fdafddb9
6
+ metadata.gz: 45f6f4eb51181c92fedeb6fbfa577b67a73a921196edfc4ce937c251dd9a660d9ff6888c1f17f2afe67cd5d2858997ce947946588448e4d2e87810138bae4764
7
+ data.tar.gz: ecb205e7db9b41cf866fce8709d894eebf1b21472fd5546b2fb529dea605bd9a06494c66d83d5d1401e00b601861d72e1108dd34243320d1059595e5b1d4fcb8
data/README.md CHANGED
@@ -22,7 +22,44 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ ```ruby
26
+ # create a new tree
27
+ rtree = ::FFI::RadixTree::Tree.new
28
+
29
+ # add key/value pairs to the tree
30
+ rtree.push("key1", "value1")
31
+ rtree.push("key2", ["value2", "value3"])
32
+ rtree.push("key3", { :attr1 => "value4", :attr2 => "value5" })
33
+
34
+ # work with the collection
35
+ if rtree.has_key?("key1")
36
+ val = rtree.get("key2")
37
+ rtree.set("key3", "value6")
38
+ end
39
+
40
+ # search the tree using the keys
41
+
42
+ # keys based off the root word: "manage"
43
+ rtree.push("manage", "base verb")
44
+ rtree.push("managed", "past tense")
45
+ rtree.push("manager", "noun")
46
+ rtree.push("managers", "plural noun")
47
+ rtree.push("managing", "present tense")
48
+
49
+ # Find the key that matches the _most_ of the beggining of the search term
50
+ rtree.longest_prefix("managerial") # returns "manager"
51
+ rtree.longest_prefix_and_value("managerial") # returns ["manager", "noun"]
52
+ rtree.longest_prefix_value("managerial") # returns "noun"
53
+
54
+ # Find all values whose keys match the _most_ of the beginning of the search term
55
+ rtree.greedy_match("managerial") # returns ["noun", "plural noun"]
56
+
57
+ # Find all values whose keys are included _anywhere_ in the search term
58
+ rtree.greedy_substring_match("I managed to jump") # returns ["base verb", "past tense"]
59
+
60
+ # cleanup
61
+ rtree.destroy!
62
+ ```
26
63
 
27
64
  ## Development
28
65
 
data/Rakefile CHANGED
@@ -7,6 +7,67 @@ namespace :radixtree do
7
7
  task :compile do
8
8
  Rake::Task[:compile_radixtree].invoke
9
9
  end
10
+
11
+ desc "run benchmarks for radixtree lib"
12
+ task :benchmark do
13
+ require "benchmark/ips"
14
+ require "./lib/ffi/radix_tree"
15
+
16
+ radix_tree = ::FFI::RadixTree::Tree.new
17
+
18
+ (1..1000).each do |number|
19
+ radix_tree.push(number.to_s, "DERP#{number}" * (number % 10))
20
+ radix_tree.push(number.to_s * 100, "DERP#{number}" * (number % 10))
21
+ radix_tree.push(number.to_s * 1000, "DERP#{number}" * (number % 10))
22
+ end
23
+
24
+ ::Benchmark.ips do |x|
25
+ x.config(:warmup => 10)
26
+
27
+ x.report("get") do
28
+ radix_tree.get(rand(1000).to_s * [1, 100, 100].sample)
29
+ end
30
+
31
+ x.report("longest prefix") do
32
+ radix_tree.longest_prefix(rand(1000).to_s * [1, 100, 100].sample)
33
+ end
34
+
35
+ x.report("get then value") do
36
+ val = rand(1000).to_s * [1, 100, 100].sample
37
+
38
+ radix_tree.longest_prefix(val)
39
+ radix_tree.longest_prefix_value(val)
40
+ end
41
+
42
+ x.report("prefix and value (combined)") do
43
+ radix_tree.longest_prefix_and_value(rand(1000).to_s * [1, 100, 100].sample)
44
+ end
45
+
46
+ x.report("longest prefix (miss)") do
47
+ radix_tree.longest_prefix("DERP DERPIE")
48
+ end
49
+
50
+ x.report("prefix and value (combined/miss)") do
51
+ radix_tree.longest_prefix_and_value("DERP DERPIE")
52
+ end
53
+
54
+ x.report("greedy match") do
55
+ radix_tree.greedy_match(rand(1000).to_s * [1, 100, 100].sample)
56
+ end
57
+
58
+ x.report("greedy match (miss)") do
59
+ radix_tree.greedy_match("DERP DERPIE")
60
+ end
61
+
62
+ x.report("greedy substring match") do
63
+ radix_tree.greedy_substring_match(rand(1000).to_s * [1, 100, 100].sample)
64
+ end
65
+
66
+ x.report("greedy substring match (miss)") do
67
+ radix_tree.greedy_substring_match("DERP DERPIE")
68
+ end
69
+ end
70
+ end
10
71
  end
11
72
 
12
73
  Rake::TestTask.new(:spec) do |t|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "msgpack"
35
35
  spec.add_dependency "ffi"
36
36
 
37
+ spec.add_development_dependency "benchmark-ips"
37
38
  spec.add_development_dependency "bundler", "~> 1.15"
38
39
  spec.add_development_dependency "rake", "~> 10.0"
39
40
  spec.add_development_dependency "mocha"
@@ -77,9 +77,14 @@ module FFI
77
77
  attach_function :erase, [:pointer, :string], :void
78
78
  attach_function :fetch, [:pointer, :string, :pointer], :pointer
79
79
  attach_function :insert, [:pointer, :string, :pointer, :size_t], :void
80
+ attach_function :update, [:pointer, :string, :pointer, :size_t], :bool
80
81
  attach_function :longest_prefix, [:pointer, :string], :string
82
+ attach_function :longest_prefix_and_value, [:pointer, :string, :pointer, :pointer], :pointer
81
83
  attach_function :longest_prefix_value, [:pointer, :string, :pointer], :pointer
84
+ attach_function :greedy_match, [:pointer, :string, :pointer, :pointer], :int
85
+ attach_function :greedy_substring_match, [:pointer, :string, :pointer, :pointer], :int
82
86
  attach_function :match_free, [:pointer], :void
87
+ attach_function :multi_match_free, [:pointer, :int], :void
83
88
  attach_function :has_key, [:pointer, :string], :bool
84
89
 
85
90
  class Tree
@@ -115,6 +120,21 @@ module FFI
115
120
  push_response
116
121
  end
117
122
 
123
+ def push_or_update(key, value)
124
+ response = nil
125
+ @first_character_present[key[0]] = true
126
+ storage_data = ::MessagePack.pack(value)
127
+ bytesize = storage_data.bytesize
128
+
129
+ ::FFI::MemoryPointer.new(:char, bytesize, true) do |memory_buffer|
130
+ memory_buffer.put_bytes(0, storage_data)
131
+ response = ::FFI::RadixTree.update(@ptr, key, memory_buffer, bytesize)
132
+ response ||= ::FFI::RadixTree.insert(@ptr, key, memory_buffer, bytesize)
133
+ end
134
+
135
+ response
136
+ end
137
+
118
138
  def get(key)
119
139
  return nil unless @first_character_present[key[0]]
120
140
  byte_pointer = get_response = nil
@@ -122,7 +142,11 @@ module FFI
122
142
  ::FFI::MemoryPointer.new(:int) do |byte_length|
123
143
  byte_pointer = ::FFI::RadixTree.fetch(@ptr, key, byte_length)
124
144
  bytesize = byte_length.read_int
125
- get_response = ::MessagePack.unpack(byte_pointer.get_bytes(0, bytesize)) if bytesize && bytesize > 0
145
+
146
+ if bytesize && bytesize > 0
147
+ bytes = byte_pointer.get_bytes(0, bytesize)
148
+ get_response = ::MessagePack.unpack(bytes)
149
+ end
126
150
  end
127
151
 
128
152
  get_response
@@ -139,6 +163,29 @@ module FFI
139
163
  ::FFI::RadixTree.match_free(p_out) if p_out
140
164
  end
141
165
 
166
+ def longest_prefix_and_value(string)
167
+ return [nil, nil] unless @first_character_present[string[0]]
168
+ byte_pointer = prefix_response = get_response = nil
169
+
170
+ ::FFI::MemoryPointer.new(:int) do |byte_length|
171
+ ::FFI::MemoryPointer.new(:int) do |prefix_length|
172
+ byte_pointer = ::FFI::RadixTree.longest_prefix_and_value(@ptr, string, byte_length, prefix_length)
173
+ bytesize = byte_length.read_int
174
+
175
+ if bytesize && bytesize > 0
176
+ prefix_size = prefix_length.read_int
177
+ get_response = byte_pointer.get_bytes(0, bytesize)
178
+ prefix_response = get_response[0..(prefix_size - 1)]
179
+ get_response = ::MessagePack.unpack(get_response[prefix_size..-1])
180
+ end
181
+ end
182
+ end
183
+
184
+ [prefix_response, get_response]
185
+ ensure
186
+ ::FFI::RadixTree.match_free(byte_pointer) if byte_pointer
187
+ end
188
+
142
189
  def longest_prefix_value(string)
143
190
  return nil unless @first_character_present[string[0]]
144
191
  byte_pointer = get_response = nil
@@ -153,6 +200,61 @@ module FFI
153
200
  ensure
154
201
  ::FFI::RadixTree.match_free(byte_pointer) if byte_pointer
155
202
  end
203
+
204
+ def greedy_match(string)
205
+ return [] unless @first_character_present[string[0]]
206
+ array_pointer = nil
207
+ match_sizes_pointer = nil
208
+ array_size = 0
209
+ get_response = []
210
+
211
+ ::FFI::MemoryPointer.new(:pointer) do |match_array|
212
+ ::FFI::MemoryPointer.new(:pointer) do |match_sizes_array|
213
+ array_size = ::FFI::RadixTree.greedy_match(@ptr, string, match_array, match_sizes_array)
214
+ if array_size > 0
215
+ array_sizes_pointer = match_sizes_array.read_pointer
216
+ match_sizes = array_sizes_pointer.get_array_of_int(0, array_size)
217
+ array_pointer = match_array.read_pointer
218
+ char_arrays = array_pointer.get_array_of_pointer(0, array_size)
219
+ char_arrays.each_with_index do |ptr, index|
220
+ get_response << ::MessagePack.unpack(ptr.get_bytes(0, match_sizes[index]))
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ get_response
227
+ ensure
228
+ ::FFI::RadixTree.multi_match_free(array_pointer, array_size) if array_pointer
229
+ ::FFI::RadixTree.match_free(match_sizes_pointer) if match_sizes_pointer
230
+ end
231
+
232
+ def greedy_substring_match(string)
233
+ array_pointer = nil
234
+ match_sizes_pointer = nil
235
+ array_size = 0
236
+ get_response = []
237
+
238
+ ::FFI::MemoryPointer.new(:pointer) do |match_array|
239
+ ::FFI::MemoryPointer.new(:pointer) do |match_sizes_array|
240
+ array_size = ::FFI::RadixTree.greedy_substring_match(@ptr, string, match_array, match_sizes_array)
241
+ if array_size > 0
242
+ array_sizes_pointer = match_sizes_array.read_pointer
243
+ match_sizes = array_sizes_pointer.get_array_of_int(0, array_size)
244
+ array_pointer = match_array.read_pointer
245
+ char_arrays = array_pointer.get_array_of_pointer(0, array_size)
246
+ char_arrays.each_with_index do |ptr, index|
247
+ get_response << ::MessagePack.unpack(ptr.get_bytes(0, match_sizes[index]))
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ get_response
254
+ ensure
255
+ ::FFI::RadixTree.multi_match_free(array_pointer, array_size) if array_pointer
256
+ ::FFI::RadixTree.match_free(match_sizes_pointer) if match_sizes_pointer
257
+ end
156
258
  end
157
259
  end
158
260
  end
@@ -1,5 +1,5 @@
1
1
  module FFI
2
2
  module RadixTree
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -38,13 +38,23 @@ void match_free(const char* match) {
38
38
  }
39
39
  }
40
40
 
41
+ void multi_match_free(const char** match, int length) {
42
+ if (match != NULL) {
43
+ for (int i=0; i<length; ++i) {
44
+ delete[] match[i];
45
+ match[i] = NULL;
46
+ }
47
+ delete[] match;
48
+ match = NULL;
49
+ }
50
+ }
51
+
41
52
  const char* longest_prefix(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key) {
42
53
  std::string string_key(key);
43
54
  auto iter = map_pointer->longest_match(string_key);
44
55
 
45
56
  if (iter != map_pointer->end()) {
46
57
  char *val = new char[iter->first.size() + 1]{0};
47
- val[iter->first.size()] = '\0';
48
58
  memcpy(val, iter->first.c_str(), iter->first.size());
49
59
 
50
60
  return val;
@@ -53,6 +63,33 @@ const char* longest_prefix(radix_tree<std::string, std::vector<char>>* map_point
53
63
  return NULL;
54
64
  }
55
65
 
66
+ const char* longest_prefix_and_value(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, int* read_size, int* prefix_size) {
67
+ std::string string_key(key);
68
+ auto iter = map_pointer->longest_match(string_key);
69
+
70
+ if (iter != map_pointer->end()) {
71
+ long counter = 0;
72
+ int size_of_response = iter->second.size() + iter->first.size();
73
+ char *return_val = new char[size_of_response]{0};
74
+
75
+ strncpy(return_val, iter->first.c_str(), iter->first.size());
76
+ counter = iter->first.size();
77
+
78
+ for( auto& val3 : iter->second) {
79
+ return_val[counter] = val3;
80
+ counter++;
81
+ }
82
+
83
+ *prefix_size = iter->first.size();
84
+ *read_size = size_of_response;
85
+ return return_val;
86
+ }
87
+
88
+ *prefix_size = 0;
89
+ *read_size = 0;
90
+ return NULL;
91
+ }
92
+
56
93
  const char* longest_prefix_value(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, int* read_size) {
57
94
  std::string string_key(key);
58
95
  auto iter = map_pointer->longest_match(string_key);
@@ -73,12 +110,12 @@ const char* longest_prefix_value(radix_tree<std::string, std::vector<char>>* map
73
110
  return NULL;
74
111
  }
75
112
 
76
- const char* fetch(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, int* read_size) {
113
+ unsigned char* fetch(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, int* read_size) {
77
114
  auto iter = map_pointer->find(std::string(key));
78
115
  long counter = 0;
79
116
 
80
117
  if (iter != map_pointer->end()) {
81
- char *return_val = new char[iter->second.size()]{0};
118
+ unsigned char *return_val = new unsigned char[iter->second.size()]{0};
82
119
  for( auto& val : iter->second ) {
83
120
  return_val[counter] = val;
84
121
  counter++;
@@ -92,10 +129,64 @@ const char* fetch(radix_tree<std::string, std::vector<char>>* map_pointer, const
92
129
  return NULL;
93
130
  }
94
131
 
132
+ int greedy_match(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, const char*** matches, int** match_sizes) {
133
+ std::string string_key(key);
134
+ typedef radix_tree<std::string, std::vector<char>>::iterator iterator;
135
+ std::vector<iterator> vec;
136
+ map_pointer->greedy_match(string_key, vec);
137
+ long counter = 0;
138
+
139
+ if (vec.size() > 0) {
140
+ *matches = new const char* [vec.size()]{nullptr};
141
+ *match_sizes = new int [vec.size()]{0};
142
+ for (auto& iter : vec) {
143
+ auto ret_str = new char[iter->second.size()];
144
+ long char_index = 0;
145
+ for (auto& val : iter->second) {
146
+ ret_str[char_index] = val;
147
+ ++char_index;
148
+ }
149
+ (*matches)[counter] = ret_str;
150
+ (*match_sizes)[counter] = iter->second.size();
151
+ ++counter;
152
+ }
153
+ }
154
+ return counter;
155
+ }
156
+
157
+ int greedy_substring_match(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, const char*** matches, int** match_sizes) {
158
+ std::string string_key(key);
159
+ typedef radix_tree<std::string, std::vector<char>>::iterator iterator;
160
+ std::vector<iterator> vec;
161
+ map_pointer->greedy_substring_match(string_key, vec);
162
+ long counter = 0;
163
+
164
+ if (vec.size() > 0) {
165
+ *matches = new const char* [vec.size()]{nullptr};
166
+ *match_sizes = new int [vec.size()]{0};
167
+ for (auto& iter : vec) {
168
+ auto ret_str = new char[iter->second.size()];
169
+ long char_index = 0;
170
+ for (auto& val : iter->second) {
171
+ ret_str[char_index] = val;
172
+ ++char_index;
173
+ }
174
+ (*matches)[counter] = ret_str;
175
+ (*match_sizes)[counter] = iter->second.size();
176
+ ++counter;
177
+ }
178
+ }
179
+ return counter;
180
+ }
181
+
95
182
  void insert(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, char* value, size_t size) {
96
183
  map_pointer->insert({std::string(key), std::vector<char>(value, value + size)});
97
184
  }
98
185
 
186
+ bool update(radix_tree<std::string, std::vector<char>>* map_pointer, const char* key, char* value, size_t size) {
187
+ return map_pointer->update({std::string(key), std::vector<char>(value, value + size)});
188
+ }
189
+
99
190
  void destroy(radix_tree<std::string, std::vector<char>>* map_pointer) {
100
191
  delete map_pointer;
101
192
  map_pointer = NULL;
@@ -67,10 +67,12 @@ public:
67
67
  iterator end();
68
68
 
69
69
  std::pair<iterator, bool> insert(const value_type &val);
70
+ bool update(const value_type &val);
70
71
  bool erase(const K &key);
71
72
  void erase(iterator it);
72
73
  void prefix_match(const K &key, std::vector<iterator> &vec);
73
74
  void greedy_match(const K &key, std::vector<iterator> &vec);
75
+ void greedy_substring_match(const K &key, std::vector<iterator> &vec);
74
76
  iterator longest_match(const K &key);
75
77
 
76
78
  T& operator[] (const K &lhs);
@@ -81,9 +83,11 @@ private:
81
83
 
82
84
  radix_tree_node<K, T>* begin(radix_tree_node<K, T> *node);
83
85
  radix_tree_node<K, T>* find_node(const K &key, radix_tree_node<K, T> *node, int depth);
86
+ std::vector<radix_tree_node<K, T>*> find_leaf_nodes_with_substr(const K &key, radix_tree_node<K, T> *node, const K &ancestor_key);
84
87
  radix_tree_node<K, T>* append(radix_tree_node<K, T> *parent, const value_type &val);
85
88
  radix_tree_node<K, T>* prepend(radix_tree_node<K, T> *node, const value_type &val);
86
89
  void greedy_match(radix_tree_node<K, T> *node, std::vector<iterator> &vec);
90
+ void greedy_substring_match(radix_tree_node<K, T> *node, std::vector<iterator> &vec);
87
91
 
88
92
  radix_tree(const radix_tree& other); // delete
89
93
  radix_tree& operator =(const radix_tree other); // delete
@@ -232,6 +236,23 @@ void radix_tree<K, T>::greedy_match(radix_tree_node<K, T> *node, std::vector<ite
232
236
  }
233
237
  }
234
238
 
239
+ template <typename K, typename T>
240
+ void radix_tree<K, T>::greedy_substring_match(const K &key, std::vector<iterator> &vec)
241
+ {
242
+ std::vector<radix_tree_node<K, T>*> nodes;
243
+
244
+ vec.clear();
245
+
246
+ if (m_root == NULL)
247
+ return;
248
+
249
+ nodes = find_leaf_nodes_with_substr(key, m_root, "");
250
+
251
+ for (auto& node : nodes) {
252
+ vec.push_back(node);
253
+ }
254
+ }
255
+
235
256
  template <typename K, typename T>
236
257
  void radix_tree<K, T>::erase(iterator it)
237
258
  {
@@ -448,6 +469,22 @@ std::pair<typename radix_tree<K, T>::iterator, bool> radix_tree<K, T>::insert(co
448
469
  }
449
470
  }
450
471
 
472
+ template <typename K, typename T>
473
+ bool radix_tree<K, T>::update(const value_type &val)
474
+ {
475
+ if (m_root == NULL)
476
+ return false;
477
+
478
+ radix_tree_node<K, T> *node = find_node(val.first, m_root, 0);
479
+
480
+ // if the node is a internal node, return false
481
+ if (! node->m_is_leaf)
482
+ return false;
483
+
484
+ node->m_value = new value_type(val);
485
+ return true;
486
+ }
487
+
451
488
  template <typename K, typename T>
452
489
  typename radix_tree<K, T>::iterator radix_tree<K, T>::find(const K &key)
453
490
  {
@@ -495,6 +532,30 @@ radix_tree_node<K, T>* radix_tree<K, T>::find_node(const K &key, radix_tree_node
495
532
  return node;
496
533
  }
497
534
 
535
+ template <typename K, typename T>
536
+ std::vector<radix_tree_node<K, T>*> radix_tree<K, T>::find_leaf_nodes_with_substr(const K &key, radix_tree_node<K, T> *node, const K &ancestor_key)
537
+ {
538
+ std::vector<radix_tree_node<K, T>*> nodes;
539
+
540
+ if (node->m_children.empty())
541
+ return nodes;
542
+
543
+ typename radix_tree_node<K, T>::it_child it;
544
+ for (it = node->m_children.begin(); it != node->m_children.end(); ++it) {
545
+ if (it->second->m_is_leaf) {
546
+ nodes.push_back(it->second);
547
+ } else {
548
+ auto child_key = ancestor_key + it->first;
549
+ if (key.find(child_key) != std::string::npos ) {
550
+ auto found_nodes = find_leaf_nodes_with_substr(key, it->second, child_key);
551
+ nodes.insert(nodes.end(), found_nodes.begin(), found_nodes.end());
552
+ }
553
+ }
554
+ }
555
+
556
+ return nodes;
557
+ }
558
+
498
559
  /*
499
560
 
500
561
  (root)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-radix_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Dewitt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-23 00:00:00.000000000 Z
11
+ date: 2018-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: benchmark-ips
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement