Skip to content

Commit

Permalink
Support string interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
jwosty committed Dec 4, 2024
1 parent 9bc98ff commit 5abde0a
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 35 deletions.
8 changes: 4 additions & 4 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ nuget BlackFox.MasterOfFoo 2.1.0
nuget Expecto 8.1.0
nuget Fable.Core 4.0
nuget Fable.Mocha
nuget FSharp.Core = 4.6.2
nuget FSharp.Core = 5.0.0
nuget Microsoft.Extensions.Logging 6.0.0
nuget Microsoft.Extensions.Logging.Console
nuget Microsoft.NET.Test.Sdk
nuget Serilog
nuget Serilog.Extensions.Logging
nuget Serilog.Sinks.File
nuget Serilog.Sinks.InMemory
nuget Suave 2.5.0
nuget Suave 2.6.0
nuget YoloDev.Expecto.TestSdk

group Build
Expand Down Expand Up @@ -50,7 +50,7 @@ group Test
nuget Microsoft.NET.Test.Sdk
nuget Serilog.Extensions.Logging
nuget Serilog.Sinks.TextWriter
nuget Suave >= 2.5.0
nuget Suave >= 2.6.0
nuget YoloDev.Expecto.TestSdk
nuget FSharp.Core >= 6.0.0
nuget FSharp.Core >= 7.0.0

6 changes: 3 additions & 3 deletions paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ NUGET
Fable.Mocha (2.6)
Fable.Core (>= 3.0)
FSharp.Core (>= 4.6.2)
FSharp.Core (4.6.2)
FSharp.Core (5.0)
Gee.External.Capstone (2.3)
Iced (1.21)
Microsoft.Bcl.AsyncInterfaces (9.0) - restriction: || (&& (== net9.0) (>= net461)) (&& (== net9.0) (>= net462)) (&& (== net9.0) (>= net471)) (&& (== net9.0) (< net8.0)) (&& (== net9.0) (< netcoreapp3.1)) (&& (== net9.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1)
Expand Down Expand Up @@ -174,8 +174,8 @@ NUGET
Serilog (>= 4.0)
Serilog.Sinks.InMemory (0.11)
Serilog (>= 2.12)
Suave (2.5)
FSharp.Core (>= 4.0 < 5.0)
Suave (2.6)
FSharp.Core - restriction: || (== net9.0) (&& (== netstandard2.0) (>= netstandard2.1)) (== netstandard2.1)
System.Buffers (4.6) - restriction: || (&& (== net9.0) (>= net462)) (&& (== net9.0) (< netcoreapp2.1)) (== netstandard2.0) (== netstandard2.1)
System.CodeDom (9.0)
System.Collections.Immutable (9.0)
Expand Down
23 changes: 14 additions & 9 deletions src/Fable.FSharp.Logf/logf.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ open BlackFox.MasterOfFoo
open Fable.Microsoft.Extensions.Logging
#endif

type StringFormat<'T, 'Result, 'Tuple> = Format<'T, unit, string, 'Result, 'Tuple>

// see: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting#format-specifiers-for-printf
// TODO: This probably isn't quite perfect -- there are likely still some obscure corner cases that this doesn't
// handle correctly. For anyone looking into this, you could prod it with things that aren't quite valid printf format
Expand Down Expand Up @@ -209,12 +211,15 @@ type private LogfEnv<'Result>(continuation: string -> obj[] -> 'Result) =
// * Output: foo{{bar}}
let bracketGroupOrUnpairedBracketRegex = Regex("""(?<a>""" + printfFmtSpecPattern + netMsgHolePattern + """)|(?<b>[\{\}])""")

