Crafter.Debug/interfaces/Crafter.Debug.Core-Result.cppm
Jorijn van der Graaf 89a12bcd61
feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client
both need, without the session:

- Value: a JSON value with its own parser and printer, so no consumer
  interface sees a JSON library. Integers stay integers and whole
  doubles stay doubles; NaN and the infinities, which JSON cannot
  spell, are written as strings rather than hidden, because a debugger
  must not lose a NaN; invalid UTF-8 is replaced so output always
  parses. Objects keep insertion order, the order an agent reads.
- Reflect: aggregate members and names through structured-binding
  packs and source_location, taken from Crafter.DB, plus scoped-enum
  names by probing a range of values. Self-tested with static_asserts
  so a compiler that changes function_name()'s shape fails the build.
- Codec<T>: encode, decode and describe for bool, integers, floating
  point, strings, enums, optional, vector, array, string-keyed maps,
  Value, Required<T> and every aggregate. Decode reports every
  problem in a call with its path and, for a typo, the likely intended
  name, and never throws: a bad call cannot take an app down, which a
  wrongly typed parameter could in 3DForts' hand-written from_json.
- Result, Error and Async: handler results, including ones that take
  frames to arrive, so callers need no ready:false polling.

project.cpp now builds Crafter.Debug.Core as its own library, used by
both the app library and the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00

93 lines
3.7 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// What a command handler returns besides a plain value: an Error with a
// stable code and a message written for the agent reading it, a Result that
// is either, and an Async for a result that takes frames to produce.
export module Crafter.Debug.Core:Result;
import std;
export namespace Crafter::Debug {
struct Error {
// Stable and machine-readable, "InvalidScene": scripts branch on it.
std::string code;
// For whoever reads the reply: what went wrong, and the next step
// ("not in a game; run Lobby_Start first").
std::string message;
};
template<class T = void>
class Result {
public:
// Anything T can be made from, so `return "text";` works for a
// Result<std::string>.
template<class U = T>
requires (std::constructible_from<T, U &&> && !std::same_as<std::remove_cvref_t<U>, Error> && !std::same_as<std::remove_cvref_t<U>, Result>)
Result(U&& value) : state(std::in_place_index<0>, std::forward<U>(value)) {}
Result(Error error) : state(std::in_place_index<1>, std::move(error)) {}
bool Ok() const { return state.index() == 0; }
explicit operator bool() const { return Ok(); }
T& Get() { return std::get<0>(state); }
const T& Get() const { return std::get<0>(state); }
const Error& GetError() const { return std::get<1>(state); }
private:
std::variant<T, Error> state;
};
template<>
class Result<void> {
public:
Result() = default;
Result(Error error) : error(std::move(error)) {}
bool Ok() const { return !error.has_value(); }
explicit operator bool() const { return Ok(); }
const Error& GetError() const { return *error; }
private:
std::optional<Error> error;
};
// A result that takes frames to arrive, such as a GPU readback. The
// session polls it once per Pump, on the app's thread, until it yields;
// the caller simply waits, with no ready:false loop of its own. The
// poller returns std::optional<T> (empty while not ready), or
// std::optional<Result<T>> to fail; for Async<void>, a bool or a
// std::optional<Result<>>.
template<class T = void>
class Async {
public:
template<class F>
requires std::invocable<F&>
Async(F poller) : poll(Wrap(std::move(poller))) {}
// Empty while not ready.
std::optional<Result<T>> Poll() { return poll(); }
private:
std::function<std::optional<Result<T>>()> poll;
template<class F>
static std::function<std::optional<Result<T>>()> Wrap(F poller) {
using R = std::invoke_result_t<F&>;
if constexpr (std::is_same_v<R, std::optional<Result<T>>>) {
return poller;
} else if constexpr (std::is_void_v<T>) {
static_assert(std::is_same_v<R, bool>, "an Async<void> poller returns bool or std::optional<Result<>>");
return [poller = std::move(poller)]() mutable -> std::optional<Result<T>> {
if (!poller()) return std::nullopt;
return Result<T>();
};
} else {
static_assert(std::is_same_v<R, std::optional<T>>, "an Async<T> poller returns std::optional<T> or std::optional<Result<T>>");
return [poller = std::move(poller)]() mutable -> std::optional<Result<T>> {
std::optional<T> value = poller();
if (!value) return std::nullopt;
return Result<T>(std::move(*value));
};
}
}
};
}