Skip to content

Commit

Permalink
Merge pull request #106 from microsoft/feature/multi-part
Browse files Browse the repository at this point in the history
feature/multi part
  • Loading branch information
baywet authored Aug 2, 2023
2 parents a6dde5c + 127b6ac commit a03d4f0
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 22 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"cSpell.words": [
"Kiota"
]
],
"editor.formatOnSave": true,
"dotnet-test-explorer.testProjectPath": "**/*.Tests.csproj"
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.0] - 2023-08-01

### Added

- Added support for multipart form data request body serialization.

## [1.2.1] - 2023-07-03

### Fixed
Expand Down
53 changes: 43 additions & 10 deletions Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void SetUriExtractsQueryParameters()
// Assert
Assert.Equal("http://localhost/baz/me?foo=bar", testRequest.URI.ToString());
Assert.NotEmpty(testRequest.QueryParameters);
Assert.Equal("foo",testRequest.QueryParameters.First().Key);
Assert.Equal("foo", testRequest.QueryParameters.First().Key);
Assert.Equal("bar", testRequest.QueryParameters.First().Value.ToString());
}
[Fact]
Expand All @@ -40,7 +40,7 @@ public void AddsAndRemovesRequestOptions()
var testRequestOption = new Mock<IRequestOption>().Object;
Assert.Empty(testRequest.RequestOptions);
// Act
testRequest.AddRequestOptions(new IRequestOption[] {testRequestOption});
testRequest.AddRequestOptions(new IRequestOption[] { testRequestOption });
// Assert
Assert.NotEmpty(testRequest.RequestOptions);
Assert.Equal(testRequestOption, testRequest.RequestOptions.First());
Expand Down Expand Up @@ -68,7 +68,7 @@ public void SetsSelectQueryParameters()
// Assert
Assert.True(requestInfo.QueryParameters.ContainsKey("%24select"));
Assert.False(requestInfo.QueryParameters.ContainsKey("select"));
Assert.Equal("%24select",requestInfo.QueryParameters.First().Key);
Assert.Equal("%24select", requestInfo.QueryParameters.First().Key);
}
[Fact]
public void DoesNotSetEmptyStringQueryParameters()
Expand Down Expand Up @@ -180,7 +180,7 @@ public void BuildsUrlOnProvidedBaseUrl()
};

