From cd7ebbe54f16999b74c2c32a64336bad131ec5f3 Mon Sep 17 00:00:00 2001 From: Adam Greene Date: Sun, 3 May 2015 20:42:11 -0700 Subject: make repeated_field quack like an array --- ruby/ext/google/protobuf_c/protobuf.h | 4 +- ruby/ext/google/protobuf_c/repeated_field.c | 125 ++-- ruby/lib/google/protobuf/repeated_field.rb | 152 ++++- .../google/protobuf/jruby/RubyFieldDescriptor.java | 43 +- .../google/protobuf/jruby/RubyRepeatedField.java | 136 ++--- ruby/tests/basic.rb | 4 +- ruby/tests/repeated_field_test.rb | 640 +++++++++++++++++++++ 7 files changed, 979 insertions(+), 125 deletions(-) create mode 100644 ruby/tests/repeated_field_test.rb diff --git a/ruby/ext/google/protobuf_c/protobuf.h b/ruby/ext/google/protobuf_c/protobuf.h index d8a327aa..985b7f3d 100644 --- a/ruby/ext/google/protobuf_c/protobuf.h +++ b/ruby/ext/google/protobuf_c/protobuf.h @@ -361,13 +361,13 @@ extern VALUE cRepeatedField; RepeatedField* ruby_to_RepeatedField(VALUE value); VALUE RepeatedField_each(VALUE _self); -VALUE RepeatedField_index(VALUE _self, VALUE _index); +VALUE RepeatedField_index(int argc, VALUE* argv, VALUE _self); void* RepeatedField_index_native(VALUE _self, int index); VALUE RepeatedField_index_set(VALUE _self, VALUE _index, VALUE val); void RepeatedField_reserve(RepeatedField* self, int new_size); VALUE RepeatedField_push(VALUE _self, VALUE val); void RepeatedField_push_native(VALUE _self, void* data); -VALUE RepeatedField_pop(VALUE _self); +VALUE RepeatedField_pop_one(VALUE _self); VALUE RepeatedField_insert(int argc, VALUE* argv, VALUE _self); VALUE RepeatedField_replace(VALUE _self, VALUE list); VALUE RepeatedField_clear(VALUE _self); diff --git a/ruby/ext/google/protobuf_c/repeated_field.c b/ruby/ext/google/protobuf_c/repeated_field.c index 5148ee87..dc1d0355 100644 --- a/ruby/ext/google/protobuf_c/repeated_field.c +++ b/ruby/ext/google/protobuf_c/repeated_field.c @@ -55,6 +55,21 @@ static int index_position(VALUE _index, RepeatedField* repeated_field) { return index; } +VALUE RepeatedField_subarray(VALUE _self, long beg, long len) { + RepeatedField* self = ruby_to_RepeatedField(_self); + int element_size = native_slot_size(self->field_type); + upb_fieldtype_t field_type = self->field_type; + VALUE field_type_class = self->field_type_class; + + size_t off = beg * element_size; + VALUE ary = rb_ary_new2(len); + for (int i = beg; i < beg + len; i++, off += element_size) { + void* mem = ((uint8_t *)self->elements) + off; + VALUE elem = native_slot_get(field_type, field_type_class, mem); + rb_ary_push(ary, elem); + } + return ary; +} /* * call-seq: @@ -76,28 +91,57 @@ VALUE RepeatedField_each(VALUE _self) { VALUE val = native_slot_get(field_type, field_type_class, memory); rb_yield(val); } - return Qnil; + return _self; } + /* * call-seq: * RepeatedField.[](index) => value * * Accesses the element at the given index. Returns nil on out-of-bounds */ -VALUE RepeatedField_index(VALUE _self, VALUE _index) { +VALUE RepeatedField_index(int argc, VALUE* argv, VALUE _self) { RepeatedField* self = ruby_to_RepeatedField(_self); int element_size = native_slot_size(self->field_type); upb_fieldtype_t field_type = self->field_type; VALUE field_type_class = self->field_type_class; - int index = index_position(_index, self); - if (index < 0 || index >= self->size) { + VALUE arg = argv[0]; + long beg, len; + + if (argc == 1){ + if (FIXNUM_P(arg)) { + /* standard case */ + int index = index_position(argv[0], self); + if (index < 0 || index >= self->size) { + return Qnil; + } + void* memory = (void *) (((uint8_t *)self->elements) + index * element_size); + return native_slot_get(field_type, field_type_class, memory); + }else{ + /* check if idx is Range */ + size_t off; + switch (rb_range_beg_len(arg, &beg, &len, self->size, 0)) { + case Qfalse: + break; + case Qnil: + return Qnil; + default: + return RepeatedField_subarray(_self, beg, len); + } + } + } + /* assume 2 arguments */ + beg = NUM2LONG(argv[0]); + len = NUM2LONG(argv[1]); + if (beg < 0) { + beg += self->size; + } + if (beg >= self->size) { return Qnil; } - - void* memory = (void *) (((uint8_t *)self->elements) + index * element_size); - return native_slot_get(field_type, field_type_class, memory); + return RepeatedField_subarray(_self, beg, len); } /* @@ -173,6 +217,7 @@ VALUE RepeatedField_push(VALUE _self, VALUE val) { return _self; } + // Used by parsing handlers. void RepeatedField_push_native(VALUE _self, void* data) { RepeatedField* self = ruby_to_RepeatedField(_self); @@ -193,19 +238,15 @@ void* RepeatedField_index_native(VALUE _self, int index) { } /* - * call-seq: - * RepeatedField.pop => value - * - * Removes the last element and returns it. Throws an exception if the repeated - * field is empty. + * Private ruby method, used by RepeatedField.pop */ -VALUE RepeatedField_pop(VALUE _self) { +VALUE RepeatedField_pop_one(VALUE _self) { RepeatedField* self = ruby_to_RepeatedField(_self); upb_fieldtype_t field_type = self->field_type; VALUE field_type_class = self->field_type_class; int element_size = native_slot_size(field_type); if (self->size == 0) { - rb_raise(rb_eRangeError, "Pop from empty repeated field is not allowed."); + return Qnil; } int index = self->size - 1; void* memory = (void *) (((uint8_t *)self->elements) + index * element_size); @@ -214,19 +255,6 @@ VALUE RepeatedField_pop(VALUE _self) { return ret; } -/* - * call-seq: - * RepeatedField.insert(*args) - * - * Pushes each arg in turn onto the end of the repeated field. - */ -VALUE RepeatedField_insert(int argc, VALUE* argv, VALUE _self) { - for (int i = 0; i < argc; i++) { - RepeatedField_push(_self, argv[i]); - } - return Qnil; -} - /* * call-seq: * RepeatedField.replace(list) @@ -240,7 +268,7 @@ VALUE RepeatedField_replace(VALUE _self, VALUE list) { for (int i = 0; i < RARRAY_LEN(list); i++) { RepeatedField_push(_self, rb_ary_entry(list, i)); } - return Qnil; + return list; } /* @@ -252,7 +280,7 @@ VALUE RepeatedField_replace(VALUE _self, VALUE list) { VALUE RepeatedField_clear(VALUE _self) { RepeatedField* self = ruby_to_RepeatedField(_self); self->size = 0; - return Qnil; + return _self; } /* @@ -341,7 +369,6 @@ VALUE RepeatedField_to_ary(VALUE _self) { for (int i = 0; i < self->size; i++, off += elem_size) { void* mem = ((uint8_t *)self->elements) + off; VALUE elem = native_slot_get(field_type, self->field_type_class, mem); - rb_ary_push(ary, elem); } return ary; @@ -417,19 +444,6 @@ VALUE RepeatedField_hash(VALUE _self) { return hash; } -/* - * call-seq: - * RepeatedField.inspect => string - * - * Returns a string representing this repeated field's elements. It will be - * formated as "[, , ...]", with each element's string - * representation computed by its own #inspect method. - */ -VALUE RepeatedField_inspect(VALUE _self) { - VALUE self_ary = RepeatedField_to_ary(_self); - return rb_funcall(self_ary, rb_intern("inspect"), 0); -} - /* * call-seq: * RepeatedField.+(other) => repeated field @@ -466,6 +480,22 @@ VALUE RepeatedField_plus(VALUE _self, VALUE list) { return dupped; } +/* + * call-seq: + * RepeatedField.concat(other) => self + * + * concats the passed in array to self. Returns a Ruby array. + */ +VALUE RepeatedField_concat(VALUE _self, VALUE list) { + RepeatedField* self = ruby_to_RepeatedField(_self); + Check_Type(list, T_ARRAY); + for (int i = 0; i < RARRAY_LEN(list); i++) { + RepeatedField_push(_self, rb_ary_entry(list, i)); + } + return _self; +} + + void validate_type_class(upb_fieldtype_t type, VALUE klass) { if (rb_iv_get(klass, kDescriptorInstanceVar) == Qnil) { rb_raise(rb_eArgError, @@ -585,22 +615,23 @@ void RepeatedField_register(VALUE module) { rb_define_method(klass, "initialize", RepeatedField_init, -1); rb_define_method(klass, "each", RepeatedField_each, 0); - rb_define_method(klass, "[]", RepeatedField_index, 1); + rb_define_method(klass, "[]", RepeatedField_index, -1); + rb_define_method(klass, "at", RepeatedField_index, -1); rb_define_method(klass, "[]=", RepeatedField_index_set, 2); rb_define_method(klass, "push", RepeatedField_push, 1); rb_define_method(klass, "<<", RepeatedField_push, 1); - rb_define_method(klass, "pop", RepeatedField_pop, 0); - rb_define_method(klass, "insert", RepeatedField_insert, -1); + rb_define_private_method(klass, "pop_one", RepeatedField_pop_one, 0); rb_define_method(klass, "replace", RepeatedField_replace, 1); rb_define_method(klass, "clear", RepeatedField_clear, 0); rb_define_method(klass, "length", RepeatedField_length, 0); + rb_define_method(klass, "size", RepeatedField_length, 0); rb_define_method(klass, "dup", RepeatedField_dup, 0); // Also define #clone so that we don't inherit Object#clone. rb_define_method(klass, "clone", RepeatedField_dup, 0); rb_define_method(klass, "==", RepeatedField_eq, 1); rb_define_method(klass, "to_ary", RepeatedField_to_ary, 0); rb_define_method(klass, "hash", RepeatedField_hash, 0); - rb_define_method(klass, "inspect", RepeatedField_inspect, 0); rb_define_method(klass, "+", RepeatedField_plus, 1); + rb_define_method(klass, "concat", RepeatedField_concat, 1); rb_include_module(klass, rb_mEnumerable); } diff --git a/ruby/lib/google/protobuf/repeated_field.rb b/ruby/lib/google/protobuf/repeated_field.rb index 5b934e56..16c843c0 100644 --- a/ruby/lib/google/protobuf/repeated_field.rb +++ b/ruby/lib/google/protobuf/repeated_field.rb @@ -28,12 +28,160 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# add syntatic sugar on top of the core library +require 'forwardable' + +# +# This class makes RepeatedField act (almost-) like a Ruby Array. +# It has convenience methods that extend the core C or Java based +# methods. +# +# This is a best-effort to mirror Array behavior. Two comments: +# 1) patches always welcome :) +# 2) if performance is an issue, feel free to rewrite the method +# in jruby and C. The source code has plenty of examples +# +# KNOWN ISSUES +# - #[]= doesn't allow less used approaches such as `arr[1, 2] = 'fizz'` +# - #concat should return the orig array +# - #push should accept multiple arguments and push them all at the same time +# module Google module Protobuf class RepeatedField + extend Forwardable + + # methods defined in C or Java: + # + + # [], at + # []= + # concat + # clear + # dup, clone + # each + # push, << + # replace + # length, size + # == + # to_ary, to_a + # also all enumerable + # + # NOTE: using delegators rather than method_missing to make the + # relationship explicit instead of implicit + def_delegators :to_ary, + :&, :*, :-, :'<=>', + :assoc, :bsearch, :combination, :compact, :count, :cycle, + :drop, :drop_while, :eql?, :fetch, :find_index, :flatten, + :include?, :index, :inspect, :join, + :pack, :permutation, :product, :pretty_print, :pretty_print_cycle, + :rassoc, :repeated_combination, :repeated_permutation, :reverse, + :rindex, :rotate, :sample, :shuffle, :shelljoin, :slice, + :to_s, :transpose, :uniq, :| + + + def first(n=nil) + n ? self[0..n] : self[0] + end + + + def last(n=nil) + n ? self[(self.size-n-1)..-1] : self[-1] + end + + + def pop(n=nil) + if n + results = [] + n.times{ results << pop_one } + return results + else + return pop_one + end + end + + + def empty? + self.size == 0 + end + + # array aliases into enumerable + alias_method :each_index, :each_with_index + alias_method :slice, :[] + alias_method :values_at, :select + alias_method :map, :collect + + + class << self + def define_array_wrapper_method(method_name) + define_method(method_name) do |*args, &block| + arr = self.to_a + result = arr.send(method_name, *args) + self.replace(arr) + return result if result + return block ? block.call : result + end + end + private :define_array_wrapper_method + + + def define_array_wrapper_with_result_method(method_name) + define_method(method_name) do |*args, &block| + # result can be an Enumerator, Array, or nil + # Enumerator can sometimes be returned if a block is an optional argument and it is not passed in + # nil usually specifies that no change was made + result = self.to_a.send(method_name, *args, &block) + if result + new_arr = result.to_a + self.replace(new_arr) + if result.is_a?(Enumerator) + # generate a fresh enum; rewinding the exiting one, in Ruby 2.2, will + # reset the enum with the same length, but all the #next calls will + # return nil + result = new_arr.to_enum + # generate a wrapper enum so any changes which occur by a chained + # enum can be captured + ie = ProxyingEnumerator.new(self, result) + result = ie.to_enum + end + end + result + end + end + private :define_array_wrapper_with_result_method + end + + + %w(delete delete_at delete_if shift slice! unshift).each do |method_name| + define_array_wrapper_method(method_name) + end + + + %w(collect! compact! fill flatten! insert reverse! + rotate! select! shuffle! sort! sort_by! uniq!).each do |method_name| + define_array_wrapper_with_result_method(method_name) + end + alias_method :keep_if, :select! + alias_method :map!, :collect! + alias_method :reject!, :delete_if + + + # propagates changes made by user of enumerator back to the original repeated field. + # This only applies in cases where the calling function which created the enumerator, + # such as #sort!, modifies itself rather than a new array, such as #sort + class ProxyingEnumerator < Struct.new(:repeated_field, :external_enumerator) + def each(*args, &block) + results = [] + external_enumerator.each_with_index do |val, i| + result = yield(val) + results << result + #nil means no change occured from yield; usually occurs when #to_a is called + if result + repeated_field[i] = result if result != val + end + end + results + end + end - alias_method :size, :length end end diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java index 38226c4e..f3c488bc 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java @@ -71,6 +71,17 @@ public class RubyFieldDescriptor extends RubyObject { return this; } + /* + * call-seq: + * FieldDescriptor.label + * + * Return the label of this field. + */ + @JRubyMethod(name = "label") + public IRubyObject getLabel(ThreadContext context) { + return this.label; + } + /* * call-seq: * FieldDescriptor.label = label @@ -80,8 +91,10 @@ public class RubyFieldDescriptor extends RubyObject { */ @JRubyMethod(name = "label=") public IRubyObject setLabel(ThreadContext context, IRubyObject value) { + String labelName = value.asJavaString(); + this.label = context.runtime.newSymbol(labelName.toLowerCase()); this.builder.setLabel( - DescriptorProtos.FieldDescriptorProto.Label.valueOf("LABEL_" + value.asJavaString().toUpperCase())); + DescriptorProtos.FieldDescriptorProto.Label.valueOf("LABEL_" + labelName.toUpperCase())); return context.runtime.getNil(); } @@ -89,18 +102,13 @@ public class RubyFieldDescriptor extends RubyObject { * call-seq: * FieldDescriptor.name => name * - * Returns the name of this field. + * Returns the name of this field as a Ruby String, or nil if it is not set. */ @JRubyMethod(name = "name") public IRubyObject getName(ThreadContext context) { return this.name; } - @JRubyMethod(name = "subtype") - public IRubyObject getSubType(ThreadContext context) { - return subType; - } - /* * call-seq: * FieldDescriptor.name = name @@ -116,6 +124,12 @@ public class RubyFieldDescriptor extends RubyObject { return context.runtime.getNil(); } + + @JRubyMethod(name = "subtype") + public IRubyObject getSubType(ThreadContext context) { + return subType; + } + /* * call-seq: * FieldDescriptor.type => type @@ -144,6 +158,18 @@ public class RubyFieldDescriptor extends RubyObject { return context.runtime.getNil(); } + /* + * call-seq: + * FieldDescriptor.number => number + * + * Returns this field's number, as a Ruby Integer, or nil if not yet set. + * + */ + @JRubyMethod(name = "number") + public IRubyObject getnumber(ThreadContext context) { + return this.number; + } + /* * call-seq: * FieldDescriptor.number = number @@ -153,6 +179,7 @@ public class RubyFieldDescriptor extends RubyObject { */ @JRubyMethod(name = "number=") public IRubyObject setNumber(ThreadContext context, IRubyObject value) { + this.number = value; this.builder.setNumber(RubyNumeric.num2int(value)); return context.runtime.getNil(); } @@ -240,6 +267,8 @@ public class RubyFieldDescriptor extends RubyObject { private DescriptorProtos.FieldDescriptorProto.Builder builder; private IRubyObject name; + private IRubyObject label; + private IRubyObject number; private IRubyObject subType; private IRubyObject oneofName; private Descriptors.FieldDescriptor fieldDef; diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java index 84bf8956..946f9e74 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java @@ -40,6 +40,7 @@ import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import java.util.Arrays; @JRubyClass(name = "RepeatedClass", include = "Enumerable") public class RubyRepeatedField extends RubyObject { @@ -110,6 +111,10 @@ public class RubyRepeatedField extends RubyObject { public IRubyObject indexSet(ThreadContext context, IRubyObject index, IRubyObject value) { int arrIndex = normalizeArrayIndex(index); Utils.checkType(context, fieldType, value, (RubyModule) typeClass); + IRubyObject defaultValue = defaultValue(context); + for (int i = this.storage.size(); i < arrIndex; i++) { + this.storage.set(i, defaultValue); + } this.storage.set(arrIndex, value); return context.runtime.getNil(); } @@ -120,27 +125,35 @@ public class RubyRepeatedField extends RubyObject { * * Accesses the element at the given index. Returns nil on out-of-bounds */ - @JRubyMethod(name = "[]") - public IRubyObject index(ThreadContext context, IRubyObject index) { - int arrIndex = normalizeArrayIndex(index); - if (arrIndex < 0 || arrIndex >= this.storage.size()) { - return context.runtime.getNil(); + @JRubyMethod(required=1, optional=1, name = {"at", "[]"}) + public IRubyObject index(ThreadContext context, IRubyObject[] args) { + if (args.length == 1){ + IRubyObject arg = args[0]; + if (Utils.isRubyNum(arg)) { + /* standard case */ + int arrIndex = normalizeArrayIndex(arg); + if (arrIndex < 0 || arrIndex >= this.storage.size()) { + return context.runtime.getNil(); + } + return this.storage.eltInternal(arrIndex); + } else if (arg instanceof RubyRange) { + RubyRange range = ((RubyRange) arg); + int beg = RubyNumeric.num2int(range.first(context)); + int to = RubyNumeric.num2int(range.last(context)); + int len = to - beg + 1; + return this.storage.subseq(beg, len); + } } - return this.storage.eltInternal(arrIndex); - } - - /* - * call-seq: - * RepeatedField.insert(*args) - * - * Pushes each arg in turn onto the end of the repeated field. - */ - @JRubyMethod(rest = true) - public IRubyObject insert(ThreadContext context, IRubyObject[] args) { - for (int i = 0; i < args.length; i++) { - push(context, args[i]); + /* assume 2 arguments */ + int beg = RubyNumeric.num2int(args[0]); + int len = RubyNumeric.num2int(args[1]); + if (beg < 0) { + beg += this.storage.size(); } - return context.runtime.getNil(); + if (beg >= this.storage.size()) { + return context.runtime.getNil(); + } + return this.storage.subseq(beg, len); } /* @@ -151,20 +164,19 @@ public class RubyRepeatedField extends RubyObject { */ @JRubyMethod(name = {"push", "<<"}) public IRubyObject push(ThreadContext context, IRubyObject value) { - Utils.checkType(context, fieldType, value, (RubyModule) typeClass); + if (!(fieldType == Descriptors.FieldDescriptor.Type.MESSAGE && + value == context.runtime.getNil())) { + Utils.checkType(context, fieldType, value, (RubyModule) typeClass); + } this.storage.add(value); - return this; + return this.storage; } /* - * call-seq: - * RepeatedField.pop => value - * - * Removes the last element and returns it. Throws an exception if the repeated - * field is empty. + * private Ruby method used by RepeatedField.pop */ - @JRubyMethod - public IRubyObject pop(ThreadContext context) { + @JRubyMethod(visibility = org.jruby.runtime.Visibility.PRIVATE) + public IRubyObject pop_one(ThreadContext context) { IRubyObject ret = this.storage.last(); this.storage.remove(ret); return ret; @@ -181,7 +193,7 @@ public class RubyRepeatedField extends RubyObject { RubyArray arr = (RubyArray) list; checkArrayElementType(context, arr); this.storage = arr; - return context.runtime.getNil(); + return this.storage; } /* @@ -193,7 +205,7 @@ public class RubyRepeatedField extends RubyObject { @JRubyMethod public IRubyObject clear(ThreadContext context) { this.storage.clear(); - return context.runtime.getNil(); + return this.storage; } /* @@ -202,7 +214,7 @@ public class RubyRepeatedField extends RubyObject { * * Returns the length of this repeated field. */ - @JRubyMethod(name = {"count", "length"}) + @JRubyMethod(name = {"length", "size"}) public IRubyObject length(ThreadContext context) { return context.runtime.newFixnum(this.storage.size()); } @@ -215,7 +227,7 @@ public class RubyRepeatedField extends RubyObject { * repeated field's elements and other's elements. The other (second) list may * be either another repeated field or a Ruby array. */ - @JRubyMethod(name = "+") + @JRubyMethod(name = {"+"}) public IRubyObject plus(ThreadContext context, IRubyObject list) { RubyRepeatedField dup = (RubyRepeatedField) dup(context); if (list instanceof RubyArray) { @@ -231,6 +243,27 @@ public class RubyRepeatedField extends RubyObject { return dup; } + /* + * call-seq: + * RepeatedField.concat(other) => self + * + * concats the passed in array to self. Returns a Ruby array. + */ + @JRubyMethod + public IRubyObject concat(ThreadContext context, IRubyObject list) { + if (list instanceof RubyArray) { + checkArrayElementType(context, (RubyArray) list); + this.storage.addAll((RubyArray) list); + } else { + RubyRepeatedField repeatedField = (RubyRepeatedField) list; + if (! fieldType.equals(repeatedField.fieldType) || (typeClass != null && ! + typeClass.equals(repeatedField.typeClass))) + throw context.runtime.newArgumentError("Attempt to append RepeatedField with different element type."); + this.storage.addAll((RubyArray) repeatedField.toArray(context)); + } + return this.storage; + } + /* * call-seq: * RepeatedField.hash => hash_value @@ -239,7 +272,7 @@ public class RubyRepeatedField extends RubyObject { */ @JRubyMethod public IRubyObject hash(ThreadContext context) { - int hashCode = System.identityHashCode(this.storage); + int hashCode = this.storage.hashCode(); return context.runtime.newFixnum(hashCode); } @@ -268,17 +301,12 @@ public class RubyRepeatedField extends RubyObject { @JRubyMethod public IRubyObject each(ThreadContext context, Block block) { this.storage.each(context, block); - return context.runtime.getNil(); + return this.storage; } + @JRubyMethod(name = {"to_ary", "to_a"}) public IRubyObject toArray(ThreadContext context) { - for (int i = 0; i < this.storage.size(); i++) { - IRubyObject defaultValue = defaultValue(context); - if (storage.eltInternal(i).isNil()) { - storage.set(i, defaultValue); - } - } return this.storage; } @@ -298,31 +326,6 @@ public class RubyRepeatedField extends RubyObject { return dup; } - /* - * call-seq: - * RepeatedField.inspect => string - * - * Returns a string representing this repeated field's elements. It will be - * formated as "[, , ...]", with each element's string - * representation computed by its own #inspect method. - */ - @JRubyMethod - public IRubyObject inspect() { - StringBuilder str = new StringBuilder("["); - for (int i = 0; i < this.storage.size(); i++) { - str.append(storage.eltInternal(i).inspect()); - str.append(", "); - } - - if (str.length() > 1) { - str.replace(str.length() - 2, str.length(), "]"); - } else { - str.append("]"); - } - - return getRuntime().newString(str.toString()); - } - // Java API protected IRubyObject get(int index) { return this.storage.eltInternal(index); @@ -376,6 +379,9 @@ public class RubyRepeatedField extends RubyObject { case STRING: value = sentinel.getDefaultString(); break; + case ENUM: + IRubyObject defaultEnumLoc = context.runtime.newFixnum(0); + return RubyEnum.lookup(context, typeClass, defaultEnumLoc); default: return context.runtime.getNil(); } diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index a00f0b05..b6e26108 100644 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -178,7 +178,7 @@ module BasicTest :optional_msg => TestMessage2.new, :repeated_string => ["hello", "there", "world"]) expected = ', optional_enum: :A, repeated_int32: [], repeated_int64: [], repeated_uint32: [], repeated_uint64: [], repeated_bool: [], repeated_float: [], repeated_double: [], repeated_string: ["hello", "there", "world"], repeated_bytes: [], repeated_msg: [], repeated_enum: []>' - assert m.inspect == expected + assert_equal expected, m.inspect end def test_hash @@ -276,7 +276,7 @@ module BasicTest assert l.inspect == '[5, 2, 3, 4]' - l.insert(7, 8, 9) + l.concat([7, 8, 9]) assert l == [5, 2, 3, 4, 7, 8, 9] assert l.pop == 9 assert l == [5, 2, 3, 4, 7, 8] diff --git a/ruby/tests/repeated_field_test.rb b/ruby/tests/repeated_field_test.rb new file mode 100644 index 00000000..25727b7b --- /dev/null +++ b/ruby/tests/repeated_field_test.rb @@ -0,0 +1,640 @@ +#!/usr/bin/ruby + +require 'google/protobuf' +require 'test/unit' + +class RepeatedFieldTest < Test::Unit::TestCase + + def test_acts_like_enumerator + m = TestMessage.new + (Enumerable.instance_methods - TestMessage.new.repeated_string.methods).each do |method_name| + assert m.repeated_string.respond_to?(method_name) == true, "does not respond to #{method_name}" + end + end + + def test_acts_like_an_array + m = TestMessage.new + arr_methods = ([].methods - TestMessage.new.repeated_string.methods) + # jRuby additions to the Array class that we can ignore + arr_methods -= [ :indices, :iter_for_each, :iter_for_each_index, + :iter_for_each_with_index, :dimensions, :copy_data, :copy_data_simple, + :nitems, :iter_for_reverse_each, :indexes] + arr_methods.each do |method_name| + assert m.repeated_string.respond_to?(method_name) == true, "does not respond to #{method_name}" + end + end + + def test_first + m = TestMessage.new + repeated_field_names(TestMessage).each do |field_name| + assert_nil m.send(field_name).first + end + fill_test_msg(m) + assert_equal -10, m.repeated_int32.first + assert_equal -1_000_000, m.repeated_int64.first + assert_equal 10, m.repeated_uint32.first + assert_equal 1_000_000, m.repeated_uint64.first + assert_equal true, m.repeated_bool.first + assert_equal -1.01, m.repeated_float.first.round(2) + assert_equal -1.0000000000001, m.repeated_double.first + assert_equal 'foo', m.repeated_string.first + assert_equal "bar".encode!('ASCII-8BIT'), m.repeated_bytes.first + assert_equal TestMessage2.new(:foo => 1), m.repeated_msg.first + assert_equal :A, m.repeated_enum.first + end + + + def test_last + m = TestMessage.new + repeated_field_names(TestMessage).each do |field_name| + assert_nil m.send(field_name).first + end + fill_test_msg(m) + assert_equal -11, m.repeated_int32.last + assert_equal -1_000_001, m.repeated_int64.last + assert_equal 11, m.repeated_uint32.last + assert_equal 1_000_001, m.repeated_uint64.last + assert_equal false, m.repeated_bool.last + assert_equal -1.02, m.repeated_float.last.round(2) + assert_equal -1.0000000000002, m.repeated_double.last + assert_equal 'bar', m.repeated_string.last + assert_equal "foo".encode!('ASCII-8BIT'), m.repeated_bytes.last + assert_equal TestMessage2.new(:foo => 2), m.repeated_msg.last + assert_equal :B, m.repeated_enum.last + end + + + def test_pop + m = TestMessage.new + repeated_field_names(TestMessage).each do |field_name| + assert_nil m.send(field_name).pop + end + fill_test_msg(m) + + assert_equal -11, m.repeated_int32.pop + assert_equal -10, m.repeated_int32.pop + assert_equal -1_000_001, m.repeated_int64.pop + assert_equal -1_000_000, m.repeated_int64.pop + assert_equal 11, m.repeated_uint32.pop + assert_equal 10, m.repeated_uint32.pop + assert_equal 1_000_001, m.repeated_uint64.pop + assert_equal 1_000_000, m.repeated_uint64.pop + assert_equal false, m.repeated_bool.pop + assert_equal true, m.repeated_bool.pop + assert_equal -1.02, m.repeated_float.pop.round(2) + assert_equal -1.01, m.repeated_float.pop.round(2) + assert_equal -1.0000000000002, m.repeated_double.pop + assert_equal -1.0000000000001, m.repeated_double.pop + assert_equal 'bar', m.repeated_string.pop + assert_equal 'foo', m.repeated_string.pop + assert_equal "foo".encode!('ASCII-8BIT'), m.repeated_bytes.pop + assert_equal "bar".encode!('ASCII-8BIT'), m.repeated_bytes.pop + assert_equal TestMessage2.new(:foo => 2), m.repeated_msg.pop + assert_equal TestMessage2.new(:foo => 1), m.repeated_msg.pop + assert_equal :B, m.repeated_enum.pop + assert_equal :A, m.repeated_enum.pop + repeated_field_names(TestMessage).each do |field_name| + assert_nil m.send(field_name).pop + end + + fill_test_msg(m) + assert_equal ['bar', 'foo'], m.repeated_string.pop(2) + assert_nil m.repeated_string.pop + end + + + def test_each + m = TestMessage.new + 5.times{|i| m.repeated_string << 'string' } + count = 0 + m.repeated_string.each do |val| + assert_equal 'string', val + count += 1 + end + assert_equal 5, count + result = m.repeated_string.each{|val| val + '_junk'} + assert_equal ['string'] * 5, result + end + + + def test_empty? + m = TestMessage.new + assert_equal true, m.repeated_string.empty? + m.repeated_string << 'foo' + assert_equal false, m.repeated_string.empty? + m.repeated_string << 'bar' + assert_equal false, m.repeated_string.empty? + end + + def test_array_accessor + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[1] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[-2] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[20] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[1, 2] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[0..2] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[-1, 1] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[10, 12] + end + end + + def test_array_settor + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[1] = 'junk' + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[-2] = 'snappy' + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr[3] = '' + end + # slight deviation; we are strongly typed, and nil is not allowed + # for string types; + m.repeated_string[5] = 'spacious' + assert_equal ["foo", "snappy", "baz", "", "", "spacious"], m.repeated_string + + #make sure it sests the default types for other fields besides strings + %w(repeated_int32 repeated_int64 repeated_uint32 repeated_uint64).each do |field_name| + m.send(field_name)[3] = 10 + assert_equal [0,0,0,10], m.send(field_name) + end + m.repeated_float[3] = 10.1 + #wonky mri float handling + assert_equal [0,0,0], m.repeated_float.to_a[0..2] + assert_equal 10.1, m.repeated_float[3].round(1) + m.repeated_double[3] = 10.1 + assert_equal [0,0,0,10.1], m.repeated_double + m.repeated_bool[3] = true + assert_equal [false, false, false, true], m.repeated_bool + m.repeated_bytes[3] = "bar".encode!('ASCII-8BIT') + assert_equal ['', '', '', "bar".encode!('ASCII-8BIT')], m.repeated_bytes + m.repeated_msg[3] = TestMessage2.new(:foo => 1) + assert_equal [nil, nil, nil, TestMessage2.new(:foo => 1)], m.repeated_msg + m.repeated_enum[3] = :A + assert_equal [:Default, :Default, :Default, :A], m.repeated_enum + + # check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + # arr[20] = 'spacious' + # end + # TODO: accessor doesn't allow other ruby-like methods + # check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + # arr[1, 2] = 'fizz' + # end + # check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + # arr[0..2] = 'buzz' + # end + end + + def test_push + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.push('fizz') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr << 'fizz' + end + #TODO: push should support multiple + # check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + # arr.push('fizz', 'buzz') + # end + end + + def test_clear + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.clear + end + end + + def test_concat + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + m.repeated_string.concat(['fizz', 'buzz']) + assert_equal %w(foo bar baz fizz buzz), m.repeated_string + #TODO: concat should return the orig array + # check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + # arr.concat(['fizz', 'buzz']) + # end + end + + def test_equal + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + assert_equal reference_arr, m.repeated_string + reference_arr << 'fizz' + assert_not_equal reference_arr, m.repeated_string + m.repeated_string << 'fizz' + assert_equal reference_arr, m.repeated_string + end + + def test_hash + # just a sanity check + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + assert m.repeated_string.hash.is_a?(Integer) + hash = m.repeated_string.hash + assert_equal hash, m.repeated_string.hash + m.repeated_string << 'j' + assert_not_equal hash, m.repeated_string.hash + end + + def test_plus + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr + ['fizz', 'buzz'] + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr += ['fizz', 'buzz'] + end + end + + def test_replace + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.replace(['fizz', 'buzz']) + end + end + + def test_to_a + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.to_a + end + end + + def test_to_ary + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.to_ary + end + end + + # emulate Array behavior + ########################## + + def test_collect! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.collect!{|x| x + "!" } + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.collect!.with_index{|x, i| x[0...i] } + end + end + + def test_compact! + m = TestMessage.new + m.repeated_msg << TestMessage2.new(:foo => 1) + m.repeated_msg << nil + m.repeated_msg << TestMessage2.new(:foo => 2) + reference_arr = m.repeated_string.to_a + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.compact! + end + end + + def test_delete + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.delete('bar') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.delete('nope') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.delete('nope'){'within'} + end + end + + def test_delete_at + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.delete_at(2) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.delete_at(10) + end + end + + def test_fill + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.fill("x") + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.fill("z", 2, 2) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.fill("y", 0..1) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.fill { |i| (i*i).to_s } + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.fill(-2) { |i| (i*i*i).to_s } + end + end + + def test_flatten! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.flatten! + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.flatten!(1) + end + end + + def test_insert + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.insert(2, 'fizz') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.insert(3, 'fizz', 'buzz', 'bazz') + end + end + + def test_inspect + m = TestMessage.new + assert_equal '[]', m.repeated_string.inspect + m.repeated_string << 'foo' + assert_equal m.repeated_string.to_a.inspect, m.repeated_string.inspect + m.repeated_string << 'bar' + assert_equal m.repeated_string.to_a.inspect, m.repeated_string.inspect + end + + def test_reverse! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.reverse! + end + end + + def test_rotate! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.rotate! + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.rotate!(2) + end + end + + def test_select! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.select! { |v| v =~ /[aeiou]/ } + end + end + + def test_shift + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + # should return an element + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.shift + end + # should return an array + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.shift(2) + end + # should return nil + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.shift + end + end + + def test_shuffle! + m = TestMessage.new + m.repeated_string += %w(foo bar baz) + orig_repeated_string = m.repeated_string.clone + result = m.repeated_string.shuffle! + assert_equal m.repeated_string, result + # NOTE: sometimes it doesn't change the order... + # assert_not_equal m.repeated_string.to_a, orig_repeated_string.to_a + end + + def test_slice! + m = TestMessage.new + reference_arr = %w(foo bar baz bar fizz buzz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.slice!(2) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.slice!(1,2) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.slice!(0..1) + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.slice!(10) + end + end + + def test_sort! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.sort! + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.sort! { |x,y| y <=> x } + end + end + + def test_sort_by! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.sort_by! + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.sort_by!(&:hash) + end + end + + def test_uniq! + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.uniq! + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.uniq!{|s| s[0] } + end + end + + def test_unshift + m = TestMessage.new + reference_arr = %w(foo bar baz) + m.repeated_string += reference_arr.clone + + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.unshift('1') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.unshift('a', 'b') + end + check_self_modifying_method(m.repeated_string, reference_arr) do |arr| + arr.unshift('') + end + end + + + ##### HELPER METHODS + + def check_self_modifying_method(repeated_field, ref_array) + expected_result = yield(ref_array) + actual_result = yield(repeated_field) + if expected_result.is_a?(Enumerator) + assert_equal expected_result.to_a, actual_result.to_a + else + assert_equal expected_result, actual_result + end + assert_equal ref_array, repeated_field + end + + + def repeated_field_names(klass) + klass.descriptor.find_all{|f| f.label == :repeated}.map(&:name) + end + + + def fill_test_msg(test_msg) + test_msg.repeated_int32 += [-10, -11] + test_msg.repeated_int64 += [-1_000_000, -1_000_001] + test_msg.repeated_uint32 += [10, 11] + test_msg.repeated_uint64 += [1_000_000, 1_000_001] + test_msg.repeated_bool += [true, false] + test_msg.repeated_float += [-1.01, -1.02] + test_msg.repeated_double += [-1.0000000000001, -1.0000000000002] + test_msg.repeated_string += %w(foo bar) + test_msg.repeated_bytes += ["bar".encode!('ASCII-8BIT'), "foo".encode!('ASCII-8BIT')] + test_msg.repeated_msg << TestMessage2.new(:foo => 1) + test_msg.repeated_msg << TestMessage2.new(:foo => 2) + test_msg.repeated_enum << :A + test_msg.repeated_enum << :B + end + + + pool = Google::Protobuf::DescriptorPool.new + pool.build do + + add_message "TestMessage" do + optional :optional_int32, :int32, 1 + optional :optional_int64, :int64, 2 + optional :optional_uint32, :uint32, 3 + optional :optional_uint64, :uint64, 4 + optional :optional_bool, :bool, 5 + optional :optional_float, :float, 6 + optional :optional_double, :double, 7 + optional :optional_string, :string, 8 + optional :optional_bytes, :bytes, 9 + optional :optional_msg, :message, 10, "TestMessage2" + optional :optional_enum, :enum, 11, "TestEnum" + + repeated :repeated_int32, :int32, 12 + repeated :repeated_int64, :int64, 13 + repeated :repeated_uint32, :uint32, 14 + repeated :repeated_uint64, :uint64, 15 + repeated :repeated_bool, :bool, 16 + repeated :repeated_float, :float, 17 + repeated :repeated_double, :double, 18 + repeated :repeated_string, :string, 19 + repeated :repeated_bytes, :bytes, 20 + repeated :repeated_msg, :message, 21, "TestMessage2" + repeated :repeated_enum, :enum, 22, "TestEnum" + end + add_message "TestMessage2" do + optional :foo, :int32, 1 + end + + add_enum "TestEnum" do + value :Default, 0 + value :A, 1 + value :B, 2 + value :C, 3 + end + end + + TestMessage = pool.lookup("TestMessage").msgclass + TestMessage2 = pool.lookup("TestMessage2").msgclass + TestEnum = pool.lookup("TestEnum").enummodule + + +end -- cgit v1.2.3