Packages
oaspec
0.50.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
src/oaspec/internal/progress.gleam
//// Progress reporter used by the long-running pipeline phases (parse,
//// normalize, resolve, codegen, ...) to emit human-readable status
//// lines without forcing the pure pipeline to depend on `io`. The
//// pure entry points all accept a `Reporter`; library callers that
//// don't need progress hand in `noop()` and pay no cost, while the
//// CLI passes a reporter that prints `[mm:ss.mmm] message` lines.
////
//// Stages on the GitHub REST OpenAPI spec (~12 MB JSON, ~10k schemas)
//// take long enough that without progress lines the user can't tell
//// whether the process is hung or working — see issue #352.
import gleam/int
import gleam/io
import gleam/string
@external(erlang, "oaspec_ffi", "monotonic_ms")
@external(javascript, "../../oaspec_ffi.mjs", "monotonic_ms")
fn monotonic_ms() -> Int
/// A progress reporter. The `emit` callback receives a one-line
/// human-readable status message and decides where to send it (CLI
/// writes to stdout, library callers pass `noop()`).
pub opaque type Reporter {
Reporter(emit: fn(String) -> Nil)
}
/// A reporter that drops every event. Used by the pure library API
/// when the caller does not want progress output.
pub fn noop() -> Reporter {
Reporter(emit: fn(_) { Nil })
}
/// Build a reporter from a side-effecting callback. The CLI uses
/// `from_fn` with a stdout-printing callback; tests can use it to
/// capture events into a list ref.
pub fn from_fn(emit: fn(String) -> Nil) -> Reporter {
Reporter(emit:)
}
/// Emit a single progress event. Cheap when the reporter is `noop()`.
pub fn report(reporter reporter: Reporter, message message: String) -> Nil {
reporter.emit(message)
}
/// Time `body` in milliseconds and return both the elapsed time and
/// the body's result. Callers wrap each pipeline stage so the
/// reporter line includes "(took 1.23s)".
pub fn timed(body: fn() -> a) -> #(Int, a) {
let start = monotonic_ms()
let result = body()
let end = monotonic_ms()
#(end - start, result)
}
/// Format a millisecond duration as a compact human string, e.g.
/// `"123ms"`, `"4.56s"`, `"1m23.4s"`. Used in progress lines so the
/// user can tell at a glance which stage is the slow one.
pub fn format_ms(ms: Int) -> String {
case ms < 1000 {
True -> int.to_string(ms) <> "ms"
False -> {
let total_seconds = ms / 1000
let tenths = { ms - total_seconds * 1000 } / 100
case total_seconds >= 60 {
True -> {
let minutes = total_seconds / 60
let seconds = total_seconds - minutes * 60
int.to_string(minutes)
<> "m"
<> int.to_string(seconds)
<> "."
<> int.to_string(tenths)
<> "s"
}
False -> {
int.to_string(total_seconds) <> "." <> int.to_string(tenths) <> "s"
}
}
}
}
}
/// Build a reporter that prints `[+elapsed] message` lines to stdout,
/// where `elapsed` is the time since the reporter was created. Used
/// by the CLI so the user can see which phase is currently running
/// and how long each phase took.
pub fn stdout_with_elapsed() -> Reporter {
let started_at = monotonic_ms()
from_fn(fn(message) {
let elapsed = monotonic_ms() - started_at
io.println("[+" <> pad_left(format_ms(elapsed), 7) <> "] " <> message)
})
}
fn pad_left(value: String, width: Int) -> String {
let len = string.length(value)
case len >= width {
True -> value
False -> string.repeat(" ", width - len) <> value
}
}