Skip to content

Commit

Permalink
feat: Support HTTP rule overrides, primarily for mix-ins
Browse files Browse the repository at this point in the history
  • Loading branch information
jskeet committed Oct 11, 2022
1 parent 8ab8884 commit cbfdfe0
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 6 deletions.
19 changes: 19 additions & 0 deletions Google.Api.Gax.Grpc.Tests/ApiMetadataTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* https://developers.google.com/open-source/licenses/bsd
*/

using Google.Protobuf;
using Google.Protobuf.Reflection;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -68,6 +69,24 @@ public void WithRequestNumericEnumJsonEncoding()
Assert.False(withFalse.RequestNumericEnumJsonEncoding);
}

[Fact]
public void WithHttpRuleOverrides()
{
var original = TestApiMetadata.Test;
var rule = new HttpRule { Get = "/v1/xyz" };
var overrides = new Dictionary<string, ByteString> { { "x.y.z", rule.ToByteString() } };
var withOverrides = original.WithHttpRuleOverrides(overrides);

// Original metadata has not changed
Assert.Empty(original.HttpRuleOverrides);
Assert.Equal(1, withOverrides.HttpRuleOverrides.Count);
Assert.True(withOverrides.HttpRuleOverrides.TryGetValue("x.y.z", out var ruleBytes));

// We could just keep hold of the ByteString, but this demonstrates the expected usage more clearly.
var decoded = HttpRule.Parser.ParseFrom(ruleBytes);
Assert.Equal(rule, decoded);
}

private class CountingSequence : IEnumerable<FileDescriptor>
{
public int EvaluationCount { get; private set; } = 0;
Expand Down
17 changes: 17 additions & 0 deletions Google.Api.Gax.Grpc.Tests/Rest/RestMethodTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using Google.Api.Gax.Grpc.Tests;
using Google.Protobuf;
using System.Collections.Generic;
using System.Linq;
using Xunit;

Expand All @@ -29,4 +30,20 @@ public void CreateRequest_WithRequestNumericEnumJson(bool value, string expected
var httpRequest = restMethod.CreateRequest(request, null);
Assert.Equal(httpRequest.RequestUri.ToString(), expectedUri);
}

[Fact]
public void CreateRequest_WithHttpOverrides()
{
var rule = new HttpRule { Get = "/v2/def/{name}" };
var methodDescriptor = TestServiceReflection.Descriptor.Services
.Single(svc => svc.Name == "Sample")
.FindMethodByName("SimpleMethod");
var overrides = new Dictionary<string, ByteString> { { methodDescriptor.FullName, rule.ToByteString() } };
var apiMetadata = TestApiMetadata.Test.WithHttpRuleOverrides(overrides);
var restMethod = RestMethod.Create(apiMetadata, methodDescriptor, JsonParser.Default);

var request = new SimpleRequest { Name = "ghi" };
var httpRequest = restMethod.CreateRequest(request, null);
Assert.Equal("/v2/def/ghi", httpRequest.RequestUri.ToString());
}
}
40 changes: 34 additions & 6 deletions Google.Api.Gax.Grpc/ApiMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
*/

using Google.Api.Gax.Grpc.Rest;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

