Browse Source

Ruby: added API support for well-known types.

Josh Haberman 9 years ago
parent
commit
e3094a8d80
3 changed files with 305 additions and 0 deletions
  1. 1 0
      Makefile.am
  2. 206 0
      ruby/lib/google/protobuf/well_known_types.rb
  3. 98 0
      ruby/tests/well_known_types_test.rb

+ 1 - 0
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=                              \

+ 206 - 0
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

+ 98 - 0
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