Skip to content

Commit 738115a

Browse files
authored
Merge pull request #4 from Semior001/feature/multiline-strings
feat: added multiline strings
2 parents abd9627 + 1b6ddb3 commit 738115a

File tree

7 files changed

+185
-20
lines changed

7 files changed

+185
-20
lines changed

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ message SomeMessage {
136136
}
137137
```
138138

139+
#### multiline strings
140+
protobuf itself doesn't support multiline strings, so gRoxy introduces it's own syntax for them in order to allow to specify complex values in `groxypb.value` option. Multiline strings should be enclosed in triple backticks:
141+
```protobuf
142+
message Dependency {
143+
string field1 = 1;
144+
int32 field2 = 2;
145+
bool field3 = 3;
146+
}
147+
148+
message SomeMessage {
149+
option (groxypb.target) = true;
150+
string message = 1 [(groxypb.value) = `Hello,
151+
World!`];
152+
Dependency dependency = 2 [(groxypb.value) = `{
153+
"field1": "value1",
154+
"field2": 2,
155+
"field3": true
156+
}`];
157+
}
158+
```
159+
139160
#### nested messages
140161
In case of nested messages, there are two options how to set values:
141162
1. Set the value in the nested message:
@@ -156,7 +177,7 @@ message NestedMessage {
156177
message SomeMessage {
157178
option (groxypb.target) = true;
158179
string parent_value = 1 [(groxypb.value) = "parent"];
159-
NestedMessage nested = 2 [(groxypb.value) = "{\"nested_value\": \"nested\"}"];
180+
NestedMessage nested = 2 [(groxypb.value) = '{"nested_value": "nested"}'];
160181
}
161182
```
162183

_example/mock.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,19 @@ rules:
4646
body: |
4747
message Dependency {
4848
string some_dependant_value = 6;
49+
bool some_dependant_bool = 7;
50+
string some_rich_text = 8;
4951
}
5052
5153
message StubResponse {
5254
// this option specifies that the message is a response
5355
option (groxypb.target) = true;
54-
Dependency dependency = 3 [(groxypb.value) = '{"some_dependant_value": "Hello, World!"}'];
56+
57+
Dependency dependency = 3 [(groxypb.value) = `{
58+
"some_dependant_value": "some value",
59+
"some_dependant_bool": true,
60+
"some_rich_text": "some text"
61+
}`];
5562
}
5663
5764
# The next rule will respond with an error message.

pkg/protodef/definer.go

