diff --git a/README.md b/README.md index 281e619..618694b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Simple utility CLI for importing data into SwaggerHub Explore. > `import-postman-collection` Import Postman Collection (v2.1) from a file into SwaggerHub Explore > > `import-insomnia-collection` Import Insomnia Collection (v4) from a file into SwaggerHub Explore +> +> `import-pact-file` Import a Pact file (v2/v3/v4) into SwaggerHub Explore (HTTP interactions only) ### Prerequisites You will need the following: @@ -295,6 +297,46 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the > - Authorization - only Basic and Bearer Token variants are supported +### Running the `import-pact-file` command + +**Command Options** +``` + _____ _ ____ _ _ + | ____| __ __ _ __ | | ___ _ __ ___ / ___| | | (_) + | _| \ \/ / | '_ \ | | / _ \ | '__| / _ \ | | | | | | + | |___ > < | |_) | | | | (_) | | | | __/ _ | |___ | | | | + |_____| /_/\_\ | .__/ |_| \___/ |_| \___| (_) \____| |_| |_| + |_| +``` +**Description:** + > Import a Pact file (v2/v3/v4) into SwaggerHub Explore (HTTP interactions only) + +**Usage:** + > Explore.CLI import-pact-file [options] + +**Options:** + > `-ec`, `--explore-cookie` (REQUIRED) A valid and active SwaggerHub Explore session cookie + + > `-fp`, `--file-path` (REQUIRED) The path to the Insomnia collection + + > `-b`, `--base-uri` The base url to use for all imported files + + > `-v`, `--verbose` Include verbose output during processing + + > `-?`, `-h`, `--help` Show help and usage information + +**Note** - the format for SwaggerHub Explore cookies is as follows: `"cookie-name=cookie-value; cookie-name=cookie-value"` + +>Example: `"SESSION=5a0a2e2f-97c6-4405-b72a-299fa8ce07c8; XSRF-TOKEN=3310cb20-2ec1-4655-b1e3-4ab76a2ac2c8"` + +> **Notes:** +> - Compatible with valid Pact v2 / v3 / v4 specification files +> - Users are advised to provide the base url when importing pact files with `--base-uri` / `-b`, to the required server you wish to explore. +> Pact files do not contain this information +> - Currently only supports HTTP interactions. +> - V3 message based pacts are unsupported +> - V4 interactions other than synchronous/http will be ignored + ## More Information on SwaggerHub Explore - For SwaggerHub Explore info, see - https://swagger.io/tools/swaggerhub-explore/ diff --git a/src/Explore.Cli/InsomniaCollectionMappingHelper.cs b/src/Explore.Cli/MappingHelpers/Insomnia/InsomniaCollectionMappingHelper.cs similarity index 100% rename from src/Explore.Cli/InsomniaCollectionMappingHelper.cs rename to src/Explore.Cli/MappingHelpers/Insomnia/InsomniaCollectionMappingHelper.cs diff --git a/src/Explore.Cli/MappingHelper.cs b/src/Explore.Cli/MappingHelpers/MappingHelper.cs similarity index 100% rename from src/Explore.Cli/MappingHelper.cs rename to src/Explore.Cli/MappingHelpers/MappingHelper.cs diff --git a/src/Explore.Cli/MappingHelpers/Pact/PactMappingHelper.cs b/src/Explore.Cli/MappingHelpers/Pact/PactMappingHelper.cs new file mode 100644 index 0000000..2c15499 --- /dev/null +++ b/src/Explore.Cli/MappingHelpers/Pact/PactMappingHelper.cs @@ -0,0 +1,438 @@ +using System.Text.Json; +using Explore.Cli.Models.Explore; + +public static class PactMappingHelper +{ + + public static bool hasPactVersion(string json) + { + var jsonObject = JsonSerializer.Deserialize>(json); + + if (jsonObject != null && jsonObject.ContainsKey("metadata")) + { + var metadata = JsonSerializer.Deserialize>(jsonObject["metadata"].ToString() ?? string.Empty); + if (metadata != null && metadata.ContainsKey("pactSpecification")) + { + var pactSpecification = JsonSerializer.Deserialize>(metadata["pactSpecification"].ToString() ?? string.Empty); + if (pactSpecification != null && pactSpecification["version"] != null && pactSpecification["version"].ToString() != null) + { + var version = pactSpecification["version"].ToString(); + var validVersions = new List { "1.0.0", "2.0.0", "3.0.0", "4.0.0", "4.0" }; + if (version != null && validVersions.Contains(version)) + { + return true; + } + } + } + } + + return false; + } + + public static string getPactVersion(string json) + { + var jsonObject = JsonSerializer.Deserialize>(json); + + if (jsonObject != null && jsonObject.ContainsKey("metadata")) + { + var metadata = JsonSerializer.Deserialize>(jsonObject["metadata"].ToString() ?? string.Empty); + if (metadata != null && metadata.ContainsKey("pactSpecification")) + { + var pactSpecification = JsonSerializer.Deserialize>(metadata["pactSpecification"].ToString() ?? string.Empty); + if (pactSpecification != null && pactSpecification["version"] != null && pactSpecification["version"].ToString() != null) + { + var version = pactSpecification["version"].ToString(); + var validVersions = new List { "1.0.0", "2.0.0", "3.0.0", "4.0.0", "4.0" }; + if (version != null && validVersions.Contains(version)) + { + var formattedVersion = version.Split('.')[0]; + return $"pact-v{formattedVersion}"; + } + } + } + } + + return string.Empty; + } + public static Dictionary CreatePactPathsDictionary(object request) + { + if (request is PactV4.Request v4Request) + { + if (v4Request?.Path != null) + { + var pathsContent = new PathsContent() + { + Parameters = MapHeaderAndQueryParams(request) + }; + + // //add request body + if (v4Request.Body.Content != null) + { + var examplesJson = new Dictionary + { + { "examples", MapEntryBodyToContentExamples(v4Request.Body.Content.ToString()) } + }; + + var contentJson = new Dictionary + { + { "*/*", examplesJson } + }; + + pathsContent.RequestBody = new RequestBody() + { + Content = contentJson + }; + } + + + // add header and query params + if (v4Request.Method != null) + { + var methodJson = new Dictionary + { + { v4Request.Method?.ToString()?.Replace("Method", string.Empty) ?? string.Empty, pathsContent } + }; + + var json = new Dictionary + { + { v4Request.Path, methodJson } + }; + + return json; + } + } + + return new Dictionary(); + } + else if (request is PactV3.Request v3Request) + { + if (v3Request?.Path != null) + { + var pathsContent = new PathsContent() + { + Parameters = MapHeaderAndQueryParams(request) + }; + + // //add request body + if (v3Request.Body != null) + { + var examplesJson = new Dictionary + { + { "examples", MapEntryBodyToContentExamples(v3Request.Body.ToString()) } + }; + + var contentJson = new Dictionary + { + { "*/*", examplesJson } + }; + + pathsContent.RequestBody = new RequestBody() + { + Content = contentJson + }; + } + + + // add header and query params + if (v3Request.Method != null) + { + var methodJson = new Dictionary + { + { v3Request.Method?.ToString()?.Replace("Method", string.Empty) ?? string.Empty, pathsContent } + }; + + var json = new Dictionary + { + { v3Request.Path, methodJson } + }; + + return json; + } + } + + return new Dictionary(); + } + else if (request is PactV2.Request v2Request) + { + if (v2Request?.Path != null) + { + var pathsContent = new PathsContent() + { + Parameters = MapHeaderAndQueryParams(request) + }; + + // //add request body + if (v2Request.Body != null) + { + var examplesJson = new Dictionary + { + { "examples", MapEntryBodyToContentExamples(v2Request.Body.ToString()) } + }; + + var contentJson = new Dictionary + { + { "*/*", examplesJson } + }; + + pathsContent.RequestBody = new RequestBody() + { + Content = contentJson + }; + } + + + // add header and query params + if (v2Request.Method != null) + { + var methodJson = new Dictionary + { + { v2Request.Method?.ToString()?.Replace("Method", string.Empty) ?? string.Empty, pathsContent } + }; + + var json = new Dictionary + { + { v2Request.Path, methodJson } + }; + + return json; + } + } + + return new Dictionary(); + } + else + { + return new Dictionary(); + } + } + + public static Examples MapEntryBodyToContentExamples(string? rawBody) + { + return new Examples() + { + Example = new Example() + { + Value = rawBody + } + }; + } + + public static List MapHeaderAndQueryParams(object request) + { + List parameters = new List(); + + if (request is PactV2.Request v2Request) + { + if (v2Request.Headers != null && v2Request.Headers.Any()) + { + // map the headers + foreach (var hdr in v2Request.Headers) + { + parameters.Add(new Parameter() + { + In = "header", + Name = hdr.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = hdr.Value.ToString() + } + } + }); + } + } + + if (v2Request.Query != null) + { + foreach (var param in v2Request.Query.Split("&")) + { + parameters.Add(new Parameter() + { + In = "query", + Name = param.Split("=")[0], + Examples = new Examples() + { + Example = new Example() + { + Value = param.Split("=")[1] + } + } + }); + } + } + } + else if (request is PactV3.Request v3Request) + { + if (v3Request.Headers != null && v3Request.Headers.Any()) + { + // map the headers + foreach (var hdr in v3Request.Headers) + { + parameters.Add(new Parameter() + { + In = "header", + Name = hdr.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = hdr.Value.ToString() + } + } + }); + } + } + + if (v3Request.Query != null) + { + foreach (var param in v3Request.Query) + { + if (param.Value.Length > 1) + { + foreach (var value in param.Value) + { + parameters.Add(new Parameter() + { + In = "query", + Name = $"{param.Key}[]", + Examples = new Examples() + { + Example = new Example() + { + Value = value.ToString() + } + } + }); + } + } + else + { + parameters.Add(new Parameter() + { + In = "query", + Name = param.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = param.Value.First().ToString() + } + } + }); + } + } + } + } + else if (request is PactV4.Request v4Request) + { + if (v4Request.Headers != null && v4Request.Headers.Any()) + { + // map the headers + foreach (var hdr in v4Request.Headers) + { + parameters.Add(new Parameter() + { + In = "header", + Name = hdr.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = string.Join(",", hdr.Value.Select(x => x.ToString())) + } + } + }); + } + } + + if (v4Request.Query != null) + { + foreach (var param in v4Request.Query) + { + if (param.Value.Length > 1) + { + foreach (var value in param.Value) + { + parameters.Add(new Parameter() + { + In = "query", + Name = $"{param.Key}[]", + Examples = new Examples() + { + Example = new Example() + { + Value = value.ToString() + } + } + }); + } + } + else + { + parameters.Add(new Parameter() + { + In = "query", + Name = param.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = param.Value.First().ToString() + } + } + }); + } + } + } + } + + return parameters; + } + + + public static Connection MapPactInteractionToExploreConnection(object pactInteraction, string url = "") + { + var connection = new Connection() + { + Type = "ConnectionRequest", + Name = "REST", + Schema = "OpenAPI", + SchemaVersion = "3.0.1", + ConnectionDefinition = new ConnectionDefinition() + { + Servers = new List() + { + new Server() + { + Url = url + } + }, + }, + Settings = new Settings() + { + Type = "RestConnectionSettings", + ConnectTimeout = 30, + FollowRedirects = true, + EncodeUrl = true + }, + }; + + if (pactInteraction is PactV2.Interaction v2Interaction) + { + connection.ConnectionDefinition.Paths = CreatePactPathsDictionary(v2Interaction.Request); + } + else if (pactInteraction is PactV3.Interaction v3Interaction) + { + connection.ConnectionDefinition.Paths = CreatePactPathsDictionary(v3Interaction.Request); + } + else if (pactInteraction is PactV4.Interaction v4Interaction) + { + connection.ConnectionDefinition.Paths = CreatePactPathsDictionary(v4Interaction.Request); + } + + return connection; + } + +} + diff --git a/src/Explore.Cli/PostmanCollectionMappingHelper.cs b/src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs similarity index 100% rename from src/Explore.Cli/PostmanCollectionMappingHelper.cs rename to src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs diff --git a/src/Explore.Cli/Models/Pact/PactV1Contract.cs b/src/Explore.Cli/Models/Pact/PactV1Contract.cs new file mode 100644 index 0000000..eac048e --- /dev/null +++ b/src/Explore.Cli/Models/Pact/PactV1Contract.cs @@ -0,0 +1,381 @@ +// +// +// To parse this JSON data, add NuGet 'System.Text.Json' then do: +// +// using PactV1; +// +// var contract = Contract.FromJson(jsonString); +#nullable enable +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace PactV1 +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + /// + /// Schema for a Pact file + /// + public partial class Contract + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("consumer")] + public Pacticipant Consumer { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interactions")] + public List Interactions { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider")] + public Pacticipant Provider { get; set; } + } + + public partial class Pacticipant + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public partial class Interaction + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider_state")] + public string InteractionProviderState { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerState")] + public string ProviderState { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request")] + public Request Request { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("response")] + public Response Response { get; set; } + } + + public partial class Request + { + [JsonPropertyName("body")] + public object Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("method")] + public Method? Method { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("query")] + public string Query { get; set; } + } + + public partial class Response + { + [JsonPropertyName("body")] + public object Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public long? Status { get; set; } + } + + public partial class Metadata + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pact-specification")] + public PactSpecification PactSpecification { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pactSpecification")] + public PactSpecificationClass MetadataPactSpecification { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pactSpecificationVersion")] + public string PactSpecificationVersion { get; set; } + } + + public partial class PactSpecificationClass + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public partial class PactSpecification + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public enum Method { Connect, Delete, Get, Head, MethodConnect, MethodDelete, MethodGet, MethodHead, MethodOptions, MethodPost, MethodPut, MethodTrace, Options, Post, Put, Trace }; + + public partial class Contract + { + public static Contract FromJson(string json) => JsonSerializer.Deserialize(json, PactV1.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Contract self) => JsonSerializer.Serialize(self, PactV1.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + MethodConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; + } + + internal class MethodConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Method); + + public override Method Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "CONNECT": + return Method.MethodConnect; + case "DELETE": + return Method.MethodDelete; + case "GET": + return Method.MethodGet; + case "HEAD": + return Method.MethodHead; + case "OPTIONS": + return Method.MethodOptions; + case "POST": + return Method.MethodPost; + case "PUT": + return Method.MethodPut; + case "TRACE": + return Method.MethodTrace; + case "connect": + return Method.Connect; + case "delete": + return Method.Delete; + case "get": + return Method.Get; + case "head": + return Method.Head; + case "options": + return Method.Options; + case "post": + return Method.Post; + case "put": + return Method.Put; + case "trace": + return Method.Trace; + } + throw new Exception("Cannot unmarshal type Method"); + } + + public override void Write(Utf8JsonWriter writer, Method value, JsonSerializerOptions options) + { + switch (value) + { + case Method.MethodConnect: + JsonSerializer.Serialize(writer, "CONNECT", options); + return; + case Method.MethodDelete: + JsonSerializer.Serialize(writer, "DELETE", options); + return; + case Method.MethodGet: + JsonSerializer.Serialize(writer, "GET", options); + return; + case Method.MethodHead: + JsonSerializer.Serialize(writer, "HEAD", options); + return; + case Method.MethodOptions: + JsonSerializer.Serialize(writer, "OPTIONS", options); + return; + case Method.MethodPost: + JsonSerializer.Serialize(writer, "POST", options); + return; + case Method.MethodPut: + JsonSerializer.Serialize(writer, "PUT", options); + return; + case Method.MethodTrace: + JsonSerializer.Serialize(writer, "TRACE", options); + return; + case Method.Connect: + JsonSerializer.Serialize(writer, "connect", options); + return; + case Method.Delete: + JsonSerializer.Serialize(writer, "delete", options); + return; + case Method.Get: + JsonSerializer.Serialize(writer, "get", options); + return; + case Method.Head: + JsonSerializer.Serialize(writer, "head", options); + return; + case Method.Options: + JsonSerializer.Serialize(writer, "options", options); + return; + case Method.Post: + JsonSerializer.Serialize(writer, "post", options); + return; + case Method.Put: + JsonSerializer.Serialize(writer, "put", options); + return; + case Method.Trace: + JsonSerializer.Serialize(writer, "trace", options); + return; + } + throw new Exception("Cannot marshal type Method"); + } + + public static readonly MethodConverter Singleton = new MethodConverter(); + } + + public class DateOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + public class TimeOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + internal class IsoDateTimeOffsetConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); + } +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 diff --git a/src/Explore.Cli/Models/Pact/PactV2Contract.cs b/src/Explore.Cli/Models/Pact/PactV2Contract.cs new file mode 100644 index 0000000..a76c8b9 --- /dev/null +++ b/src/Explore.Cli/Models/Pact/PactV2Contract.cs @@ -0,0 +1,377 @@ +// +// +// To parse this JSON data, add NuGet 'System.Text.Json' then do: +// +// using PactV2; +// +// var contract = Contract.FromJson(jsonString); +#nullable enable +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace PactV2 +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + /// + /// Schema for a Pact file + /// + public partial class Contract + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("consumer")] + public Pacticipant Consumer { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interactions")] + public List Interactions { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider")] + public Pacticipant Provider { get; set; } + } + + public partial class Pacticipant + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public partial class Interaction + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerState")] + public string ProviderState { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request")] + public Request Request { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("response")] + public Response Response { get; set; } + } + + public partial class Request + { + [JsonPropertyName("body")] + public object Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("method")] + public Method? Method { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("query")] + public string Query { get; set; } + } + + public partial class Response + { + [JsonPropertyName("body")] + public object Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public long? Status { get; set; } + } + + public partial class Metadata + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pact-specification")] + public PactSpecification PactSpecification { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pactSpecification")] + public PactSpecificationClass MetadataPactSpecification { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pactSpecificationVersion")] + public string PactSpecificationVersion { get; set; } + } + + public partial class PactSpecificationClass + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public partial class PactSpecification + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public enum Method { Connect, Delete, Get, Head, MethodConnect, MethodDelete, MethodGet, MethodHead, MethodOptions, MethodPost, MethodPut, MethodTrace, Options, Post, Put, Trace }; + + public partial class Contract + { + public static Contract FromJson(string json) => JsonSerializer.Deserialize(json, PactV2.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Contract self) => JsonSerializer.Serialize(self, PactV2.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + MethodConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; + } + + internal class MethodConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Method); + + public override Method Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "CONNECT": + return Method.MethodConnect; + case "DELETE": + return Method.MethodDelete; + case "GET": + return Method.MethodGet; + case "HEAD": + return Method.MethodHead; + case "OPTIONS": + return Method.MethodOptions; + case "POST": + return Method.MethodPost; + case "PUT": + return Method.MethodPut; + case "TRACE": + return Method.MethodTrace; + case "connect": + return Method.Connect; + case "delete": + return Method.Delete; + case "get": + return Method.Get; + case "head": + return Method.Head; + case "options": + return Method.Options; + case "post": + return Method.Post; + case "put": + return Method.Put; + case "trace": + return Method.Trace; + } + throw new Exception("Cannot unmarshal type Method"); + } + + public override void Write(Utf8JsonWriter writer, Method value, JsonSerializerOptions options) + { + switch (value) + { + case Method.MethodConnect: + JsonSerializer.Serialize(writer, "CONNECT", options); + return; + case Method.MethodDelete: + JsonSerializer.Serialize(writer, "DELETE", options); + return; + case Method.MethodGet: + JsonSerializer.Serialize(writer, "GET", options); + return; + case Method.MethodHead: + JsonSerializer.Serialize(writer, "HEAD", options); + return; + case Method.MethodOptions: + JsonSerializer.Serialize(writer, "OPTIONS", options); + return; + case Method.MethodPost: + JsonSerializer.Serialize(writer, "POST", options); + return; + case Method.MethodPut: + JsonSerializer.Serialize(writer, "PUT", options); + return; + case Method.MethodTrace: + JsonSerializer.Serialize(writer, "TRACE", options); + return; + case Method.Connect: + JsonSerializer.Serialize(writer, "connect", options); + return; + case Method.Delete: + JsonSerializer.Serialize(writer, "delete", options); + return; + case Method.Get: + JsonSerializer.Serialize(writer, "get", options); + return; + case Method.Head: + JsonSerializer.Serialize(writer, "head", options); + return; + case Method.Options: + JsonSerializer.Serialize(writer, "options", options); + return; + case Method.Post: + JsonSerializer.Serialize(writer, "post", options); + return; + case Method.Put: + JsonSerializer.Serialize(writer, "put", options); + return; + case Method.Trace: + JsonSerializer.Serialize(writer, "trace", options); + return; + } + throw new Exception("Cannot marshal type Method"); + } + + public static readonly MethodConverter Singleton = new MethodConverter(); + } + + public class DateOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + public class TimeOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + internal class IsoDateTimeOffsetConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); + } +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 diff --git a/src/Explore.Cli/Models/Pact/PactV3Contract.cs b/src/Explore.Cli/Models/Pact/PactV3Contract.cs new file mode 100644 index 0000000..8c5c31d --- /dev/null +++ b/src/Explore.Cli/Models/Pact/PactV3Contract.cs @@ -0,0 +1,671 @@ +// +// +// To parse this JSON data, add NuGet 'System.Text.Json' then do: +// +// using PactV3; +// +// var contract = Contract.FromJson(jsonString); +#nullable enable +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace PactV3 +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + /// + /// Schema for a Pact file + /// + public partial class Contract + { + + [JsonPropertyName("consumer")] + public Pacticipant Consumer { get; set; } + + + [JsonPropertyName("interactions")] + public List Interactions { get; set; } + + + [JsonPropertyName("messages")] + public List Messages { get; set; } + + + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + + [JsonPropertyName("provider")] + public Pacticipant Provider { get; set; } + } + + public partial class Pacticipant + { + + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public partial class Interaction + { + + [JsonPropertyName("description")] + public string Description { get; set; } + + + [JsonPropertyName("providerStates")] + public ProviderStates? ProviderStates { get; set; } + + + [JsonPropertyName("request")] + public Request Request { get; set; } + + + [JsonPropertyName("response")] + public Response Response { get; set; } + } + + public partial class ProviderState + { + + [JsonPropertyName("name")] + public string Name { get; set; } + + + [JsonPropertyName("params")] + public Dictionary Params { get; set; } + } + + public partial class Request + { + [JsonPropertyName("body")] + public object Body { get; set; } + + + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + + [JsonPropertyName("method")] + public Method? Method { get; set; } + + + [JsonPropertyName("path")] + public string Path { get; set; } + + + [JsonPropertyName("query")] + public Dictionary Query { get; set; } + } + + public partial class Response + { + [JsonPropertyName("body")] + public object Body { get; set; } + + + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + + [JsonPropertyName("status")] + public long? Status { get; set; } + } + + public partial class Message + { + [JsonPropertyName("contents")] + public object Contents { get; set; } + + + [JsonPropertyName("description")] + public string Description { get; set; } + + + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + + [JsonPropertyName("metaData")] + public Dictionary MetaData { get; set; } + + + [JsonPropertyName("providerState")] + public string ProviderState { get; set; } + } + + public partial class Metadata + { + + [JsonPropertyName("pact-specification")] + public PactSpecification PactSpecification { get; set; } + + + [JsonPropertyName("pactSpecification")] + public PactSpecificationClass MetadataPactSpecification { get; set; } + + + [JsonPropertyName("pactSpecificationVersion")] + public string PactSpecificationVersion { get; set; } + } + + public partial class PactSpecificationClass + { + + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public partial class PactSpecification + { + + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public enum TypeEnum { Date, DateTime, RandomBoolean, RandomDecimal, RandomHexadecimal, RandomInt, RandomString, Regex, Time, Uuid }; + + public enum Combine { And, Or }; + + public enum MatchEnum { Boolean, ContentType, Date, Datetime, Decimal, Equality, Include, Integer, Null, Number, Regex, Time, Type, Values }; + + public enum Method { Connect, Delete, Get, Head, MethodConnect, MethodDelete, MethodGet, MethodHead, MethodOptions, MethodPost, MethodPut, MethodTrace, Options, Post, Put, Trace }; + + public partial struct ProviderStates + { + public List ProviderStateArray; + public string String; + + public static implicit operator ProviderStates(List ProviderStateArray) => new ProviderStates { ProviderStateArray = ProviderStateArray }; + public static implicit operator ProviderStates(string String) => new ProviderStates { String = String }; + } + + public partial class Contract + { + public static Contract FromJson(string json) => JsonSerializer.Deserialize(json, PactV3.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Contract self) => JsonSerializer.Serialize(self, PactV3.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + ProviderStatesConverter.Singleton, + TypeEnumConverter.Singleton, + CombineConverter.Singleton, + MatchEnumConverter.Singleton, + MethodConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; + } + + internal class ProviderStatesConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ProviderStates); + + public override ProviderStates Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + var stringValue = reader.GetString(); + return new ProviderStates { String = stringValue }; + case JsonTokenType.StartArray: + var arrayValue = JsonSerializer.Deserialize>(ref reader, options); + return new ProviderStates { ProviderStateArray = arrayValue }; + } + throw new Exception("Cannot unmarshal type ProviderStates"); + } + + public override void Write(Utf8JsonWriter writer, ProviderStates value, JsonSerializerOptions options) + { + if (value.String != null) + { + JsonSerializer.Serialize(writer, value.String, options); + return; + } + if (value.ProviderStateArray != null) + { + JsonSerializer.Serialize(writer, value.ProviderStateArray, options); + return; + } + throw new Exception("Cannot marshal type ProviderStates"); + } + + public static readonly ProviderStatesConverter Singleton = new ProviderStatesConverter(); + } + + internal class TypeEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(TypeEnum); + + public override TypeEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "Date": + return TypeEnum.Date; + case "DateTime": + return TypeEnum.DateTime; + case "RandomBoolean": + return TypeEnum.RandomBoolean; + case "RandomDecimal": + return TypeEnum.RandomDecimal; + case "RandomHexadecimal": + return TypeEnum.RandomHexadecimal; + case "RandomInt": + return TypeEnum.RandomInt; + case "RandomString": + return TypeEnum.RandomString; + case "Regex": + return TypeEnum.Regex; + case "Time": + return TypeEnum.Time; + case "Uuid": + return TypeEnum.Uuid; + } + throw new Exception("Cannot unmarshal type TypeEnum"); + } + + public override void Write(Utf8JsonWriter writer, TypeEnum value, JsonSerializerOptions options) + { + switch (value) + { + case TypeEnum.Date: + JsonSerializer.Serialize(writer, "Date", options); + return; + case TypeEnum.DateTime: + JsonSerializer.Serialize(writer, "DateTime", options); + return; + case TypeEnum.RandomBoolean: + JsonSerializer.Serialize(writer, "RandomBoolean", options); + return; + case TypeEnum.RandomDecimal: + JsonSerializer.Serialize(writer, "RandomDecimal", options); + return; + case TypeEnum.RandomHexadecimal: + JsonSerializer.Serialize(writer, "RandomHexadecimal", options); + return; + case TypeEnum.RandomInt: + JsonSerializer.Serialize(writer, "RandomInt", options); + return; + case TypeEnum.RandomString: + JsonSerializer.Serialize(writer, "RandomString", options); + return; + case TypeEnum.Regex: + JsonSerializer.Serialize(writer, "Regex", options); + return; + case TypeEnum.Time: + JsonSerializer.Serialize(writer, "Time", options); + return; + case TypeEnum.Uuid: + JsonSerializer.Serialize(writer, "Uuid", options); + return; + } + throw new Exception("Cannot marshal type TypeEnum"); + } + + public static readonly TypeEnumConverter Singleton = new TypeEnumConverter(); + } + + internal class CombineConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Combine); + + public override Combine Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "AND": + return Combine.And; + case "OR": + return Combine.Or; + } + throw new Exception("Cannot unmarshal type Combine"); + } + + public override void Write(Utf8JsonWriter writer, Combine value, JsonSerializerOptions options) + { + switch (value) + { + case Combine.And: + JsonSerializer.Serialize(writer, "AND", options); + return; + case Combine.Or: + JsonSerializer.Serialize(writer, "OR", options); + return; + } + throw new Exception("Cannot marshal type Combine"); + } + + public static readonly CombineConverter Singleton = new CombineConverter(); + } + + internal class MatchEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(MatchEnum); + + public override MatchEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "boolean": + return MatchEnum.Boolean; + case "contentType": + return MatchEnum.ContentType; + case "date": + return MatchEnum.Date; + case "datetime": + return MatchEnum.Datetime; + case "decimal": + return MatchEnum.Decimal; + case "equality": + return MatchEnum.Equality; + case "include": + return MatchEnum.Include; + case "integer": + return MatchEnum.Integer; + case "null": + return MatchEnum.Null; + case "number": + return MatchEnum.Number; + case "regex": + return MatchEnum.Regex; + case "time": + return MatchEnum.Time; + case "type": + return MatchEnum.Type; + case "values": + return MatchEnum.Values; + } + throw new Exception("Cannot unmarshal type MatchEnum"); + } + + public override void Write(Utf8JsonWriter writer, MatchEnum value, JsonSerializerOptions options) + { + switch (value) + { + case MatchEnum.Boolean: + JsonSerializer.Serialize(writer, "boolean", options); + return; + case MatchEnum.ContentType: + JsonSerializer.Serialize(writer, "contentType", options); + return; + case MatchEnum.Date: + JsonSerializer.Serialize(writer, "date", options); + return; + case MatchEnum.Datetime: + JsonSerializer.Serialize(writer, "datetime", options); + return; + case MatchEnum.Decimal: + JsonSerializer.Serialize(writer, "decimal", options); + return; + case MatchEnum.Equality: + JsonSerializer.Serialize(writer, "equality", options); + return; + case MatchEnum.Include: + JsonSerializer.Serialize(writer, "include", options); + return; + case MatchEnum.Integer: + JsonSerializer.Serialize(writer, "integer", options); + return; + case MatchEnum.Null: + JsonSerializer.Serialize(writer, "null", options); + return; + case MatchEnum.Number: + JsonSerializer.Serialize(writer, "number", options); + return; + case MatchEnum.Regex: + JsonSerializer.Serialize(writer, "regex", options); + return; + case MatchEnum.Time: + JsonSerializer.Serialize(writer, "time", options); + return; + case MatchEnum.Type: + JsonSerializer.Serialize(writer, "type", options); + return; + case MatchEnum.Values: + JsonSerializer.Serialize(writer, "values", options); + return; + } + throw new Exception("Cannot marshal type MatchEnum"); + } + + public static readonly MatchEnumConverter Singleton = new MatchEnumConverter(); + } + + internal class MethodConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Method); + + public override Method Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "CONNECT": + return Method.MethodConnect; + case "DELETE": + return Method.MethodDelete; + case "GET": + return Method.MethodGet; + case "HEAD": + return Method.MethodHead; + case "OPTIONS": + return Method.MethodOptions; + case "POST": + return Method.MethodPost; + case "PUT": + return Method.MethodPut; + case "TRACE": + return Method.MethodTrace; + case "connect": + return Method.Connect; + case "delete": + return Method.Delete; + case "get": + return Method.Get; + case "head": + return Method.Head; + case "options": + return Method.Options; + case "post": + return Method.Post; + case "put": + return Method.Put; + case "trace": + return Method.Trace; + } + throw new Exception("Cannot unmarshal type Method"); + } + + public override void Write(Utf8JsonWriter writer, Method value, JsonSerializerOptions options) + { + switch (value) + { + case Method.MethodConnect: + JsonSerializer.Serialize(writer, "CONNECT", options); + return; + case Method.MethodDelete: + JsonSerializer.Serialize(writer, "DELETE", options); + return; + case Method.MethodGet: + JsonSerializer.Serialize(writer, "GET", options); + return; + case Method.MethodHead: + JsonSerializer.Serialize(writer, "HEAD", options); + return; + case Method.MethodOptions: + JsonSerializer.Serialize(writer, "OPTIONS", options); + return; + case Method.MethodPost: + JsonSerializer.Serialize(writer, "POST", options); + return; + case Method.MethodPut: + JsonSerializer.Serialize(writer, "PUT", options); + return; + case Method.MethodTrace: + JsonSerializer.Serialize(writer, "TRACE", options); + return; + case Method.Connect: + JsonSerializer.Serialize(writer, "connect", options); + return; + case Method.Delete: + JsonSerializer.Serialize(writer, "delete", options); + return; + case Method.Get: + JsonSerializer.Serialize(writer, "get", options); + return; + case Method.Head: + JsonSerializer.Serialize(writer, "head", options); + return; + case Method.Options: + JsonSerializer.Serialize(writer, "options", options); + return; + case Method.Post: + JsonSerializer.Serialize(writer, "post", options); + return; + case Method.Put: + JsonSerializer.Serialize(writer, "put", options); + return; + case Method.Trace: + JsonSerializer.Serialize(writer, "trace", options); + return; + } + throw new Exception("Cannot marshal type Method"); + } + + public static readonly MethodConverter Singleton = new MethodConverter(); + } + + public class DateOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + public class TimeOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + internal class IsoDateTimeOffsetConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); + } +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 diff --git a/src/Explore.Cli/Models/Pact/PactV4Contract.cs b/src/Explore.Cli/Models/Pact/PactV4Contract.cs new file mode 100644 index 0000000..42d764d --- /dev/null +++ b/src/Explore.Cli/Models/Pact/PactV4Contract.cs @@ -0,0 +1,982 @@ +// +// +// To parse this JSON data, add NuGet 'System.Text.Json' then do: +// +// using PactV4; +// +// var contract = Contract.FromJson(jsonString); +#nullable enable +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace PactV4 +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + /// + /// Schema for a Pact file + /// + public partial class Contract + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("consumer")] + public Pacticipant Consumer { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interactions")] + public List Interactions { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("provider")] + public Pacticipant Provider { get; set; } + } + + public partial class Pacticipant + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public partial class Interaction + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("comments")] + public Comments Comments { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interactionMarkup")] + public InteractionMarkup InteractionMarkup { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pending")] + public bool? Pending { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pluginConfiguration")] + public Dictionary PluginConfiguration { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerStates")] + public ProviderStates? ProviderStates { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request")] + public Request Request { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("response")] + public ResponseUnion? Response { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + public InteractionType? Type { get; set; } + + [JsonPropertyName("contents")] + public object Contents { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metaData")] + public Dictionary MetaData { get; set; } + } + + public partial class Comments + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("testname")] + public string Testname { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("text")] + public List Text { get; set; } + } + + public partial class InteractionMarkup + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("markup")] + public string Markup { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("markupType")] + public MarkupType? MarkupType { get; set; } + } + + public partial class ProviderState + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("params")] + public Dictionary Params { get; set; } + } + + public partial class Request + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("body")] + public Body Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("method")] + public Method? Method { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("query")] + public Dictionary Query { get; set; } + + [JsonPropertyName("contents")] + public object Contents { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metaData")] + public Dictionary MetaData { get; set; } + } + + public partial class Body + { + [JsonPropertyName("content")] + public object Content { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contentType")] + public string ContentType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contentTypeHint")] + public ContentTypeHint? ContentTypeHint { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("encoded")] + public Encoded? Encoded { get; set; } + } + + public partial class MessageContents + { + [JsonPropertyName("contents")] + public object Contents { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metaData")] + public Dictionary MetaData { get; set; } + } + + + public partial class ResponseClass + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("body")] + public Body Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public long? Status { get; set; } + } + + public partial class Metadata + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pactSpecification")] + public PactSpecification PactSpecification { get; set; } + } + + public partial class PactSpecification + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string Version { get; set; } + } + + public enum MarkupType { CommonMark, Html }; + + public enum ContentTypeHint { Binary, Text }; + + public enum GeneratorType { Date, DateTime, MockServerUrl, ProviderState, RandomBoolean, RandomDecimal, RandomHexadecimal, RandomInt, RandomString, Regex, Time, Uuid }; + + public enum Combine { And, Or }; + + public enum MatchEnum { ArrayContains, Boolean, ContentType, Date, Datetime, Decimal, EachKey, EachValue, Equality, Include, Integer, NotEmpty, Null, Number, Regex, Semver, StatusCode, Time, Type, Values }; + + public enum Method { Connect, Delete, Get, Head, MethodConnect, MethodDelete, MethodGet, MethodHead, MethodOptions, MethodPost, MethodPut, MethodTrace, Options, Post, Put, Trace }; + + public enum InteractionType { AsynchronousMessages, SynchronousHttp, SynchronousMessages }; + + public partial struct ProviderStates + { + public List ProviderStateArray; + public string String; + + public static implicit operator ProviderStates(List ProviderStateArray) => new ProviderStates { ProviderStateArray = ProviderStateArray }; + public static implicit operator ProviderStates(string String) => new ProviderStates { String = String }; + } + + public partial struct Encoded + { + public bool? Bool; + public string String; + + public static implicit operator Encoded(bool Bool) => new Encoded { Bool = Bool }; + public static implicit operator Encoded(string String) => new Encoded { String = String }; + } + + public partial struct ResponseUnion + { + public List MessageContentsArray; + public ResponseClass ResponseClass; + + public static implicit operator ResponseUnion(List MessageContentsArray) => new ResponseUnion { MessageContentsArray = MessageContentsArray }; + public static implicit operator ResponseUnion(ResponseClass ResponseClass) => new ResponseUnion { ResponseClass = ResponseClass }; + } + + public partial class Contract + { + public static Contract FromJson(string json) => JsonSerializer.Deserialize(json, PactV4.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Contract self) => JsonSerializer.Serialize(self, PactV4.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + MarkupTypeConverter.Singleton, + ProviderStatesConverter.Singleton, + ContentTypeHintConverter.Singleton, + EncodedConverter.Singleton, + GeneratorTypeConverter.Singleton, + CombineConverter.Singleton, + MatchEnumConverter.Singleton, + MethodConverter.Singleton, + ResponseUnionConverter.Singleton, + InteractionTypeConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; + } + + internal class MarkupTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(MarkupType); + + public override MarkupType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "COMMON_MARK": + return MarkupType.CommonMark; + case "HTML": + return MarkupType.Html; + } + throw new Exception("Cannot unmarshal type MarkupType"); + } + + public override void Write(Utf8JsonWriter writer, MarkupType value, JsonSerializerOptions options) + { + switch (value) + { + case MarkupType.CommonMark: + JsonSerializer.Serialize(writer, "COMMON_MARK", options); + return; + case MarkupType.Html: + JsonSerializer.Serialize(writer, "HTML", options); + return; + } + throw new Exception("Cannot marshal type MarkupType"); + } + + public static readonly MarkupTypeConverter Singleton = new MarkupTypeConverter(); + } + + internal class ProviderStatesConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ProviderStates); + + public override ProviderStates Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + var stringValue = reader.GetString(); + return new ProviderStates { String = stringValue }; + case JsonTokenType.StartArray: + var arrayValue = JsonSerializer.Deserialize>(ref reader, options); + return new ProviderStates { ProviderStateArray = arrayValue }; + } + throw new Exception("Cannot unmarshal type ProviderStates"); + } + + public override void Write(Utf8JsonWriter writer, ProviderStates value, JsonSerializerOptions options) + { + if (value.String != null) + { + JsonSerializer.Serialize(writer, value.String, options); + return; + } + if (value.ProviderStateArray != null) + { + JsonSerializer.Serialize(writer, value.ProviderStateArray, options); + return; + } + throw new Exception("Cannot marshal type ProviderStates"); + } + + public static readonly ProviderStatesConverter Singleton = new ProviderStatesConverter(); + } + + internal class ContentTypeHintConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ContentTypeHint); + + public override ContentTypeHint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "BINARY": + return ContentTypeHint.Binary; + case "TEXT": + return ContentTypeHint.Text; + } + throw new Exception("Cannot unmarshal type ContentTypeHint"); + } + + public override void Write(Utf8JsonWriter writer, ContentTypeHint value, JsonSerializerOptions options) + { + switch (value) + { + case ContentTypeHint.Binary: + JsonSerializer.Serialize(writer, "BINARY", options); + return; + case ContentTypeHint.Text: + JsonSerializer.Serialize(writer, "TEXT", options); + return; + } + throw new Exception("Cannot marshal type ContentTypeHint"); + } + + public static readonly ContentTypeHintConverter Singleton = new ContentTypeHintConverter(); + } + + internal class EncodedConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Encoded); + + public override Encoded Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + case JsonTokenType.False: + var boolValue = reader.GetBoolean(); + return new Encoded { Bool = boolValue }; + case JsonTokenType.String: + var stringValue = reader.GetString(); + return new Encoded { String = stringValue }; + } + throw new Exception("Cannot unmarshal type Encoded"); + } + + public override void Write(Utf8JsonWriter writer, Encoded value, JsonSerializerOptions options) + { + if (value.Bool != null) + { + JsonSerializer.Serialize(writer, value.Bool.Value, options); + return; + } + if (value.String != null) + { + JsonSerializer.Serialize(writer, value.String, options); + return; + } + throw new Exception("Cannot marshal type Encoded"); + } + + public static readonly EncodedConverter Singleton = new EncodedConverter(); + } + + internal class GeneratorTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(GeneratorType); + + public override GeneratorType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "Date": + return GeneratorType.Date; + case "DateTime": + return GeneratorType.DateTime; + case "MockServerURL": + return GeneratorType.MockServerUrl; + case "ProviderState": + return GeneratorType.ProviderState; + case "RandomBoolean": + return GeneratorType.RandomBoolean; + case "RandomDecimal": + return GeneratorType.RandomDecimal; + case "RandomHexadecimal": + return GeneratorType.RandomHexadecimal; + case "RandomInt": + return GeneratorType.RandomInt; + case "RandomString": + return GeneratorType.RandomString; + case "Regex": + return GeneratorType.Regex; + case "Time": + return GeneratorType.Time; + case "Uuid": + return GeneratorType.Uuid; + } + throw new Exception("Cannot unmarshal type GeneratorType"); + } + + public override void Write(Utf8JsonWriter writer, GeneratorType value, JsonSerializerOptions options) + { + switch (value) + { + case GeneratorType.Date: + JsonSerializer.Serialize(writer, "Date", options); + return; + case GeneratorType.DateTime: + JsonSerializer.Serialize(writer, "DateTime", options); + return; + case GeneratorType.MockServerUrl: + JsonSerializer.Serialize(writer, "MockServerURL", options); + return; + case GeneratorType.ProviderState: + JsonSerializer.Serialize(writer, "ProviderState", options); + return; + case GeneratorType.RandomBoolean: + JsonSerializer.Serialize(writer, "RandomBoolean", options); + return; + case GeneratorType.RandomDecimal: + JsonSerializer.Serialize(writer, "RandomDecimal", options); + return; + case GeneratorType.RandomHexadecimal: + JsonSerializer.Serialize(writer, "RandomHexadecimal", options); + return; + case GeneratorType.RandomInt: + JsonSerializer.Serialize(writer, "RandomInt", options); + return; + case GeneratorType.RandomString: + JsonSerializer.Serialize(writer, "RandomString", options); + return; + case GeneratorType.Regex: + JsonSerializer.Serialize(writer, "Regex", options); + return; + case GeneratorType.Time: + JsonSerializer.Serialize(writer, "Time", options); + return; + case GeneratorType.Uuid: + JsonSerializer.Serialize(writer, "Uuid", options); + return; + } + throw new Exception("Cannot marshal type GeneratorType"); + } + + public static readonly GeneratorTypeConverter Singleton = new GeneratorTypeConverter(); + } + + internal class CombineConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Combine); + + public override Combine Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "AND": + return Combine.And; + case "OR": + return Combine.Or; + } + throw new Exception("Cannot unmarshal type Combine"); + } + + public override void Write(Utf8JsonWriter writer, Combine value, JsonSerializerOptions options) + { + switch (value) + { + case Combine.And: + JsonSerializer.Serialize(writer, "AND", options); + return; + case Combine.Or: + JsonSerializer.Serialize(writer, "OR", options); + return; + } + throw new Exception("Cannot marshal type Combine"); + } + + public static readonly CombineConverter Singleton = new CombineConverter(); + } + + internal class MatchEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(MatchEnum); + + public override MatchEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "arrayContains": + return MatchEnum.ArrayContains; + case "boolean": + return MatchEnum.Boolean; + case "contentType": + return MatchEnum.ContentType; + case "date": + return MatchEnum.Date; + case "datetime": + return MatchEnum.Datetime; + case "decimal": + return MatchEnum.Decimal; + case "eachKey": + return MatchEnum.EachKey; + case "eachValue": + return MatchEnum.EachValue; + case "equality": + return MatchEnum.Equality; + case "include": + return MatchEnum.Include; + case "integer": + return MatchEnum.Integer; + case "notEmpty": + return MatchEnum.NotEmpty; + case "null": + return MatchEnum.Null; + case "number": + return MatchEnum.Number; + case "regex": + return MatchEnum.Regex; + case "semver": + return MatchEnum.Semver; + case "statusCode": + return MatchEnum.StatusCode; + case "time": + return MatchEnum.Time; + case "type": + return MatchEnum.Type; + case "values": + return MatchEnum.Values; + } + throw new Exception("Cannot unmarshal type MatchEnum"); + } + + public override void Write(Utf8JsonWriter writer, MatchEnum value, JsonSerializerOptions options) + { + switch (value) + { + case MatchEnum.ArrayContains: + JsonSerializer.Serialize(writer, "arrayContains", options); + return; + case MatchEnum.Boolean: + JsonSerializer.Serialize(writer, "boolean", options); + return; + case MatchEnum.ContentType: + JsonSerializer.Serialize(writer, "contentType", options); + return; + case MatchEnum.Date: + JsonSerializer.Serialize(writer, "date", options); + return; + case MatchEnum.Datetime: + JsonSerializer.Serialize(writer, "datetime", options); + return; + case MatchEnum.Decimal: + JsonSerializer.Serialize(writer, "decimal", options); + return; + case MatchEnum.EachKey: + JsonSerializer.Serialize(writer, "eachKey", options); + return; + case MatchEnum.EachValue: + JsonSerializer.Serialize(writer, "eachValue", options); + return; + case MatchEnum.Equality: + JsonSerializer.Serialize(writer, "equality", options); + return; + case MatchEnum.Include: + JsonSerializer.Serialize(writer, "include", options); + return; + case MatchEnum.Integer: + JsonSerializer.Serialize(writer, "integer", options); + return; + case MatchEnum.NotEmpty: + JsonSerializer.Serialize(writer, "notEmpty", options); + return; + case MatchEnum.Null: + JsonSerializer.Serialize(writer, "null", options); + return; + case MatchEnum.Number: + JsonSerializer.Serialize(writer, "number", options); + return; + case MatchEnum.Regex: + JsonSerializer.Serialize(writer, "regex", options); + return; + case MatchEnum.Semver: + JsonSerializer.Serialize(writer, "semver", options); + return; + case MatchEnum.StatusCode: + JsonSerializer.Serialize(writer, "statusCode", options); + return; + case MatchEnum.Time: + JsonSerializer.Serialize(writer, "time", options); + return; + case MatchEnum.Type: + JsonSerializer.Serialize(writer, "type", options); + return; + case MatchEnum.Values: + JsonSerializer.Serialize(writer, "values", options); + return; + } + throw new Exception("Cannot marshal type MatchEnum"); + } + + public static readonly MatchEnumConverter Singleton = new MatchEnumConverter(); + } + + internal class MethodConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Method); + + public override Method Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "CONNECT": + return Method.MethodConnect; + case "DELETE": + return Method.MethodDelete; + case "GET": + return Method.MethodGet; + case "HEAD": + return Method.MethodHead; + case "OPTIONS": + return Method.MethodOptions; + case "POST": + return Method.MethodPost; + case "PUT": + return Method.MethodPut; + case "TRACE": + return Method.MethodTrace; + case "connect": + return Method.Connect; + case "delete": + return Method.Delete; + case "get": + return Method.Get; + case "head": + return Method.Head; + case "options": + return Method.Options; + case "post": + return Method.Post; + case "put": + return Method.Put; + case "trace": + return Method.Trace; + } + throw new Exception("Cannot unmarshal type Method"); + } + + public override void Write(Utf8JsonWriter writer, Method value, JsonSerializerOptions options) + { + switch (value) + { + case Method.MethodConnect: + JsonSerializer.Serialize(writer, "CONNECT", options); + return; + case Method.MethodDelete: + JsonSerializer.Serialize(writer, "DELETE", options); + return; + case Method.MethodGet: + JsonSerializer.Serialize(writer, "GET", options); + return; + case Method.MethodHead: + JsonSerializer.Serialize(writer, "HEAD", options); + return; + case Method.MethodOptions: + JsonSerializer.Serialize(writer, "OPTIONS", options); + return; + case Method.MethodPost: + JsonSerializer.Serialize(writer, "POST", options); + return; + case Method.MethodPut: + JsonSerializer.Serialize(writer, "PUT", options); + return; + case Method.MethodTrace: + JsonSerializer.Serialize(writer, "TRACE", options); + return; + case Method.Connect: + JsonSerializer.Serialize(writer, "connect", options); + return; + case Method.Delete: + JsonSerializer.Serialize(writer, "delete", options); + return; + case Method.Get: + JsonSerializer.Serialize(writer, "get", options); + return; + case Method.Head: + JsonSerializer.Serialize(writer, "head", options); + return; + case Method.Options: + JsonSerializer.Serialize(writer, "options", options); + return; + case Method.Post: + JsonSerializer.Serialize(writer, "post", options); + return; + case Method.Put: + JsonSerializer.Serialize(writer, "put", options); + return; + case Method.Trace: + JsonSerializer.Serialize(writer, "trace", options); + return; + } + throw new Exception("Cannot marshal type Method"); + } + + public static readonly MethodConverter Singleton = new MethodConverter(); + } + + internal class ResponseUnionConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ResponseUnion); + + public override ResponseUnion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + var objectValue = JsonSerializer.Deserialize(ref reader, options); + return new ResponseUnion { ResponseClass = objectValue }; + case JsonTokenType.StartArray: + var arrayValue = JsonSerializer.Deserialize>(ref reader, options); + return new ResponseUnion { MessageContentsArray = arrayValue }; + } + throw new Exception("Cannot unmarshal type ResponseUnion"); + } + + public override void Write(Utf8JsonWriter writer, ResponseUnion value, JsonSerializerOptions options) + { + if (value.MessageContentsArray != null) + { + JsonSerializer.Serialize(writer, value.MessageContentsArray, options); + return; + } + if (value.ResponseClass != null) + { + JsonSerializer.Serialize(writer, value.ResponseClass, options); + return; + } + throw new Exception("Cannot marshal type ResponseUnion"); + } + + public static readonly ResponseUnionConverter Singleton = new ResponseUnionConverter(); + } + + internal class InteractionTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(InteractionType); + + public override InteractionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + switch (value) + { + case "Asynchronous/Messages": + return InteractionType.AsynchronousMessages; + case "Synchronous/HTTP": + return InteractionType.SynchronousHttp; + case "Synchronous/Messages": + return InteractionType.SynchronousMessages; + } + throw new Exception("Cannot unmarshal type InteractionType"); + } + + public override void Write(Utf8JsonWriter writer, InteractionType value, JsonSerializerOptions options) + { + switch (value) + { + case InteractionType.AsynchronousMessages: + JsonSerializer.Serialize(writer, "Asynchronous/Messages", options); + return; + case InteractionType.SynchronousHttp: + JsonSerializer.Serialize(writer, "Synchronous/HTTP", options); + return; + case InteractionType.SynchronousMessages: + JsonSerializer.Serialize(writer, "Synchronous/Messages", options); + return; + } + throw new Exception("Cannot marshal type InteractionType"); + } + + public static readonly InteractionTypeConverter Singleton = new InteractionTypeConverter(); + } + + public class DateOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + public class TimeOnlyConverter : JsonConverter + { + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); + } + + internal class IsoDateTimeOffsetConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); + } +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 diff --git a/src/Explore.Cli/Program.cs b/src/Explore.Cli/Program.cs index 55a0cec..f025ec5 100644 --- a/src/Explore.Cli/Program.cs +++ b/src/Explore.Cli/Program.cs @@ -63,7 +63,17 @@ public static async Task Main(string[] args) importInsomniaCollectionCommand.SetHandler(async (ec, fp, v) => { await ImportInsomniaCollection(ec, fp, v); }, exploreCookie, importFilePath, verbose); - + + var baseUri = new Option(name: "--base-uri", description: "The base uri to use for all imported requests. ie: http://localhost:3000") { IsRequired = false }; + baseUri.AddAlias("-b"); + var ignorePactFileSchemaValidationResult = new Option(name: "--ignore-pact-schema-verification-result", description: "Ignore pact schema verification result, performed prior to upload") { IsRequired = false }; + var importPactFileCommand = new Command("import-pact-file") { exploreCookie, importFilePath, baseUri, verbose }; + importPactFileCommand.Description = "Import a Pact file (v2/v3/v4) into SwaggerHub Explore (HTTP interactions only)"; + rootCommand.Add(importPactFileCommand); + + importPactFileCommand.SetHandler(async (ec, fp, b, v, ignorePactFileSchemaValidationResult) => + { await ImportPactFile(ec, fp, b, v); }, exploreCookie, importFilePath, baseUri, verbose, ignorePactFileSchemaValidationResult); + AnsiConsole.Write(new FigletText("Explore.Cli").Color(new Color(133, 234, 45))); return await rootCommand.InvokeAsync(args); @@ -103,7 +113,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string return; } - try + try { //validate collection against postman collection schema string json = File.ReadAllText(filePath); @@ -137,14 +147,14 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string { BaseAddress = new Uri("https://api.explore.swaggerhub.com") }; - + //iterate over the items and import if(postmanCollection != null && postmanCollection.Item != null) { //create an initial space to hold the collection items var resultTable = new Table() { Title = new TableTitle(text: $"PROCESSING [green]{postmanCollection.Info?.Name}[/]"), Width = 100, UseSafeBorder = true }; resultTable.AddColumn("Result"); - resultTable.AddColumn(new TableColumn("Details").Centered()); + resultTable.AddColumn(new TableColumn("Details").Centered()); var cleanedCollectionName = UtilityHelper.CleanString(postmanCollection.Info?.Name); var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = cleanedCollectionName }), Encoding.UTF8, "application/json"); @@ -152,7 +162,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string exploreHttpClient.DefaultRequestHeaders.Clear(); exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); - var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); + var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); if (spacesResponse.StatusCode == HttpStatusCode.Created) { @@ -166,7 +176,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string //Postman Items cant contain nested items, so we can flatten the list var flattenedItems = PostmanCollectionMappingHelper.FlattenItems(postmanCollection.Item); - + foreach(var item in flattenedItems) { if(item.Request != null) @@ -177,7 +187,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string apiImportResults.AddRow("[orange3]skipped[/]", $"Item '{item.Name}' skipped", $"Request method not supported"); continue; } - + //now let's create an API entry in the space var cleanedAPIName = UtilityHelper.CleanString(item.Name); var apiContent = new StringContent(JsonSerializer.Serialize(new ApiRequest() { Name = cleanedAPIName, Type = "REST", Description = $"{item.Request.Description?.Content + "\n" }imported from postman on {DateTime.UtcNow.ToShortDateString()}" }), Encoding.UTF8, "application/json"); @@ -211,7 +221,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string else { apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {apiResponse.StatusCode}", ""); - } + } } } @@ -221,9 +231,9 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string if (verboseOutput == null || verboseOutput == false) { AnsiConsole.MarkupLine($"[green]\u2713 [/]{cleanedCollectionName}"); - } - - } + } + + } else { switch (spacesResponse.StatusCode) @@ -258,13 +268,275 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string if (verboseOutput != null && verboseOutput == true) { AnsiConsole.Write(resultTable); - } + } } Console.WriteLine(); - AnsiConsole.MarkupLine($"[green]Import completed[/]"); - + AnsiConsole.MarkupLine($"[green]Import completed[/]"); + + //ToDo - deal with scenario of item-groups + } + catch (FileNotFoundException) + { + Console.WriteLine("File not found."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + + } + + internal static async Task ImportPactFile(string exploreCookie, string filePath, string pactBaseUri, bool? verboseOutput, bool ignorePactFileSchemaValidationResult = false) + { + //check file existence and read permissions + try + { + using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + //let's verify it's a JSON file now + if (!UtilityHelper.IsJsonFile(filePath)) + { + AnsiConsole.MarkupLine($"[red]The file provided is not a JSON file. Please review.[/]"); + return; + } + + // You can read from the file if this point is reached + AnsiConsole.MarkupLine($"processing ..."); + } + } + catch (UnauthorizedAccessException) + { + AnsiConsole.MarkupLine($"[red]Access to {filePath} is denied. Please review file permissions any try again.[/]"); + return; + } + catch (FileNotFoundException) + { + AnsiConsole.MarkupLine($"[red]The file {filePath} does not exist. Please review the provided file path.[/]"); + return; + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]An error occurred accessing the file: {ex.Message}[/]"); + return; + } + + try + { + // validate collection against pact schemas + // https://github.com/pactflow/pact-schemas + Console.WriteLine($"Reading file"); + string json = File.ReadAllText(filePath); + + if (!PactMappingHelper.hasPactVersion(json)) + { + Console.WriteLine($"Cannot determine pact specification version"); + return; + } + var pactSpecVersion = PactMappingHelper.getPactVersion(json); + dynamic pactContract; + switch (pactSpecVersion) + { + case "pact-v1": + pactContract = PactV1.Contract.FromJson(json); + break; + case "pact-v2": + pactContract = PactV2.Contract.FromJson(json); + break; + case "pact-v3": + pactContract = PactV3.Contract.FromJson(json); + break; + case "pact-v4": + pactContract = PactV4.Contract.FromJson(json); + break; + default: + throw new Exception($"Invalid pact specification version"); + } + //validate json against known (high level) schema + var validationResult = await UtilityHelper.ValidateSchema(json, pactSpecVersion); + + if (!validationResult.isValid && ignorePactFileSchemaValidationResult) + { + Console.WriteLine($"WARN: The provided pact file does not conform to the expected schema. Errors: {validationResult.Message}"); + } + else if (!validationResult.isValid) + { + throw new Exception($"The provided pact file does not conform to the expected schema. Errors: {validationResult.Message}"); + } + + int interactionCount = 0; + switch (pactContract) + { + case PactV1.Contract: + PactV1.Contract pactV1Contract = pactContract; + pactSpecVersion = pactV1Contract.Metadata.MetadataPactSpecification.Version; + interactionCount = pactV1Contract.Interactions.Count(); + break; + case PactV2.Contract: + PactV2.Contract pactV2Contract = pactContract; + pactSpecVersion = pactV2Contract.Metadata.MetadataPactSpecification.Version; + interactionCount = pactV2Contract.Interactions.Count(); + break; + case PactV3.Contract: + PactV3.Contract pactV3Contract = pactContract; + pactSpecVersion = pactV3Contract.Metadata.MetadataPactSpecification.Version; + interactionCount = pactV3Contract.Interactions.Count(); + break; + case PactV4.Contract: + PactV4.Contract pactV4Contract = pactContract; + pactSpecVersion = pactV4Contract.Metadata.PactSpecification.Version; + interactionCount = pactV4Contract.Interactions.Count(); + break; + default: + throw new Exception($"The provided pact file is unsupported."); + }; + + var panel = new Panel($"You have [green]{interactionCount} items[/] to import") + { + Width = 100, + Header = new PanelHeader("Pact Data").Centered() + }; + AnsiConsole.Write(panel); + Console.WriteLine(""); + + var exploreHttpClient = new HttpClient + { + BaseAddress = new Uri("https://api.explore.swaggerhub.com") + }; + + // iterate over the items and import + if (pactContract != null && pactContract?.Interactions != null) + { + + var resultTable = new Table() { Title = new TableTitle(text: $"PROCESSING [green]{pactContract?.Consumer.Name}-{pactContract?.Provider.Name}[/]"), Width = 100, UseSafeBorder = true }; + resultTable.AddColumn("Result"); + resultTable.AddColumn(new TableColumn("Details").Centered()); + + var cleanedCollectionName = UtilityHelper.CleanString($"{pactContract?.Consumer.Name}-{pactContract?.Provider.Name}"); + var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = cleanedCollectionName }), Encoding.UTF8, "application/json"); + + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); + + if (spacesResponse.StatusCode == HttpStatusCode.Created) + { + 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"); + + var spaceResponse = spacesResponse.Content.ReadFromJsonAsync(); + var interactions = (pactContract?.Interactions) ?? throw new Exception("No interactions found in Pact file"); + if (interactions.Count == 0) + { + throw new Exception("No interactions found in Pact file"); + } + + foreach (var interaction in interactions) + { + if (interaction.Request != null) + { + if (interaction is PactV4.Interaction pactV4Interaction) + { + if (pactV4Interaction.Type.GetValueOrDefault() != PactV4.InteractionType.SynchronousHttp) { + Console.WriteLine($"Skipping interaction, as {pactV4Interaction.Type} is not Synchronous/HTTP"); + break; + } + } + //now let's create an API entry in the space + var cleanedAPIName = UtilityHelper.CleanString(interaction.Description.ToString()); + var apiContent = new StringContent(JsonSerializer.Serialize(new ApiRequest() { Name = cleanedAPIName, Type = "REST", Description = $"imported from pact file on {DateTime.UtcNow.ToShortDateString()}\nPact Specification: {pactSpecVersion}" }), Encoding.UTF8, "application/json"); + + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var apiResponse = await exploreHttpClient.PostAsync($"/spaces-api/v1/spaces/{spaceResponse.Result?.Id}/apis", apiContent); + + if (apiResponse.StatusCode == HttpStatusCode.Created) + { + var createdApiResponse = apiResponse.Content.ReadFromJsonAsync(); + var connectionRequestBody = JsonSerializer.Serialize(PactMappingHelper.MapPactInteractionToExploreConnection(interaction, pactBaseUri)); + var connectionContent = new StringContent(connectionRequestBody, Encoding.UTF8, "application/json"); + + // //now let's do the work and import the connection + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var connectionResponse = await exploreHttpClient.PostAsync($"/spaces-api/v1/spaces/{spaceResponse.Result?.Id}/apis/{createdApiResponse.Result?.Id}/connections", connectionContent); + + if (connectionResponse.StatusCode == HttpStatusCode.Created) + { + apiImportResults.AddRow("[green]OK[/]", $"API '{cleanedAPIName}' created", "Connection created"); + } + else + { + apiImportResults.AddRow("[orange3]OK[/]", $"API '{cleanedAPIName}' created", "[orange3]Connection NOT created[/]"); + } + } + else + { + apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {apiResponse.StatusCode}", ""); + } + } + } + + + resultTable.AddRow(new Markup("[green]success[/]"), apiImportResults); + + if (verboseOutput == null || verboseOutput == false) + { + AnsiConsole.MarkupLine($"[green]\u2713 [/]{cleanedCollectionName}"); + } + + } + else + { + switch (spacesResponse.StatusCode) + { + case HttpStatusCode.OK: + // not expecting a 200 OK here - this would be returned for a failed auth and a redirect to SB ID + resultTable.AddRow(new Markup("[red]failure[/]"), new Markup($"[red] Auth failed connecting to SwaggerHub Explore. Please review provided cookie.[/]")); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + + case HttpStatusCode.Conflict: + var apiImportResults = new Table() { Title = new TableTitle(text: $"[orange3]SPACE[/] {cleanedCollectionName} [orange3]ALREADY EXISTS[/]") }; + apiImportResults.AddColumn("Result"); + apiImportResults.AddColumn("API Imported"); + apiImportResults.AddColumn("Connection Imported"); + apiImportResults.AddRow("skipped", "", ""); + + resultTable.AddRow(new Markup("[orange3]skipped[/]"), apiImportResults); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + + default: + resultTable.AddRow(new Markup("[red]failure[/]"), new Markup($"[red] StatusCode: {spacesResponse.StatusCode}, Details: {spacesResponse.ReasonPhrase}[/]")); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + } + } + + if (verboseOutput != null && verboseOutput == true) + { + AnsiConsole.Write(resultTable); + } + + } + else + { + throw new Exception($"No interactions found"); + } + + Console.WriteLine(); + AnsiConsole.MarkupLine($"[green]Import completed[/]"); + //ToDo - deal with scenario of item-groups } catch (FileNotFoundException) @@ -348,7 +620,7 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string //create an initial space to hold the collection items var resultTable = new Table() { Title = new TableTitle(text: $"PROCESSING [green]{collectionResource?.Name}[/]"), Width = 100, UseSafeBorder = true }; resultTable.AddColumn("Result"); - resultTable.AddColumn(new TableColumn("Details").Centered()); + resultTable.AddColumn(new TableColumn("Details").Centered()); var cleanedCollectionName = UtilityHelper.CleanString(collectionResource?.Name); var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = cleanedCollectionName }), Encoding.UTF8, "application/json"); @@ -356,7 +628,7 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string exploreHttpClient.DefaultRequestHeaders.Clear(); exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); - var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); + var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); if (spacesResponse.StatusCode == HttpStatusCode.Created) { @@ -371,7 +643,7 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string //separate requests and environment resources var requestResources = insomniaCollection.Resources.Where(r => string.Equals(r.Type, "request", StringComparison.OrdinalIgnoreCase)).ToList(); var environmentResources = insomniaCollection.Resources.Where(r => string.Equals(r.Type, "environment", StringComparison.OrdinalIgnoreCase)).ToList(); - + foreach(var resource in requestResources) { if(!InsomniaCollectionMappingHelper.IsItemRequestModeSupported(resource)) @@ -414,7 +686,7 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string else { apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {apiResponse.StatusCode}", ""); - } + } } @@ -423,9 +695,9 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string if (verboseOutput == null || verboseOutput == false) { AnsiConsole.MarkupLine($"[green]\u2713 [/]{cleanedCollectionName}"); - } - - } + } + + } else { switch (spacesResponse.StatusCode) @@ -461,12 +733,12 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string if (verboseOutput != null && verboseOutput == true) { AnsiConsole.Write(resultTable); - } + } } Console.WriteLine(); - AnsiConsole.MarkupLine($"[green]Import completed[/]"); - } + AnsiConsole.MarkupLine($"[green]Import completed[/]"); + } catch (FileNotFoundException) { Console.WriteLine("File not found."); @@ -474,7 +746,7 @@ internal static async Task ImportInsomniaCollection(string exploreCookie, string catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); - } + } } internal static async Task ExportSpaces(string exploreCookie, string filePath, string exportFileName, string names, bool? verboseOutput) @@ -777,7 +1049,7 @@ internal static async Task ImportSpaces(string exploreCookie, string filePath, s else { apiImportResults.AddRow("[orange3]skipped[/]", $"API '{exportedAPI.Name}' skipped", $"Kafka not yet supported by export"); - } + } } } diff --git a/src/Explore.Cli/UtilityHelper.cs b/src/Explore.Cli/UtilityHelper.cs index e469082..b469506 100644 --- a/src/Explore.Cli/UtilityHelper.cs +++ b/src/Explore.Cli/UtilityHelper.cs @@ -1354,6 +1354,1325 @@ private static string GetSchemaByApplicationName(string name) } } }", + "pact-v1" => @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema"", + ""title"": ""Pact V1"", + ""description"": ""Schema for a Pact file"", + ""definitions"": { + ""headers"": { + ""$id"": ""#/definitions/headers"", + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + }, + ""interaction"": { + ""$id"": ""#/definitions/interaction"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""description"": { + ""type"": ""string"" + }, + ""providerState"": { + ""type"": ""string"" + }, + ""provider_state"": { + ""type"": ""string"" + }, + ""request"": { + ""$ref"": ""#/definitions/request"" + }, + ""response"": { + ""$ref"": ""#/definitions/response"" + } + }, + ""required"": [ + ""description"", + ""request"", + ""response"" + ] + }, + ""interactions"": { + ""$id"": ""#/definitions/interactions"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/interaction"" + } + }, + ""metadata"": { + ""$id"": ""#/definitions/metadata"", + ""type"": ""object"", + ""properties"": { + ""pactSpecification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + }, + ""pactSpecificationVersion"": { + ""type"": ""string"" + }, + ""pact-specification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + } + } + }, + ""pacticipant"": { + ""$id"": ""#/definitions/pacticipant"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""name"" + ] + }, + ""request"": { + ""$id"": ""#/definitions/request"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""method"": { + ""type"": ""string"", + ""enum"": [ + ""connect"", + ""CONNECT"", + ""delete"", + ""DELETE"", + ""get"", + ""GET"", + ""head"", + ""HEAD"", + ""options"", + ""OPTIONS"", + ""post"", + ""POST"", + ""put"", + ""PUT"", + ""trace"", + ""TRACE"" + ] + }, + ""path"": { + ""type"": ""string"" + }, + ""query"": { + ""type"": ""string"", + ""pattern"": ""^$|^[^=&]+=[^=&]+&?$|^[^=&]+=[^=&]+(&[^=&]+=[^=&]+)*&?$"" + } + }, + ""required"": [ + ""method"", + ""path"" + ] + }, + ""response"": { + ""$id"": ""#/definitions/response"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""status"": { + ""minimum"": 100, + ""maximum"": 599, + ""type"": ""integer"" + } + }, + ""required"": [ + ""status"" + ] + } + }, + ""type"": ""object"", + ""properties"": { + ""consumer"": { + ""$ref"": ""#/definitions/pacticipant"" + }, + ""interactions"": { + ""$ref"": ""#/definitions/interactions"" + }, + ""metadata"": { + ""$ref"": ""#/definitions/metadata"" + }, + ""provider"": { + ""$ref"": ""#/definitions/pacticipant"" + } + }, + ""required"": [ + ""consumer"", + ""interactions"", + ""provider"" + ] + }", + "pact-v2" =>@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema"", + ""title"": ""Pact V2"", + ""description"": ""Schema for a Pact file"", + ""definitions"": { + ""headers"": { + ""$id"": ""#/definitions/headers"", + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + }, + ""interaction"": { + ""$id"": ""#/definitions/interaction"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""description"": { + ""type"": ""string"" + }, + ""providerState"": { + ""type"": ""string"" + }, + ""request"": { + ""$ref"": ""#/definitions/request"" + }, + ""response"": { + ""$ref"": ""#/definitions/response"" + } + }, + ""required"": [ + ""description"", + ""request"", + ""response"" + ] + }, + ""interactions"": { + ""$id"": ""#/definitions/interactions"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/interaction"" + } + }, + ""matchingRules"": { + ""$id"": ""#/definitions/matchingRules"", + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""metadata"": { + ""$id"": ""#/definitions/metadata"", + ""type"": ""object"", + ""properties"": { + ""pactSpecification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + }, + ""pactSpecificationVersion"": { + ""type"": ""string"" + }, + ""pact-specification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + } + } + }, + ""pacticipant"": { + ""$id"": ""#/definitions/pacticipant"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""name"" + ] + }, + ""request"": { + ""$id"": ""#/definitions/request"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""method"": { + ""type"": ""string"", + ""enum"": [ + ""connect"", + ""CONNECT"", + ""delete"", + ""DELETE"", + ""get"", + ""GET"", + ""head"", + ""HEAD"", + ""options"", + ""OPTIONS"", + ""post"", + ""POST"", + ""put"", + ""PUT"", + ""trace"", + ""TRACE"" + ] + }, + ""path"": { + ""type"": ""string"" + }, + ""query"": { + ""type"": ""string"", + ""pattern"": ""^$|^[^=&]+=[^=&]+&?$|^[^=&]+=[^=&]+(&[^=&]+=[^=&]+)*&?$"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + } + }, + ""required"": [ + ""method"", + ""path"" + ] + }, + ""response"": { + ""$id"": ""#/definitions/response"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""status"": { + ""minimum"": 100, + ""maximum"": 599, + ""type"": ""integer"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + } + }, + ""required"": [ + ""status"" + ] + } + }, + ""type"": ""object"", + ""properties"": { + ""consumer"": { + ""$ref"": ""#/definitions/pacticipant"" + }, + ""interactions"": { + ""$ref"": ""#/definitions/interactions"" + }, + ""metadata"": { + ""$ref"": ""#/definitions/metadata"" + }, + ""provider"": { + ""$ref"": ""#/definitions/pacticipant"" + } + }, + ""required"": [ + ""consumer"", + ""interactions"", + ""provider"" + ] + }", + "pact-v3" => @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema"", + ""title"": ""Pact V3"", + ""description"": ""Schema for a Pact file"", + ""definitions"": { + ""headers"": { + ""$id"": ""#/definitions/headers"", + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + }, + ""interaction"": { + ""$id"": ""#/definitions/interaction"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""description"": { + ""type"": ""string"" + }, + ""request"": { + ""$ref"": ""#/definitions/request"" + }, + ""response"": { + ""$ref"": ""#/definitions/response"" + }, + ""providerStates"": { + ""anyOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""params"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + }, + ""required"": [ + ""name"" + ] + } + } + ] + } + }, + ""required"": [ + ""description"", + ""request"", + ""response"" + ] + }, + ""interactions"": { + ""$id"": ""#/definitions/interactions"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/interaction"" + } + }, + ""matchers"": { + ""$id"": ""#/definitions/matchers"", + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""matchingRules"": { + ""$id"": ""#/definitions/matchingRules"", + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""message"": { + ""$id"": ""#/definitions/message"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""contents"": {}, + ""metadata"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""metaData"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""matchingRules"": { + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""generators"": { + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""description"": { + ""type"": ""string"" + }, + ""providerState"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""contents"", + ""description"" + ] + }, + ""messages"": { + ""$id"": ""#/definitions/messages"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/message"" + } + }, + ""metadata"": { + ""$id"": ""#/definitions/metadata"", + ""type"": ""object"", + ""properties"": { + ""pactSpecification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + }, + ""pactSpecificationVersion"": { + ""type"": ""string"" + }, + ""pact-specification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + } + } + }, + ""pacticipant"": { + ""$id"": ""#/definitions/pacticipant"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""name"" + ] + }, + ""request"": { + ""$id"": ""#/definitions/request"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""method"": { + ""type"": ""string"", + ""enum"": [ + ""connect"", + ""CONNECT"", + ""delete"", + ""DELETE"", + ""get"", + ""GET"", + ""head"", + ""HEAD"", + ""options"", + ""OPTIONS"", + ""post"", + ""POST"", + ""put"", + ""PUT"", + ""trace"", + ""TRACE"" + ] + }, + ""path"": { + ""type"": ""string"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + }, + ""generators"": { + ""type"": ""object"", + }, + ""query"": { + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + } + }, + ""required"": [ + ""method"", + ""path"" + ] + }, + ""response"": { + ""$id"": ""#/definitions/response"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""body"": {}, + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""status"": { + ""minimum"": 100, + ""maximum"": 599, + ""type"": ""integer"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + }, + ""generators"": { + ""type"": ""object"", + } + }, + ""required"": [ + ""status"" + ] + } + }, + ""type"": ""object"", + ""properties"": { + ""consumer"": { + ""$ref"": ""#/definitions/pacticipant"" + }, + ""interactions"": { + ""$ref"": ""#/definitions/interactions"" + }, + ""messages"": { + ""$ref"": ""#/definitions/messages"" + }, + ""metadata"": { + ""$ref"": ""#/definitions/metadata"" + }, + ""provider"": { + ""$ref"": ""#/definitions/pacticipant"" + } + }, + ""required"": [ + ""consumer"", + ""provider"" + ] + }", + "pact-v4" =>@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema"", + ""title"": ""Pact V4"", + ""description"": ""Schema for a Pact file"", + ""definitions"": { + ""body"": { + ""$id"": ""#/definitions/body"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""content"": { + ""anyOf"": [ + { + ""type"": ""string"" + }, + {} + ] + }, + ""contentType"": { + ""type"": ""string"" + }, + ""contentTypeHint"": { + ""anyOf"": [ + { + ""const"": ""BINARY"", + ""type"": ""string"" + }, + { + ""const"": ""TEXT"", + ""type"": ""string"" + } + ] + }, + ""encoded"": { + ""anyOf"": [ + { + ""type"": ""boolean"" + }, + { + ""type"": ""string"" + } + ] + } + }, + ""required"": [ + ""content"", + ""contentType"", + ""encoded"" + ] + }, + ""headers"": { + ""$id"": ""#/definitions/headers"", + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + }, + ""interaction"": { + ""$id"": ""#/definitions/interaction"", + ""anyOf"": [ + { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""comments"": { + ""type"": ""object"", + ""properties"": { + ""testname"": { + ""type"": ""string"" + }, + ""text"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + }, + ""interactionMarkup"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""markup"": { + ""type"": ""string"" + }, + ""markupType"": { + ""anyOf"": [ + { + ""const"": ""COMMON_MARK"", + ""type"": ""string"" + }, + { + ""const"": ""HTML"", + ""type"": ""string"" + } + ] + } + }, + ""required"": [ + ""markup"", + ""markupType"" + ] + }, + ""key"": { + ""type"": ""string"" + }, + ""pending"": { + ""type"": ""boolean"" + }, + ""pluginConfiguration"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + } + }, + ""description"": { + ""type"": ""string"" + }, + ""transport"": { + ""type"": ""string"" + }, + ""request"": { + ""$ref"": ""#/definitions/request"" + }, + ""response"": { + ""$ref"": ""#/definitions/response"" + }, + ""providerStates"": { + ""anyOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""params"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + }, + ""required"": [ + ""name"" + ] + } + } + ] + }, + ""type"": { + ""const"": ""Synchronous/HTTP"", + ""type"": ""string"" + } + }, + ""required"": [ + ""description"", + ""request"", + ""response"", + ""type"" + ] + }, + { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""comments"": { + ""type"": ""object"", + ""properties"": { + ""testname"": { + ""type"": ""string"" + }, + ""text"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + }, + ""interactionMarkup"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""markup"": { + ""type"": ""string"" + }, + ""markupType"": { + ""anyOf"": [ + { + ""const"": ""COMMON_MARK"", + ""type"": ""string"" + }, + { + ""const"": ""HTML"", + ""type"": ""string"" + } + ] + } + }, + ""required"": [ + ""markup"", + ""markupType"" + ] + }, + ""key"": { + ""type"": ""string"" + }, + ""pending"": { + ""type"": ""boolean"" + }, + ""pluginConfiguration"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + } + }, + ""description"": { + ""type"": ""string"" + }, + ""transport"": { + ""type"": ""string"" + }, + ""providerStates"": { + ""anyOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""params"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + }, + ""required"": [ + ""name"" + ] + } + } + ] + }, + ""contents"": {}, + ""metadata"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""metaData"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""matchingRules"": { + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""generators"": { + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""type"": { + ""const"": ""Asynchronous/Messages"", + ""type"": ""string"" + } + }, + ""required"": [ + ""description"", + ""contents"", + ""type"" + ] + }, + { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""comments"": { + ""type"": ""object"", + ""properties"": { + ""testname"": { + ""type"": ""string"" + }, + ""text"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + }, + ""interactionMarkup"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""markup"": { + ""type"": ""string"" + }, + ""markupType"": { + ""anyOf"": [ + { + ""const"": ""COMMON_MARK"", + ""type"": ""string"" + }, + { + ""const"": ""HTML"", + ""type"": ""string"" + } + ] + } + }, + ""required"": [ + ""markup"", + ""markupType"" + ] + }, + ""key"": { + ""type"": ""string"" + }, + ""pending"": { + ""type"": ""boolean"" + }, + ""pluginConfiguration"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + } + }, + ""description"": { + ""type"": ""string"" + }, + ""transport"": { + ""type"": ""string"" + }, + ""providerStates"": { + ""anyOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""params"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + } + }, + ""required"": [ + ""name"" + ] + } + } + ] + }, + ""request"": { + ""$ref"": ""#/definitions/messageContents"" + }, + ""response"": { + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/messageContents"" + } + }, + ""type"": { + ""const"": ""Synchronous/Messages"", + ""type"": ""string"" + } + }, + ""required"": [ + ""description"", + ""request"", + ""response"", + ""type"" + ] + } + ] + }, + ""interactions"": { + ""$id"": ""#/definitions/interactions"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/interaction"" + } + }, + ""matchers"": { + ""$id"": ""#/definitions/matchers"", + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""matchingRules"": { + ""$id"": ""#/definitions/matchingRules"", + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""messageContents"": { + ""$id"": ""#/definitions/messageContents"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""contents"": {}, + ""metadata"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""metaData"": { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": {} + } + }, + ""matchingRules"": { + ""additionalProperties"": true, + ""type"": ""object"", + }, + ""generators"": { + ""additionalProperties"": true, + ""type"": ""object"", + } + }, + ""required"": [ + ""contents"" + ] + }, + ""metadata"": { + ""$id"": ""#/definitions/metadata"", + ""type"": ""object"", + ""properties"": { + ""pactSpecification"": { + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""version"" + ] + } + } + }, + ""pacticipant"": { + ""$id"": ""#/definitions/pacticipant"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""name"" + ] + }, + ""request"": { + ""$id"": ""#/definitions/request"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""method"": { + ""type"": ""string"", + ""enum"": [ + ""connect"", + ""CONNECT"", + ""delete"", + ""DELETE"", + ""get"", + ""GET"", + ""head"", + ""HEAD"", + ""options"", + ""OPTIONS"", + ""post"", + ""POST"", + ""put"", + ""PUT"", + ""trace"", + ""TRACE"" + ] + }, + ""path"": { + ""type"": ""string"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + }, + ""generators"": { + ""type"": ""object"", + }, + ""query"": { + ""anyOf"": [ + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""string"" + } + } + }, + { + ""type"": ""object"", + ""patternProperties"": { + ""^(.*)$"": { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + } + } + } + } + ] + }, + ""body"": { + ""$ref"": ""#/definitions/body"" + } + }, + ""required"": [ + ""method"", + ""path"" + ] + }, + ""response"": { + ""$id"": ""#/definitions/response"", + ""additionalProperties"": false, + ""type"": ""object"", + ""properties"": { + ""headers"": { + ""$ref"": ""#/definitions/headers"" + }, + ""status"": { + ""minimum"": 100, + ""maximum"": 599, + ""type"": ""integer"" + }, + ""matchingRules"": { + ""$ref"": ""#/definitions/matchingRules"" + }, + ""generators"": { + ""type"": ""object"", + }, + ""body"": { + ""$ref"": ""#/definitions/body"" + } + }, + ""required"": [ + ""status"" + ] + } + }, + ""type"": ""object"", + ""properties"": { + ""consumer"": { + ""$ref"": ""#/definitions/pacticipant"" + }, + ""interactions"": { + ""$ref"": ""#/definitions/interactions"" + }, + ""metadata"": { + ""$ref"": ""#/definitions/metadata"" + }, + ""provider"": { + ""$ref"": ""#/definitions/pacticipant"" + } + }, + ""required"": [ + ""consumer"", + ""interactions"", + ""provider"" + ] + }", _ => string.Empty, }; } @@ -1361,7 +2680,7 @@ private static string GetSchemaByApplicationName(string name) public static async Task ValidateSchema(string jsonAsString, string schemaName) { var validationResult = new SchemaValidationResult(); - var schemaAsString = GetSchemaByApplicationName(schemaName); + var schemaAsString = GetSchemaByApplicationName(schemaName); //var schema = await JsonSchema.FromFileAsync($"/schemas/{schemaName}"); var schema = await JsonSchema.FromJsonAsync(schemaAsString); diff --git a/test/Explore.Cli.Tests/PactMappingHelperTests.cs b/test/Explore.Cli.Tests/PactMappingHelperTests.cs new file mode 100644 index 0000000..4e29dab --- /dev/null +++ b/test/Explore.Cli.Tests/PactMappingHelperTests.cs @@ -0,0 +1,100 @@ +using Explore.Cli.Models; +using System.Text.Json; + +public class PactMappingHelperTests +{ + [Fact] + public void hasPactVersion() + { + // Arrange + + var mockPactContractV3 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + + // Act + + var result = PactMappingHelper.hasPactVersion(mockPactContractV3); + + // Assert + + Assert.True(result); + } + [Fact] + public void getPactVersion() + { + // Arrange + + var mockPactContractV3 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + string expectedResult = "pact-v3"; + + // Act + + var result = PactMappingHelper.getPactVersion(mockPactContractV3); + + // Assert + + Assert.Contains(expectedResult, result); + } + [Fact] + public async Task IsNotMatchingPactV1Schema() + { + // Arrange + + var mockPactContractV1 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + string expectedError = "3 total errors"; + + // Act + + var validationResult = await UtilityHelper.ValidateSchema(mockPactContractV1, "pact-v1"); + + // Assert + Assert.False(validationResult.isValid); + Assert.Contains(expectedError, validationResult.Message); + } + [Fact] + public async Task IsNotMatchingPactV2Schema() + { + // Arrange + + var mockPactContractV2 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + string expectedError = "3 total errors"; + + // Act + + var validationResult = await UtilityHelper.ValidateSchema(mockPactContractV2, "pact-v2"); + + // Assert + Assert.False(validationResult.isValid); + Assert.Contains(expectedError, validationResult.Message); + } + [Fact] + public async Task IsMatchingPactV3Schema() + { + // Arrange + + var mockPactContractV3 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + + // Act + + var validationResult = await UtilityHelper.ValidateSchema(mockPactContractV3, "pact-v3"); + + // Assert + Assert.True(validationResult.isValid); + } + [Fact] + public async Task IsNotMatchingPactV4Schema() + { + // Arrange + + var mockPactContractV4 = "{\"consumer\":{\"name\":\"swaggerhub-pactflow-consumer-codegen\"},\"interactions\":[{\"description\":\"a request to get a product\",\"providerStates\":[{\"name\":\"a product with ID 10 exists\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/product\\/10\"},\"response\":{\"body\":{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request for to create product 1234\",\"providerStates\":[{\"name\":\"a product with id 1234 does not exist\"}],\"request\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}}},\"method\":\"POST\",\"path\":\"\\/products\"},\"response\":{\"body\":{\"id\":\"1234\",\"name\":\"burger\",\"price\":42,\"type\":\"food\"},\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}},{\"description\":\"a request to get all products\",\"providerStates\":[{\"name\":\"products exist\"}],\"request\":{\"method\":\"GET\",\"path\":\"\\/products\"},\"response\":{\"body\":[{\"id\":\"10\",\"name\":\"28 Degrees\",\"type\":\"CREDIT_CARD\"}],\"headers\":{\"Content-Type\":\"application\\/json\"},\"matchingRules\":{\"body\":{\"$\":{\"combine\":\"AND\",\"matchers\":[{\"match\":\"type\"}]}},\"header\":{}},\"status\":200}}],\"metadata\":{\"pact-js\":{\"version\":\"10.1.4\"},\"pactRust\":{\"ffi\":\"0.3.12\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"3.0.0\"}},\"provider\":{\"name\":\"swaggerhub-pactflow-provider\"}}"; + string expectedError = "3 total errors"; + + // Act + + var validationResult = await UtilityHelper.ValidateSchema(mockPactContractV4, "pact-v4"); + + // Assert + Assert.False(validationResult.isValid); + Assert.Contains(expectedError, validationResult.Message); + + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo1.json b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo1.json new file mode 100644 index 0000000..49f05e9 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo1.json @@ -0,0 +1,102 @@ +{ + "consumer": { + "name": "PactGoV2Consumer" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerState": "User foo exists", + "request": { + "body": { + "datetime": "2020-01-01'T'08:00:45", + "id": 27, + "lastName": "billy", + "name": "billy" + }, + "headers": { + "Authorization": "Bearer 1234", + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "type" + }, + "$.body.id": { + "match": "type" + }, + "$.body.lastName": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.header.Authorization": { + "match": "type" + }, + "$.path": { + "match": "regex", + "regex": "\\/foo.*" + }, + "$.query.baz[0]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[1]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[2]": { + "match": "regex", + "regex": "[a-z]+" + } + }, + "method": "POST", + "path": "/foobar", + "query": "baz=bar&baz=bat&baz=baz" + }, + "response": { + "body": { + "datetime": "2020-01-01", + "itemsMin": [ + "thereshouldbe3ofthese", + "thereshouldbe3ofthese", + "thereshouldbe3ofthese" + ], + "lastName": "Sampson", + "name": "Billy" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "regex", + "regex": "[0-9\\-]+" + }, + "$.body.itemsMin": { + "match": "type", + "min": 3 + }, + "$.header['Content-Type']": { + "match": "regex", + "regex": "application\\/json" + } + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "2.0.0" + } + }, + "provider": { + "name": "V2Provider" + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo2.json b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo2.json new file mode 100644 index 0000000..2816db5 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo2.json @@ -0,0 +1,86 @@ +{ + "consumer": { + "name": "PactGoV2ConsumerAllInOne" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerState": "User foo exists", + "request": { + "body": { + "datetime": "2020-01-01'T'08:00:45", + "id": 27, + "lastName": "billy", + "name": "billy" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "type" + }, + "$.body.id": { + "match": "type" + }, + "$.body.lastName": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.path": { + "match": "regex", + "regex": "\\/foo.*" + }, + "$.query.baz": { + "match": "regex", + "regex": "[a-zA-Z]+" + } + }, + "method": "POST", + "path": "/foobar", + "query": "baz=bat" + }, + "response": { + "body": { + "datetime": "2020-01-01", + "itemsMin": [ + "thereshouldbe3ofthese", + "thereshouldbe3ofthese", + "thereshouldbe3ofthese" + ], + "lastName": "Sampson", + "name": "Billy" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "regex", + "regex": "[0-9\\-]+" + }, + "$.body.itemsMin": { + "match": "type", + "min": 3 + } + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "2.0.0" + } + }, + "provider": { + "name": "V2Provider" + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo3.json b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo3.json new file mode 100644 index 0000000..bf15991 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo3.json @@ -0,0 +1,98 @@ +{ + "consumer": { + "name": "PactGoV2ConsumerMatch" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerState": "User foo exists", + "request": { + "body": { + "datetime": "2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime", + "id": 27, + "lastName": "Sampson", + "name": "Billy" + }, + "headers": { + "Authorization": "Bearer 1234", + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "type" + }, + "$.body.id": { + "match": "type" + }, + "$.body.lastName": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.header.Authorization": { + "match": "type" + }, + "$.query.baz[0]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[1]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[2]": { + "match": "regex", + "regex": "[a-z]+" + } + }, + "method": "POST", + "path": "/foobar", + "query": "baz=bar&baz=bat&baz=baz" + }, + "response": { + "body": { + "datetime": "2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime", + "id": 27, + "lastName": "Sampson", + "name": "Billy" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "type" + }, + "$.body.id": { + "match": "type" + }, + "$.body.lastName": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.header['Content-Type']": { + "match": "regex", + "regex": "application\\/json" + } + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "2.0.0" + } + }, + "provider": { + "name": "V2ProviderMatch" + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo4.json b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo4.json new file mode 100644 index 0000000..4020c8e --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v2-pactgo4.json @@ -0,0 +1,50 @@ +{ + "consumer": { + "name": "PactGoProductAPIConsumer" + }, + "interactions": [ + { + "description": "A request for Product 10", + "providerState": "A product with ID 10 exists", + "request": { + "method": "GET", + "path": "/products/10" + }, + "response": { + "body": { + "id": 10, + "name": "Billy", + "price": "23.33" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.id": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.body.price": { + "match": "type" + } + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "2.0.0" + } + }, + "provider": { + "name": "PactGoProductAPI" + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v3-pact-go.json b/test/Explore.Cli.Tests/fixtures/pact-v3-pact-go.json new file mode 100644 index 0000000..f4bf668 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v3-pact-go.json @@ -0,0 +1,274 @@ +{ + "consumer": { + "name": "PactGoV3Consumer" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerStates": [ + { + "name": "state 1" + }, + { + "name": "User foo exists", + "params": { + "id": "foo" + } + } + ], + "request": { + "body": { + "datetime": "2020-01-01T08:00:45", + "id": 27, + "lastName": "billy", + "name": "billy" + }, + "generators": { + "body": { + "$.datetime": { + "format": "yyyy-MM-dd'T'HH:mm:ss", + "type": "DateTime" + }, + "$.name": { + "expression": "${name}", + "type": "ProviderState" + } + } + }, + "headers": { + "Authorization": "Bearer 1234", + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "format": "yyyy-MM-dd'T'HH:mm:ss", + "match": "datetime" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.lastName": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.name": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": { + "Authorization": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "query": { + "baz": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[a-z]+" + } + ] + } + } + }, + "method": "POST", + "path": "/foobar", + "query": { + "baz": [ + "bar", + "bat", + "baz" + ] + } + }, + "response": { + "body": { + "accountBalance": 123.76, + "arrayContaining": [ + "string", + 1, + { + "foo": "bar" + } + ], + "datetime": "2020-01-01", + "equality": "a thing", + "id": 12, + "itemsMin": [ + "thereshouldbe3ofthese", + "thereshouldbe3ofthese", + "thereshouldbe3ofthese" + ], + "itemsMinMax": [ + 27, + 27, + 27, + 27, + 27 + ], + "lastName": "Sampson", + "name": "Billy", + "superstring": "foo" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$.accountBalance": { + "combine": "AND", + "matchers": [ + { + "match": "decimal" + } + ] + }, + "$.arrayContaining": { + "combine": "AND", + "matchers": [ + { + "match": "arrayContains", + "variants": [ + { + "index": 0, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + { + "index": 1, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + } + } + }, + { + "index": 2, + "rules": { + "$.foo": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + } + ] + } + ] + }, + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[0-9\\-]+" + } + ] + }, + "$.equality": { + "combine": "AND", + "matchers": [ + { + "match": "equality" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.itemsMin": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "min": 3 + } + ] + }, + "$.itemsMinMax": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "max": 5, + "min": 3 + } + ] + }, + "$.superstring": { + "combine": "AND", + "matchers": [ + { + "match": "include", + "value": "foo" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "V3Provider" + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v3.json b/test/Explore.Cli.Tests/fixtures/pact-v3.json new file mode 100644 index 0000000..c1f8ba1 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v3.json @@ -0,0 +1,154 @@ +{ + "consumer": { + "name": "swaggerhub-pactflow-consumer-codegen" + }, + "interactions": [ + { + "description": "a request to get a product", + "providerStates": [ + { + "name": "a product with ID 10 exists" + } + ], + "request": { + "method": "GET", + "path": "/product/10" + }, + "response": { + "body": { + "id": "10", + "name": "28 Degrees", + "type": "CREDIT_CARD" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + }, + { + "description": "a request for to create product 1234", + "providerStates": [ + { + "name": "a product with id 1234 does not exist" + } + ], + "request": { + "body": { + "id": "1234", + "name": "burger", + "price": 42, + "type": "food" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + "method": "POST", + "path": "/products" + }, + "response": { + "body": { + "id": "1234", + "name": "burger", + "price": 42, + "type": "food" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + }, + { + "description": "a request to get all products", + "providerStates": [ + { + "name": "products exist" + } + ], + "request": { + "method": "GET", + "path": "/products" + }, + "response": { + "body": [ + { + "id": "10", + "name": "28 Degrees", + "type": "CREDIT_CARD" + } + ], + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + } + ], + "metadata": { + "pact-js": { + "version": "10.1.4" + }, + "pactRust": { + "ffi": "0.3.12", + "models": "0.4.5" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "swaggerhub-pactflow-provider" + } + } \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/pact-v4.json b/test/Explore.Cli.Tests/fixtures/pact-v4.json new file mode 100644 index 0000000..de7ecc9 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/pact-v4.json @@ -0,0 +1,291 @@ +{ + "consumer": { + "name": "PactGoV4Consumer" + }, + "interactions": [ + { + "description": "A request to do a foo", + "pending": false, + "providerStates": [ + { + "name": "state 1" + }, + { + "name": "User foo exists", + "params": { + "id": "foo" + } + } + ], + "request": { + "body": { + "content": { + "datetime": "2020-01-01T08:00:45", + "id": 27, + "lastName": "billy", + "name": "billy" + }, + "contentType": "application/json", + "encoded": false + }, + "generators": { + "body": { + "$.datetime": { + "format": "yyyy-MM-dd'T'HH:mm:ss", + "type": "DateTime" + }, + "$.name": { + "expression": "${name}", + "type": "ProviderState" + } + } + }, + "headers": { + "Authorization": [ + "Bearer 1234" + ], + "Content-Type": [ + "application/json" + ] + }, + "matchingRules": { + "body": { + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "format": "yyyy-MM-dd'T'HH:mm:ss", + "match": "datetime" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.lastName": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.name": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": { + "Authorization": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "query": { + "baz": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[a-z]+" + } + ] + } + } + }, + "method": "POST", + "path": "/foobar", + "query": { + "baz": [ + "bar", + "bat", + "baz" + ] + } + }, + "response": { + "body": { + "content": { + "accountBalance": 123.76, + "arrayContaining": [ + "string", + 1, + { + "foo": "bar" + } + ], + "datetime": "2020-01-01", + "equality": "a thing", + "id": 12, + "itemsMin": [ + "thereshouldbe3ofthese", + "thereshouldbe3ofthese", + "thereshouldbe3ofthese" + ], + "itemsMinMax": [ + 27, + 27, + 27, + 27, + 27 + ], + "lastName": "Sampson", + "name": "Billy", + "superstring": "foo" + }, + "contentType": "application/json", + "encoded": false + }, + "headers": { + "Content-Type": [ + "application/json" + ] + }, + "matchingRules": { + "body": { + "$.accountBalance": { + "combine": "AND", + "matchers": [ + { + "match": "decimal" + } + ] + }, + "$.arrayContaining": { + "combine": "AND", + "matchers": [ + { + "match": "arrayContains", + "variants": [ + { + "index": 0, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + { + "index": 1, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + } + } + }, + { + "index": 2, + "rules": { + "$.foo": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + } + ] + } + ] + }, + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[0-9\\-]+" + } + ] + }, + "$.equality": { + "combine": "AND", + "matchers": [ + { + "match": "equality" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.itemsMin": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "min": 3 + } + ] + }, + "$.itemsMinMax": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "max": 5, + "min": 3 + } + ] + }, + "$.superstring": { + "combine": "AND", + "matchers": [ + { + "match": "include", + "value": "foo" + } + ] + } + }, + "header": {} + }, + "status": 200 + }, + "transport": "http", + "type": "Synchronous/HTTP" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.22", + "mockserver": "1.2.9", + "models": "1.2.2" + }, + "pactSpecification": { + "version": "4.0" + } + }, + "provider": { + "name": "V4Provider" + } +} \ No newline at end of file