Skip to content

feat: change all collections to ordered collections #2347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/upgrade-guide-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,57 @@ var outputString = openApiDocument.Serialize(OpenApiSpecVersion.OpenApi2_0, Open
// After (2.0)
var outputString = openApiDocument.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Json);
```
### Use OrderedDictionary string Instead of Dictionary

Changed all collections to ordered collections.

**Example:**

```csharp
// Before (1.6)
new OpenApiSchema
{
Required = new HashSet<string> { "id", "name" },
Properties = new Dictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema
{
Type = JsonSchemaType.Integer,
Format = "int64"
},
["name"] = new OpenApiSchema
{
Type = JsonSchemaType.String
},
["tag"] = new OpenApiSchema
{
Type = JsonSchemaType.String
}
}
}

// After (2.0)
new OpenApiSchema
{
Required = new HashSet<string> { "id", "name" },
Properties = new OrderedDictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema
{
Type = JsonSchemaType.Integer,
Format = "int64"
},
["name"] = new OpenApiSchema
{
Type = JsonSchemaType.String
},
["tag"] = new OpenApiSchema
{
Type = JsonSchemaType.String
}
}
}
```

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ namespace Microsoft.OpenApi.Hidi.Extensions
internal static class OpenApiExtensibleExtensions
{
/// <summary>
/// Gets an extension value from the extensions dictionary.
/// Gets an extension value from the extensions OrderedDictionary.
/// </summary>
/// <param name="extensions">A dictionary of <see cref="IOpenApiExtension"/>.</param>
/// <param name="extensions">A OrderedDictionary of <see cref="IOpenApiExtension"/>.</param>
/// <param name="extensionKey">The key corresponding to the <see cref="IOpenApiExtension"/>.</param>
/// <returns>A <see cref="string"/> value matching the provided extensionKey. Return null when extensionKey is not found. </returns>
internal static string GetExtension(this Dictionary<string, IOpenApiExtension> extensions, string extensionKey)
internal static string GetExtension(this OrderedDictionary<string, IOpenApiExtension> extensions, string extensionKey)
{
if (extensions.TryGetValue(extensionKey, out var value) && value is OpenApiAny { Node: JsonValue castValue } && castValue.TryGetValue<string>(out var stringValue))
{
Expand Down
20 changes: 10 additions & 10 deletions src/Microsoft.OpenApi.Hidi/OpenApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog

private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger, ApiDependency? apiDependency, JsonDocument? postmanCollection, OpenApiDocument document)
{
Dictionary<string, List<string>> requestUrls;
OrderedDictionary<string, List<string>> requestUrls;
if (apiDependency != null)
{
requestUrls = GetRequestUrlsFromManifest(apiDependency);
Expand Down Expand Up @@ -262,7 +262,7 @@ private static async Task WriteOpenApiAsync(HidiOptions options, string openApiF
return document;
}

private static Func<string, HttpMethod, OpenApiOperation, bool>? FilterOpenApiDocument(string? filterByOperationIds, string? filterByTags, Dictionary<string, List<string>> requestUrls, OpenApiDocument document, ILogger logger)
private static Func<string, HttpMethod, OpenApiOperation, bool>? FilterOpenApiDocument(string? filterByOperationIds, string? filterByTags, OrderedDictionary<string, List<string>> requestUrls, OpenApiDocument document, ILogger logger)
{
Func<string, HttpMethod, OpenApiOperation, bool>? predicate = null;

Expand Down Expand Up @@ -295,18 +295,18 @@ private static async Task WriteOpenApiAsync(HidiOptions options, string openApiF
return predicate;
}

private static Dictionary<string, List<string>> GetRequestUrlsFromManifest(ApiDependency apiDependency)
private static OrderedDictionary<string, List<string>> GetRequestUrlsFromManifest(ApiDependency apiDependency)
{
// Get the request URLs from the API Dependencies in the API manifest
var requests = apiDependency
.Requests.Where(static r => !r.Exclude && !string.IsNullOrEmpty(r.UriTemplate) && !string.IsNullOrEmpty(r.Method))
.Select(static r => new { UriTemplate = r.UriTemplate!, Method = r.Method! })
.GroupBy(static r => r.UriTemplate)
.ToDictionary(static g => g.Key, static g => g.Select(static r => r.Method).ToList());
.ToOrderedDictionary(static g => g.Key, static g => g.Select(static r => r.Method).ToList());
// This makes the assumption that the UriTemplate in the ApiManifest matches exactly the UriTemplate in the OpenAPI document
// This does not need to be the case. The URI template in the API manifest could map to a set of OpenAPI paths.
// Additional logic will be required to handle this scenario. I suggest we build this into the OpenAPI.Net library at some point.
return requests;
return new OrderedDictionary<string, List<string>>(requests);
}

private static XslCompiledTransform GetFilterTransform()
Expand Down Expand Up @@ -451,10 +451,10 @@ private static async Task<ReadResult> ParseOpenApiAsync(string openApiFile, bool
/// Takes in a file stream, parses the stream into a JsonDocument and gets a list of paths and Http methods
/// </summary>
/// <param name="stream"> A file stream.</param>
/// <returns> A dictionary of request urls and http methods from a collection.</returns>
public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream stream, ILogger logger)
/// <returns> A OrderedDictionary of request urls and http methods from a collection.</returns>
public static OrderedDictionary<string, List<string>> ParseJsonCollectionFile(Stream stream, ILogger logger)
{
var requestUrls = new Dictionary<string, List<string>>();
var requestUrls = new OrderedDictionary<string, List<string>>();

logger.LogTrace("Parsing the json collection file into a JsonDocument");
using var document = JsonDocument.Parse(stream);
Expand All @@ -466,7 +466,7 @@ public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream st
return requestUrls;
}

private static Dictionary<string, List<string>> EnumerateJsonDocument(JsonElement itemElement, Dictionary<string, List<string>> paths)
private static OrderedDictionary<string, List<string>> EnumerateJsonDocument(JsonElement itemElement, OrderedDictionary<string, List<string>> paths)
{
var itemsArray = itemElement.GetProperty("item");

Expand All @@ -476,7 +476,7 @@ private static Dictionary<string, List<string>> EnumerateJsonDocument(JsonElemen
{
if (item.TryGetProperty("request", out var request))
{
// Fetch list of methods and urls from collection, store them in a dictionary
// Fetch list of methods and urls from collection, store them in a OrderedDictionary
var path = request.GetProperty("url").GetProperty("raw").ToString();
var method = request.GetProperty("method").ToString();
if (paths.TryGetValue(path, out var value))
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.Hidi/StatsVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public override void Visit(IOpenApiSchema schema)

public int HeaderCount { get; set; }

public override void Visit(Dictionary<string, IOpenApiHeader> headers)
public override void Visit(OrderedDictionary<string, IOpenApiHeader> headers)
{
HeaderCount++;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.Workbench/StatsVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public override void Visit(IOpenApiSchema schema)

public int HeaderCount { get; set; }

public override void Visit(Dictionary<string, IOpenApiHeader> headers)
public override void Visit(OrderedDictionary<string, IOpenApiHeader> headers)
{
HeaderCount++;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public static class OpenApiServerExtensions
/// <returns>A URL with the provided variables substituted.</returns>
/// <exception cref="ArgumentException">
/// Thrown when:
/// 1. A substitution has no valid value in both the supplied dictionary and the default
/// 1. A substitution has no valid value in both the supplied OrderedDictionary and the default
/// 2. A substitution's value is not available in the enum provided
/// </exception>
public static string? ReplaceServerUrlVariables(this OpenApiServer server, Dictionary<string, string>? values = null)
public static string? ReplaceServerUrlVariables(this OpenApiServer server, OrderedDictionary<string, string>? values = null)
{
var parsedUrl = server.Url;
if (server.Variables is not null && parsedUrl is not null)
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static string[] ToIdentifiers(this JsonSchemaType schemaType)
return schemaType.ToIdentifiersInternal().ToArray();
}

private static readonly Dictionary<JsonSchemaType, string> allSchemaTypes = new()
private static readonly OrderedDictionary<JsonSchemaType, string> allSchemaTypes = new()
{
{ JsonSchemaType.Boolean, "boolean" },
{ JsonSchemaType.Integer, "integer" },
Expand Down Expand Up @@ -115,7 +115,7 @@ public static JsonSchemaType ToJsonSchemaType(this string identifier)
return type;
}

private static readonly Dictionary<Type, Func<OpenApiSchema>> _simpleTypeToOpenApiSchema = new()
private static readonly OrderedDictionary<Type, Func<OpenApiSchema>> _simpleTypeToOpenApiSchema = new()
{
[typeof(bool)] = () => new() { Type = JsonSchemaType.Boolean },
[typeof(byte)] = () => new() { Type = JsonSchemaType.String, Format = "byte" },
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ internal static class StringExtensions
}
private static ReadOnlyDictionary<string, object> GetEnumValues<T>([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type enumType) where T : Enum
{
var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var result = new OrderedDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var field in enumType.GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.GetCustomAttribute<DisplayAttribute>() is { } displayAttribute
Expand Down
130 changes: 130 additions & 0 deletions src/Microsoft.OpenApi/HashSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Microsoft.OpenApi
{
/// <summary>
/// Represents a collection of unique elements that preserves insertion order.
/// </summary>
/// <typeparam name="T">The type of elements in the set.</typeparam>
public class HashSet<T> : ICollection<T>
where T : notnull
{
private readonly OrderedDictionary<T, bool> _dict;

/// <summary>
/// Gets the equality comparer used to determine element equality.
/// </summary>
public IEqualityComparer<T> Comparer { get; }

/// <summary>
/// Initializes a new instance of the <see cref="HashSet{T}"/> class that is empty
/// and uses the default equality comparer for the type.
/// </summary>
public HashSet() : this(EqualityComparer<T>.Default) { }

/// <summary>
/// Initializes a new instance of the <see cref="HashSet{T}"/> class that is empty
/// and uses the specified equality comparer.
/// </summary>
/// <param name="comparer">The comparer to use when determining equality of elements.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="comparer"/> is null.</exception>
public HashSet(IEqualityComparer<T> comparer)
{
Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
_dict = new OrderedDictionary<T, bool>(comparer);
}

/// <summary>
/// Initializes a new instance of the <see cref="HashSet{T}"/> class that contains elements copied
/// from the specified collection and uses the specified equality comparer.
/// </summary>
/// <param name="collection">The collection whose elements are copied to the new set.</param>
/// <param name="comparer">The comparer to use when determining equality of elements.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="collection"/> or <paramref name="comparer"/> is null.</exception>
public HashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(comparer)
{
if (collection is null) throw new ArgumentNullException(nameof(collection));

foreach (var item in collection)
{
Add(item);
}
}

/// <summary>
/// Initializes a new instance of the <see cref="HashSet{T}"/> class that contains elements copied
/// from the specified collection and uses the default equality comparer.
/// </summary>
/// <param name="collection">The collection whose elements are copied to the new set.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="collection"/> is null.</exception>
public HashSet(IEnumerable<T> collection)
: this(collection, EqualityComparer<T>.Default)
{
}

/// <inheritdoc />
public int Count => _dict.Count;

/// <inheritdoc />
public bool IsReadOnly => false;

/// <summary>
/// Attempts to add the specified element to the set.
/// </summary>
/// <param name="item">The element to add.</param>
/// <returns><c>true</c> if the element was added; <c>false</c> if it already exists in the set.</returns>
public bool Add(T item) => _dict.TryAdd(item, true);

void ICollection<T>.Add(T item) => Add(item);

/// <summary>
/// Removes all elements from the set.
/// </summary>
public void Clear() => _dict.Clear();

/// <summary>
/// Determines whether the set contains a specific element.
/// </summary>
/// <param name="item">The element to locate.</param>
/// <returns><c>true</c> if the set contains the element; otherwise, <c>false</c>.</returns>
public bool Contains(T item) => _dict.ContainsKey(item);

/// <summary>
/// Copies the elements of the set to an array, starting at a particular array index.
/// </summary>
/// <param name="array">The destination array.</param>
/// <param name="arrayIndex">The zero-based index in the array at which copying begins.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="array"/> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="arrayIndex"/> is less than 0.</exception>
/// <exception cref="ArgumentException">Thrown if the number of elements in the source set is greater than the available space in the destination array.</exception>
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null) throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex));
if (array.Length - arrayIndex < Count) throw new ArgumentException("The array is too small.");

foreach (var key in _dict.Keys)
{
array[arrayIndex++] = key;
}
}

/// <summary>
/// Removes the specified element from the set.
/// </summary>
/// <param name="item">The element to remove.</param>
/// <returns><c>true</c> if the element was successfully removed; <c>false</c> if it was not found.</returns>
public bool Remove(T item) => _dict.Remove(item);

/// <summary>
/// Returns an enumerator that iterates through the set in insertion order.
/// </summary>
/// <returns>An enumerator for the set.</returns>
public IEnumerator<T> GetEnumerator() => _dict.Keys.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Interfaces/IMetadataContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public interface IMetadataContainer
/// A collection of properties associated with the current OpenAPI element to be used by the application.
/// Metadata are NOT (de)serialized with the schema and can be used for custom properties.
/// </summary>
Dictionary<string, object>? Metadata { get; set; }
OrderedDictionary<string, object>? Metadata { get; set; }
}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ public interface IOpenApiExtensible : IOpenApiElement
/// <summary>
/// Specification extensions.
/// </summary>
Dictionary<string, IOpenApiExtension>? Extensions { get; set; }
OrderedDictionary<string, IOpenApiExtension>? Extensions { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public interface IOpenApiReadOnlyExtensible
/// <summary>
/// Specification extensions.
/// </summary>
Dictionary<string, IOpenApiExtension>? Extensions { get; }
OrderedDictionary<string, IOpenApiExtension>? Extensions { get; }

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public interface IOpenApiCallback : IOpenApiReadOnlyExtensible, IShallowCopyable
/// <summary>
/// A Path Item Object used to define a callback request and expected responses.
/// </summary>
public Dictionary<RuntimeExpression, IOpenApiPathItem>? PathItems { get; }
public OrderedDictionary<RuntimeExpression, IOpenApiPathItem>? PathItems { get; }
}
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ public interface IOpenApiHeader : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// <summary>
/// Examples of the media type.
/// </summary>
public Dictionary<string, IOpenApiExample>? Examples { get; }
public OrderedDictionary<string, IOpenApiExample>? Examples { get; }

/// <summary>
/// A map containing the representations for the header.
/// </summary>
public Dictionary<string, OpenApiMediaType>? Content { get; }
public OrderedDictionary<string, OpenApiMediaType>? Content { get; }

}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/Interfaces/IOpenApiLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface IOpenApiLink : IOpenApiDescribedElement, IOpenApiReadOnlyExtens
/// <summary>
/// A map representing parameters to pass to an operation as specified with operationId or identified via operationRef.
/// </summary>
public Dictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get; }
public OrderedDictionary<string, RuntimeExpressionAnyWrapper>? Parameters { get; }

/// <summary>
/// A literal value or {expression} to use as a request body when calling the target operation.
Expand Down
Loading
Loading