Skip to content

Commit

Permalink
feat: Improve support for nested postman collection items (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
frankkilcommins authored Jan 15, 2025
1 parent 0aba46a commit 78a0dd0
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 28 deletions.
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

0 comments on commit 78a0dd0

Please sign in to comment.