feat: add OTP logger handler bindings + fix latent bare-ok Result bug - #108
Conversation
Add faithful 1:1 bindings for logger:add_handler/3, remove_handler/1,
get_handler_config/1, and set_handler_config/2,3 to Fable.Beam.Logger,
so downstream apps no longer need to fall back to raw emitErlExpr for
handler management.
All return the raw `ok | {error, term()}` Erlang term rather than
swallowing it. Also fix update_handler_config to return obj instead of
unit for the same reason (no in-repo callers, so non-breaking here).
Add a TestLogger test that round-trips a handler through add_handler/3
and remove_handler/1 on the BEAM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw obj returns/params from the first pass with the house
conventions:
- Returns: ok | {error, term()} now maps to Result<unit, Dynamic>
(matching Supervisor/File). add/remove/set/update_handler_config use
an [<Emit>] case-wrapper to bridge OTP's bare `ok` into Fable's
{ok, ok} Result representation, the same pattern Ets.fs/File.fs use.
- get_handler_config returns Result<BeamMap<Atom, obj>, Dynamic>; OTP
already returns {ok, Config} | {error, _}, which is Fable's Result
shape, so no wrapper is needed.
- config params are now BeamMap<Atom, obj> (the typed map already used
for log metadata in this file) instead of obj. value stays obj since
handler-config values are heterogeneous.
Update the round-trip test to assert Ok (), which only passes if the
bare-ok bridge is correct on the BEAM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Updated to address the
The round-trip test now asserts case logger:add_handler(HandlerId, Modle, Config) of
ok -> {ok, ok};
{error, LoggerAddHandlerReason__} -> {error, LoggerAddHandlerReason__}
end
|
supervisor:terminate_child/2 and delete_child/2 return a bare `ok` atom,
but the bindings typed them as Result<unit, Atom> — which Fable encodes
as {ok, ok}. A successful call therefore never matched Ok () on the F#
side (a latent bug; no test exercised the ok path). Wrap both in the
same [<Emit>] case-converter used for File/Logger so OTP's `ok` surfaces
as Ok () and {error, Reason} as Error Reason.
Add a test_basic_sup supervisor callback (one transient counter child)
and four tests covering both the Ok () and Error not_found paths of
terminate_child and delete_child.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed Both return a bare Added verification:
While writing these I confirmed against OTP directly that a
|
Document the latent bug fixed in this PR: a plain Result<unit, _> binding
over an OTP function that returns a bare `ok` atom compiles cleanly but is
silently wrong at runtime, because Fable encodes Ok () as {ok, ok}. The
error path masks it (it's already in Result shape), so only a success-path
test reveals the missing [<Emit>] wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Started as: add the missing OTP
loggerhandler-management bindings so downstream apps don't fall back to rawemitErlExpr. While doing it we found — and fixed — a latent runtime bug in the existingSupervisorbindings, and documented the underlying gotcha in the bindings guide.1. New
loggerhandler bindings (src/otp/Logger.fs)Faithful 1:1 bindings for the handler-management functions (OTP docs):
add_handler/3,remove_handler/1,get_handler_config/1,set_handler_config/2,set_handler_config/3Design:
obj.ok | {error, term()}→Result<unit, Dynamic>;get_handler_config→Result<BeamMap<Atom, obj>, Dynamic>. Matches the house convention (File/Supervisor).obj. Config params areBeamMap<Atom, obj>(the same typed map already used for log metadata in this file).valuestaysobjsince handler-config values are genuinely heterogeneous.update_handler_config's latent bug: it returnedunit, silently swallowing{error, _}(e.g. when OTP rejects changing alogger_std_hhandler's type at runtime). NowResult<unit, Dynamic>.2. Fix: latent bare-
okbug inSupervisor(src/otp/Supervisor.fs)supervisor:terminate_child/2anddelete_child/2were typedResult<unit, Atom>but bind to OTP functions that return a bareokatom. Fable encodesOk ()as the tuple{ok, ok}, so a successful call matched neither Result branch — every success was silently wrong at runtime. (No test covered theokpath; the existing test only hitwhich_childrenon a missing supervisor.) Both now use an[<Emit>]casewrapper to bridgeok -> {ok, ok}, the same patternFile/Loggeruse.3. Verification
test/test_basic_sup.erl— aone_for_onesupervisor with a singletransientchild, wired into thejust testpipeline (justfile).test/TestSupervisor.fs— 4 new tests covering both theOk ()andError not_foundpaths ofterminate_childanddelete_child(the success path is the one that exposes a missing wrapper).test/TestLogger.fs— round-trips a handler throughadd_handler/3+remove_handler/1, assertingOk ()(only passes if the bare-okbridge is correct on the BEAM).Confirmed against OTP directly that a
temporarychild's spec is auto-removed on termination — hence thetransientchild sodelete_childcan succeed afterterminate_child.just buildclean (0 warnings) ·just test→ 356/356 passing.Downstream apps can now write, with no
emitErlExpr:4. Docs (
BINDINGS-GUIDE.md)Documented the bare-
oktrap as a new anti-pattern: a plainResult<unit, _>binding over a bare-okOTP function compiles cleanly but is silently wrong at runtime, because Fable encodesOk ()as{ok, ok}and the error path masks the bug. Includes the fix, the carve-out (no wrapper needed when OTP already returns{ok, V} | {error, R}), and the rule: always test the success path of a bare-okbinding.Commits
feat:add OTP logger handler-management bindingsrefactor:type logger handler bindings with Result and BeamMapfix:bridge bare ok in supervisor terminate_child/delete_childdocs:warn about Result<unit,_> over bare-ok OTP functions🤖 Generated with Claude Code