|
| 1 | +diff --git a/lib/protobuf/field/bytes_field.rb b/lib/protobuf/field/bytes_field.rb |
| 2 | +index 81a3634d..bce24c80 100644 |
| 3 | +--- a/lib/protobuf/field/bytes_field.rb |
| 4 | ++++ b/lib/protobuf/field/bytes_field.rb |
| 5 | +@@ -48,7 +48,18 @@ def wire_type |
| 6 | + |
| 7 | + def coerce!(value) |
| 8 | + case value |
| 9 | +- when String, Symbol |
| 10 | ++ when String |
| 11 | ++ if value.encoding == Encoding::ASCII_8BIT |
| 12 | ++ # This is a "binary" string |
| 13 | ++ value |
| 14 | ++ else |
| 15 | ++ # Assume the value is Base64 encoded (from JSON) |
| 16 | ++ # Ideally we'd do the Base64 decoding while processing the JSON, |
| 17 | ++ # but this is tricky to do since we don't know the protobuf field |
| 18 | ++ # types when we do that. |
| 19 | ++ Base64.decode64(value) |
| 20 | ++ end |
| 21 | ++ when Symbol |
| 22 | + value.to_s |
| 23 | + when NilClass |
| 24 | + nil |
| 25 | +diff --git a/lib/protobuf/field/enum_field.rb b/lib/protobuf/field/enum_field.rb |
| 26 | +index 6993faff..12867adf 100644 |
| 27 | +--- a/lib/protobuf/field/enum_field.rb |
| 28 | ++++ b/lib/protobuf/field/enum_field.rb |
| 29 | +@@ -37,6 +37,11 @@ def coerce!(value) |
| 30 | + type_class.fetch(value) || fail(TypeError, "Invalid Enum value: #{value.inspect} for #{name}") |
| 31 | + end |
| 32 | + |
| 33 | ++ def json_encode(value, options={}) |
| 34 | ++ enum = type_class.enums.find { |e| e.to_i == value } |
| 35 | ++ enum.to_s(:name) |
| 36 | ++ end |
| 37 | ++ |
| 38 | + private |
| 39 | + |
| 40 | + ## |
| 41 | +diff --git a/lib/protobuf/field/field_array.rb b/lib/protobuf/field/field_array.rb |
| 42 | +index eb1f29d9..47f9c379 100644 |
| 43 | +--- a/lib/protobuf/field/field_array.rb |
| 44 | ++++ b/lib/protobuf/field/field_array.rb |
| 45 | +@@ -81,6 +81,8 @@ def normalize(value) |
| 46 | + |
| 47 | + if field.is_a?(::Protobuf::Field::EnumField) |
| 48 | + field.type_class.fetch(value) |
| 49 | ++ elsif field.is_a?(::Protobuf::Field::BytesField) |
| 50 | ++ field.coerce!(value) |
| 51 | + elsif field.is_a?(::Protobuf::Field::MessageField) && value.is_a?(field.type_class) |
| 52 | + value |
| 53 | + elsif field.is_a?(::Protobuf::Field::MessageField) && value.respond_to?(:to_hash) |
| 54 | +diff --git a/lib/protobuf/message.rb b/lib/protobuf/message.rb |
| 55 | +index 06b26b94..d2b3f862 100644 |
| 56 | +--- a/lib/protobuf/message.rb |
| 57 | ++++ b/lib/protobuf/message.rb |
| 58 | +@@ -21,6 +21,22 @@ def self.to_json |
| 59 | + name |
| 60 | + end |
| 61 | + |
| 62 | ++ def self.from_json(json) |
| 63 | ++ fields = normalize_json(JSON.parse(json)) |
| 64 | ++ new(fields) |
| 65 | ++ end |
| 66 | ++ |
| 67 | ++ def self.normalize_json(ob) |
| 68 | ++ case ob |
| 69 | ++ when Array |
| 70 | ++ ob.map { |value| normalize_json(value) } |
| 71 | ++ when Hash |
| 72 | ++ Hash[*ob.flat_map { |key, value| [key.underscore, normalize_json(value)] }] |
| 73 | ++ else |
| 74 | ++ ob |
| 75 | ++ end |
| 76 | ++ end |
| 77 | ++ |
| 78 | + ## |
| 79 | + # Constructor |
| 80 | + # |
| 81 | +@@ -150,7 +166,7 @@ def to_json_hash(options = {}) |
| 82 | + |
| 83 | + # NB: to_json_hash_value should come before json_encode so as to handle |
| 84 | + # repeated fields without extra logic. |
| 85 | +- hashed_value = if value.respond_to?(:to_json_hash_value) |
| 86 | ++ hashed_value = if value.respond_to?(:to_json_hash_value) && !field.is_a?(::Protobuf::Field::EnumField) |
| 87 | + value.to_json_hash_value(options) |
| 88 | + elsif field.respond_to?(:json_encode) |
| 89 | + field.json_encode(value) |
| 90 | +diff --git a/spec/encoding/all_types_spec.rb b/spec/encoding/all_types_spec.rb |
| 91 | +index fbd38b46..04ddb866 100644 |
| 92 | +--- a/spec/encoding/all_types_spec.rb |
| 93 | ++++ b/spec/encoding/all_types_spec.rb |
| 94 | +@@ -18,7 +18,7 @@ |
| 95 | + :optional_double => 112, |
| 96 | + :optional_bool => true, |
| 97 | + :optional_string => "115", |
| 98 | +- :optional_bytes => "116", |
| 99 | ++ :optional_bytes => "116".force_encoding(Encoding::ASCII_8BIT), |
| 100 | + :optional_nested_message => Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 118), |
| 101 | + :optional_foreign_message => Protobuf_unittest::ForeignMessage.new(:c => 119), |
| 102 | + :optional_import_message => Protobuf_unittest_import::ImportMessage.new(:d => 120), |
| 103 | +@@ -43,7 +43,7 @@ |
| 104 | + :repeated_double => [212, 312], |
| 105 | + :repeated_bool => [true, false], |
| 106 | + :repeated_string => ["215", "315"], |
| 107 | +- :repeated_bytes => ["216", "316"], |
| 108 | ++ :repeated_bytes => ["216".force_encoding(Encoding::ASCII_8BIT), "316".force_encoding(Encoding::ASCII_8BIT)], |
| 109 | + :repeated_nested_message => [ |
| 110 | + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 218), |
| 111 | + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 318), |
| 112 | +@@ -88,7 +88,7 @@ |
| 113 | + :default_double => 412, |
| 114 | + :default_bool => false, |
| 115 | + :default_string => "415", |
| 116 | +- :default_bytes => "416", |
| 117 | ++ :default_bytes => "416".force_encoding(Encoding::ASCII_8BIT), |
| 118 | + :default_nested_enum => ::Protobuf_unittest::TestAllTypes::NestedEnum::FOO, |
| 119 | + :default_foreign_enum => ::Protobuf_unittest::ForeignEnum::FOREIGN_FOO, |
| 120 | + :default_import_enum => ::Protobuf_unittest_import::ImportEnum::IMPORT_FOO, |
| 121 | +diff --git a/spec/encoding/extreme_values_spec.rb b/spec/encoding/extreme_values_spec.rb |
| 122 | +index 477e695a..7f3d516f 100644 |
| 123 | +Binary files a/spec/encoding/extreme_values_spec.rb and b/spec/encoding/extreme_values_spec.rb differ |
| 124 | +diff --git a/spec/lib/protobuf/field/enum_field_spec.rb b/spec/lib/protobuf/field/enum_field_spec.rb |
| 125 | +index cd72760d..c2e04eed 100644 |
| 126 | +--- a/spec/lib/protobuf/field/enum_field_spec.rb |
| 127 | ++++ b/spec/lib/protobuf/field/enum_field_spec.rb |
| 128 | +@@ -23,4 +23,22 @@ |
| 129 | + expect(message.decode(instance.encode).enum).to eq(-33) |
| 130 | + end |
| 131 | + end |
| 132 | ++ |
| 133 | ++ # https://developers.google.com/protocol-buffers/docs/proto3#json |
| 134 | ++ describe '.{to_json, from_json}' do |
| 135 | ++ it 'serialises enum value as string' do |
| 136 | ++ instance = message.new(:enum => :POSITIVE) |
| 137 | ++ expect(instance.to_json).to eq('{"enum":"POSITIVE"}') |
| 138 | ++ end |
| 139 | ++ |
| 140 | ++ it 'deserialises enum value as integer' do |
| 141 | ++ instance = message.from_json('{"enum":22}') |
| 142 | ++ expect(instance.enum).to eq(22) |
| 143 | ++ end |
| 144 | ++ |
| 145 | ++ it 'deserialises enum value as string' do |
| 146 | ++ instance = message.from_json('{"enum":"NEGATIVE"}') |
| 147 | ++ expect(instance.enum).to eq(-33) |
| 148 | ++ end |
| 149 | ++ end |
| 150 | + end |
| 151 | +diff --git a/spec/lib/protobuf/message_spec.rb b/spec/lib/protobuf/message_spec.rb |
| 152 | +index 13110bc9..40b68a6d 100644 |
| 153 | +--- a/spec/lib/protobuf/message_spec.rb |
| 154 | ++++ b/spec/lib/protobuf/message_spec.rb |
| 155 | +@@ -429,7 +429,7 @@ |
| 156 | + specify { expect(subject.to_json).to eq '{"name":"Test Name","active":false}' } |
| 157 | + |
| 158 | + context 'for byte fields' do |
| 159 | +- let(:bytes) { "\x06\x8D1HP\x17:b" } |
| 160 | ++ let(:bytes) { "\x06\x8D1HP\x17:b".force_encoding(Encoding::ASCII_8BIT) } |
| 161 | + |
| 162 | + subject do |
| 163 | + ::Test::ResourceFindRequest.new(:widget_bytes => [bytes]) |
| 164 | +@@ -439,7 +439,7 @@ |
| 165 | + end |
| 166 | + |
| 167 | + context 'using lower camel case field names' do |
| 168 | +- let(:bytes) { "\x06\x8D1HP\x17:b" } |
| 169 | ++ let(:bytes) { "\x06\x8D1HP\x17:b".force_encoding(Encoding::ASCII_8BIT) } |
| 170 | + |
| 171 | + subject do |
| 172 | + ::Test::ResourceFindRequest.new(:widget_bytes => [bytes]) |
| 173 | +@@ -449,6 +449,26 @@ |
| 174 | + end |
| 175 | + end |
| 176 | + |
| 177 | ++ describe '.from_json' do |
| 178 | ++ it 'decodes optional bytes field with base64' do |
| 179 | ++ expected_single_bytes = "\x06\x8D1HP\x17:b".unpack('C*') |
| 180 | ++ single_bytes = ::Test::ResourceFindRequest |
| 181 | ++ .from_json('{"singleBytes":"Bo0xSFAXOmI="}') |
| 182 | ++ .single_bytes.unpack('C*') |
| 183 | ++ |
| 184 | ++ expect(single_bytes).to(eq(expected_single_bytes)) |
| 185 | ++ end |
| 186 | ++ |
| 187 | ++ it 'decodes repeated bytes field with base64' do |
| 188 | ++ expected_widget_bytes = ["\x06\x8D1HP\x17:b"].map { |s| s.unpack('C*') } |
| 189 | ++ widget_bytes = ::Test::ResourceFindRequest |
| 190 | ++ .from_json('{"widgetBytes":["Bo0xSFAXOmI="]}') |
| 191 | ++ .widget_bytes.map { |s| s.unpack('C*') } |
| 192 | ++ |
| 193 | ++ expect(widget_bytes).to(eq(expected_widget_bytes)) |
| 194 | ++ end |
| 195 | ++ end |
| 196 | ++ |
| 197 | + describe '.to_json' do |
| 198 | + it 'returns the class name of the message for use in json encoding' do |
| 199 | + expect do |
| 200 | +diff --git a/spec/support/protos/resource.pb.rb b/spec/support/protos/resource.pb.rb |
| 201 | +index f81ef52f..e765b1ce 100644 |
| 202 | +--- a/spec/support/protos/resource.pb.rb |
| 203 | ++++ b/spec/support/protos/resource.pb.rb |
| 204 | +@@ -72,6 +72,7 @@ class ResourceFindRequest |
| 205 | + optional :bool, :active, 2 |
| 206 | + repeated :string, :widgets, 3 |
| 207 | + repeated :bytes, :widget_bytes, 4 |
| 208 | ++ optional :bytes, :single_bytes, 5 |
| 209 | + end |
| 210 | + |
| 211 | + class ResourceSleepRequest |
| 212 | +@@ -169,4 +170,3 @@ class ResourceService < ::Protobuf::Rpc::Service |
| 213 | + end |
| 214 | + |
| 215 | + end |
| 216 | +- |
| 217 | +diff --git a/spec/support/protos/resource.proto b/spec/support/protos/resource.proto |
| 218 | +index 70b338b3..a5573e24 100644 |
| 219 | +--- a/spec/support/protos/resource.proto |
| 220 | ++++ b/spec/support/protos/resource.proto |
| 221 | +@@ -47,6 +47,7 @@ message ResourceFindRequest { |
| 222 | + optional bool active = 2; |
| 223 | + repeated string widgets = 3; |
| 224 | + repeated bytes widget_bytes = 4; |
| 225 | ++ optional bytes single_bytes = 5; |
| 226 | + } |
| 227 | + |
| 228 | + message ResourceSleepRequest { |
0 commit comments