Expand All @@ -20,14 +22,16 @@ namespace Google.Api.Gax.Grpc
/// </summary>
public sealed partial class ApiMetadata
{
private Lazy<IReadOnlyList<FileDescriptor>> _fileDescriptorsProvider;
private static readonly IReadOnlyDictionary<string, ByteString> s_emptyHttpRuleOverrides = new ReadOnlyDictionary<string, ByteString>(new Dictionary<string, ByteString>());

private readonly Lazy<IReadOnlyList<FileDescriptor>> _fileDescriptorsProvider;

/// <summary>
/// The protobuf descriptors used by this API.
/// </summary>
public IReadOnlyList<FileDescriptor> ProtobufDescriptors => _fileDescriptorsProvider.Value;

private Lazy<TypeRegistry> _typeRegistryProvider;
private readonly Lazy<TypeRegistry> _typeRegistryProvider;

/// <summary>
/// A type registry containing all the types in <see cref="ProtobufDescriptors"/>.
Expand All @@ -46,12 +50,21 @@ public sealed partial class ApiMetadata
/// </summary>
public bool RequestNumericEnumJsonEncoding { get; }

private ApiMetadata(string name, Lazy<IReadOnlyList<FileDescriptor>> fileDescriptorsProvider, bool requestNumericEnumJsonEncoding)
/// <summary>
/// A dictionary (based on ordinal string comparisons) from fully-qualified RPC names
/// to byte strings representing overrides for the HTTP rule. This is designed to support
/// mixins which are hosted at individual APIs, but which are exposed via different URLs
/// to the original mixin definition. This is never null, but may be empty.
/// </summary>
public IReadOnlyDictionary<string, ByteString> HttpRuleOverrides { get; }

private ApiMetadata(string name, Lazy<IReadOnlyList<FileDescriptor>> fileDescriptorsProvider, bool requestNumericEnumJsonEncoding, IReadOnlyDictionary<string, ByteString> httpRuleOverrides)
{
Name = GaxPreconditions.CheckNotNullOrEmpty(name, nameof(name));
RequestNumericEnumJsonEncoding = requestNumericEnumJsonEncoding;
_fileDescriptorsProvider = fileDescriptorsProvider;
_typeRegistryProvider = new Lazy<TypeRegistry>(() => TypeRegistry.FromFiles(ProtobufDescriptors));
HttpRuleOverrides = httpRuleOverrides;
}

/// <summary>
Expand All @@ -62,7 +75,7 @@ private ApiMetadata(string name, Lazy<IReadOnlyList<FileDescriptor>> fileDescrip
/// </remarks>
/// <param name="name">The name of the API. Must not be null or empty.</param>
/// <param name="descriptors">The protobuf descriptors of the API. Must not be null.</param>
public ApiMetadata(string name, IEnumerable<FileDescriptor> descriptors) : this(name, BuildDescriptorsProviderFromDescriptors(descriptors), false)
public ApiMetadata(string name, IEnumerable<FileDescriptor> descriptors) : this(name, BuildDescriptorsProviderFromDescriptors(descriptors), false, s_emptyHttpRuleOverrides)
{
}

Expand All @@ -82,7 +95,7 @@ private static Lazy<IReadOnlyList<FileDescriptor>> BuildDescriptorsProviderFromD
/// <param name="name">The name of the API. Must not be null or empty.</param>
/// <param name="descriptorsProvider">A provider function for the protobuf descriptors of the API. Must not be null, and must not
/// return a null value. This will only be called once by this API descriptor, when first requested.</param>
public ApiMetadata(string name, Func<IEnumerable<FileDescriptor>> descriptorsProvider) : this(name, BuildDescriptorsProviderFromOtherProvider(descriptorsProvider), false)
public ApiMetadata(string name, Func<IEnumerable<FileDescriptor>> descriptorsProvider) : this(name, BuildDescriptorsProviderFromOtherProvider(descriptorsProvider), false, s_emptyHttpRuleOverrides)
{
}

Expand All @@ -102,6 +115,21 @@ private static Lazy<IReadOnlyList<FileDescriptor>> BuildDescriptorsProviderFromO
/// <param name="value">The desired value of <see cref="RequestNumericEnumJsonEncoding"/> in the new instance.</param>
/// <returns>The new instance.</returns>
public ApiMetadata WithRequestNumericEnumJsonEncoding(bool value) =>
new ApiMetadata(Name, _fileDescriptorsProvider, value);
new ApiMetadata(Name, _fileDescriptorsProvider, value, HttpRuleOverrides);

/// <summary>
/// Creates a new instance with the same values as this one, other than the given set of HttpRule overrides.
/// </summary>
/// <param name="overrides">The HttpRule overrides for services in this package; typically used to override
/// URLs for the REST transport. Must not be null. Will be cloned in the form of an immutable dictionary,
/// after which the original sequence is discarded.</param>
/// <returns>The new instance.</returns>
public ApiMetadata WithHttpRuleOverrides(IEnumerable<KeyValuePair<string, ByteString>> overrides)
{
GaxPreconditions.CheckNotNull(overrides, nameof(overrides));
var dict = overrides.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
var readOnlyDict = new ReadOnlyDictionary<string, ByteString>(dict);
return new ApiMetadata(Name, _fileDescriptorsProvider, RequestNumericEnumJsonEncoding, readOnlyDict);
}
}
}
5 changes: 5 additions & 0 deletions Google.Api.Gax.Grpc/Rest/RestMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ internal static RestMethod Create(ApiMetadata apiMetadata, MethodDescriptor meth
{
throw new ArgumentException($"Method {method.Name} in service {method.Service.Name} has no HTTP rule");
}
// If we have an override, it completely replaces the original rule.
if (apiMetadata.HttpRuleOverrides.TryGetValue(method.FullName, out var overrideByteString))
{
rule = HttpRule.Parser.ParseFrom(overrideByteString);
}
var transcoder = new HttpRuleTranscoder(method.FullName, method.InputType, rule);
return new RestMethod(apiMetadata, method, parser, transcoder);
}
Expand Down
3 changes: 3 additions & 0 deletions Google.Api.Gax.Grpc/Rest/RestServiceCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ internal static RestServiceCollection Create(ApiMetadata metadata)
var methodsByName = services.SelectMany(service => service.Methods)
// We don't yet support streaming methods.
.Where(x => !x.IsClientStreaming && !x.IsServerStreaming)
// Ignore methods without HTTP annotations. Ideally there wouldn't be any, but
// operations.proto doesn't specify an HTTP rule for WaitOperation.
.Where(x => x.GetOptions()?.GetExtension(AnnotationsExtensions.Http) is not null)
.Select(method => RestMethod.Create(metadata, method, parser))
.ToDictionary(restMethod => restMethod.FullName);
return new RestServiceCollection(new ReadOnlyDictionary<string, RestMethod>(methodsByName));
Expand Down

0 comments on commit cbfdfe0

Please sign in to comment.