diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs
index 4df0d422..b33bec10 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs
@@ -15,6 +15,11 @@ internal static class Constants
///
public static string ApplicationJsonMediaType = "application/json";
+ ///
+ /// application/octet-stream
+ ///
+ public static string ApplicationOctetStreamMediaType = "application/octet-stream";
+
///
/// Status code: 200
///
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
index bd5abab9..a052bfe9 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
@@ -258,7 +258,11 @@ public int CompareTo(ODataPath other)
private ODataPathKind CalcPathType()
{
- if (Segments.Any(c => c.Kind == ODataSegmentKind.Ref))
+ if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent))
+ {
+ return ODataPathKind.MediaEntity;
+ }
+ else if (Segments.Any(c => c.Kind == ODataSegmentKind.Ref))
{
return ODataPathKind.Ref;
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs
index 2a412150..14498a00 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs
@@ -36,15 +36,20 @@ public enum ODataPathKind
OperationImport,
///
- /// Represents an navigation propert path, for example: ~/users/{id}/onedrive
+ /// Represents an navigation property path, for example: ~/users/{id}/onedrive
///
NavigationProperty,
///
- /// Represents an navigation propert $ref path, for example: ~/users/{id}/onedrive/$ref
+ /// Represents an navigation property $ref path, for example: ~/users/{id}/onedrive/$ref
///
Ref,
+ ///
+ /// Represents a media entity path, for example: ~/me/photo/$value or ~/reports/deviceConfigurationUserActivity/Content
+ ///
+ MediaEntity,
+
///
/// Represents an un-supported/unknown path.
///
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
index 4273bb73..a6b877b9 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
@@ -126,6 +126,7 @@ private void AppendPath(ODataPath path)
case ODataPathKind.Entity:
case ODataPathKind.EntitySet:
case ODataPathKind.Singleton:
+ case ODataPathKind.MediaEntity:
ODataNavigationSourceSegment navigationSourceSegment = (ODataNavigationSourceSegment)path.FirstSegment;
if (!_allNavigationSourcePaths.TryGetValue(navigationSourceSegment.EntityType, out IList nsList))
{
@@ -182,6 +183,33 @@ private void RetrieveNavigationSourcePaths(IEdmNavigationSource navigationSource
AppendPath(path.Clone());
}
+ // media entity
+ bool createValuePath = true;
+ foreach (IEdmStructuralProperty sp in entityType.DeclaredStructuralProperties())
+ {
+ if (sp.Type.AsPrimitive().IsStream())
+ {
+ path.Push(new ODataStreamPropertySegment(sp.Name));
+ AppendPath(path.Clone());
+ path.Pop();
+ }
+
+ if (sp.Name.Equals("content", System.StringComparison.OrdinalIgnoreCase))
+ {
+ createValuePath = false;
+ }
+ }
+
+ /* Create a /$value path only if entity has stream and
+ * does not contain a structural property named Content
+ */
+ if (createValuePath && entityType.HasStream)
+ {
+ path.Push(new ODataStreamContentSegment());
+ AppendPath(path.Clone());
+ path.Pop();
+ }
+
// navigation property
foreach (IEdmNavigationProperty np in entityType.DeclaredNavigationProperties())
{
@@ -369,7 +397,8 @@ private bool AppendBoundOperationOnNavigationSourcePath(IEdmOperation edmOperati
foreach (var subPath in value)
{
if ((isCollection && subPath.Kind == ODataPathKind.EntitySet) ||
- (!isCollection && subPath.Kind != ODataPathKind.EntitySet))
+ (!isCollection && subPath.Kind != ODataPathKind.EntitySet &&
+ subPath.Kind != ODataPathKind.MediaEntity))
{
ODataPath newPath = subPath.Clone();
newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction));
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs
index ad43a9f0..b6fb0e8b 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs
@@ -47,7 +47,17 @@ public enum ODataSegmentKind
///
/// $ref
///
- Ref
+ Ref,
+
+ ///
+ /// Stream content -> $value
+ ///
+ StreamContent,
+
+ ///
+ /// Stream property
+ ///
+ StreamProperty
}
///
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs
new file mode 100644
index 00000000..aba66263
--- /dev/null
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs
@@ -0,0 +1,24 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using System.Collections.Generic;
+
+namespace Microsoft.OpenApi.OData.Edm
+{
+ ///
+ /// Stream segment.
+ ///
+ public class ODataStreamContentSegment : ODataSegment
+ {
+ ///
+ public override ODataSegmentKind Kind => ODataSegmentKind.StreamContent;
+
+ ///
+ public override string Identifier => "$value";
+
+ ///
+ public override string GetPathItemName(OpenApiConvertSettings settings, HashSet parameters) => "$value";
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs
new file mode 100644
index 00000000..efca6864
--- /dev/null
+++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using System.Collections.Generic;
+using Microsoft.OpenApi.OData.Common;
+
+namespace Microsoft.OpenApi.OData.Edm
+{
+ ///
+ /// Property Stream segment.
+ ///
+ public class ODataStreamPropertySegment : ODataSegment
+ {
+ private readonly string _streamPropertyName;
+ ///
+ /// Initializes a new instance of class.
+ ///
+ /// The name of the stream property.
+ public ODataStreamPropertySegment(string streamPropertyName)
+ {
+ _streamPropertyName = streamPropertyName ?? throw Error.ArgumentNull(nameof(streamPropertyName));
+ }
+
+ ///
+ public override ODataSegmentKind Kind => ODataSegmentKind.StreamProperty;
+
+ ///
+ public override string Identifier { get => _streamPropertyName; }
+
+ ///
+ public override string GetPathItemName(OpenApiConvertSettings settings, HashSet parameters) => _streamPropertyName;
+ }
+}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs
new file mode 100644
index 00000000..fa6cf701
--- /dev/null
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs
@@ -0,0 +1,109 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.OData.Common;
+using Microsoft.OpenApi.OData.Edm;
+using Microsoft.OpenApi.OData.Generator;
+using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.OpenApi.OData.Operation
+{
+ ///
+ /// Retrieve a media content for an Entity
+ ///
+ internal class MediaEntityGetOperationHandler : EntitySetOperationHandler
+ {
+ ///
+ public override OperationType OperationType => OperationType.Get;
+
+ ///
+ protected override void SetBasicInfo(OpenApiOperation operation)
+ {
+ string typeName = EntitySet.EntityType().Name;
+
+ // Summary
+ operation.Summary = $"Get media content for {typeName} from {EntitySet.Name}";
+
+ // OperationId
+ if (Context.Settings.EnableOperationId)
+ {
+ string identifier = Path.LastSegment.Kind == ODataSegmentKind.StreamContent ? "Content" : Path.LastSegment.Identifier;
+ operation.OperationId = EntitySet.Name + "." + typeName + ".Get" + Utils.UpperFirstChar(identifier);
+ }
+
+ base.SetBasicInfo(operation);
+ }
+
+ ///
+ protected override void SetResponses(OpenApiOperation operation)
+ {
+ OpenApiSchema schema = null;
+
+ if (Context.Settings.EnableDerivedTypesReferencesForResponses)
+ {
+ schema = EdmModelHelper.GetDerivedTypesReferenceSchema(EntitySet.EntityType(), Context.Model);
+ }
+
+ if (schema == null)
+ {
+ schema = new OpenApiSchema
+ {
+ Type = "string",
+ Format = "binary"
+ };
+ }
+
+ operation.Responses = new OpenApiResponses
+ {
+ {
+ Constants.StatusCode200,
+ new OpenApiResponse
+ {
+ Description = "Retrieved media content",
+ Content = new Dictionary
+ {
+ {
+ Constants.ApplicationOctetStreamMediaType,
+ new OpenApiMediaType
+ {
+ Schema = schema
+ }
+ }
+ }
+ }
+ }
+ };
+ operation.Responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse());
+
+ base.SetResponses(operation);
+ }
+ ///
+ protected override void SetSecurity(OpenApiOperation operation)
+ {
+ ReadRestrictionsType read = Context.Model.GetRecord(EntitySet, CapabilitiesConstants.ReadRestrictions);
+ if (read == null)
+ {
+ return;
+ }
+
+ ReadRestrictionsBase readBase = read;
+ if (read.ReadByKeyRestrictions != null)
+ {
+ readBase = read.ReadByKeyRestrictions;
+ }
+
+ if (readBase == null && readBase.Permissions == null)
+ {
+ return;
+ }
+
+ operation.Security = Context.CreateSecurityRequirements(readBase.Permissions).ToList();
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs
new file mode 100644
index 00000000..a8ca7cf9
--- /dev/null
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs
@@ -0,0 +1,104 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.OData.Edm;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.OData.Common;
+using Microsoft.OpenApi.OData.Edm;
+using Microsoft.OpenApi.OData.Generator;
+using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
+
+namespace Microsoft.OpenApi.OData.Operation
+{
+ ///
+ /// Update a media content for an Entity
+ ///
+ internal class MediaEntityPutOperationHandler : EntitySetOperationHandler
+ {
+ ///
+ public override OperationType OperationType => OperationType.Put;
+
+ ///
+ protected override void SetBasicInfo(OpenApiOperation operation)
+ {
+ string typeName = EntitySet.EntityType().Name;
+
+ // Summary
+ operation.Summary = $"Update media content for {typeName} in {EntitySet.Name}";
+
+ // OperationId
+ if (Context.Settings.EnableOperationId)
+ {
+ string identifier = Path.LastSegment.Kind == ODataSegmentKind.StreamContent ? "Content" : Path.LastSegment.Identifier;
+ operation.OperationId = EntitySet.Name + "." + typeName + ".Update" + Utils.UpperFirstChar(identifier);
+ }
+
+ base.SetBasicInfo(operation);
+ }
+
+ ///
+ protected override void SetRequestBody(OpenApiOperation operation)
+ {
+ OpenApiSchema schema = null;
+
+ if (Context.Settings.EnableDerivedTypesReferencesForRequestBody)
+ {
+ schema = EdmModelHelper.GetDerivedTypesReferenceSchema(EntitySet.EntityType(), Context.Model);
+ }
+
+ if (schema == null)
+ {
+ schema = new OpenApiSchema
+ {
+ Type = "string",
+ Format = "binary"
+ };
+ }
+
+ operation.RequestBody = new OpenApiRequestBody
+ {
+ Required = true,
+ Description = "New media content.",
+ Content = new Dictionary
+ {
+ {
+ Constants.ApplicationOctetStreamMediaType, new OpenApiMediaType
+ {
+ Schema = schema
+ }
+ }
+ }
+ };
+
+ base.SetRequestBody(operation);
+ }
+
+ ///
+ protected override void SetResponses(OpenApiOperation operation)
+ {
+ operation.Responses = new OpenApiResponses
+ {
+ { Constants.StatusCode204, Constants.StatusCode204.GetResponse() },
+ { Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse() }
+ };
+
+ base.SetResponses(operation);
+ }
+
+ ///
+ protected override void SetSecurity(OpenApiOperation operation)
+ {
+ UpdateRestrictionsType update = Context.Model.GetRecord(EntitySet, CapabilitiesConstants.UpdateRestrictions);
+ if (update == null || update.Permissions == null)
+ {
+ return;
+ }
+
+ operation.Security = Context.CreateSecurityRequirements(update.Permissions).ToList();
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs
index 763f84a8..3d3abce2 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs
@@ -76,6 +76,13 @@ public OperationHandlerProvider()
{OperationType.Post, new RefPostOperationHandler() },
{OperationType.Delete, new RefDeleteOperationHandler() }
};
+
+ // media entity operation (Get|Put)
+ _handlers[ODataPathKind.MediaEntity] = new Dictionary
+ {
+ {OperationType.Get, new MediaEntityGetOperationHandler() },
+ {OperationType.Put, new MediaEntityPutOperationHandler() }
+ };
}
///
diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs
new file mode 100644
index 00000000..24d75c5c
--- /dev/null
+++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.OData.Edm;
+using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
+
+namespace Microsoft.OpenApi.OData.PathItem
+{
+ ///
+ /// Create a for a media entity.
+ ///
+ internal class MediaEntityPathItemHandler : EntitySetPathItemHandler
+ {
+ ///
+ protected override ODataPathKind HandleKind => ODataPathKind.MediaEntity;
+
+ ///
+ protected override void SetOperations(OpenApiPathItem item)
+ {
+ ReadRestrictionsType read = Context.Model.GetRecord(EntitySet);
+ if (read == null ||
+ (read.ReadByKeyRestrictions == null && read.IsReadable) ||
+ (read.ReadByKeyRestrictions != null && read.ReadByKeyRestrictions.IsReadable))
+ {
+ AddOperation(item, OperationType.Get);
+ }
+
+ UpdateRestrictionsType update = Context.Model.GetRecord(EntitySet);
+ if (update == null || update.IsUpdatable)
+ {
+ AddOperation(item, OperationType.Put);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs
index 380fbdcb..5cf3d570 100644
--- a/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs
@@ -36,6 +36,9 @@ internal class PathItemHandlerProvider : IPathItemHandlerProvider
// Edm Ref
{ ODataPathKind.Ref, new RefPathItemHandler() },
+ // Media Entity
+ {ODataPathKind.MediaEntity, new MediaEntityPathItemHandler() },
+
// Unknown
{ ODataPathKind.Unknown, null },
};
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs
index e17177af..df679253 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs
@@ -4,13 +4,12 @@
// ------------------------------------------------------------
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
+using System.Xml.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Csdl;
-using Microsoft.OData.Edm.Validation;
using Microsoft.OpenApi.OData.Tests;
using Xunit;
@@ -45,7 +44,7 @@ public void GetPathsForGraphBetaModelReturnsAllPaths()
// Assert
Assert.NotNull(paths);
- Assert.Equal(4583, paths.Count());
+ Assert.Equal(4585, paths.Count());
}
[Fact]
@@ -84,7 +83,7 @@ public void GetPathsWithSingletonWorks()
public void GetPathsWithBoundFunctionOperationWorks()
{
// Arrange
- string boundFunction =
+ string boundFunction =
@"
@@ -182,6 +181,44 @@ public void GetPathsWithNavigationPropertytWorks()
Assert.Contains("/Orders({id})/MultipleCustomers/$ref", pathItems);
}
+ [Theory]
+ [InlineData(true, "Logo")]
+ [InlineData(false, "Logo")]
+ [InlineData(true, "Content")]
+ [InlineData(false, "Content")]
+ public void GetPathsWithStreamPropertyAndWithEntityHasStreamWorks(bool hasStream, string streamPropName)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel(hasStream, streamPropName);
+ ODataPathProvider provider = new ODataPathProvider();
+
+ // Act
+ var paths = provider.GetPaths(model);
+
+ // Assert
+ Assert.NotNull(paths);
+
+ if (hasStream && !streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase))
+ {
+ Assert.Equal(4, paths.Count());
+ Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/$value", "/Todos({Id})/Logo" },
+ paths.Select(p => p.GetPathItemName()));
+ }
+ else if ((hasStream && streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase)) ||
+ (!hasStream && streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase)))
+ {
+ Assert.Equal(3, paths.Count());
+ Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/Content" },
+ paths.Select(p => p.GetPathItemName()));
+ }
+ else // !hasStream && !streamPropName.Equals("Content")
+ {
+ Assert.Equal(3, paths.Count());
+ Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/Logo" },
+ paths.Select(p => p.GetPathItemName()));
+ }
+ }
+
private static IEdmModel GetEdmModel(string schemaElement, string containerElement)
{
string template = @"
@@ -198,12 +235,35 @@ private static IEdmModel GetEdmModel(string schemaElement, string containerEleme
{1}
";
- string schema = String.Format(template, schemaElement, containerElement);
- IEdmModel parsedModel;
- IEnumerable errors;
- bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out parsedModel, out errors);
+ string schema = string.Format(template, schemaElement, containerElement);
+ bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out _);
Assert.True(parsed);
return parsedModel;
}
+
+ private static IEdmModel GetEdmModel(bool hasStream, string streamPropName)
+ {
+ string template = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+";
+ string modelText = string.Format(template, hasStream, streamPropName);
+ bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _);
+ Assert.True(result);
+ return model;
+ }
}
}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs
index 255d95d9..57045cda 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs
@@ -105,7 +105,7 @@ public void ODataPathLastSegmentWorks()
}
[Fact]
- public void KindPropertyReturnsUnknow()
+ public void KindPropertyReturnsUnknown()
{
// Arrange
ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType);
@@ -198,6 +198,32 @@ public void KindPropertyReturnsOperationImport()
Assert.Equal(ODataPathKind.OperationImport, path.Kind);
}
+ [Fact]
+ public void KindPropertyReturnsStreamProperty()
+ {
+ // Arrange
+ ODataNavigationSourceSegment nsSegment = new ODataNavigationSourceSegment(_simpleKeyEntitySet);
+ ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType);
+ ODataStreamPropertySegment streamPropSegment = new ODataStreamPropertySegment("Logo");
+ ODataPath path = new ODataPath(nsSegment, keySegment, streamPropSegment);
+
+ // Act & Assert
+ Assert.Equal(ODataPathKind.MediaEntity, path.Kind);
+ }
+
+ [Fact]
+ public void KindPropertyReturnsStreamContent()
+ {
+ // Arrange
+ ODataNavigationSourceSegment nsSegment = new ODataNavigationSourceSegment(_simpleKeyEntitySet);
+ ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType);
+ ODataStreamContentSegment streamContSegment = new ODataStreamContentSegment();
+ ODataPath path = new ODataPath(nsSegment, keySegment, streamContSegment);
+
+ // Act & Assert
+ Assert.Equal(ODataPathKind.MediaEntity, path.Kind);
+ }
+
[Theory]
[InlineData(true, true, "/Orders/{Order-Id}")]
[InlineData(true, false, "/Orders/{Id}")]
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs
new file mode 100644
index 00000000..92439491
--- /dev/null
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs
@@ -0,0 +1,57 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Xunit;
+
+namespace Microsoft.OpenApi.OData.Edm.Tests
+{
+ public class ODataStreamContentSegmentTests
+ {
+ private readonly EdmEntityType _todo;
+
+ public ODataStreamContentSegmentTests()
+ {
+ _todo = new EdmEntityType("microsoft.graph", "Todo",
+ new EdmEntityType("microsoft.graph", "Task"),
+ isAbstract: false,
+ isOpen: false,
+ hasStream: true);
+ _todo.AddKeys(_todo.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String));
+ _todo.AddKeys(_todo.AddStructuralProperty("Logo", EdmPrimitiveTypeKind.Stream));
+ _todo.AddKeys(_todo.AddStructuralProperty("Description", EdmPrimitiveTypeKind.String));
+ }
+
+ [Fact]
+ public void StreamContentSegmentIdentifierPropertyReturnsCorrectDefaultValue()
+ {
+ // Arrange & Act
+ ODataStreamContentSegment segment = new ODataStreamContentSegment();
+
+ // Assert
+ Assert.Same("$value", segment.Identifier);
+ }
+
+ [Fact]
+ public void KindPropertyReturnsStreamContentEnumMember()
+ {
+ // Arrange & Act
+ ODataStreamContentSegment segment = new ODataStreamContentSegment();
+
+ // Assert
+ Assert.Equal(ODataSegmentKind.StreamContent, segment.Kind);
+ }
+
+ [Fact]
+ public void GetPathItemNameReturnsCorrectDefaultStreamContentValue()
+ {
+ // Arrange & Act
+ ODataStreamContentSegment segment = new ODataStreamContentSegment();
+
+ // Assert
+ Assert.Equal("$value", segment.GetPathItemName(new OpenApiConvertSettings()));
+ }
+ }
+}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs
new file mode 100644
index 00000000..d5858a1b
--- /dev/null
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs
@@ -0,0 +1,70 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using System;
+using System.Linq;
+using Microsoft.OData.Edm;
+using Xunit;
+
+namespace Microsoft.OpenApi.OData.Edm.Tests
+{
+ public class ODataStreamPropertySegmentTests
+ {
+ private readonly EdmEntityType _todo;
+
+ public ODataStreamPropertySegmentTests()
+ {
+ _todo = new EdmEntityType("microsoft.graph", "Todo");
+ _todo.AddKeys(_todo.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String));
+ _todo.AddKeys(_todo.AddStructuralProperty("Logo", EdmPrimitiveTypeKind.Stream));
+ _todo.AddKeys(_todo.AddStructuralProperty("Description", EdmPrimitiveTypeKind.String));
+ }
+
+ [Fact]
+ public void StreamPropertySegmentConstructorThrowsArgumentNull()
+ {
+ Assert.Throws("streamPropertyName", () => new ODataStreamPropertySegment(null));
+ }
+
+ [Fact]
+ public void StreamPropertySegmentIdentifierPropertyReturnsStreamPropertyNameOfEntity()
+ {
+ // Arrange
+ var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name;
+
+ // Act
+ ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName);
+
+ // Assert
+ Assert.Same(streamPropName, segment.Identifier);
+ }
+
+ [Fact]
+ public void KindPropertyReturnsStreamPropertyEnumMember()
+ {
+ // Arrange
+ var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name;
+
+ // Act
+ ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName);
+
+ // Assert
+ Assert.Equal(ODataSegmentKind.StreamProperty, segment.Kind);
+ }
+
+ [Fact]
+ public void GetPathItemNameReturnsCorrectStreamPropertyNameOfEntity()
+ {
+ // Arrange
+ var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name;
+
+ // Act
+ ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName);
+
+ // Assert
+ Assert.Equal(streamPropName, segment.GetPathItemName(new OpenApiConvertSettings()));
+ }
+ }
+}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs
new file mode 100644
index 00000000..1e9f2522
--- /dev/null
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs
@@ -0,0 +1,89 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Microsoft.OData.Edm.Csdl;
+using Microsoft.OpenApi.OData.Edm;
+using System.Linq;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.OpenApi.OData.Operation.Tests
+{
+ public class MediaEntityGetOperationHandlerTests
+ {
+ private readonly MediaEntityGetOperationHandler _operationalHandler = new MediaEntityGetOperationHandler();
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void CreateMediaEntityGetOperationReturnsCorrectOperation(bool enableOperationId)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel();
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ EnableOperationId = enableOperationId
+ };
+
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet todos = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(todos);
+
+ IEdmEntityType todo = model.SchemaElements.OfType().First(c => c.Name == "Todo");
+ IEdmStructuralProperty sp = todo.DeclaredStructuralProperties().First(c => c.Name == "Logo");
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(todos),
+ new ODataKeySegment(todos.EntityType()),
+ new ODataStreamPropertySegment(sp.Name));
+
+ // Act
+ var getOperation = _operationalHandler.CreateOperation(context, path);
+
+ // Assert
+ Assert.NotNull(getOperation);
+ Assert.Equal("Get media content for Todo from Todos", getOperation.Summary);
+ Assert.NotNull(getOperation.Tags);
+ var tag = Assert.Single(getOperation.Tags);
+ Assert.Equal("Todos.Todo", tag.Name);
+
+ Assert.NotNull(getOperation.Responses);
+ Assert.Equal(2, getOperation.Responses.Count);
+ Assert.Equal(new[] { "200", "default" }, getOperation.Responses.Select(r => r.Key));
+
+ if (enableOperationId)
+ {
+ Assert.Equal("Todos.Todo.GetLogo", getOperation.OperationId);
+ }
+ else
+ {
+ Assert.Null(getOperation.OperationId);
+ }
+ }
+
+ public static IEdmModel GetEdmModel()
+ {
+ const string modelText = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+";
+ bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _);
+ Assert.True(result);
+ return model;
+ }
+ }
+}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs
new file mode 100644
index 00000000..14fe92d4
--- /dev/null
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Microsoft.OpenApi.OData.Edm;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.OpenApi.OData.Operation.Tests
+{
+ public class MediaEntityPutOperationHandlerTests
+ {
+ private readonly MediaEntityPutOperationHandler _operationalHandler = new MediaEntityPutOperationHandler();
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void CreateEntityPutOperationReturnsCorrectOperation(bool enableOperationId)
+ {
+ // Arrange
+ IEdmModel model = MediaEntityGetOperationHandlerTests.GetEdmModel();
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ EnableOperationId = enableOperationId
+ };
+
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet todos = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(todos);
+
+ IEdmEntityType todo = model.SchemaElements.OfType().First(c => c.Name == "Todo");
+ IEdmStructuralProperty sp = todo.DeclaredStructuralProperties().First(c => c.Name == "Logo");
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(todos),
+ new ODataKeySegment(todos.EntityType()),
+ new ODataStreamPropertySegment(sp.Name));
+
+ // Act
+ var getOperation = _operationalHandler.CreateOperation(context, path);
+
+ // Assert
+ Assert.NotNull(getOperation);
+ Assert.Equal("Update media content for Todo in Todos", getOperation.Summary);
+ Assert.NotNull(getOperation.Tags);
+ var tag = Assert.Single(getOperation.Tags);
+ Assert.Equal("Todos.Todo", tag.Name);
+
+ Assert.NotNull(getOperation.Responses);
+ Assert.Equal(2, getOperation.Responses.Count);
+ Assert.Equal(new[] { "204", "default" }, getOperation.Responses.Select(r => r.Key));
+
+ if (enableOperationId)
+ {
+ Assert.Equal("Todos.Todo.UpdateLogo", getOperation.OperationId);
+ }
+ else
+ {
+ Assert.Null(getOperation.OperationId);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs
index ca6e588b..f28668d4 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs
@@ -32,6 +32,8 @@ public class OperationHandlerProviderTests
[InlineData(ODataPathKind.Ref, OperationType.Delete, typeof(RefDeleteOperationHandler))]
[InlineData(ODataPathKind.Ref, OperationType.Get, typeof(RefGetOperationHandler))]
[InlineData(ODataPathKind.Ref, OperationType.Put, typeof(RefPutOperationHandler))]
+ [InlineData(ODataPathKind.MediaEntity, OperationType.Get, typeof(MediaEntityGetOperationHandler))]
+ [InlineData(ODataPathKind.MediaEntity, OperationType.Put, typeof(MediaEntityPutOperationHandler))]
public void GetHandlerReturnsCorrectOperationHandlerType(ODataPathKind pathKind, OperationType operationType, Type handlerType)
{
// Arrange
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs
new file mode 100644
index 00000000..53f164f4
--- /dev/null
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs
@@ -0,0 +1,206 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// ------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Microsoft.OData.Edm.Csdl;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.OData.Edm;
+using Microsoft.OpenApi.OData.Properties;
+using System;
+using System.Linq;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.OpenApi.OData.PathItem.Tests
+{
+ public class MediaEntityPathItemHandlerTests
+ {
+ private readonly MediaEntityPathItemHandler _pathItemHandler = new MyMediaEntityPathItemHandler();
+
+ [Fact]
+ public void CreatePathItemThrowsForNullContext()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws("context",
+ () => _pathItemHandler.CreatePathItem(context: null, path: new ODataPath()));
+ }
+
+ [Fact]
+ public void CreatePathItemThrowsForNullPath()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws("path",
+ () => _pathItemHandler.CreatePathItem(new ODataContext(EdmCoreModel.Instance), path: null));
+ }
+
+ [Fact]
+ public void CreatePathItemThrowsForNonMediaEntityPath()
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel("");
+ ODataContext context = new ODataContext(model);
+ var entitySet = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(entitySet); // guard
+ var path = new ODataPath(new ODataNavigationSourceSegment(entitySet));
+
+ // Act
+ void test() => _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ var exception = Assert.Throws(test);
+ Assert.Equal(string.Format(SRResource.InvalidPathKindForPathItemHandler, _pathItemHandler.GetType().Name, path.Kind), exception.Message);
+ }
+
+ [Fact]
+ public void CreateMediaEntityPathItemReturnsCorrectItem()
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel("");
+ ODataContext context = new ODataContext(model);
+ var entitySet = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType();
+
+ IEdmStructuralProperty sp = entityType.DeclaredStructuralProperties().First(c => c.Name == "Logo");
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataStreamPropertySegment(sp.Name));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(2, pathItem.Operations.Count);
+ Assert.Equal(new OperationType[] { OperationType.Get, OperationType.Put },
+ pathItem.Operations.Select(o => o.Key));
+ }
+
+ [Theory]
+ [InlineData(true, new OperationType[] { OperationType.Get, OperationType.Put })]
+ [InlineData(false, new OperationType[] { OperationType.Put })]
+ public void CreateMediaEntityPathItemWorksForReadByKeyRestrictionsCapablities(bool readable, OperationType[] expected)
+ {
+ // Arrange
+ string annotation = $@"
+
+
+
+
+
+
+
+
+";
+
+ // Assert
+ VerifyPathItemOperationsForStreamPropertySegment(annotation, expected);
+ VerifyPathItemOperationsForStreamContentSegment(annotation, expected);
+ }
+
+ [Theory]
+ [InlineData(true, new OperationType[] { OperationType.Get, OperationType.Put })]
+ [InlineData(false, new OperationType[] { OperationType.Get })]
+ public void CreateMediaEntityPathItemWorksForUpdateRestrictionsCapablities(bool updatable, OperationType[] expected)
+ {
+ // Arrange
+ string annotation = $@"
+
+
+
+
+";
+
+ // Assert
+ VerifyPathItemOperationsForStreamPropertySegment(annotation, expected);
+ VerifyPathItemOperationsForStreamContentSegment(annotation, expected);
+ }
+
+ private void VerifyPathItemOperationsForStreamPropertySegment(string annotation, OperationType[] expected)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel(annotation);
+ ODataContext context = new ODataContext(model);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType();
+
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataStreamContentSegment());
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(expected, pathItem.Operations.Select(e => e.Key));
+ }
+
+ private void VerifyPathItemOperationsForStreamContentSegment(string annotation, OperationType[] expected)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel(annotation);
+ ODataContext context = new ODataContext(model);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Todos");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType();
+
+ IEdmStructuralProperty sp = entityType.DeclaredStructuralProperties().First(c => c.Name == "Logo");
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataStreamPropertySegment(sp.Name));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(expected, pathItem.Operations.Select(e => e.Key));
+ }
+
+ private IEdmModel GetEdmModel(string annotation)
+ {
+ const string template = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {0}
+
+
+
+";
+ string modelText = string.Format(template, annotation);
+ bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _);
+ Assert.True(result);
+ return model;
+ }
+ }
+
+ internal class MyMediaEntityPathItemHandler : MediaEntityPathItemHandler
+ {
+ protected override void AddOperation(OpenApiPathItem item, OperationType operationType)
+ {
+ item.AddOperation(operationType, new OpenApiOperation());
+ }
+ }
+}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs
index 4722b86a..3de9155f 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs
@@ -19,6 +19,7 @@ public class PathItemHandlerProviderTests
[InlineData(ODataPathKind.Operation, typeof(OperationPathItemHandler))]
[InlineData(ODataPathKind.OperationImport, typeof(OperationImportPathItemHandler))]
[InlineData(ODataPathKind.Ref, typeof(RefPathItemHandler))]
+ [InlineData(ODataPathKind.MediaEntity, typeof(MediaEntityPathItemHandler))]
public void GetHandlerReturnsCorrectHandlerType(ODataPathKind pathKind, Type handlerType)
{
// Arrange