Skip to content
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

Improve support for nested postman collection items #40

Merged
merged 1 commit into from
Jan 15, 2025
Merged
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
2 changes: 1 addition & 1 deletion .github/gitversion.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
next-version: 0.7.0
next-version: 0.8.0
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatchTag
assembly-informational-format: '{InformationalVersion}'
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the

> **Notes:**
> - Compatible with Postman Collections v2.1
> - Nested collections get flattened into a single Explore space
> - Root level request get bundled into API folder with same name as collection
> - Nested collections get added to an API folder with naming format (`parent folder - nested folder`)
> - GraphQL collections/requests not supported
> - Environments, Authorization data (not including explicit headers), Pre-request Scripts, Tests are not included in import

Expand Down Expand Up @@ -298,7 +299,6 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the
> - Environments variables are inlined and set within the Explore Space
> - Authorization - only Basic and Bearer Token variants are supported


### Running the `import-pact-file` command

**Command Options**
Expand Down
2 changes: 1 addition & 1 deletion src/Explore.Cli/Explore.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<PackageReference Include="NJsonSchema" Version="11.1.0" />
<PackageReference Include="Spectre.Console" Version="0.47.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.Json;
using Explore.Cli.Models.Explore;
using Explore.Cli.Models.Postman;
using Explore.Cli.Models;

public static class PostmanCollectionMappingHelper
{
Expand Down Expand Up @@ -289,4 +290,91 @@ public static bool IsCollectionVersion2_1(string json)

return false;
}

public static List<Connection> MapPostmanCollectionItemsToExploreConnections(Item collectionItem)
{
var connections = new List<Connection>();

if(collectionItem.Request != null && IsItemRequestModeSupported(collectionItem.Request))
{
connections.Add(MapPostmanCollectionItemToExploreConnection(collectionItem));
}

// if nested item exists, then add it to the connections list if it has request data
if (collectionItem.ItemList != null)
{
foreach(var item in collectionItem.ItemList)
{
if(item.Request != null && IsItemRequestModeSupported(item.Request))
{
connections.Add(MapPostmanCollectionItemToExploreConnection(item));
}
}
}

return connections;
}

public static List<StagedAPI> MapPostmanCollectionToStagedAPI(PostmanCollection postmanCollection, string rootName)
{
var stagedAPIs = new List<StagedAPI>();

stagedAPIs.Add(new StagedAPI()
{
APIName = rootName,
});


if(postmanCollection.Item != null)
{
foreach(var item in postmanCollection.Item)
{

if(item.Request != null && IsItemRequestModeSupported(item.Request))
{
StagedAPI api = new StagedAPI()
{
APIName = item.Name ?? string.Empty,
APIUrl = GetServerUrlFromItemRequest(item.Request),
Connections = MapPostmanCollectionItemsToExploreConnections(item)
};

//if an API with same name already exists, add the connection to the existing API
var existingAPI = stagedAPIs.FirstOrDefault(x => x.APIName == rootName);
if(existingAPI != null)
{
existingAPI.Connections.AddRange(api.Connections);
}
else
{
stagedAPIs.Add(new StagedAPI()
{
APIUrl = api.APIUrl,
APIName = api.APIName,
Connections = api.Connections
});
}
}

// if nested item exists, then add it to the staged API list
if (item.ItemList != null)
{
PostmanCollection tempCollection = new PostmanCollection()
{
Item = item.ItemList,
Info = postmanCollection.Info
};

if (tempCollection.Info != null)
{
tempCollection.Info.Name = item.Name;
}

stagedAPIs.AddRange(MapPostmanCollectionToStagedAPI(tempCollection, $"{rootName} - {item.Name}"));
}
}
}

return stagedAPIs;
}
}
9 changes: 9 additions & 0 deletions src/Explore.Cli/Models/ExploreCliModels.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
using Explore.Cli.Models.Explore;

namespace Explore.Cli.Models;


public partial class SchemaValidationResult {
public bool isValid { get; set; } = false;
public string? Message { get; set; }
}

public partial class StagedAPI {
public string APIName { get; set; } = string.Empty;
public string APIUrl { get; set; } = string.Empty;
public List<Connection> Connections { get; set; } = new List<Connection>();
}
58 changes: 34 additions & 24 deletions src/Explore.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Linq;
using Spectre.Console;
using Explore.Cli.Models.Explore;
using Explore.Cli.Models.Postman;
Expand Down Expand Up @@ -132,7 +133,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string

if (!validationResult.isValid)
{
Console.WriteLine($"The provide json does not conform to the expected schema. Errors: {validationResult.Message}");
Console.WriteLine($"The provided json does not conform to the expected schema. Errors: {validationResult.Message}");
return;
}

Expand All @@ -156,37 +157,46 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string

var cleanedCollectionName = UtilityHelper.CleanString(postmanCollection.Info?.Name);
var createSpacesResult = await exploreHttpClient.CreateSpace(exploreCookie, cleanedCollectionName);

if (createSpacesResult.Result)
{
var apiImportResults = new Table() { Title = new TableTitle(text: $"SPACE [green]{cleanedCollectionName}[/] CREATED"), Width = 75, UseSafeBorder = true };
apiImportResults.AddColumn("Result");
apiImportResults.AddColumn("API Imported");
apiImportResults.AddColumn("Connection Imported");

//Postman Items cant contain nested items, so we can flatten the list
var flattenedItems = PostmanCollectionMappingHelper.FlattenItems(postmanCollection.Item);
var apisToImport = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, cleanedCollectionName);

