Skip to content

Commit

Permalink
Added COnfigurationService.GetBatch (Azure#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
KrzysztofCwalina authored Nov 30, 2018
1 parent d58a31c commit cb3fc26
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 19 deletions.
88 changes: 86 additions & 2 deletions Azure.Configuration.Test/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Azure.Configuration.Test;
using Azure.Core.Testing;
using System.Collections.Generic;

namespace Azure.Configuration.Tests
{
Expand Down Expand Up @@ -40,7 +41,7 @@ public async Task SetKeyValue()
var pool = new TestPool<byte>();
service.Pipeline.Pool = pool;

Response<KeyValue> added = await service.SetKeyValueAsync(s_testKey, CancellationToken.None);
Response<KeyValue> added = await service.SetAsync(s_testKey, CancellationToken.None);

Assert.AreEqual(s_testKey.Key, added.Result.Key);
Assert.AreEqual(s_testKey.Label, added.Result.Label);
Expand All @@ -67,13 +68,49 @@ public async Task GetKeyValue()
var pool = new TestPool<byte>();
service.Pipeline.Pool = pool;

Response<KeyValue> added = await service.GetKeyValueAsync("test", default, CancellationToken.None);
Response<KeyValue> added = await service.GetAsync("test", default, CancellationToken.None);

Assert.AreEqual(s_testKey.Key, added.Result.Key);
Assert.AreEqual(s_testKey.Label, added.Result.Label);
Assert.AreEqual(s_testKey.ContentType, added.Result.ContentType);
Assert.AreEqual(s_testKey.Locked, added.Result.Locked);
}

[Test]
public async Task GetKeyValues()
{
string connectionString = "Endpoint=https://contoso.azconfig.io;Id=b1d9b31;Secret=aabbccdd";
ConfigurationService.ParseConnectionString(connectionString, out var uri, out var credential, out var secret);

var service = new ConfigurationService(uri, credential, secret);

var transport = new GetBatchAsyncMockTransport(5);
transport.Batches.Add((0, 4));
transport.Batches.Add((4, 1));

transport.Responses.Add(HttpStatusCode.NotFound);
transport.Responses.Add(HttpStatusCode.OK);
service.Pipeline.Transport = transport;
var pool = new TestPool<byte>();
service.Pipeline.Pool = pool;

var query = new QueryKeyValueCollectionOptions();
int keyIndex = 0;
while(true)
{
using (var response = await service.GetBatchAsync(query, CancellationToken.None))
{
KeyValueBatch batch = response.Result;
foreach (var value in batch)
{
Assert.AreEqual("key" + keyIndex.ToString(), value.Key);
keyIndex++;
}
query.Index = batch.NextIndex;
if (query.Index == 0) break;
}
}
}
}

class SetKeyValueMockTransport : MockHttpClientTransport
Expand Down Expand Up @@ -117,4 +154,51 @@ protected override void WriteResponseCore(HttpResponseMessage response)
response.Content.Headers.Add("Content-Length", jsonByteCount.ToString());
}
}

class GetBatchAsyncMockTransport : MockHttpClientTransport
{
public List<KeyValue> KeyValues = new List<KeyValue>();
public List<(int index, int count)> Batches = new List<(int index, int count)>();
int _currentBathIndex = 0;

public GetBatchAsyncMockTransport(int numberOfItems)
{
_expectedMethod = HttpMethod.Get;
_expectedUri = null;
_expectedContent = null;
for(int i=0; i< numberOfItems; i++)
{
var item = new KeyValue()
{
Key = $"key{i}",
Label = "label",
Value = "val",
ETag = "c3c231fd-39a0-4cb6-3237-4614474b92c1",
ContentType = "text"
};
KeyValues.Add(item);
}
}

protected override void WriteResponseCore(HttpResponseMessage response)
{
var batch = Batches[_currentBathIndex++];
var bathItems = new List<KeyValue>(batch.count);
int itemIndex = batch.index;
int count = batch.count;
while(count -- > 0)
{
bathItems.Add(KeyValues[itemIndex++]);
}
string json = JsonConvert.SerializeObject(bathItems).ToLowerInvariant();
response.Content = new StringContent(json, Encoding.UTF8, "application/json");

long jsonByteCount = Encoding.UTF8.GetByteCount(json);
response.Content.Headers.Add("Content-Length", jsonByteCount.ToString());
if (itemIndex < KeyValues.Count)
{
response.Headers.Add("Link", $"</kv?after={itemIndex}>;rel=\"next\"");
}
}
}
}
11 changes: 7 additions & 4 deletions Azure.Configuration.Test/MockHttpClientTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ protected override Task<HttpResponseMessage> ProcessCoreAsync(CancellationToken
VerifyUserAgentHeader(request);
VerifyRequestCore(request);
HttpResponseMessage response = new HttpResponseMessage();
WriteResponse(response);
WriteResponseCore(response);
if (WriteResponse(response))
{
WriteResponseCore(response);
}
return Task.FromResult(response);
}

