Skip to content

Commit

Permalink
Support camelcase for HttpJsonTranscodingService Query Parameters (#4428
Browse files Browse the repository at this point in the history
)

Motivation:

fixes #4401 

Modifications:

- Add HttpJsonTranscodingOptions and HttpJsonTranscodingOptionsBuilder
- add GrpcServiceBuilder Api that consumes HttpJsonTranscodingOptions when setting enableHttpJsonTranscoding
- check HttpJsonTranscodingOptions in HttpJsonTranscodingService, then add fields with camelCase keys in buildField function if camelCaseQueryParam option is set to true

Result:

- Closes #4401
- No breaking changes: changes are only applied when the new camelCaseQueryParam option is set to true
- HttpJsonTranscoding endpoint can handle both snake_case and camelCase form of query parameters when option is set
  ```java
  HttpJsonTranscodingOptions options =
    HttpJsonTranscodingOptions.builder()
                              .queryParamMatchRules(LOWER_CAMEL_CASE)
                              ...
                              .build();
  GrpcService.builder()
             // Enable HttpJsonTranscoding and use the specified HttpJsonTranscodingOption
             .enableHttpJsonTranscoding(options)
             .build();
  ```
  • Loading branch information
mscheong01 authored Oct 5, 2022
1 parent ec7c425 commit c888c86
Show file tree
Hide file tree
Showing 9 changed files with 609 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.server.grpc;

import java.util.Objects;
import java.util.Set;

import com.google.common.base.MoreObjects;

final class DefaultHttpJsonTranscodingOptions implements HttpJsonTranscodingOptions {

static final HttpJsonTranscodingOptions DEFAULT = HttpJsonTranscodingOptions.builder().build();

private final Set<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules;
private final UnframedGrpcErrorHandler errorHandler;

DefaultHttpJsonTranscodingOptions(Set<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules,
UnframedGrpcErrorHandler errorHandler) {
this.queryParamMatchRules = queryParamMatchRules;
this.errorHandler = errorHandler;
}

@Override
public Set<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules() {
return queryParamMatchRules;
}

@Override
public UnframedGrpcErrorHandler errorHandler() {
return errorHandler;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof HttpJsonTranscodingOptions)) {
return false;
}
final HttpJsonTranscodingOptions that = (HttpJsonTranscodingOptions) o;
return queryParamMatchRules.equals(that.queryParamMatchRules()) &&
errorHandler.equals(that.errorHandler());
}