+93-18
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ func NewDefiner(opts ...Option) *Definer {
4141
// BuildTarget seeks the target message in the given protobuf snippet and
4242
// returns a proto.Message that can be used to respond requests or match requests to.
4343
func (b *Definer) BuildTarget(def string) (proto.Message, error) {
44+
def, err := b.joinMultilineStrings(def)
45+
if err != nil {
46+
return nil, fmt.Errorf("invalid file: %w", err)
47+
}
48+
4449
def = b.enrich(def)
4550

4651
fd, err := b.parseDefinition(def)
@@ -61,6 +66,59 @@ func (b *Definer) BuildTarget(def string) (proto.Message, error) {
6166
return protoadapt.MessageV2Of(msg), nil
6267
}
6368

69+
// joinMultilineStrings replaces the multiline strings enclosed in "`" symbol into
70+
// a single line string, enclosed in double quotes, with escaped newlines, tabs,
71+
// and double quotes.
72+
func (b *Definer) joinMultilineStrings(def string) (string, error) {
73+
var sb strings.Builder
74+
75+
type pos struct{ Line, Col int }
76+
curr, backtickStart := pos{Line: 1}, pos{}
77+
78+
countPos := func(r rune) {
79+
if r != '\n' {
80+
curr.Col++
81+
return
82+
}
83+
84+
curr.Line++
85+
curr.Col = 0
86+
}
87+
88+
inMultiline := false
89+
for _, r := range def {
90+
countPos(r)
91+
92+
switch {
93+
case r == '`':
94+
if !inMultiline {
95+
backtickStart = curr
96+
}
97+
inMultiline = !inMultiline
98+
_, _ = sb.WriteRune('"')
99+
case inMultiline:
100+
switch r {
101+
case '\n':
102+
_, _ = sb.WriteString(`\n`)
103+
case '\t':
104+
_, _ = sb.WriteString(`\t`)
105+
case '"':
106+
_, _ = sb.WriteString(`\"`)
107+
default:
108+
_, _ = sb.WriteRune(r)
109+
}
110+
default:
111+
_, _ = sb.WriteRune(r)
112+
}
113+
}
114+
115+
if inMultiline {
116+
return "", errUnclosedMultilineString(backtickStart)
117+
}
118+
119+
return sb.String(), nil
120+
}
121+
64122
func (b *Definer) enrich(def string) string {
65123
sb := &strings.Builder{}
66124
_, _ = fmt.Fprintln(sb, `syntax = "proto3";`)
@@ -335,7 +393,20 @@ var types = map[descriptorpb.FieldDescriptorProto_Type]any{
335393
descriptorpb.FieldDescriptorProto_TYPE_SINT64: int64(0),
336394
}
337395

396+
func floatparser(_ fieldDescriptor, s string) (any, error) {
397+
return strconv.ParseFloat(s, 64)
398+
}
399+
400+
func intparser(_ fieldDescriptor, s string) (any, error) {
401+
return strconv.ParseInt(s, 10, 64)
402+
}
403+
404+
func uintparser(_ fieldDescriptor, s string) (any, error) {
405+
return strconv.ParseUint(s, 10, 64)
406+
}
407+
338408
var parsers = map[descriptorpb.FieldDescriptorProto_Type]func(fd fieldDescriptor, s string) (any, error){
409+
descriptorpb.FieldDescriptorProto_TYPE_STRING: func(_ fieldDescriptor, s string) (any, error) { return s, nil },
339410
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: func(fd fieldDescriptor, s string) (any, error) {
340411
msg := dynamic.NewMessage(fd.GetMessageType())
341412
if err := msg.UnmarshalJSON([]byte(s)); err != nil {
@@ -351,24 +422,28 @@ var parsers = map[descriptorpb.FieldDescriptorProto_Type]func(fd fieldDescriptor
351422
return nil, fmt.Errorf("unknown enum value: %s", s)
352423
},
353424

354-
descriptorpb.FieldDescriptorProto_TYPE_STRING: func(_ fieldDescriptor, s string) (any, error) { return s, nil },
355-
descriptorpb.FieldDescriptorProto_TYPE_BYTES: func(_ fieldDescriptor, s string) (any, error) { return base64.StdEncoding.DecodeString(s) },
356-
descriptorpb.FieldDescriptorProto_TYPE_BOOL: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseBool(s) },
357-
358-
descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseFloat(s, 64) },
359-
descriptorpb.FieldDescriptorProto_TYPE_FLOAT: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseFloat(s, 64) },
360-
361-
descriptorpb.FieldDescriptorProto_TYPE_INT64: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
362-
descriptorpb.FieldDescriptorProto_TYPE_INT32: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
363-
descriptorpb.FieldDescriptorProto_TYPE_FIXED64: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
364-
descriptorpb.FieldDescriptorProto_TYPE_FIXED32: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
365-
descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
366-
descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
367-
descriptorpb.FieldDescriptorProto_TYPE_SINT32: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
368-
descriptorpb.FieldDescriptorProto_TYPE_SINT64: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseInt(s, 10, 64) },
369-
370-
descriptorpb.FieldDescriptorProto_TYPE_UINT32: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseUint(s, 10, 64) },
371-
descriptorpb.FieldDescriptorProto_TYPE_UINT64: func(_ fieldDescriptor, s string) (any, error) { return strconv.ParseUint(s, 10, 64) },
425+
descriptorpb.FieldDescriptorProto_TYPE_BYTES: func(_ fieldDescriptor, s string) (any, error) {
426+
return base64.StdEncoding.DecodeString(s)
427+
},
428+
429+
descriptorpb.FieldDescriptorProto_TYPE_BOOL: func(_ fieldDescriptor, s string) (any, error) {
430+
return strconv.ParseBool(s)
431+
},
432+
433+
descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: floatparser,
434+
descriptorpb.FieldDescriptorProto_TYPE_FLOAT: floatparser,
435+
436+
descriptorpb.FieldDescriptorProto_TYPE_INT64: intparser,
437+
descriptorpb.FieldDescriptorProto_TYPE_INT32: intparser,
438+
descriptorpb.FieldDescriptorProto_TYPE_FIXED64: intparser,
439+
descriptorpb.FieldDescriptorProto_TYPE_FIXED32: intparser,
440+
descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: intparser,
441+
descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: intparser,
442+
descriptorpb.FieldDescriptorProto_TYPE_SINT32: intparser,
443+
descriptorpb.FieldDescriptorProto_TYPE_SINT64: intparser,
444+
445+
descriptorpb.FieldDescriptorProto_TYPE_UINT32: uintparser,
446+
descriptorpb.FieldDescriptorProto_TYPE_UINT64: uintparser,
372447
}
373448

