arvo_api/lib.rs
1//! The types of Arvo's engine API, generated from its protobuf contract.
2//!
3//! Every message in the [contract](https://github.com/wjpin84/arvo-engine-api)
4//! is here, one module per proto package and every shape at the root as
5//! well. Each carries the comment written on it in the proto: that is the
6//! single source of what a field means, and nothing is documented here that
7//! is not documented there.
8//!
9//! # Messages only
10//!
11//! There is no transport in this crate, which is what lets it build for
12//! WebAssembly. A browser-side front end links this; the gRPC stubs live in
13//! `arvo-client`, which depends on this and re-exports it.
14//!
15//! # What is not generated
16//!
17//! proto3 makes every nested message optional and spells a Rust enum with
18//! data as a `oneof`. The [ergonomics](#accessors-and-constructors) at the
19//! bottom of this crate are the small amount of hand-written Rust that says
20//! what the generated shapes cannot: which nested fields the engine always
21//! sends, and how to build or match a `oneof` without naming its `Of`.
22//!
23//! # Accessors and constructors
24//!
25//! - A field the engine always fills has an accessor of the same name that
26//! returns it by reference: `study.strategy()` rather than
27//! `study.strategy.as_ref().unwrap()`. It panics only if the sender left
28//! the field out, which would mean the two sides disagree about the
29//! contract, and an empty panel would hide that.
30//! - A `oneof` has constructors (`RecordView::study(view)`,
31//! `EventKindView::plugin(id, reachable)`) and, where a reader wants one
32//! question answered, an accessor (`plugin.reachable()`, `event.of()`).
33//! - [`count`] and [`size`] convert a count between `usize` and the `u32` it
34//! is on the wire, saturating rather than panicking.
35//!
36//! # Serde
37//!
38//! Every shape derives `Serialize` and `Deserialize` with serde's defaults,
39//! because the same shapes cross JSON bridges as well as gRPC.
40
41#![warn(missing_docs)]
42
43/// Generated from `protos/` by `cargo run -p generate` and committed.
44///
45/// The module tree mirrors the package names, because that is how a type in
46/// one package refers to a type in another. The services are not here;
47/// `arvo-client` generates those and points them back at these types.
48mod generated;
49
50/// One module per domain, for code that wants to say which it means.
51pub use generated::arvo::{
52 common::v1 as common, market::v1 as market, platform::v1 as platform,
53 portfolio::v1 as portfolio, research::v1 as research, session::v1 as session,
54};
55
56/// Every shape at the root as well, because a window names a view on nearly
57/// every line and `arvo_api::research::StudyView` would be noise. The names
58/// do not collide: proto has no overloading and neither does this.
59pub use generated::arvo::{
60 common::v1::*, market::v1::*, platform::v1::*, portfolio::v1::*, research::v1::*,
61 session::v1::*,
62};
63
64/// What the generated shapes cannot say for themselves.
65///
66/// proto3 makes every message field absent-able, so a nested shape arrives as
67/// an `Option` even where it is always sent. These accessors say "the sender
68/// always fills this", in one place, instead of at every reader.
69mod ergonomics {
70 use super::{
71 platform::{event_kind_view, plugin_status_view},
72 research::record_view,
73 EventKindView, EventView, FeedEvent, FindingsEvent, PluginEvent, SessionEvent, SeverityView,
74 StreamEvent,
75 BookView, ImportView, MetricsView, PanelView, PluginStatusView, PluginStatusViewReachable,
76 PluginStatusViewUnreachable, PluginView, PortfolioView, RecordView, ReportedView, StudyView,
77 TradesView, WalkForwardView,
78 };
79
80 /// A count on the wire. Rust counts in `usize`; the contract is explicit
81 /// about width, because a reader in another language has to be.
82 #[must_use]
83 pub fn count(n: usize) -> u32 {
84 u32::try_from(n).unwrap_or(u32::MAX)
85 }
86
87 /// A count read back, for code that indexes with it.
88 #[must_use]
89 pub fn size(n: u32) -> usize {
90 usize::try_from(n).unwrap_or(usize::MAX)
91 }
92
93 macro_rules! always {
94 ($owner:ty, $field:ident, $ty:ty) => {
95 impl $owner {
96 /// The sender always fills this.
97 ///
98 /// # Panics
99 ///
100 /// Only if it did not, which would mean the two sides disagree
101 /// about the contract.
102 #[must_use]
103 pub fn $field(&self) -> &$ty {
104 self.$field.as_ref().expect(concat!(stringify!($owner), " always carries ", stringify!($field)))
105 }
106 }
107 };
108 }
109 always!(StudyView, strategy, MetricsView);
110 always!(StudyView, benchmark, MetricsView);
111 always!(StudyView, trades_detail, TradesView);
112 always!(WalkForwardView, strategy, MetricsView);
113 always!(WalkForwardView, benchmark, MetricsView);
114 always!(WalkForwardView, trades_detail, TradesView);
115 always!(BookView, metrics, MetricsView);
116 always!(PortfolioView, import, ImportView);
117
118 impl PluginView {
119 /// Whether the last probe reached it.
120 #[must_use]
121 pub fn reachable(&self) -> bool {
122 matches!(self.status.as_ref().and_then(|s| s.of.as_ref()), Some(plugin_status_view::Of::Reachable(_)))
123 }
124
125 /// What it said it is, if the last probe reached it.
126 #[must_use]
127 pub fn reached(&self) -> Option<&PluginStatusViewReachable> {
128 match self.status.as_ref().and_then(|status| status.of.as_ref()) {
129 Some(plugin_status_view::Of::Reachable(status)) => Some(status),
130 _ => None,
131 }
132 }
133
134 /// Why the last probe did not reach it, if it did not.
135 ///
136 /// A plugin whose status is missing counts as unreached: the window
137 /// says so rather than drawing it as healthy.
138 #[must_use]
139 pub fn unreachable(&self) -> Option<&str> {
140 match self.status.as_ref().and_then(|status| status.of.as_ref()) {
141 Some(plugin_status_view::Of::Unreachable(status)) => Some(&status.reason),
142 Some(plugin_status_view::Of::Reachable(_)) => None,
143 None => Some("the engine sent no status"),
144 }
145 }
146 }
147
148 impl PluginStatusView {
149 /// It answered, and said what it is.
150 #[must_use]
151 pub fn reachable(name: String, version: String, capabilities: Vec<String>) -> Self {
152 Self {
153 of: Some(plugin_status_view::Of::Reachable(PluginStatusViewReachable { name, version, capabilities })),
154 }
155 }
156
157 /// It did not answer, and why.
158 #[must_use]
159 pub fn unreachable(reason: String) -> Self {
160 Self { of: Some(plugin_status_view::Of::Unreachable(PluginStatusViewUnreachable { reason })) }
161 }
162 }
163
164 impl EventKindView {
165 /// A plugin became reachable, or stopped being.
166 #[must_use]
167 pub fn plugin(id: String, reachable: bool) -> Self {
168 Self { of: Some(event_kind_view::Of::Plugin(PluginEvent { id, reachable })) }
169 }
170
171 /// A broker connection came up or went away.
172 #[must_use]
173 pub fn feed(id: String, connected: bool) -> Self {
174 Self { of: Some(event_kind_view::Of::Feed(FeedEvent { id, connected })) }
175 }
176
177 /// The live price stream stopped or came back.
178 #[must_use]
179 pub fn stream(live: bool) -> Self {
180 Self { of: Some(event_kind_view::Of::Stream(StreamEvent { live })) }
181 }
182
183 /// Stored findings went stale.
184 #[must_use]
185 pub fn findings(count: u32) -> Self {
186 Self { of: Some(event_kind_view::Of::Findings(FindingsEvent { count })) }
187 }
188
189 /// A trading session changed state.
190 #[must_use]
191 pub fn session(id: String, state: String) -> Self {
192 Self { of: Some(event_kind_view::Of::Session(SessionEvent { id, state })) }
193 }
194 }
195
196 impl EventView {
197 /// An event, with the kind and the text a renderer shows.
198 #[must_use]
199 pub fn new(kind: EventKindView, title: String, detail: String, severity: SeverityView) -> Self {
200 Self { kind: Some(kind), title, detail, severity: severity as i32 }
201 }
202
203 /// What happened, structurally, if the sender said.
204 #[must_use]
205 pub fn of(&self) -> Option<&event_kind_view::Of> {
206 self.kind.as_ref()?.of.as_ref()
207 }
208 }
209
210 impl RecordView {
211 /// A study, as a reopened record.
212 #[must_use]
213 pub fn study(view: StudyView) -> Self {
214 Self { of: Some(record_view::Of::Study(view)) }
215 }
216
217 /// A walk-forward, as a reopened record.
218 #[must_use]
219 pub fn walk_forward(view: WalkForwardView) -> Self {
220 Self { of: Some(record_view::Of::Walkforward(view)) }
221 }
222
223 /// A panel, as a reopened record.
224 #[must_use]
225 pub fn panel(view: PanelView) -> Self {
226 Self { of: Some(record_view::Of::Panel(view)) }
227 }
228
229 /// Evidence Arvo did not compute, as a reopened record.
230 #[must_use]
231 pub fn reported(view: ReportedView) -> Self {
232 Self { of: Some(record_view::Of::Reported(view)) }
233 }
234 }
235}
236
237pub use ergonomics::{count, size};