@Override
public int hashCode() {
return Objects.hash(queryParamMatchRules, errorHandler);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("queryParamMatchRules", queryParamMatchRules)
.add("errorHandler", errorHandler)
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ public final class GrpcServiceBuilder {
@Nullable
private UnframedGrpcErrorHandler httpJsonTranscodingErrorHandler;

private HttpJsonTranscodingOptions httpJsonTranscodingOptions = HttpJsonTranscodingOptions.of();

private Set<SerializationFormat> supportedSerializationFormats = DEFAULT_SUPPORTED_SERIALIZATION_FORMATS;

private int maxRequestMessageLength = AbstractMessageDeframer.NO_MAX_INBOUND_MESSAGE_SIZE;
Expand Down Expand Up @@ -657,11 +659,56 @@ public GrpcServiceBuilder enableHttpJsonTranscoding(boolean enableHttpJsonTransc
return this;
}

/**
* Enables HTTP/JSON transcoding using the gRPC wire protocol.
* Provide {@link HttpJsonTranscodingOptions} to customize HttpJsonTranscoding.
*
* <p>Example:
* <pre>{@code
* HttpJsonTranscodingOptions options =
* HttpJsonTranscodingOptions.builder()
* .queryParamMatchRules(ORIGINAL_FIELD)
* ...
* .build();
*
* GrpcService.builder()
* // Enable HttpJsonTranscoding and use the specified HttpJsonTranscodingOption
* .enableHttpJsonTranscoding(options)
* .build();
* }</pre>
*
* <p>Limitations:
* <ul>
* <li>Only unary methods (single request, single response) are supported.</li>
* <li>
* Message compression is not supported.
* {@link EncodingService} should be used instead for
* transport level encoding.
* </li>
* <li>
* Transcoding will not work if the {@link GrpcService} is configured with
* {@link ServerBuilder#serviceUnder(String, HttpService)}.
* </li>
* </ul>
*
* @see <a href="https://cloud.google.com/endpoints/docs/grpc/transcoding">Transcoding HTTP/JSON to gRPC</a>
*/
@UnstableApi
public GrpcServiceBuilder enableHttpJsonTranscoding(HttpJsonTranscodingOptions httpJsonTranscodingOptions) {
requireNonNull(httpJsonTranscodingOptions, "httpJsonTranscodingOptions");
enableHttpJsonTranscoding = true;
this.httpJsonTranscodingOptions = httpJsonTranscodingOptions;
return this;
}

/**
* Sets an error handler which handles an exception raised while serving a gRPC request transcoded from
* an HTTP/JSON request. By default, {@link UnframedGrpcErrorHandler#ofJson()} would be set.
*
* @deprecated Use {@link HttpJsonTranscodingOptionsBuilder#errorHandler(UnframedGrpcErrorHandler)} instead.
*/
@UnstableApi
@Deprecated
public GrpcServiceBuilder httpJsonTranscodingErrorHandler(
UnframedGrpcErrorHandler httpJsonTranscodingErrorHandler) {
requireNonNull(httpJsonTranscodingErrorHandler, "httpJsonTranscodingErrorHandler");
Expand Down Expand Up @@ -978,10 +1025,19 @@ public GrpcService build() {
: UnframedGrpcErrorHandler.of());
}
if (enableHttpJsonTranscoding) {
grpcService = HttpJsonTranscodingService.of(
grpcService,
httpJsonTranscodingErrorHandler != null ? httpJsonTranscodingErrorHandler
: UnframedGrpcErrorHandler.ofJson());
final HttpJsonTranscodingOptions httpJsonTranscodingOptions;
if (httpJsonTranscodingErrorHandler != null) {
httpJsonTranscodingOptions =
HttpJsonTranscodingOptions
.builder()
.queryParamMatchRules(
this.httpJsonTranscodingOptions.queryParamMatchRules())
.errorHandler(httpJsonTranscodingErrorHandler)
.build();
} else {
httpJsonTranscodingOptions = this.httpJsonTranscodingOptions;
}
grpcService = HttpJsonTranscodingService.of(grpcService, httpJsonTranscodingOptions);
}
if (handlerRegistry.containsDecorators()) {
grpcService = new GrpcDecoratingService(grpcService, handlerRegistry);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.server.grpc;

import java.util.Set;

import com.google.protobuf.Message;

import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* User provided options for customizing {@link HttpJsonTranscodingService}.
*/
@UnstableApi
public interface HttpJsonTranscodingOptions {

/**
* Returns a new {@link HttpJsonTranscodingOptionsBuilder}.
*/
static HttpJsonTranscodingOptionsBuilder builder() {
return new HttpJsonTranscodingOptionsBuilder();
}

/**
* Returns the default {@link HttpJsonTranscodingOptions}.
*/
static HttpJsonTranscodingOptions of() {
return DefaultHttpJsonTranscodingOptions.DEFAULT;
}

/**
* Returns the {@link HttpJsonTranscodingQueryParamMatchRule}s which is used to match fields in a
* {@link Message} with query parameters.
*/
Set<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules();

/**
* Return the {@link UnframedGrpcErrorHandler} which handles an exception raised while serving a gRPC
* request transcoded from an HTTP/JSON request.
*/
UnframedGrpcErrorHandler errorHandler();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.server.grpc;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import java.util.EnumSet;
import java.util.Set;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.protobuf.Message;

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.QueryParams;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A builder for {@link HttpJsonTranscodingOptions}.
*/
@UnstableApi
public final class HttpJsonTranscodingOptionsBuilder {

private static final EnumSet<HttpJsonTranscodingQueryParamMatchRule> DEFAULT_QUERY_PARAM_MATCH_RULES =
EnumSet.of(HttpJsonTranscodingQueryParamMatchRule.ORIGINAL_FIELD);

private UnframedGrpcErrorHandler errorHandler = UnframedGrpcErrorHandler.ofJson();

@Nullable
private Set<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules;

HttpJsonTranscodingOptionsBuilder() {}

/**
* Adds the specified {@link HttpJsonTranscodingQueryParamMatchRule} which is used
* to match {@link QueryParams} of an {@link HttpRequest} with fields in a {@link Message}.
* If not set, {@link HttpJsonTranscodingQueryParamMatchRule#ORIGINAL_FIELD} is used by default.
*/
public HttpJsonTranscodingOptionsBuilder queryParamMatchRules(
HttpJsonTranscodingQueryParamMatchRule... queryParamMatchRules) {
requireNonNull(queryParamMatchRules, "queryParamMatchRules");
queryParamMatchRules(ImmutableList.copyOf(queryParamMatchRules));
return this;
}

/**
* Adds the specified {@link HttpJsonTranscodingQueryParamMatchRule} which is used
* to match {@link QueryParams} of an {@link HttpRequest} with fields in a {@link Message}.
* If not set, {@link HttpJsonTranscodingQueryParamMatchRule#ORIGINAL_FIELD} is used by default.
*/
public HttpJsonTranscodingOptionsBuilder queryParamMatchRules(
Iterable<HttpJsonTranscodingQueryParamMatchRule> queryParamMatchRules) {
requireNonNull(queryParamMatchRules, "queryParamMatchRules");
checkArgument(!Iterables.isEmpty(queryParamMatchRules), "Can't set an empty queryParamMatchRules");
if (this.queryParamMatchRules == null) {
this.queryParamMatchRules = EnumSet.noneOf(HttpJsonTranscodingQueryParamMatchRule.class);
}
this.queryParamMatchRules.addAll(ImmutableList.copyOf(queryParamMatchRules));
return this;
}

/**
* Sets an error handler which handles an exception raised while serving a gRPC request transcoded from
* an HTTP/JSON request. By default, {@link UnframedGrpcErrorHandler#ofJson()} would be set.
*/
@UnstableApi
public HttpJsonTranscodingOptionsBuilder errorHandler(UnframedGrpcErrorHandler errorHandler) {
requireNonNull(errorHandler, "errorHandler");
this.errorHandler = errorHandler;
return this;
}

/**
* Returns a newly created {@link HttpJsonTranscodingOptions}.
*/
public HttpJsonTranscodingOptions build() {
final Set<HttpJsonTranscodingQueryParamMatchRule> matchRules;
if (queryParamMatchRules == null) {
matchRules = DEFAULT_QUERY_PARAM_MATCH_RULES;
} else {
matchRules = Sets.immutableEnumSet(queryParamMatchRules);
}
return new DefaultHttpJsonTranscodingOptions(matchRules, errorHandler);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.server.grpc;

import com.google.protobuf.Message;

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.QueryParams;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A naming rule to map {@link QueryParams} of an {@link HttpRequest} to fields in a {@link Message} for
* HTTP-JSON transcoding endpoint.
*/
@UnstableApi
public enum HttpJsonTranscodingQueryParamMatchRule {
/**
* Converts field names that are
* <a href="https://developers.google.com/protocol-buffers/docs/style#message_and_field_names">underscore_separated</a>
* into lowerCamelCase before matching with {@link QueryParams} of an {@link HttpRequest}.
*
* <p>Note that field names which aren't {@code underscore_separated} may fail to
* convert correctly to lowerCamelCase. Therefore, don't use this option if you aren't following
* Protocol Buffer's
* <a href="https://developers.google.com/protocol-buffers/docs/style">naming conventions</a>.
*/
LOWER_CAMEL_CASE,
/**
* Uses the original fields in .proto files to match {@link QueryParams} of an {@link HttpRequest}.
*/
ORIGINAL_FIELD
}
Loading

0 comments on commit c888c86

Please sign in to comment.