Skip to content

Commit e965be5

Browse files
committed
Add basic code generation
This just walks the AST returned by the parser and generates code
1 parent 52317ae commit e965be5

File tree

4 files changed

+220
-287
lines changed

4 files changed

+220
-287
lines changed

lib/protobuff/codegen.rb

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
require "erb"
2+
3+
module ProtoBuff
4+
class CodeGen
5+
CLASS_TEMPLATE = ERB.new(<<-ruby, trim_mode: '-')
6+
class <%= message.name %>
7+
<%- if message.fields.length > 0 -%>
8+
attr_accessor <%= message.fields.map { |f| ":" + f.name }.join(", ") %>
9+
10+
def self.decode(buff)
11+
decode_from(ProtoBuff::Decoder.new(buff), buff.bytesize)
12+
end
13+
14+
def self.decode_from(decoder, len)
15+
obj = <%= message.name %>.new
16+
17+
while true
18+
break if decoder.index >= len
19+
20+
tag = decoder.pull_tag
21+
<%- message.fields.each_with_index do |field, idx| -%>
22+
<%= idx == 0 ? "if" : "elsif" %> tag == <%= tag_for_field(field, field.number) %>
23+
<%= decode_code(field) %>
24+
<%- end -%>
25+
else
26+
raise
27+
end
28+
end
29+
30+
obj
31+
end
32+
33+
def initialize(<%= initialize_signature(message) %>)
34+
<%- for field in message.fields -%>
35+
@<%= field.name %> = <%= field.name %>
36+
<%- end -%>
37+
<%- end -%>
38+
end
39+
end
40+
ruby
41+
42+
PACKED_REPEATED = ERB.new(<<ruby)
43+
idx = 0
44+
goal = decoder.index + decoder.pull_uint64
45+
list = obj.<%= field.name %>
46+
while true
47+
break if decoder.index >= goal
48+
list[idx] = <%= decode_subtype(field) %>
49+
idx += 1
50+
end
51+
ruby
52+
53+
def initialize(ast)
54+
@ast = ast
55+
end
56+
57+
def to_ruby
58+
@ast.messages.map { |message|
59+
CLASS_TEMPLATE.result(binding)
60+
}.join
61+
end
62+
63+
private
64+
65+
def default_for(field)
66+
case field.qualifier
67+
when :optional
68+
case field.type
69+
when "string"
70+
'""'
71+
when "uint64", "int32", "sint32", "uint32"
72+
0
73+
when "bool"
74+
false
75+
when /[A-Z]+\w+/ # FIXME: this doesn't seem right...
76+
'nil'
77+
else
78+
raise "Unknown field type #{field.type}"
79+
end
80+
when :repeated
81+
"[]"
82+
else
83+
raise "Unknown qualifier #{field.type}"
84+
end
85+
end
86+
87+
def initialize_signature(msg)
88+
msg.fields.map { |f| "#{f.name}: #{default_for(f)}" }.join(", ")
89+
end
90+
91+
def tag_for_field(field, idx)
92+
sprintf("%#02x", (idx << 3 | wire_type(field)))
93+
end
94+
95+
VARINT = 0
96+
I64 = 1
97+
LEN = 2
98+
I32 = 5
99+
100+
def wire_type(field)
101+
case field.qualifier
102+
when :optional
103+
case field.type
104+
when "string"
105+
LEN
106+
when "int32", "uint64", "bool", "sint32", "uint32"
107+
VARINT
108+
when /[A-Z]+\w+/ # FIXME: this doesn't seem right...
109+
LEN
110+
else
111+
raise "Unknown wire type for field #{field.type}"
112+
end
113+
when :repeated
114+
LEN
115+
else
116+
raise "Unknown qualifier #{field.qualifier}"
117+
end
118+
end
119+
120+
def decode_subtype(field)
121+
case field.type
122+
when "string"
123+
"decoder.pull_string"
124+
when "uint64"
125+
"decoder.pull_uint64"
126+
when "int32"
127+
"decoder.pull_int32"
128+
when "uint32"
129+
"decoder.pull_uint32"
130+
when "sint32"
131+
"decoder.pull_sint32"
132+
when "bool"
133+
"decoder.pull_boolean"
134+
when /[A-Z]+\w+/ # FIXME: this doesn't seem right...
135+
"#{field.type}.decode_from(decoder, decoder.index + decoder.pull_uint64)"
136+
else
137+
raise "Unknown field type #{field.type}"
138+
end
139+
end
140+
141+
def decode_code(field)
142+
case field.qualifier
143+
when :optional
144+
"obj.#{field.name} = #{decode_subtype(field)}"
145+
when :repeated
146+
case field.type
147+
when "uint32"
148+
PACKED_REPEATED.result(binding)
149+
else
150+
raise "Unknown field type #{field.type}"
151+
end
152+
else
153+
raise "Unknown qualifier #{field.qualifier}"
154+
end
155+
end
156+
end
157+
end

test/codegen_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require "helper"
2+
3+
module ProtoBuff
4+
class CodeGenTest < Test
5+
def test_make_ruby
6+
unit = ProtoBuff.parse_string('message TestMessage { string id = 1; uint64 shop_id = 2; bool boolean = 3; }')
7+
gen = CodeGen.new unit
8+
klass = Class.new { self.class_eval gen.to_ruby }
9+
obj = klass::TestMessage.new
10+
assert_equal "", obj.id
11+
assert_equal 0, obj.shop_id
12+
assert_equal false, obj.boolean
13+
end
14+
15+
def test_int32
16+
unit = ProtoBuff.parse_string('message Test1 { optional int32 a = 1; }')
17+
gen = CodeGen.new unit
18+
klass = Class.new { self.class_eval gen.to_ruby }
19+
obj = klass::Test1.new
20+
assert_equal 0, obj.a
21+
end
22+
23+
def test_fixture_file
24+
unit = ProtoBuff.parse_file('./test/fixtures/test.proto')
25+
26+
gen = CodeGen.new unit
27+
28+
klass = Class.new { self.class_eval gen.to_ruby }
29+
obj = klass::Test1.new
30+
assert_equal 0, obj.a
31+
32+
assert_equal [], klass::TestRepeatedField.new.e
33+
end
34+
end
35+
end

test/helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "proto/test/fixtures/test_pb"
44
require "protobuff/decoder"
55
require "protobuff/parser"
6+
require "protobuff/codegen"
67

78
module ProtoBuff
89
class Test < Minitest::Test

0 commit comments

Comments
 (0)