From e3094a8d803c0b9df33a8a506cce28a89c5fd6e8 Mon Sep 17 00:00:00 2001 From: Josh Haberman Date: Wed, 27 Jul 2016 17:31:16 -0700 Subject: Ruby: added API support for well-known types. --- Makefile.am | 1 + ruby/lib/google/protobuf/well_known_types.rb | 206 +++++++++++++++++++++++++++ ruby/tests/well_known_types_test.rb | 98 +++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 ruby/lib/google/protobuf/well_known_types.rb create mode 100644 ruby/tests/well_known_types_test.rb diff --git a/Makefile.am b/Makefile.am index ccbfd065..3b177da1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -707,6 +707,7 @@ ruby_EXTRA_DIST= \ ruby/tests/generated_code.proto \ ruby/tests/test_import.proto \ ruby/tests/generated_code_test.rb \ + ruby/tests/well_known_types_test.rb \ ruby/travis-test.sh js_EXTRA_DIST= \ diff --git a/ruby/lib/google/protobuf/well_known_types.rb b/ruby/lib/google/protobuf/well_known_types.rb new file mode 100644 index 00000000..d0d15045 --- /dev/null +++ b/ruby/lib/google/protobuf/well_known_types.rb @@ -0,0 +1,206 @@ +#!/usr/bin/ruby +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'google/protobuf/any_pb' +require 'google/protobuf/duration_pb' +require 'google/protobuf/field_mask_pb' +require 'google/protobuf/struct_pb' +require 'google/protobuf/timestamp_pb' + +module Google + module Protobuf + + Any.class_eval do + def pack(msg, type_url_prefix='type.googleapis.com/') + if type_url_prefix.empty? or type_url_prefix[-1] != '/' then + self.type_url = "#{type_url_prefix}/#{msg.class.descriptor.name}" + else + self.type_url = "#{type_url_prefix}#{msg.class.descriptor.name}" + end + self.value = msg.to_proto + end + + def unpack(klass) + if self.is(klass) then + klass.decode(self.value) + else + nil + end + end + + def type_name + return self.type_url.split("/")[-1] + end + + def is(klass) + return self.type_name == klass.descriptor.name + end + end + + Timestamp.class_eval do + def to_time + Time.at(self.to_f) + end + + def from_time(time) + self.seconds = time.to_i + self.nanos = time.nsec + end + + def to_i + self.seconds + end + + def to_f + self.seconds + (self.nanos.to_f / 1_000_000_000) + end + end + + Duration.class_eval do + def to_f + self.seconds + (self.nanos.to_f / 1_000_000_000) + end + end + + Value.class_eval do + def to_ruby(recursive = false) + case self.kind + when :struct_value + if recursive + self.struct_value.to_h + else + self.struct_value + end + when :list_value + if recursive + self.list_value.to_a + else + self.list_value + end + when :null_value + nil + when :number_value + self.number_value + when :string_value + self.string_value + when :bool_value + self.bool_value + else + raise "Value not set" + end + end + + def from_ruby(value) + if value.nil? + self.null_value = 0 + elsif value.is_a?(Numeric) + self.number_value = value + elsif value.is_a?(String) + self.string_value = value + elsif value.is_a?(TrueClass) + self.bool_value = true + elsif value.is_a?(FalseClass) + self.bool_value = false + elsif value.is_a?(Struct) + self.struct_value = value + elsif value.is_a?(Hash) + self.struct_value = Struct.from_hash(value) + elsif value.is_a?(ListValue) + self.list_value = value + elsif value.is_a?(Array) + self.list_value = ListValue.from_a(value) + else + raise "Unexpected type" + end + end + end + + Struct.class_eval do + def [](key) + self.fields[key].to_ruby + end + + def []=(key, value) + self.fields[key] ||= Google::Protobuf::Value.new + self.fields[key].from_ruby(value) + end + + def to_h + ret = {} + self.fields.each { |key, val| ret[key] = val.to_ruby(true) } + ret + end + + def self.from_hash(hash) + ret = Struct.new + hash.each { |key, val| ret[key] = val } + ret + end + end + + ListValue.class_eval do + include Enumerable + + def length + self.values.length + end + + def [](index) + self.values[index].to_ruby + end + + def []=(index, value) + self.values[index].from_ruby(value) + end + + def <<(value) + wrapper = Google::Protobuf::Value.new + wrapper.from_ruby(value) + self.values << wrapper + end + + def each + self.values.each { |x| yield(x.to_ruby) } + end + + def to_a + self.values.map { |x| x.to_ruby(true) } + end + + def self.from_a(arr) + ret = ListValue.new + arr.each { |val| ret << val } + ret + end + end + + end +end diff --git a/ruby/tests/well_known_types_test.rb b/ruby/tests/well_known_types_test.rb new file mode 100644 index 00000000..cf9a3aa0 --- /dev/null +++ b/ruby/tests/well_known_types_test.rb @@ -0,0 +1,98 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'google/protobuf/well_known_types' + +class TestWellKnownTypes < Test::Unit::TestCase + def test_timestamp + ts = Google::Protobuf::Timestamp.new + + assert_equal Time.at(0), ts.to_time + + ts.seconds = 12345 + assert_equal Time.at(12345), ts.to_time + assert_equal 12345, ts.to_i + + ts.from_time(Time.at(123456, 654321)) + assert_equal 123456, ts.seconds + assert_equal 654321000, ts.nanos + assert_equal Time.at(123456.654321), ts.to_time + end + + def test_duration + duration = Google::Protobuf::Duration.new(seconds: 123, nanos: 456) + assert_equal 123.000000456, duration.to_f + end + + def test_struct + struct = Google::Protobuf::Struct.new + + substruct = { + "subkey" => 999, + "subkey2" => false + } + + sublist = ["abc", 123, {"deepkey" => "deepval"}] + + struct["number"] = 12345 + struct["boolean-true"] = true + struct["boolean-false"] = false + struct["null"] = nil + struct["string"] = "abcdef" + struct["substruct"] = substruct + struct["sublist"] = sublist + + assert_equal 12345, struct["number"] + assert_equal true, struct["boolean-true"] + assert_equal false, struct["boolean-false"] + assert_equal nil, struct["null"] + assert_equal "abcdef", struct["string"] + assert_equal(Google::Protobuf::Struct.from_hash(substruct), + struct["substruct"]) + assert_equal(Google::Protobuf::ListValue.from_a(sublist), + struct["sublist"]) + + should_equal = { + "number" => 12345, + "boolean-true" => true, + "boolean-false" => false, + "null" => nil, + "string" => "abcdef", + "substruct" => { + "subkey" => 999, + "subkey2" => false + }, + "sublist" => ["abc", 123, {"deepkey" => "deepval"}] + } + + list = struct["sublist"] + list.is_a?(Google::Protobuf::ListValue) + assert_equal "abc", list[0] + assert_equal 123, list[1] + assert_equal({"deepkey" => "deepval"}, list[2].to_h) + + # to_h returns a fully-flattened Ruby structure (Hash and Array). + assert_equal(should_equal, struct.to_h) + + # Test that we can assign Struct and ListValue directly. + struct["substruct"] = Google::Protobuf::Struct.from_hash(substruct) + struct["sublist"] = Google::Protobuf::ListValue.from_a(sublist) + + assert_equal(should_equal, struct.to_h) + + struct["sublist"] << nil + should_equal["sublist"] << nil + + assert_equal(should_equal, struct.to_h) + assert_equal(should_equal["sublist"].length, struct["sublist"].length) + end + + def test_any + any = Google::Protobuf::Any.new + ts = Google::Protobuf::Timestamp.new(seconds: 12345, nanos: 6789) + any.pack(ts) + + assert_true any.is(Google::Protobuf::Timestamp) + assert_equal ts, any.unpack(Google::Protobuf::Timestamp) + end +end -- cgit v1.2.3 From a207a2bd004f7b89699d3e52064dbed34603ae2d Mon Sep 17 00:00:00 2001 From: Josh Haberman Date: Thu, 28 Jul 2016 17:54:16 -0700 Subject: Fix for JRuby (assert_true is not present). --- ruby/tests/basic.rb | 4 ++-- ruby/tests/well_known_types_test.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index f81e456c..8b6d329e 100644 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -468,9 +468,9 @@ module BasicTest assert m.length == 2 m2 = m.dup - assert m == m2 + assert_equal m, m2 assert m.hash != 0 - assert m.hash == m2.hash + assert_equal m.hash, m2.hash collected = {} m.each { |k,v| collected[v] = k } diff --git a/ruby/tests/well_known_types_test.rb b/ruby/tests/well_known_types_test.rb index cf9a3aa0..32e73f55 100644 --- a/ruby/tests/well_known_types_test.rb +++ b/ruby/tests/well_known_types_test.rb @@ -92,7 +92,7 @@ class TestWellKnownTypes < Test::Unit::TestCase ts = Google::Protobuf::Timestamp.new(seconds: 12345, nanos: 6789) any.pack(ts) - assert_true any.is(Google::Protobuf::Timestamp) + assert any.is(Google::Protobuf::Timestamp) assert_equal ts, any.unpack(Google::Protobuf::Timestamp) end end -- cgit v1.2.3 From 3cec2ea8d61f5ebebb64af967db782e867da849f Mon Sep 17 00:00:00 2001 From: Josh Haberman Date: Mon, 1 Aug 2016 14:31:31 -0700 Subject: Ruby: added custom Struct exception type and fixed Makefile.am. --- Makefile.am | 1 + ruby/lib/google/protobuf/well_known_types.rb | 28 +++++++++++++++++----------- ruby/tests/well_known_types_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Makefile.am b/Makefile.am index 3b177da1..d4096b03 100644 --- a/Makefile.am +++ b/Makefile.am @@ -681,6 +681,7 @@ ruby_EXTRA_DIST= \ ruby/google-protobuf.gemspec \ ruby/lib/google/protobuf/message_exts.rb \ ruby/lib/google/protobuf/repeated_field.rb \ + ruby/lib/google/protobuf/well_known_types.rb \ ruby/lib/google/protobuf.rb \ ruby/pom.xml \ ruby/src/main/java/com/google/protobuf/jruby/RubyBuilder.java \ diff --git a/ruby/lib/google/protobuf/well_known_types.rb b/ruby/lib/google/protobuf/well_known_types.rb index d0d15045..547de874 100644 --- a/ruby/lib/google/protobuf/well_known_types.rb +++ b/ruby/lib/google/protobuf/well_known_types.rb @@ -90,6 +90,8 @@ module Google end end + class UnexpectedStructType < Google::Protobuf::Error; end + Value.class_eval do def to_ruby(recursive = false) case self.kind @@ -114,31 +116,32 @@ module Google when :bool_value self.bool_value else - raise "Value not set" + raise UnexpectedStructType end end def from_ruby(value) - if value.nil? + case value + when NilClass self.null_value = 0 - elsif value.is_a?(Numeric) + when Numeric self.number_value = value - elsif value.is_a?(String) + when String self.string_value = value - elsif value.is_a?(TrueClass) + when TrueClass self.bool_value = true - elsif value.is_a?(FalseClass) + when FalseClass self.bool_value = false - elsif value.is_a?(Struct) + when Struct self.struct_value = value - elsif value.is_a?(Hash) + when Hash self.struct_value = Struct.from_hash(value) - elsif value.is_a?(ListValue) + when ListValue self.list_value = value - elsif value.is_a?(Array) + when Array self.list_value = ListValue.from_a(value) else - raise "Unexpected type" + raise UnexpectedStructType end end end @@ -149,6 +152,9 @@ module Google end def []=(key, value) + unless key.is_a?(String) + raise UnexpectedStructType, "Struct keys must be strings." + end self.fields[key] ||= Google::Protobuf::Value.new self.fields[key].from_ruby(value) end diff --git a/ruby/tests/well_known_types_test.rb b/ruby/tests/well_known_types_test.rb index 32e73f55..9b46632b 100644 --- a/ruby/tests/well_known_types_test.rb +++ b/ruby/tests/well_known_types_test.rb @@ -85,6 +85,30 @@ class TestWellKnownTypes < Test::Unit::TestCase assert_equal(should_equal, struct.to_h) assert_equal(should_equal["sublist"].length, struct["sublist"].length) + + assert_raise Google::Protobuf::UnexpectedStructType do + struct[123] = 5 + end + + assert_raise Google::Protobuf::UnexpectedStructType do + struct[5] = Time.new + end + + assert_raise Google::Protobuf::UnexpectedStructType do + struct[5] = [Time.new] + end + + assert_raise Google::Protobuf::UnexpectedStructType do + struct[5] = {123 => 456} + end + + assert_raise Google::Protobuf::UnexpectedStructType do + struct = Google::Protobuf::Struct.new + struct.fields["foo"] = Google::Protobuf::Value.new + # Tries to return a Ruby value for a Value class whose type + # hasn't been filled in. + struct["foo"] + end end def test_any -- cgit v1.2.3