diff --git a/src/BuslyCLI.Console/Commands/CommonMessageSettings.cs b/src/BuslyCLI.Console/Commands/CommonMessageSettings.cs index d2e7f230..fc7cafd8 100644 --- a/src/BuslyCLI.Console/Commands/CommonMessageSettings.cs +++ b/src/BuslyCLI.Console/Commands/CommonMessageSettings.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using BuslyCLI.TypeConverters; using Spectre.Console; using Spectre.Console.Cli; @@ -25,15 +26,24 @@ public abstract class CommonMessageSettings : GlobalCommandSettings public required string EnclosedMessageType { get; set; } [CommandOption("-m|--message-body ")] - [Description("The content of the message body")] - public required string MessageBody { get; set; } + [Description("The content of the message body. Accepts a raw JSON string or a path to a file using curl-style @file syntax (e.g. @payload.json).")] + public required MessageBodyValue MessageBody { get; set; } public override ValidationResult Validate() { if (string.IsNullOrWhiteSpace(ContentType)) return ValidationResult.Error("must specify a 'content-type'."); if (string.IsNullOrWhiteSpace(EnclosedMessageType)) return ValidationResult.Error("must specify an 'enclosed-message-type'."); - if (string.IsNullOrWhiteSpace(MessageBody)) return ValidationResult.Error("must specify a 'message-body'."); + + if (MessageBody is null) return ValidationResult.Error("must specify a 'message-body'."); + if (Path.IsPathFullyQualified(MessageBody.Value)) + { + if (!File.Exists(MessageBody.Value)) + return ValidationResult.Error($"File not found: {MessageBody.Value}"); + + MessageBody.Value = File.ReadAllText(MessageBody.Value); + } + return base.Validate(); } diff --git a/src/BuslyCLI.Console/Commands/NsbCommand/SendCommand.cs b/src/BuslyCLI.Console/Commands/NsbCommand/SendCommand.cs index 4717c4c1..4edb45c5 100644 --- a/src/BuslyCLI.Console/Commands/NsbCommand/SendCommand.cs +++ b/src/BuslyCLI.Console/Commands/NsbCommand/SendCommand.cs @@ -27,7 +27,7 @@ protected override async Task ExecuteAsync(CommandContext context, SendComm var message = new OutgoingMessage( Guid.NewGuid().ToString(), headers, - Encoding.ASCII.GetBytes(settings.MessageBody) + Encoding.ASCII.GetBytes(settings.MessageBody.Value) ); var transportOperation = new TransportOperation( diff --git a/src/BuslyCLI.Console/Commands/NsbEvent/PublishCommand.cs b/src/BuslyCLI.Console/Commands/NsbEvent/PublishCommand.cs index 7cd64c37..ccbe18ec 100644 --- a/src/BuslyCLI.Console/Commands/NsbEvent/PublishCommand.cs +++ b/src/BuslyCLI.Console/Commands/NsbEvent/PublishCommand.cs @@ -30,7 +30,7 @@ protected override async Task ExecuteAsync(CommandContext context, PublishC var message = new OutgoingMessage( Guid.NewGuid().ToString(), headers, - Encoding.ASCII.GetBytes(settings.MessageBody) + Encoding.ASCII.GetBytes(settings.MessageBody.Value) ); var type = CreateTypeFromString(settings.EnclosedMessageType); diff --git a/src/BuslyCLI.Console/Commands/NsbTimeout/SendTimeout.cs b/src/BuslyCLI.Console/Commands/NsbTimeout/SendTimeout.cs index 6c46ea18..24405ef0 100644 --- a/src/BuslyCLI.Console/Commands/NsbTimeout/SendTimeout.cs +++ b/src/BuslyCLI.Console/Commands/NsbTimeout/SendTimeout.cs @@ -46,7 +46,7 @@ protected override async Task ExecuteAsync(CommandContext context, SendTime var message = new OutgoingMessage( Guid.NewGuid().ToString(), headers, - Encoding.ASCII.GetBytes(settings.MessageBody) + Encoding.ASCII.GetBytes(settings.MessageBody.Value) ); var dispatchProperties = new DispatchProperties(); diff --git a/src/BuslyCLI.Console/TypeConverters/MessageBodyTypeConverter.cs b/src/BuslyCLI.Console/TypeConverters/MessageBodyTypeConverter.cs new file mode 100644 index 00000000..c7b0ef59 --- /dev/null +++ b/src/BuslyCLI.Console/TypeConverters/MessageBodyTypeConverter.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; + +namespace BuslyCLI.TypeConverters; + +public class MessageBodyTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) + { + + if (value is string str) + { + // Curl-style @file reference: parse only, no I/O + if (str.StartsWith("@")) + { + var expandedFilePath = new ExpandedPath(str[1..]); + return new MessageBodyValue(expandedFilePath.Path); + } + } + + return base.ConvertFrom(context, culture, value); + } +} \ No newline at end of file diff --git a/src/BuslyCLI.Console/TypeConverters/MessageBodyValue.cs b/src/BuslyCLI.Console/TypeConverters/MessageBodyValue.cs new file mode 100644 index 00000000..24a1c107 --- /dev/null +++ b/src/BuslyCLI.Console/TypeConverters/MessageBodyValue.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace BuslyCLI.TypeConverters; + +[TypeConverter(typeof(MessageBodyTypeConverter))] +public class MessageBodyValue +{ + public string Value { get; set; } + + + public MessageBodyValue(string value) + { + Value = value; + } +} \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/Commands/CommonCommandSettingsTests.cs b/tests/BuslyCLI.Console.Tests/Commands/CommonCommandSettingsTests.cs index d5d4c500..e915f397 100644 --- a/tests/BuslyCLI.Console.Tests/Commands/CommonCommandSettingsTests.cs +++ b/tests/BuslyCLI.Console.Tests/Commands/CommonCommandSettingsTests.cs @@ -1,4 +1,5 @@ using BuslyCLI.Commands.NsbCommand; +using BuslyCLI.TypeConverters; namespace BuslyCLI.Console.Tests.Commands; @@ -12,7 +13,7 @@ public void ShouldFailWhenDestinationEndpointIsMissing() { ContentType = "application/json", EnclosedMessageType = "MessageContracts.Commands.CreateOrder", - MessageBody = "{}", + MessageBody = new MessageBodyValue("{}"), DestinationEndpoint = null! }; diff --git a/tests/BuslyCLI.Console.Tests/Commands/CommonMessageSettingsTests.cs b/tests/BuslyCLI.Console.Tests/Commands/CommonMessageSettingsTests.cs index c64cdb88..ef9870d6 100644 --- a/tests/BuslyCLI.Console.Tests/Commands/CommonMessageSettingsTests.cs +++ b/tests/BuslyCLI.Console.Tests/Commands/CommonMessageSettingsTests.cs @@ -1,4 +1,5 @@ using BuslyCLI.Commands.NsbEvent; +using BuslyCLI.TypeConverters; namespace BuslyCLI.Console.Tests.Commands; @@ -12,7 +13,7 @@ public void ShouldFailWhenContentTypeIsMissing() { ContentType = null!, EnclosedMessageType = "MessageContracts.Commands.CreateOrder", - MessageBody = "{}" + MessageBody = new MessageBodyValue("{}") }; var result = settings.Validate(); @@ -28,7 +29,7 @@ public void ShouldFailWhenEnclosedMessageTypeIsMissing() { ContentType = "application/json", EnclosedMessageType = null!, - MessageBody = "{}" + MessageBody = new MessageBodyValue("{}") }; var result = settings.Validate(); @@ -52,4 +53,21 @@ public void ShouldFailWhenMessageBodyIsMissing() Assert.That(result.Successful, Is.False); Assert.That(result.Message, Does.Contain("must specify a 'message-body'.")); } + + [Test] + public void ShouldFailWhenMessageBodyFileDoesNotExist() + { + var nonExistentFilePath = Path.GetFullPath("non-existent-payload.json"); + var settings = new PublishCommandSettings + { + ContentType = "application/json", + EnclosedMessageType = "MessageContracts.Commands.CreateOrder", + MessageBody = new MessageBodyValue(nonExistentFilePath) + }; + + var result = settings.Validate(); + + Assert.That(result.Successful, Is.False); + Assert.That(result.Message, Does.Contain($"File not found: {nonExistentFilePath}")); + } } \ No newline at end of file diff --git a/website/docs/cli-reference/command/send.md b/website/docs/cli-reference/command/send.md index df6a1314..dc6c0a9c 100644 --- a/website/docs/cli-reference/command/send.md +++ b/website/docs/cli-reference/command/send.md @@ -14,8 +14,7 @@ busly send command | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `-c`, `--content-type` | The fully qualified .NET type name of the enclosed message (ex: Ordering.Commands.CreateOrder ) | | `-e`, `--enclosed-message-type` | The type of serialization used for the message | -| `-m`, `--message-body` | The content of the message body | -| `-d`, `--destination-endpoint` | The destination endpoint to send a message to | +| `-m`, `--message-body` | The content of the message body. Accepts a raw JSON string or a path to a file using curl-style `@` syntax (e.g. `@payload.json`). | ## Examples diff --git a/website/docs/cli-reference/event/publish.md b/website/docs/cli-reference/event/publish.md index b51c5038..7d33d25e 100644 --- a/website/docs/cli-reference/event/publish.md +++ b/website/docs/cli-reference/event/publish.md @@ -14,7 +14,7 @@ busly event publish | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `-c`, `--content-type` | The fully qualified .NET type name of the enclosed message (ex: Ordering.Commands.CreateOrder ) | | `-e`, `--enclosed-message-type` | The type of serialization used for the message | -| `-m`, `--message-body` | The content of the message body | +| `-m`, `--message-body` | The content of the message body. Accepts a raw JSON string or a path to a file using curl-style `@` syntax (e.g. `@payload.json`). | ## Examples diff --git a/website/docs/cli-reference/timeout/send.md b/website/docs/cli-reference/timeout/send.md index 7c46bb68..0d59cf5a 100644 --- a/website/docs/cli-reference/timeout/send.md +++ b/website/docs/cli-reference/timeout/send.md @@ -14,7 +14,7 @@ busly timeout send | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `-c`, `--content-type` | The fully qualified .NET type name of the enclosed message (ex: Ordering.Commands.CreateOrder ) | | `-e`, `--enclosed-message-type` | The type of serialization used for the message | -| `-m`, `--message-body` | The content of the message body | +| `-m`, `--message-body` | The content of the message body. Accepts a raw JSON string or a path to a file using curl-style `@` syntax (e.g. `@payload.json`). | | `-d`, `--destination-endpoint` | The destination endpoint to send a message to | | `--do-not-deliver-before` | Allows specifying a date before which the delivery should not occur, using ISO-8601 format (YYYY-MM-DDTHH:mm:ssZ) | | `--delay-delivery-with` | Specifies the delay before the timeout is delivered, using a TimeSpan format | diff --git a/website/docs/introduction/quick-start.mdx b/website/docs/introduction/quick-start.mdx index 05ce6a20..dad5dc3a 100644 --- a/website/docs/introduction/quick-start.mdx +++ b/website/docs/introduction/quick-start.mdx @@ -290,4 +290,81 @@ docker run --rm ` ``` - \ No newline at end of file + + +## Passing Message Body from a File + +As you can see in the commands above, inlining JSON directly in the terminal can get complicated fast — especially in PowerShell, where double quotes require manual escaping. For anything beyond the simplest payloads, this becomes error-prone and hard to read. + +Busly supports a curl-style `@filename` syntax for `--message-body`, letting you store your JSON in a file and reference it directly. Where `payload.json` contains: + +```json +{ + "OrderNumber": "3f2d6c8a-b7a2-4c3f-9c3e-12ab45ef6789" +} +``` + + + + +```bash +busly command send \ + --content-type 'text/json' \ + --enclosed-message-type "Messages.Commands.CreateOrder" \ + --destination-endpoint "BuslyCLI.DemoEndpoint" \ + --message-body @payload.json +``` + + + + +```bash +busly command send ` + --content-type 'text/json' ` + --enclosed-message-type "Messages.Commands.CreateOrder" ` + --destination-endpoint "BuslyCLI.DemoEndpoint" ` + --message-body @payload.json +``` + + + + +When running via Docker, mount the payload file into the container using `-v` and reference its container path with `@`: + +```bash +docker run --rm \ + --network host \ + -v "$HOME/.busly-cli/config.yaml:/app/config.yaml" \ + -v "$(pwd)/payload.json:/app/payload.json" \ + tragiccode/busly-cli \ + command send \ + --content-type "text/json" \ + --enclosed-message-type "Messages.Commands.CreateOrder" \ + --destination-endpoint "BuslyCLI.DemoEndpoint" \ + --message-body @/app/payload.json \ + --config ./config.yaml +``` + + + + +When running via Docker, mount the payload file into the container using `-v` and reference its container path with `@`: + +```bash +docker run --rm ` + --network host ` + -v "$HOME/.busly-cli/config.yaml:/app/config.yaml" ` + -v "${PWD}/payload.json:/app/payload.json" ` + tragiccode/busly-cli ` + command send ` + --content-type "text/json" ` + --enclosed-message-type "Messages.Commands.CreateOrder" ` + --destination-endpoint "BuslyCLI.DemoEndpoint" ` + --message-body @/app/payload.json ` + --config ./config.yaml +``` + + + + +No escaping required on any platform. Relative paths are resolved from your current working directory, and `~` is expanded to your home directory. \ No newline at end of file