Module: Axn::Internal::Rendering

Defined in:
lib/axn/internal/rendering.rb

Overview

Facts about a foreign object, for a message being built ABOUT it.

An error path owes two obligations and every site that met one of them by hand met only that one. First, do not DISPATCH what the object can override: an inspect, a class, a message or a backtrace called to build the message can raise and replace the failure being reported, and outside StandardError that escapes the rescue meant to settle it. Second, do not join raw foreign BYTES: what a class name or a message HOLDS is foreign too, and a String with no UTF-8-compatible rendering cannot be joined to axn's own UTF-8 prose at all. Both halves live here so no caller can meet one and miss the other.

Sits above axn/exceptions because it needs Internal::ClassName; the byte half it renders through is one layer further down (Internal::Text) precisely so axn/exceptions can render its own messages without requiring the file that requires it.

Constant Summary collapse

UNKNOWN_LOCATION =
"unknown location"

Class Method Summary collapse

Class Method Details

.class_name(value) ⇒ Object

A value's CLASS named in prose, both halves composed: ClassName answers from bound base implementations so nothing the value defines runs, and the constant path it answers with is rendered because a constant may hold non-UTF-8 bytes (Object.const_set(:"Caf\xE9", Class.new) is accepted, and Module#to_s hands those bytes back).

DELEGATED to Internal::RenderedClassName, which owns that composition for the message paths built on axn/exceptions — they cannot reach this file (it requires theirs), but this file requires theirs, so the dependency runs one way and there is one composer rather than two identical ones.



40
# File 'lib/axn/internal/rendering.rb', line 40

def class_name(value) = RenderedClassName.of(value)

.exception_message(exception) ⇒ Object

An exception's own message, as a UTF-8 String this method owns.



47
# File 'lib/axn/internal/rendering.rb', line 47

def exception_message(exception) = Text.renderable(raw_exception_message(exception))

.exception_source_location(exception) ⇒ Object

Just the filename and line an exception came from, for a warning that names where a swallowed failure happened.

Read through a BOUND Exception#backtrace, which settles two things at once. An override cannot substitute a non-Array or a non-String frame — and an override that answers non-nil stops Ruby recording a real backtrace at all, so the bound reader sees nil and this degrades to UNKNOWN_LOCATION instead of crashing on the override's answer.

An empty backtrace has to be tolerated, and so does a BLANK frame inside one: raise repopulates a nil backtrace, but a backtrace reconstructed with set_backtrace (what a death handler rebuilding one from job data hands us) is kept exactly as given, [""] included. This runs from inside an ensure often enough that a raise here would replace the exception already in flight.



91
92
93
94
95
96
97
98
99
100
# File 'lib/axn/internal/rendering.rb', line 91

def exception_source_location(exception)
  frame = first_frame(exception)
  return UNKNOWN_LOCATION unless frame

  path = STRING_SPLIT.bind_call(frame).first
  return UNKNOWN_LOCATION unless Identity.kind?(path, ::String)

  basename = STRING_SPLIT.bind_call(path, "/").last
  Text.renderable(STRING_SPLIT.bind_call(basename, ":")[0, 2].join(":"))
end

.module_name(mod) ⇒ Object

A class or module named in its own right — a declared type:, a tool axn — rather than a value's class. Same two halves.



44
# File 'lib/axn/internal/rendering.rb', line 44

def module_name(mod) = Text.renderable(ClassName.of_module(mod))

.value_rendering(value) ⇒ Object

A VALUE's own rendering, as a UTF-8 String this method owns — or nil when it has none.

For a value that IS the message rather than one being described: a fail! reason, a declared error/success handler's return, a user_facing: handler's return. to_s is DISPATCHED, deliberately, on the same terms as exception_message dispatches #message — the object's own rendering is what makes the message useful, and routing everything through Object#to_s would degrade every well-behaved value to defend against broken ones. But these values are rendered while a failure is being SETTLED, and again on every later result.error/result.message read, and interpolating one dispatches its to_s under no guard at all. So the call is made here and its failure absorbed.

Absorbs every class, including those axn never swallows: the outcome being settled has to win over anything raised while rendering it, and a value's to_s is not a path a signal travels through. A non-String to_s is a failure too, since rendering its answer would dispatch again.

A String is rendered from its BYTES with nothing dispatched, which is what interpolation itself does ("#{}" takes a String as it stands), so a subclass whose to_s raises still renders as its text.

nil rather than a fallback of its own, because what an unrenderable value should degrade TO belongs to the caller: a composed message names the value's class (class_name, the fallback this module and Identity.describe both take), while a user-facing message has a better answer to fall back on.



70
71
72
73
74
75
76
77
# File 'lib/axn/internal/rendering.rb', line 70

def value_rendering(value)
  return Text.renderable(value) if Identity.kind?(value, ::String)

  rendered = value.to_s
  Identity.kind?(rendered, ::String) ? Text.renderable(rendered) : nil
rescue ::Exception # rubocop:disable Lint/RescueException
  nil
end