Expand All @@ -38,7 +40,7 @@ protected virtual void VerifyRequestCore(HttpRequestMessage request) { }
void VerifyRequestLine(HttpRequestMessage request)
{
Assert.AreEqual(_expectedMethod, request.Method);
Assert.AreEqual(_expectedUri, request.RequestUri.OriginalString);
if(_expectedUri != null) Assert.AreEqual(_expectedUri, request.RequestUri.OriginalString);
Assert.AreEqual(new Version(2, 0), request.Version);
}

Expand Down Expand Up @@ -70,11 +72,12 @@ void VerifyUserAgentHeader(HttpRequestMessage request)
Assert.Fail("could not find User-Agent header value " + expected);
}

void WriteResponse(HttpResponseMessage response)
bool WriteResponse(HttpResponseMessage response)
{
if (_nextResponse >= Responses.Count) _nextResponse = 0;
var mockResponse = Responses[_nextResponse++];
response.StatusCode = mockResponse.ResponseCode;
return response.StatusCode == HttpStatusCode.OK;
}

public class Response
Expand Down
18 changes: 12 additions & 6 deletions Azure.Configuration/ConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public ConfigurationService(Uri baseUri, string credential, byte[] secret)
: this(baseUri, credential, secret, CreateDefaultPipeline())
{ }