let escapeUnpairedBrackets (format: Format<'Printer, 'State, 'Residue, 'Result>) =
// [FS0057] Experimental library feature, requires '--langversion:preview'. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.
#nowarn "57"
let escapeUnpairedBrackets (format: Format<'Printer, 'State, 'Residue, 'Result, 'Tuple>) : Format<'Printer, 'State, 'Residue, 'Result, 'Tuple> =
let fmtValue' = bracketGroupOrUnpairedBracketRegex.Replace (format.Value, "${a}${b}${b}")
Format<'Printer, 'State, 'Residue, 'Result>(fmtValue')
Format<'Printer, 'State, 'Residue, 'Result, 'Tuple>(fmtValue', format.Captures, format.CaptureTypes)

let klogf (continuation: string -> obj[] -> 'Result) (format: StringFormat<'T, 'Result>) =
doPrintfFromEnv (escapeUnpairedBrackets format) (LogfEnv(continuation))
let klogf (continuation: string -> obj[] -> 'Result) (format: Format<'T, unit, string, 'Result, 'Tuple>) : 'T =
let fmt' = escapeUnpairedBrackets format
doPrintfFromEnv fmt' (LogfEnv(continuation))

let logf (logger: ILogger) logLevel format =
klogf (fun msg args -> logger.Log(logLevel, msg, args)) format
Expand All @@ -241,7 +246,7 @@ let logMsgParamNameRegex =

// Scans through a format literal (the first parameter to a printf-style function), removing parameter names (i.e.
// changing "%s{foo}" to "%s", and collecting all custom .Net-style formatters (i.e. from "%f{foo:#.#}" we grab ":#.#")
let processLogMsgParams (format: Format<'Printer, 'State, 'Residue, 'Result>) : string option list * Format<'Printer, 'State, 'Residue, 'Result> =
let processLogMsgParams (format: Format<'Printer, 'State, 'Residue, 'Result, 'Tuple>) : string option list * Format<'Printer, 'State, 'Residue, 'Result, 'Tuple> =
let paramReplacementFmt = System.Collections.Generic.List<string option>()
let result: string =
logMsgParamNameRegex.Replace (format.Value, (fun m ->
Expand All @@ -255,7 +260,7 @@ let processLogMsgParams (format: Format<'Printer, 'State, 'Residue, 'Result>) :
))
// TODO: amend to include .Captures and .CaptureTypes - apparently whatever version of Fable I'm using doesn't provide that overload
// (new Format<'T, unit, string, unit>(logMsgParamNameRegex.Replace(format.Value, "$1")))
Seq.toList paramReplacementFmt, (new Format<'Printer, 'State, 'Residue, 'Result>(result))
Seq.toList paramReplacementFmt, (new Format<'Printer, 'State, 'Residue, 'Result, 'Tuple>(result))

// Takes a list of .Net custom formatters (like ":#.#"), and a constructed printf function, and constructs a new
// function (wrapping around the printf function) but which applies these custom formatters to the appropriate
Expand All @@ -280,7 +285,7 @@ let rec mapReplacementsDynamic (fmts: string option list) (f: obj) : obj =
| [] ->
f

let klogf (continuation: string -> obj[] -> 'Result) (format: Format<'T, unit, string, 'Result>) : 'T =
let klogf (continuation: string -> obj[] -> 'Result) (format: Format<'T, unit, string, 'Result, 'Tuple>) : 'T =
let replacements, processedFmt = processLogMsgParams format
let f =
Printf.ksprintf (fun msg -> continuation msg [||]) processedFmt
Expand All @@ -290,15 +295,15 @@ let klogf (continuation: string -> obj[] -> 'Result) (format: Format<'T, unit, s

// Use a fallback implementation where we never attempt to provide structured logging parameters and just flatten
// everything to a string and print it, since BlackFox.MasterOfFoo uses kinds of reflection that don't work in Fable
let logf (logger: ILogger) logLevel (format: StringFormat<'T, unit>) : 'T =
let logf (logger: ILogger) logLevel (format: StringFormat<'T, unit, 'Tuple>) : 'T =
klogf
(fun msg _ -> logger.Log (logLevel, EventId(0), null, null, Func<_,_,_>(fun _ _ -> msg)))
format

let vlogf (logger: ILogger) (logLevel: LogLevel) (eventId: EventId) (exn: Exception) format =
klogf (fun msg args -> logger.Log (logLevel, eventId, null, exn, Func<_,_,_>(fun _ _ -> msg))) format

let elogf (logger: ILogger) (logLevel: LogLevel) (exn: Exception) (format: StringFormat<'T, unit>) : 'T =
let elogf (logger: ILogger) (logLevel: LogLevel) (exn: Exception) (format: StringFormat<'T, unit, 'Tuple>) : 'T =
klogf
(fun msg _ -> logger.Log (logLevel, EventId(0), null, exn, Func<_,_,_>(fun _ _ -> msg)))
format
Expand Down
40 changes: 21 additions & 19 deletions src/Fable.FSharp.Logf/logf.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ open Microsoft.Extensions.Logging
open Fable.Microsoft.Extensions.Logging
#endif

type StringFormat<'T, 'Result, 'Tuple> = Format<'T, unit, string, 'Result, 'Tuple>

/// <summary>
/// Formatted <see cref="T:Microsoft.Extensions.Logging.ILogger"/> compatible printing, using a given 'final'
/// function perform the log call and generate the result.
/// </summary>
/// <param name="continuation">The function called after formatting translation to generate the formatted log result.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val klogf : continuation: (string -> obj[] -> 'Result) -> format: StringFormat<'T, 'Result> -> 'T
val klogf : continuation: (string -> obj[] -> 'Result) -> format: StringFormat<'T, 'Result, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at the specified
Expand All @@ -51,7 +53,7 @@ val klogf : continuation: (string -> obj[] -> 'Result) -> format: StringFormat<'
/// <param name="logLevel">The LogLevel to use.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logf : logger: ILogger -> logLevel: LogLevel -> format: StringFormat<'T, unit> -> 'T
val logf : logger: ILogger -> logLevel: LogLevel -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logf" />, but with extra arguments for event id and exception.
Expand All @@ -62,7 +64,7 @@ val logf : logger: ILogger -> logLevel: LogLevel -> format: StringFormat<'T, uni
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogf : logger: ILogger -> logLevel: LogLevel -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogf : logger: ILogger -> logLevel: LogLevel -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted error printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at the specified
Expand All @@ -73,15 +75,15 @@ val vlogf : logger: ILogger -> logLevel: LogLevel -> eventId: EventId -> exn: Ex
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val elogf : logger: ILogger -> logLevel: LogLevel -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val elogf : logger: ILogger -> logLevel: LogLevel -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Trace level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logft : logger: ILogger -> format: Format<'T, unit, string, unit> -> 'T
val logft : logger: ILogger -> format: Format<'T, unit, string, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logft" />, but with extra arguments for event id and exception.
Expand All @@ -91,15 +93,15 @@ val logft : logger: ILogger -> format: Format<'T, unit, string, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogft : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogft : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Debug level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logfd : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
val logfd : logger: ILogger -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfd" />, but with extra arguments for event id and exception.
Expand All @@ -109,15 +111,15 @@ val logfd : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogfd : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogfd : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Information level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logfi : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
val logfi : logger: ILogger -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfi" />, but with extra arguments for event id and exception.
Expand All @@ -127,15 +129,15 @@ val logfi : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogfi : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogfi : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Warning level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logfw : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
val logfw : logger: ILogger -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfw" />, but with extra arguments for event id and exception.
Expand All @@ -145,7 +147,7 @@ val logfw : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogfw : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogfw : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted error printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Warning level.
Expand All @@ -154,15 +156,15 @@ val vlogfw : logger: ILogger -> eventId: EventId -> exn: Exception -> format: St
/// <param name="exn">The exception to log.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val elogfw : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val elogfw : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Error level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logfe : logger: ILogger -> format: Format<'T, unit, string, unit> -> 'T
val logfe : logger: ILogger -> format: Format<'T, unit, string, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfe" />, but with extra arguments for event id and exception.
Expand All @@ -172,7 +174,7 @@ val logfe : logger: ILogger -> format: Format<'T, unit, string, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogfe : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogfe : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfe" />, but with extra arguments for event id and exception.
Expand All @@ -181,15 +183,15 @@ val vlogfe : logger: ILogger -> eventId: EventId -> exn: Exception -> format: St
/// <param name="exn">The exception to log.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val elogfe : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val elogfe : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Critical level.
/// </summary>
/// <param name="logger">The logger to output to.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val logfc : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
val logfc : logger: ILogger -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Like <see cref="logfc" />, but with extra arguments for event id and exception.
Expand All @@ -199,7 +201,7 @@ val logfc : logger: ILogger -> format: StringFormat<'T, unit> -> 'T
/// <param name="exn">The exception to include in the message.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val vlogfc : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val vlogfc : logger: ILogger -> eventId: EventId -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T

/// <summary>
/// Formatted error printing to an <see cref="T:Microsoft.Extensions.Logging.ILogger"/> at Critical level.
Expand All @@ -208,4 +210,4 @@ val vlogfc : logger: ILogger -> eventId: EventId -> exn: Exception -> format: St
/// <param name="exn">The exception to log.</param>
/// <param name="format">The input formatter.</param>
/// <returns>The return type and arguments of the formatter.</returns>
val elogfc : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit> -> 'T
val elogfc : logger: ILogger -> exn: Exception -> format: StringFormat<'T, unit, 'Tuple> -> 'T
14 changes: 14 additions & 0 deletions tests/FSharp.Logf.Tests/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ let allTests =
#endif
["@sensorInput", x]
)
testCase "Interpolated string with no holes" (fun () ->
(fun l -> logfi l $"Hello, world!")
|> assertEquivalent
"Hello, world!"
[]
)
testCase "Interpolated string with one hole" (fun () ->
let Person = "Sam"
(fun l ->
logfi l $"Hello, {Person}")
|> assertEquivalent
$"Hello, {Person}"
[]
)
let valuesF = caseData [ 5. / 3.; 50. / 3.; 500. / 3.; -(5. / 3.); -42.; 0.; -0.; 42.; Math.PI * 1_000_000.; -Math.PI * 1_000_000.; Math.PI / 1_000_000.; -Math.PI / 1_000_000. ]
let valuesD = caseData [ 0m; 12345.98m; -10m; 0.012m ]
let valuesI = caseData [ 0xdeadbeef; 42 ]
Expand Down

0 comments on commit 5abde0a

Please sign in to comment.