Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
loadable.hpp
Go to the documentation of this file.
1// ============================================================================
2// aria/loadable.hpp
3// ----------------------------------------------------------------------------
4// `Loadable<T>` -- the standard "loadable view-model" sum type, used
5// to present an asynchronous resource to the UI without each call
6// site having to hand-roll a state machine over (is_loading, data,
7// error). Designed by analogy with:
8//
9// SwiftUI: Result<T, Error> / async-let pattern
10// Compose: LoadState (Loading / NotLoading / Error)
11// RxJava: Observable<Result<T>>
12// Apollo: useQuery -> { loading, data, error, ... }
13//
14// Aria's flavour preserves the framework's two strict properties:
15//
16// 1. Errors are always `aria::Error` (not std::exception_ptr or
17// string). The kind / source / key fields are visible to the
18// view-model so it can route validation vs async failures vs
19// cancellations differently.
20// 2. Cancellation is NEVER surfaced as `Error`. A cancelled
21// operation collapses back to `Loading` (or `Idle` if there
22// was no prior data) -- it is not a result.
23//
24// Five states (LO-1):
25//
26// Idle -- nothing requested yet, no data, no error
27// Loading -- first fetch in flight (no prior data)
28// Refreshing -- subsequent fetch in flight while a prior
29// successful value is still on display
30// (stale-while-revalidate). `value()` returns the
31// stale-but-shown value.
32// Success -- fetch completed, value held
33// Error -- fetch failed. May still expose the last good
34// value via `value()` (SWR), distinct from
35// `error()`.
36//
37// The five states are kept in a single tagged enum + payload struct
38// rather than a `std::variant`, because variant<monostate, T, T,
39// Error> would erase the Loading-vs-Refreshing distinction and force
40// callers to look at multiple Properties. With a tag we keep the
41// state machine the single source of truth.
42// ============================================================================
43#pragma once
44
45#include "aria/error.hpp"
46
47#include <optional>
48#include <concepts>
49#include <functional>
50#include <type_traits>
51#include <utility>
52
53namespace aria {
54
57enum class LoadState : unsigned char {
58 Idle = 0,
62 Error = 4,
63};
64
73template<class T>
74class Loadable {
75public:
76 using value_type = T;
77
78 // ── Factories (LO-2) -----------------------------------------------
79 [[nodiscard]] static Loadable idle() noexcept { return {}; }
80
81 [[nodiscard]] static Loadable loading() {
82 Loadable l;
83 l.state_ = LoadState::Loading;
84 return l;
85 }
86
89 [[nodiscard]] static Loadable refreshing(T prior) {
90 Loadable l;
91 l.state_ = LoadState::Refreshing;
92 l.value_.emplace(std::move(prior));
93 return l;
94 }
95
96 [[nodiscard]] static Loadable success(T v) {
97 Loadable l;
98 l.state_ = LoadState::Success;
99 l.value_.emplace(std::move(v));
100 return l;
101 }
102
105 [[nodiscard]] static Loadable error(Error err) {
106 Loadable l;
107 l.state_ = LoadState::Error;
108 l.error_ = std::move(err);
109 return l;
110 }
111 [[nodiscard]] static Loadable error(Error err, T prior) {
112 Loadable l;
113 l.state_ = LoadState::Error;
114 l.error_ = std::move(err);
115 l.value_.emplace(std::move(prior));
116 return l;
117 }
118
119 // ── Predicates (LO-3) ----------------------------------------------
120 [[nodiscard]] LoadState state() const noexcept { return state_; }
121
122 [[nodiscard]] bool is_idle() const noexcept { return state_ == LoadState::Idle; }
123 [[nodiscard]] bool is_loading() const noexcept { return state_ == LoadState::Loading; }
124 [[nodiscard]] bool is_refreshing() const noexcept { return state_ == LoadState::Refreshing; }
125 [[nodiscard]] bool is_success() const noexcept { return state_ == LoadState::Success; }
126 [[nodiscard]] bool is_error() const noexcept { return state_ == LoadState::Error; }
127
130 [[nodiscard]] bool in_flight() const noexcept {
131 return state_ == LoadState::Loading || state_ == LoadState::Refreshing;
132 }
133
136 [[nodiscard]] bool has_value() const noexcept {
137 return value_.has_value();
138 }
139
143 [[nodiscard]] bool has_error() const noexcept {
144 return state_ == LoadState::Error && error_.has_value();
145 }
146
147 // ── Accessors (LO-4) -----------------------------------------------
149 [[nodiscard]] const T* value() const noexcept {
150 return value_.has_value() ? &*value_ : nullptr;
151 }
152
154 template<class U>
155 [[nodiscard]] T value_or(U&& fallback) const {
156 return value_.has_value() ? *value_ : T{std::forward<U>(fallback)};
157 }
158
160 [[nodiscard]] const Error* error() const noexcept {
161 return has_error() ? &*error_ : nullptr;
162 }
163
164 // ── Equality (LO-5) ------------------------------------------------
165 //
166 // Loadable<T> is equality-comparable iff T is. This is required
167 // for `Property<Loadable<T>>` to drop redundant `set()` writes
168 // per L-21 / E-11.
169 friend bool operator==(const Loadable& a, const Loadable& b)
170 requires std::equality_comparable<T> {
171 return a.state_ == b.state_ && a.value_ == b.value_ && a.error_ == b.error_;
172 }
173
174 // ── Functor / monad-ish helpers (LO-6) -----------------------------
175 //
176 // `map` projects the value when present, leaving the state /
177 // error untouched. The error type does NOT change because Aria
178 // already pins it to `aria::Error` (one error model framework-wide).
179 template<class F>
180 [[nodiscard]] auto map(F&& f) const
182 {
183 using U = std::remove_cvref_t<std::invoke_result_t<F, const T&>>;
184 Loadable<U> out;
185 out.set_state_(state_);
186 if (value_.has_value()) {
187 out.set_value_(std::invoke(std::forward<F>(f), *value_));
188 }
189 if (error_.has_value()) {
190 out.set_error_(*error_);
191 }
192 return out;
193 }
194
195private:
196 template<class> friend class Loadable;
197
198 void set_state_(LoadState s) noexcept { state_ = s; }
199 template<class U>
200 void set_value_(U&& v) { value_.emplace(std::forward<U>(v)); }
201 void set_error_(Error e) { error_.emplace(std::move(e)); }
202
204 std::optional<T> value_{};
205 std::optional<Error> error_{};
206};
207
208} // namespace aria
T value_type
Definition loadable.hpp:76
friend bool operator==(const Loadable &a, const Loadable &b)
Definition loadable.hpp:169
bool is_refreshing() const noexcept
Definition loadable.hpp:124
bool is_loading() const noexcept
Definition loadable.hpp:123
bool has_error() const noexcept
True iff an error is currently surfaced.
Definition loadable.hpp:143
const T * value() const noexcept
Returns a pointer to the value if has_value(), else nullptr.
Definition loadable.hpp:149
bool has_value() const noexcept
True iff the loadable currently has a value to show (Success, Refreshing, or Error+prior).
Definition loadable.hpp:136
LoadState state() const noexcept
Definition loadable.hpp:120
static Loadable error(Error err)
Build an Error state.
Definition loadable.hpp:105
bool is_idle() const noexcept
Definition loadable.hpp:122
bool is_error() const noexcept
Definition loadable.hpp:126
bool in_flight() const noexcept
True iff a fetch is currently in flight (Loading or Refreshing).
Definition loadable.hpp:130
static Loadable loading()
Definition loadable.hpp:81
bool is_success() const noexcept
Definition loadable.hpp:125
static Loadable refreshing(T prior)
Build a Refreshing state from an existing Success payload.
Definition loadable.hpp:89
T value_or(U &&fallback) const
Returns a copy of the value, or constructs T from fallback if absent.
Definition loadable.hpp:155
const Error * error() const noexcept
Returns a pointer to the error if has_error(), else nullptr.
Definition loadable.hpp:160
auto map(F &&f) const -> Loadable< std::remove_cvref_t< std::invoke_result_t< F, const T & > > >
Definition loadable.hpp:180
static Loadable success(T v)
Definition loadable.hpp:96
friend class Loadable
Definition loadable.hpp:196
static Loadable error(Error err, T prior)
Definition loadable.hpp:111
static Loadable idle() noexcept
Definition loadable.hpp:79
Definition signal.hpp:12
LoadState
Discriminator for Loadable<T>.
Definition loadable.hpp:57
@ Loading
Definition loadable.hpp:59
@ Success
Definition loadable.hpp:61
@ Error
Definition loadable.hpp:62
@ Refreshing
Definition loadable.hpp:60
@ Idle
Definition loadable.hpp:58
One uniform error record.
Definition error.hpp:129