Skip to content

Commit 96fe1ba

Browse files
committed
Merge pull request #247 from skippy/fix-class-inheritance
allow Protobuf::Message based classes to be inherited
2 parents c0257ec + 9eb9520 commit 96fe1ba

2 files changed

Lines changed: 143 additions & 77 deletions

File tree

lib/protobuf/message/fields.rb

Lines changed: 91 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,117 +3,131 @@ class Message
33
module Fields
44

55
def self.extended(other)
6+
other.extend(ClassMethods)
67
::Protobuf.deprecator.define_deprecated_methods(
78
other.singleton_class,
89
:get_ext_field_by_name => :get_extension_field,
910
:get_ext_field_by_tag => :get_extension_field,
1011
:get_field_by_name => :get_field,
1112
:get_field_by_tag => :get_field,
1213
)
14+
15+
def inherited(subclass)
16+
inherit_fields!(subclass)
17+
end
1318
end
1419

15-
##
16-
# Field Definition Methods
17-
#
20+
module ClassMethods
1821

19-
# Define an optional field.
20-
#
21-
def optional(type_class, name, tag, options = {})
22-
define_field(:optional, type_class, name, tag, options)
23-
end
22+
##
23+
# Field Definition Methods
24+
#
2425

25-
# Define a repeated field.
26-
#
27-
def repeated(type_class, name, tag, options = {})
28-
define_field(:repeated, type_class, name, tag, options)
29-
end
26+
# Define an optional field.
27+
#
28+
def optional(type_class, name, tag, options = {})
29+
define_field(:optional, type_class, name, tag, options)
30+
end
3031

31-
# Define a required field.
32-
#
33-
def required(type_class, name, tag, options = {})
34-
define_field(:required, type_class, name, tag, options)
35-
end
32+
# Define a repeated field.
33+
#
34+
def repeated(type_class, name, tag, options = {})
35+
define_field(:repeated, type_class, name, tag, options)
36+
end
3637

37-
# Define an extension range.
38-
#
39-
def extensions(range)
40-
extension_ranges << range
41-
end
38+
# Define a required field.
39+
#
40+
def required(type_class, name, tag, options = {})
41+
define_field(:required, type_class, name, tag, options)
42+
end
4243

43-
##
44-
# Field Access Methods
45-
#
44+
# Define an extension range.
45+
#
46+
def extensions(range)
47+
extension_ranges << range
48+
end
4649

47-
def all_fields
48-
@all_fields ||= field_store.values.uniq.sort_by(&:tag)
49-
end
50+
##
51+
# Field Access Methods
52+
#
53+
def all_fields
54+
@all_fields ||= field_store.values.uniq.sort_by(&:tag)
55+
end
5056

51-
def extension_fields
52-
@extension_fields ||= all_fields.select(&:extension?)
53-
end
57+
def extension_fields
58+
@extension_fields ||= all_fields.select(&:extension?)
59+
end
5460

55-
def extension_ranges
56-
@extension_ranges ||= []
57-
end
61+
def extension_ranges
62+
@extension_ranges ||= []
63+
end
5864

59-
def extension_tag?(tag)
60-
tag.respond_to?(:to_i) && get_extension_field(tag).present?
61-
end
65+
def extension_tag?(tag)
66+
tag.respond_to?(:to_i) && get_extension_field(tag).present?
67+
end
6268

63-
def field_store
64-
@field_store ||= {}
65-
end
69+
def field_store
70+
@field_store ||= {}
71+
end
6672

67-
def fields
68-
@fields ||= all_fields.reject(&:extension?)
69-
end
73+
def fields
74+
@fields ||= all_fields.reject(&:extension?)
75+
end
7076

71-
def field_tag?(tag, allow_extension = false)
72-
tag.respond_to?(:to_i) && get_field(tag, allow_extension).present?
73-
end
77+
def field_tag?(tag, allow_extension = false)
78+
tag.respond_to?(:to_i) && get_field(tag, allow_extension).present?
79+
end
7480

75-
def get_extension_field(name_or_tag)
76-
name_or_tag = name_or_tag.to_sym if name_or_tag.respond_to?(:to_sym)
77-
field = field_store[name_or_tag]
78-
field if field.try(:extension?) { false }
79-
end
81+
def get_extension_field(name_or_tag)
82+
name_or_tag = name_or_tag.to_sym if name_or_tag.respond_to?(:to_sym)
83+
field = field_store[name_or_tag]
84+
field if field.try(:extension?) { false }
85+
end
8086