foreach (var item in flattenedItems)
foreach (var item in apisToImport)
{
if (item.Request != null)
if (item.APIName == null || item.Connections == null)
{
//check if request format is supported
if (!PostmanCollectionMappingHelper.IsItemRequestModeSupported(item.Request))
{
apiImportResults.AddRow("[orange3]skipped[/]", $"Item '{item.Name}' skipped", $"Request method not supported");
continue;
}
apiImportResults.AddRow("[orange3]skipped[/]", $"API '{item.APIName ?? "Unknown"}' skipped", $"No supported request found in collection");
continue;
}

//now let's create an API entry in the space
var cleanedAPIName = UtilityHelper.CleanString(item.Name);
var createApiEntryResult = await exploreHttpClient.CreateApiEntry(exploreCookie, createSpacesResult.Id, cleanedAPIName, "postman", item.Request.Description?.Content);
AnsiConsole.MarkupLine($"Processing API: {item.APIName} with {item.Connections.Count} connections");

if (createApiEntryResult.Result)
if(item.Connections == null || item.Connections.Count == 0)
{
apiImportResults.AddRow("[orange3]skipped[/]", $"API '{item.APIName}' skipped", $"No supported request found in collection");
continue;
}

//now let's create an API entry in the space
var cleanedAPIName = UtilityHelper.CleanString(item.APIName);
//var description = item.Connections.FirstOrDefault(c => c.Description != null)?.Description?.Content;

var createApiEntryResult = await exploreHttpClient.CreateApiEntry(exploreCookie, createSpacesResult.Id, cleanedAPIName, "postman", null);

if(createApiEntryResult.Result)
{
foreach(var connection in item.Connections)
{
var connectionRequestBody = JsonSerializer.Serialize(PostmanCollectionMappingHelper.MapPostmanCollectionItemToExploreConnection(item));
var connectionRequestBody = JsonSerializer.Serialize(connection);
//now let's do the work and import the connection
var createConnectionResponse = await exploreHttpClient.CreateApiConnection(exploreCookie, createSpacesResult.Id, createApiEntryResult.Id, connectionRequestBody);

if (createConnectionResponse.Result)
{
apiImportResults.AddRow("[green]OK[/]", $"API '{cleanedAPIName}' created", "Connection created");
Expand All @@ -196,13 +206,13 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string
apiImportResults.AddRow("[orange3]OK[/]", $"API '{cleanedAPIName}' created", "[orange3]Connection NOT created[/]");
}
}
else
{
apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {createApiEntryResult.StatusCode}", "");
}
}
}
else
{
apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {createApiEntryResult.StatusCode}", "");
}

}

resultTable.AddRow(new Markup("[green]success[/]"), apiImportResults);

Expand Down Expand Up @@ -255,9 +265,9 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string

//ToDo - deal with scenario of item-groups
}
catch (FileNotFoundException)
catch (FileNotFoundException ex)
{
Console.WriteLine("File not found.");
Console.WriteLine($"File not found: {ex.Message}");
}
catch (Exception ex)
{
Expand Down
41 changes: 41 additions & 0 deletions test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Explore.Cli.Models.Explore;
using Explore.Cli.Models.Postman;
using Explore.Cli.Models;
using System.Text.Json;

public class PostmanCollectionMappingHelperTests
Expand Down Expand Up @@ -121,4 +122,44 @@ public void ProcessesDescriptions()
Assert.Equal("GET", postmanCollection?.Item?[0].ItemList?[0].Request?.Method?.ToString());
Assert.Equal("Gets information about the authenticated user.", postmanCollection?.Item?[0].ItemList?[0].Request?.Description?.Content?.ToString());
}

[Fact]
public void ProcessNestedCollections_ShouldReturnTwoStagedAPIs()
{
// Arrange
var filePath = "../../../fixtures/API_.Payees_API.postman_collection.json";
var mockCollectionAsJson = File.ReadAllText(filePath);
var postmanCollection = JsonSerializer.Deserialize<PostmanCollection>(mockCollectionAsJson);

// Act
List<StagedAPI> result = new List<StagedAPI>();
if (postmanCollection != null)
{
result = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, "Payees API");
}
// Assert
Assert.NotNull(result);
Assert.IsType<List<StagedAPI>>(result);
Assert.Equal(2, result.Count);
}

[Fact]
public void ProcessNestedCollections_ShouldReturnMultipleStagedAPIs()
{
// Arrange
var filePath = "../../../fixtures/API.Payees_API_mixed_urls.postman_collection.json";
var mockCollectionAsJson = File.ReadAllText(filePath);
var postmanCollection = JsonSerializer.Deserialize<PostmanCollection>(mockCollectionAsJson);

// Act
List<StagedAPI> result = new List<StagedAPI>();
if (postmanCollection != null)
{
result = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, "Payees API");
}
// Assert
Assert.NotNull(result);
Assert.IsType<List<StagedAPI>>(result);
Assert.Equal(2, result.Count);
}
}
Loading
Loading