public async Task<Response<KeyValue>> SetKeyValueAsync(KeyValue setting, CancellationToken cancellation)
public async Task<Response<KeyValue>> SetAsync(KeyValue setting, CancellationToken cancellation)
{
if (setting == null) throw new ArgumentNullException(nameof(setting));
if (string.IsNullOrEmpty(setting.Key)) throw new ArgumentNullException($"{nameof(setting)}.{nameof(setting.Key)}");
Expand All @@ -57,7 +57,7 @@ public async Task<Response<KeyValue>> SetKeyValueAsync(KeyValue setting, Cancell
{
context = Pipeline.CreateContext(cancellation, ServiceMethod.Put, url);

context.AddHeader("Accept", MediaTypeKeyValueApplication);
context.AddHeader(MediaTypeKeyValueApplicationHeader);
context.AddHeader(Header.Common.JsonContentType);

WriteJsonContent(setting, context);
Expand All @@ -75,7 +75,10 @@ public async Task<Response<KeyValue>> SetKeyValueAsync(KeyValue setting, Cancell
Func<ReadOnlySequence<byte>, KeyValue> contentParser = null;
if (response.Status == 200)
{
contentParser = (ros) => { return KeyValueResultParser.Parse(ros); };
contentParser = (ros) => {
if(ConfigurationServiceParser.TryParse(ros, out KeyValue result, out _)) return result;
throw new Exception("invalid response content");
};
}
return new Response<KeyValue>(response, contentParser);
}
Expand All @@ -86,7 +89,7 @@ public async Task<Response<KeyValue>> SetKeyValueAsync(KeyValue setting, Cancell
}
}

public async Task<Response<KeyValue>> GetKeyValueAsync(string key, GetKeyValueOptions options, CancellationToken cancellation)
public async Task<Response<KeyValue>> GetAsync(string key, GetKeyValueOptions options, CancellationToken cancellation)
{
if (string.IsNullOrEmpty(key)) {
throw new ArgumentNullException(nameof(key));
Expand All @@ -98,7 +101,7 @@ public async Task<Response<KeyValue>> GetKeyValueAsync(string key, GetKeyValueOp
try {
context = Pipeline.CreateContext(cancellation, ServiceMethod.Get, url);

context.AddHeader("Accept", MediaTypeKeyValueApplication);
context.AddHeader(MediaTypeKeyValueApplicationHeader);

if (options != null && options.PreferredDateTime.HasValue) {
var dateTime = options.PreferredDateTime.Value.UtcDateTime.ToString(AcceptDateTimeFormat);
Expand All @@ -116,7 +119,10 @@ public async Task<Response<KeyValue>> GetKeyValueAsync(string key, GetKeyValueOp

Func<ReadOnlySequence<byte>, KeyValue> contentParser = null;
if (response.Status == 200) {
contentParser = (ros) => { return KeyValueResultParser.Parse(ros); };
contentParser = (ros) => {
if (ConfigurationServiceParser.TryParse(ros, out KeyValue result, out _)) return result;
throw new Exception("invalid response content");
};
}
// TODO (pri 1): make sure the right things happen for NotFound reponse
return new Response<KeyValue>(response, contentParser);
Expand Down
107 changes: 107 additions & 0 deletions Azure.Configuration/ConfigurationService_enumerable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Azure.Core;
using Azure.Core.Net;
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Azure.Configuration
{
public partial class ConfigurationService
{
public async Task<Response<KeyValueBatch>> GetBatchAsync(QueryKeyValueCollectionOptions options, CancellationToken cancellation)
{
var requestUri = BuildUrlForGetBatch(options);
ServiceCallContext context = null;
try
{
context = Pipeline.CreateContext(cancellation, ServiceMethod.Get, requestUri);

context.AddHeader(MediaTypeKeyValueApplicationHeader);
if (options.PreferredDateTime != null)
{
context.AddHeader(AcceptDatetimeHeader, options.PreferredDateTime.Value.UtcDateTime.ToString(AcceptDateTimeFormat));
}

await Pipeline.ProcessAsync(context).ConfigureAwait(false);

ServiceResponse response = context.Response;
if (!response.TryGetHeader(Header.Constants.ContentLength, out long contentLength))
{
throw new Exception("bad response: no content length header");
}

await response.ReadContentAsync(contentLength).ConfigureAwait(false);

Func<ReadOnlySequence<byte>, KeyValueBatch> contentParser = null;
if (response.Status == 200)
{
contentParser = (ros) => { return KeyValueBatch.Parse(response); };
}
return new Response<KeyValueBatch>(response, contentParser);
}
catch
{
if (context != null) context.Dispose();
throw;
}
}

Url BuildUrlForGetBatch(QueryKeyValueCollectionOptions options)
{
Uri requestUri = new Uri(_baseUri, KvRoute);
if (options == default) return new Url(requestUri);

var queryBuild = new StringBuilder();
bool hasQuery = false;

if(options.Index != 0)
{
AddSeparator(queryBuild, ref hasQuery);
queryBuild.Append($"after={options.Index}");
}

if (!string.IsNullOrEmpty(options.KeyFilter))
{
AddSeparator(queryBuild, ref hasQuery);
queryBuild.Append($"{KeyQueryFilter}={options.KeyFilter}");
}

if (options.LabelFilter != null)
{
AddSeparator(queryBuild, ref hasQuery);
if (options.LabelFilter == string.Empty) {
options.LabelFilter = "\0";
}
queryBuild.Append($"{LabelQueryFilter}={options.LabelFilter}");
}

if (options.FieldsSelector != KeyValueFields.All)
{
AddSeparator(queryBuild, ref hasQuery);
var filter = (options.FieldsSelector).ToString().ToLower().Replace(" ", "");
queryBuild.Append($"{FieldsQueryFilter}={filter}");
}

if(hasQuery) requestUri = new Uri(requestUri + queryBuild.ToString());
return new Url(requestUri);

void AddSeparator(StringBuilder builder, ref bool hasQueryBeenAdded)
{
if (hasQueryBeenAdded)
{
builder.Append('&');
}
else
{
builder.Append('?');
hasQueryBeenAdded = true;
}
}
}
}
}
6 changes: 5 additions & 1 deletion Azure.Configuration/ConfigurationService_private.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace Azure.Configuration
public partial class ConfigurationService
{
#region String Table
const string MediaTypeKeyValueApplication = "application/vnd.microsoft.appconfig.kv+json";
const string MediaTypeProblemApplication = "application/problem+json";

const string KvRoute = "/kv/";
Expand All @@ -34,6 +33,11 @@ public partial class ConfigurationService
const string AcceptDatetimeHeader = "Accept-Datetime";
#endregion

static readonly Header MediaTypeKeyValueApplicationHeader = new Header(
Header.Constants.Accept,
Encoding.ASCII.GetBytes("application/vnd.microsoft.appconfig.kv+json")
);

void WriteJsonContent(KeyValue setting, ServiceCallContext context)
{
var writer = Utf8JsonWriter.Create(context.ContentWriter);
Expand Down
Loading

0 comments on commit cb3fc26

Please sign in to comment.