81-
def get_field(name_or_tag, allow_extension = false)
82-
name_or_tag = name_or_tag.to_sym if name_or_tag.respond_to?(:to_sym)
83-
field = field_store[name_or_tag]
87+
def get_field(name_or_tag, allow_extension = false)
88+
name_or_tag = name_or_tag.to_sym if name_or_tag.respond_to?(:to_sym)
89+
field = field_store[name_or_tag]
8490

85-
if field && (allow_extension || !field.extension?)
86-
field
87-
else
88-
nil
91+
if field && (allow_extension || !field.extension?)
92+
field
93+
else
94+
nil
95+
end
8996
end
90-
end
9197

92-
def define_field(rule, type_class, field_name, tag, options)
93-
raise_if_tag_collision(tag, field_name)
94-
raise_if_name_collision(field_name)
98+
def define_field(rule, type_class, field_name, tag, options)
99+
raise_if_tag_collision(tag, field_name)
100+
raise_if_name_collision(field_name)
95101

96-
field = ::Protobuf::Field.build(self, rule, type_class, field_name, tag, options)
97-
field_store[field_name] = field
98-
field_store[tag] = field
102+
field = ::Protobuf::Field.build(self, rule, type_class, field_name, tag, options)
103+
field_store[field_name] = field
104+
field_store[tag] = field
99105

100-
define_method("#{field_name}!") do
101-
@values[field_name]
106+
define_method("#{field_name}!") do
107+
@values[field_name]
108+
end
102109
end
103-
end
104110

105-
def raise_if_tag_collision(tag, field_name)
106-
if get_field(tag, true)
107-
fail TagCollisionError, %(Field number #{tag} has already been used in "#{name}" by field "#{field_name}".)
111+
def raise_if_tag_collision(tag, field_name)
112+
if get_field(tag, true)
113+
fail TagCollisionError, %(Field number #{tag} has already been used in "#{name}" by field "#{field_name}".)
114+
end
108115
end
109-
end
110116

111-
def raise_if_name_collision(field_name)
112-
if get_field(field_name, true)
113-
fail DuplicateFieldNameError, %(Field name #{field_name} has already been used in "#{name}".)
117+
def raise_if_name_collision(field_name)
118+
if get_field(field_name, true)
119+
fail DuplicateFieldNameError, %(Field name #{field_name} has already been used in "#{name}".)
120+
end
114121
end
115-
end
116122

123+
def inherit_fields!(subclass)
124+
instance_variables.each do |iv|
125+
subclass.instance_variable_set(iv, instance_variable_get(iv))
126+
end
127+
end
128+
private :inherit_fields!
129+
130+
end
117131
end
118132
end
119133
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require 'spec_helper'
2+
require 'spec/support/test/resource_service'
3+
4+
RSpec.describe 'works through class inheritance' do
5+
module Corp
6+
module Protobuf
7+
class Error < ::Protobuf::Message
8+
required :string, :foo, 1
9+
end
10+
end
11+
end
12+
module Corp
13+
class ErrorHandler < Corp::Protobuf::Error
14+
end
15+
end
16+
17+
let(:args) { { :foo => 'bar' } }
18+
let(:parent_class) { Corp::Protobuf::Error }
19+
let(:inherited_class) { Corp::ErrorHandler }
20+
21+
specify '#encode' do
22+
expected_result = "\n\x03bar"
23+
expected_result.force_encoding(Encoding::BINARY)
24+
expect(parent_class.new(args).encode).to eq(expected_result)
25+
expect(inherited_class.new(args).encode).to eq(expected_result)
26+
end
27+
28+
specify '#to_hash' do
29+
expect(parent_class.new(args).to_hash).to eq(args)
30+
expect(inherited_class.new(args).to_hash).to eq(args)
31+
end
32+
33+
specify '#to_json' do
34+
expect(parent_class.new(args).to_json).to eq(args.to_json)
35+
expect(inherited_class.new(args).to_json).to eq(args.to_json)
36+
end
37+
38+
specify '.encode' do
39+
expected_result = "\n\x03bar"
40+
expected_result.force_encoding(Encoding::BINARY)
41+
expect(parent_class.encode(args)).to eq(expected_result)
42+
expect(inherited_class.encode(args)).to eq(expected_result)
43+
end
44+
45+
specify '.decode' do
46+
raw_value = "\n\x03bar"
47+
raw_value.force_encoding(Encoding::BINARY)
48+
expect(parent_class.decode(raw_value).to_hash).to eq(args)
49+
expect(inherited_class.decode(raw_value).to_hash).to eq(args)
50+
end
51+
52+
end

0 commit comments

Comments
 (0)