374449
// fieldDescriptor is an interface that allows to mock the desc.FieldDescriptor

pkg/protodef/definer_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ func TestBuildMessage(t *testing.T) {
135135
}`,
136136
want: &testdata.Response{Nested: &testdata.Nested{NestedValue: "Hello, World!"}},
137137
},
138+
{
139+
name: "with nested message, value defined in target",
140+
def: testdata.String(t, "multiline.txt"),
141+
want: &testdata.Response{Nested: &testdata.Nested{
142+
Enum: testdata.Enum_STUB_ENUM_FIRST,
143+
NestedValue: "Hello, World!",
144+
}},
145+
},
146+
{
147+
name: "with nested message, value defined in target",
148+
def: testdata.String(t, "multiline_not_closed.txt"),
149+
wantErr: errUnclosedMultilineString{Line: 16, Col: 42},
150+
},
138151
{
139152
name: "with nested message, value defined in target AND in message type, target value has priority",
140153
def: ` message Nested {

pkg/protodef/error.go

+9
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,12 @@ type errSyntax struct {
2929
func (e errSyntax) Error() string {
3030
return fmt.Sprintf("(%d:%d) %s", e.Line, e.Col, e.Err)
3131
}
32+
33+
type errUnclosedMultilineString struct {
34+
Line int
35+
Col int
36+
}
37+
38+
func (e errUnclosedMultilineString) Error() string {
39+
return fmt.Sprintf("(%d:%d) unclosed multiline string", e.Line, e.Col)
40+
}

pkg/protodef/testdata/multiline.txt

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
enum Enum {
2+
STUB_ENUM_UNSPECIFIED = 0;
3+
STUB_ENUM_FIRST = 1;
4+
STUB_ENUM_SECOND = 2;
5+
}
6+
7+
message Nested {
8+
Enum enum = 1;
9+
string value = 6;
10+
}
11+
12+
message StubResponse {
13+
option (groxypb.target) = true;
14+
15+
// next line contains a start of a multiline string
16+
Nested nested = 3 [(groxypb.value) = `{
17+
"enum": "STUB_ENUM_FIRST",
18+
"value": "Hello, World!"
19+
}`];
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
enum Enum {
2+
STUB_ENUM_UNSPECIFIED = 0;
3+
STUB_ENUM_FIRST = 1;
4+
STUB_ENUM_SECOND = 2;
5+
}
6+
7+
message Nested {
8+
Enum enum = 1;
9+
string value = 6;
10+
}
11+
12+
message StubResponse {
13+
option (groxypb.target) = true;
14+
15+
// next line contains a start of a multiline string
16+
Nested nested = 3 [(groxypb.value) = `{
17+
"enum": "STUB_ENUM_FIRST",
18+
"value": "Hello, World!"
19+
}];
20+
}

0 commit comments

Comments
 (0)