// Act
requestInfo.PathParameters = new Dictionary<string, object>()
requestInfo.PathParameters = new Dictionary<string, object>()
{
{ "baseurl","http://localhost" }
};
Expand Down Expand Up @@ -237,7 +237,8 @@ public void GetsAndSetsResponseHandlerByType()
Assert.NotNull(requestInfo.GetRequestOption<ResponseHandlerOption>());
}
[Fact]
public void SetsObjectContent() {
public void SetsObjectContent()
{
var requestAdapterMock = new Mock<IRequestAdapter>();
var serializationWriterMock = new Mock<ISerializationWriter>();
var serializationWriterFactoryMock = new Mock<ISerializationWriterFactory>();
Expand All @@ -256,7 +257,8 @@ public void SetsObjectContent() {
serializationWriterMock.Verify(x => x.WriteCollectionOfObjectValues(It.IsAny<string>(), It.IsAny<IEnumerable<IParsable>>()), Times.Never);
}
[Fact]
public void SetsObjectCollectionContentSingleElement() {
public void SetsObjectCollectionContentSingleElement()
{
var requestAdapterMock = new Mock<IRequestAdapter>();
var serializationWriterMock = new Mock<ISerializationWriter>();
var serializationWriterFactoryMock = new Mock<ISerializationWriterFactory>();
Expand All @@ -268,14 +270,15 @@ public void SetsObjectCollectionContentSingleElement() {
UrlTemplate = "{+baseurl}/users{?%24count}"
};

requestInfo.SetContentFromParsable(requestAdapterMock.Object, "application/json", new [] {new TestEntity()});
requestInfo.SetContentFromParsable(requestAdapterMock.Object, "application/json", new[] { new TestEntity() });

// Assert we now have an option
serializationWriterMock.Verify(x => x.WriteObjectValue(It.IsAny<string>(), It.IsAny<IParsable>()), Times.Never);
serializationWriterMock.Verify(x => x.WriteCollectionOfObjectValues(It.IsAny<string>(), It.IsAny<IEnumerable<IParsable>>()), Times.Once);
}
[Fact]
public void SetsScalarContent() {
public void SetsScalarContent()
{
var requestAdapterMock = new Mock<IRequestAdapter>();
var serializationWriterMock = new Mock<ISerializationWriter>();
var serializationWriterFactoryMock = new Mock<ISerializationWriterFactory>();
Expand All @@ -294,7 +297,8 @@ public void SetsScalarContent() {
serializationWriterMock.Verify(x => x.WriteCollectionOfPrimitiveValues(It.IsAny<string>(), It.IsAny<IEnumerable<string>>()), Times.Never);
}
[Fact]
public void SetsScalarCollectionContent() {
public void SetsScalarCollectionContent()
{
var requestAdapterMock = new Mock<IRequestAdapter>();
var serializationWriterMock = new Mock<ISerializationWriter>();
var serializationWriterFactoryMock = new Mock<ISerializationWriterFactory>();
Expand All @@ -306,7 +310,7 @@ public void SetsScalarCollectionContent() {
UrlTemplate = "{+baseurl}/users{?%24count}"
};

requestInfo.SetContentFromScalarCollection(requestAdapterMock.Object, "application/json", new[] {"foo"});
requestInfo.SetContentFromScalarCollection(requestAdapterMock.Object, "application/json", new[] { "foo" });

// Assert we now have an option
serializationWriterMock.Verify(x => x.WriteStringValue(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
Expand All @@ -328,6 +332,35 @@ public void GetUriResolvesParametersCaseInsensitive()
// Assert
Assert.Equal("http://localhost/UriTemplate/ParameterMapping?IsCaseSensitive=false", testRequest.URI.ToString());
}
[Fact]
public void SetsBoundaryOnMultipartBody()
{
// Arrange
var testRequest = new RequestInformation()
{
HttpMethod = Method.POST,
UrlTemplate = "http://localhost/{URITemplate}/ParameterMapping?IsCaseSensitive={IsCaseSensitive}"
};
var requestAdapterMock = new Mock<IRequestAdapter>();
var serializationWriterFactoryMock = new Mock<ISerializationWriterFactory>();
var serializationWriterMock = new Mock<ISerializationWriter>();
serializationWriterFactoryMock.Setup(x => x.GetSerializationWriter(It.IsAny<string>())).Returns(serializationWriterMock.Object);
requestAdapterMock.SetupGet(x => x.SerializationWriterFactory).Returns(serializationWriterFactoryMock.Object);
// Given
var multipartBody = new MultipartBody
{
RequestAdapter = requestAdapterMock.Object
};

// When
testRequest.SetContentFromParsable(requestAdapterMock.Object, "multipart/form-data", multipartBody);

// Then
Assert.NotNull(multipartBody.Boundary);
Assert.True(testRequest.Headers.TryGetValue("Content-Type", out var contentType));
Assert.Single(contentType);
Assert.Equal("multipart/form-data; boundary=" + multipartBody.Boundary, contentType.First());
}
}

/// <summary>The messages in a mailbox or folder. Read-only. Nullable.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl
Assert.True(changedNestedValues.ContainsKey("id"));
Assert.True(changedNestedValues.ContainsKey("businessPhones"));
var businessPhones = ((Tuple<ICollection, int>)changedNestedValues["businessPhones"]).Item1;
Assert.Equal(1, businessPhones.Count);
Assert.Single(businessPhones);
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.Kiota.Abstractions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageProjectUrl>https://aka.ms/kiota/docs</PackageProjectUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<Deterministic>true</Deterministic>
<VersionPrefix>1.2.1</VersionPrefix>
<VersionPrefix>1.3.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<SignAssembly>false</SignAssembly>
Expand Down
166 changes: 166 additions & 0 deletions src/MultipartBody.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;

namespace Microsoft.Kiota.Abstractions;

/// <summary>
/// Represents a multipart body for a request or a response.
/// </summary>
public class MultipartBody : IParsable
{
private readonly Lazy<string> _boundary = new Lazy<string>(() => Guid.NewGuid().ToString("N"));
/// <summary>
/// The boundary to use for the multipart body.
/// </summary>
public string Boundary => _boundary.Value;
/// <summary>
/// The request adapter to use for serialization.
/// </summary>
public IRequestAdapter? RequestAdapter { get; set; }
/// <summary>
/// Adds or replaces a part to the multipart body.
/// </summary>
/// <typeparam name="T">The type of the part value.</typeparam>
/// <param name="partName">The name of the part.</param>
/// <param name="contentType">The content type of the part.</param>
/// <param name="partValue">The value of the part.</param>
public void AddOrReplacePart<T>(string partName, string contentType, T partValue)
{
if(string.IsNullOrEmpty(partName))
{
throw new ArgumentNullException(nameof(partName));
}
if(string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
if(partValue == null)
{
throw new ArgumentNullException(nameof(partValue));
}
var value = new Tuple<string, object>(contentType, partValue);
if(!_parts.TryAdd(partName, value))
{
_parts[partName] = value;
}
}
/// <summary>
/// Gets the value of a part from the multipart body.
/// </summary>
/// <typeparam name="T">The type of the part value.</typeparam>
/// <param name="partName">The name of the part.</param>
/// <returns>The value of the part.</returns>
public T? GetPartValue<T>(string partName)
{
if(string.IsNullOrEmpty(partName))
{
throw new ArgumentNullException(nameof(partName));
}
if(_parts.TryGetValue(partName, out var value))
{
return (T)value.Item2;
}
return default;
}
/// <summary>
/// Removes a part from the multipart body.
/// </summary>
/// <param name="partName">The name of the part.</param>
/// <returns>True if the part was removed, false otherwise.</returns>
public bool RemovePart(string partName)
{
if(string.IsNullOrEmpty(partName))
{
throw new ArgumentNullException(nameof(partName));
}
return _parts.Remove(partName);
}
private readonly Dictionary<string, Tuple<string, object>> _parts = new Dictionary<string, Tuple<string, object>>(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public IDictionary<string, Action<IParseNode>> GetFieldDeserializers() => throw new NotImplementedException();
/// <inheritdoc />
public void Serialize(ISerializationWriter writer)
{
if(writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
if(RequestAdapter?.SerializationWriterFactory == null)
{
throw new InvalidOperationException(nameof(RequestAdapter.SerializationWriterFactory));
}
if(!_parts.Any())
{
throw new InvalidOperationException("No parts to serialize");
}
var first = true;
foreach(var part in _parts)
{
try
{
if(first)
first = false;
else
AddNewLine(writer);

writer.WriteStringValue(string.Empty, $"--{Boundary}");
writer.WriteStringValue("Content-Type", $"{part.Value.Item1}");
writer.WriteStringValue("Content-Disposition", $"form-data; name=\"{part.Key}\"");
AddNewLine(writer);
if(part.Value.Item2 is IParsable parsable)
{
using var partWriter = RequestAdapter.SerializationWriterFactory.GetSerializationWriter(part.Value.Item1);
partWriter.WriteObjectValue(string.Empty, parsable);
using var partContent = partWriter.GetSerializedContent();
if(partContent.CanSeek)
partContent.Seek(0, SeekOrigin.Begin);
using var ms = new MemoryStream();
partContent.CopyTo(ms);
writer.WriteByteArrayValue(string.Empty, ms.ToArray());
}
else if(part.Value.Item2 is string currentString)
{
writer.WriteStringValue(string.Empty, currentString);
}
else if(part.Value.Item2 is MemoryStream originalMemoryStream)
{
writer.WriteByteArrayValue(string.Empty, originalMemoryStream.ToArray());
}
else if(part.Value.Item2 is Stream currentStream)
{
if(currentStream.CanSeek)
currentStream.Seek(0, SeekOrigin.Begin);
using var ms = new MemoryStream();
currentStream.CopyTo(ms);
writer.WriteByteArrayValue(string.Empty, ms.ToArray());
}
else if(part.Value.Item2 is byte[] currentBinary)
{
writer.WriteByteArrayValue(string.Empty, currentBinary);
}
else
{
throw new InvalidOperationException($"Unsupported type {part.Value.Item2.GetType().Name} for part {part.Key}");
}
}
catch(InvalidOperationException) when(part.Value.Item2 is byte[] currentBinary)
{ // binary payload
writer.WriteByteArrayValue(part.Key, currentBinary);
}
}
AddNewLine(writer);
writer.WriteStringValue(string.Empty, $"--{Boundary}--");
}
private void AddNewLine(ISerializationWriter writer)
{
writer.WriteStringValue(string.Empty, string.Empty);
}
}
Loading

0 comments on commit a03d4f0

Please sign in to comment.