-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathMessageWeaver.java
More file actions
172 lines (151 loc) · 6.95 KB
/
Copy pathMessageWeaver.java
File metadata and controls
172 lines (151 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package io.vertx.grpc.transcoding.impl;
import com.google.protobuf.Descriptors;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.grpc.transcoding.impl.config.HttpVariableBinding;
import java.util.List;
import java.util.Objects;
/**
* The MessageWeaver class handles the merging and transformation of gRPC messages during HTTP-to-gRPC transcoding operations.
*
* <p>The body-field semantics follow the {@code google.api.HttpRule} contract: when {@code body} is unset, the HTTP request
* body is ignored and the gRPC message is built only from path/query bindings; {@code body = "*"} maps the entire HTTP body
* to the root of the gRPC message; {@code body = "field"} (or a dotted path) maps the HTTP body to that field. Bindings
* are applied on top of the body content so path/query values take precedence on collision.
*
* <p>Reference implementations:
* <ul>
* <li>Spec: <a href="https://github.com/googleapis/googleapis/blob/master/google/api/http.proto">google/api/http.proto</a> (the {@code HttpRule.body} field comment)</li>
* <li>C++ (reference): <a href="https://github.com/grpc-ecosystem/grpc-httpjson-transcoding/blob/master/src/include/grpc_transcoding/request_weaver.h">RequestWeaver</a></li>
* <li>C# (ASP.NET Core): <a href="https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs">JsonRequestHelpers.ReadMessage</a></li>
* </ul>
*
* @see HttpVariableBinding
*/
public final class MessageWeaver {
private static final String ROOT_LEVEL = "*";
private MessageWeaver() {
}
/**
* Weaves HTTP variable bindings and request body into a gRPC message. The return type is always a {@link JsonObject}
* because a protobuf {@code Message} is always object-shaped in JSON form.
*
* @param message The original message buffer
* @param bindings The HTTP variable bindings
* @param transcodingRequestBody The transcoding request body path
* @param descriptor The protobuf message descriptor, used to identify repeated fields
* @return The woven message as a JsonObject
* @throws DecodeException If JSON decoding fails
*/
public static JsonObject weaveRequestMessage(Buffer message, List<HttpVariableBinding> bindings, String transcodingRequestBody, Descriptors.Descriptor descriptor) throws DecodeException {
boolean hasBindings = bindings != null && !bindings.isEmpty();
boolean hasBody = transcodingRequestBody != null && !transcodingRequestBody.isEmpty();
if (!hasBindings && !hasBody) {
return new JsonObject();
}
JsonObject result = new JsonObject();
if (hasBody) {
JsonObject messageJson = null;
if (message != null && !message.toString().isBlank()) {
messageJson = message.toJsonObject();
}
if (messageJson != null && !messageJson.isEmpty()) {
if (ROOT_LEVEL.equals(transcodingRequestBody)) {
result.mergeIn(messageJson, true);
} else {
applyAtPath(result, transcodingRequestBody.split("\\."), messageJson);
}
}
}
if (hasBindings) {
applyBindings(result, bindings, descriptor);
}
return result;
}
/**
* Applies HTTP variable bindings to the result object.
*/
private static void applyBindings(JsonObject result, List<HttpVariableBinding> bindings, Descriptors.Descriptor descriptor) {
for (HttpVariableBinding binding : bindings) {
List<String> fieldPath = binding.getFieldPath();
if (fieldPath == null || fieldPath.isEmpty()) {
continue;
}
// Navigate to parent object, resolving descriptors alongside
JsonObject current = result;
Descriptors.Descriptor currentDescriptor = descriptor;
for (int i = 0; i < fieldPath.size() - 1; i++) {
String fieldName = fieldPath.get(i);
JsonObject next = current.getJsonObject(fieldName);
if (next == null) {
next = new JsonObject();
current.put(fieldName, next);
}
current = next;
if (currentDescriptor != null) {
Descriptors.FieldDescriptor fd = currentDescriptor.findFieldByName(fieldName);
currentDescriptor = (fd != null && fd.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) ? fd.getMessageType() : null;
}
}
// Check if the leaf field is repeated
String lastField = fieldPath.get(fieldPath.size() - 1);
Descriptors.FieldDescriptor fd = currentDescriptor != null ? currentDescriptor.findFieldByName(lastField) : null;
boolean repeated = fd != null && fd.isRepeated();
if (repeated) {
Object existing = current.getValue(lastField);
if (existing instanceof JsonArray) {
((JsonArray) existing).add(binding.getValue());
} else {
current.put(lastField, new JsonArray().add(binding.getValue()));
}
} else {
current.put(lastField, binding.getValue());
}
}
}
/**
* Applies an object at a specific path in the JSON structure.
*/
private static void applyAtPath(JsonObject root, String[] path, Object value) {
JsonObject current = root;
for (int i = 0; i < path.length - 1; i++) {
String fieldName = path[i];
JsonObject next = current.getJsonObject(fieldName);
if (next == null) {
next = new JsonObject();
current.put(fieldName, next);
}
current = next;
}
current.put(path[path.length - 1], value);
}
/**
* Extracts a response message portion based on the {@code response_body} selector. The result may be any JSON value
* the selector lands on: a {@link JsonObject}, a {@link JsonArray} (for selectors pointing at a repeated proto field),
* or a scalar ({@link String}, {@link Number}, {@link Boolean}, or {@code null}). Callers should use
* {@link io.vertx.core.json.Json#encodeToBuffer(Object)} to serialize the result uniformly.
*
* @param message The original message buffer
* @param transcodingResponseBody The path to extract from the response
* @return The selected JSON value (object, array, scalar, or null)
*/
public static Object weaveResponseMessage(Buffer message, String transcodingResponseBody) throws DecodeException {
Objects.requireNonNull(message, "Message cannot be null");
if (transcodingResponseBody == null || transcodingResponseBody.isEmpty() || transcodingResponseBody.equals(ROOT_LEVEL)) {
return message.toJsonObject();
}
JsonObject json = message.toJsonObject();
String[] path = transcodingResponseBody.split("\\.");
Object current = json;
for (int i = 0; i < path.length; i++) {
String field = path[i];
if (!(current instanceof JsonObject)) {
throw new IllegalArgumentException("Path segment '" + path[i - 1] + "' in transcodingResponseBody does not refer to a JSON object");
}
current = ((JsonObject) current).getValue(field);
}
return current;
}
}