ic_query/ic/model.rs
1//! Module: ic::model
2//!
3//! Responsibility: public IC Dashboard canister requests, source data, reports, and errors.
4//! Does not own: HTTP transport, source validation, report assembly, or rendering.
5//! Boundary: preserves raw Dashboard values and explicit off-chain provenance.
6
7#[cfg(feature = "host")]
8use crate::runtime::RuntimeError;
9use serde::Serialize;
10#[cfg(feature = "host")]
11use thiserror::Error as ThisError;
12
13///
14/// IcCanisterRequest
15///
16/// Request accepted by the official Dashboard canister report builder.
17///
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct IcCanisterRequest {
21 /// Dashboard API base endpoint.
22 pub source_endpoint: String,
23 /// Collection time as Unix seconds.
24 pub now_unix_secs: u64,
25 /// Canister principal to inspect.
26 pub canister_id: String,
27}
28
29impl IcCanisterRequest {
30 /// Construct a live Dashboard canister request.
31 #[must_use]
32 pub fn new(
33 source_endpoint: impl Into<String>,
34 now_unix_secs: u64,
35 canister_id: impl Into<String>,
36 ) -> Self {
37 Self {
38 source_endpoint: source_endpoint.into(),
39 now_unix_secs,
40 canister_id: canister_id.into(),
41 }
42 }
43}
44
45///
46/// IcCanisterFilters
47///
48/// Official Dashboard filters shared by canister count and page requests.
49///
50
51#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
52pub struct IcCanisterFilters {
53 /// Select canisters according to whether the Dashboard records a name.
54 pub has_name: Option<bool>,
55 /// Select canisters assigned to this Subnet principal.
56 pub subnet_id: Option<String>,
57 /// Select canisters controlled by this principal.
58 pub controller_id: Option<String>,
59 /// Raw Dashboard language labels to include.
60 pub languages: Vec<String>,
61 /// Raw Dashboard canister classifications to include.
62 pub canister_types: Vec<String>,
63 /// Raw Dashboard text search, between two and one hundred characters.
64 pub query: Option<String>,
65}
66
67///
68/// IcCanisterCountRequest
69///
70/// Request for one bounded official Dashboard canister-count lookup.
71///
72
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct IcCanisterCountRequest {
75 /// Dashboard API v4 base endpoint.
76 pub source_endpoint: String,
77 /// Collection time as Unix seconds.
78 pub now_unix_secs: u64,
79 /// Filters applied by the Dashboard.
80 pub filters: IcCanisterFilters,
81}
82
83impl IcCanisterCountRequest {
84 /// Construct a live Dashboard canister-count request without filters.
85 #[must_use]
86 pub fn new(source_endpoint: impl Into<String>, now_unix_secs: u64) -> Self {
87 Self {
88 source_endpoint: source_endpoint.into(),
89 now_unix_secs,
90 filters: IcCanisterFilters::default(),
91 }
92 }
93
94 /// Set the Dashboard filters used by this request.
95 #[must_use]
96 pub fn with_filters(mut self, filters: IcCanisterFilters) -> Self {
97 self.filters = filters;
98 self
99 }
100}
101
102///
103/// IcCanisterPageRequest
104///
105/// Request for one bounded official Dashboard canister page.
106///
107
108#[derive(Clone, Debug, Eq, PartialEq)]
109pub struct IcCanisterPageRequest {
110 /// Dashboard API v4 base endpoint.
111 pub source_endpoint: String,
112 /// Collection time as Unix seconds.
113 pub now_unix_secs: u64,
114 /// Filters applied by the Dashboard.
115 pub filters: IcCanisterFilters,
116 /// Maximum rows requested from the API.
117 pub limit: u16,
118 /// Exclusive forward cursor returned by an earlier page.
119 pub after: Option<String>,
120 /// Exclusive backward cursor returned by an earlier page.
121 pub before: Option<String>,
122}
123
124impl IcCanisterPageRequest {
125 /// Construct a live Dashboard page request with the default bounded limit.
126 #[must_use]
127 pub fn new(source_endpoint: impl Into<String>, now_unix_secs: u64) -> Self {
128 Self {
129 source_endpoint: source_endpoint.into(),
130 now_unix_secs,
131 filters: IcCanisterFilters::default(),
132 limit: super::DEFAULT_IC_CANISTER_PAGE_LIMIT,
133 after: None,
134 before: None,
135 }
136 }
137
138 /// Set the Dashboard filters used by this request.
139 #[must_use]
140 pub fn with_filters(mut self, filters: IcCanisterFilters) -> Self {
141 self.filters = filters;
142 self
143 }
144
145 /// Set the maximum number of returned rows.
146 #[must_use]
147 pub const fn with_limit(mut self, limit: u16) -> Self {
148 self.limit = limit;
149 self
150 }
151
152 /// Set an exclusive forward cursor.
153 #[must_use]
154 pub fn with_after(mut self, after: impl Into<String>) -> Self {
155 self.after = Some(after.into());
156 self
157 }
158
159 /// Set an exclusive backward cursor.
160 #[must_use]
161 pub fn with_before(mut self, before: impl Into<String>) -> Self {
162 self.before = Some(before.into());
163 self
164 }
165}
166
167///
168/// IcCanisterUpgrade
169///
170/// One proposal-linked canister upgrade recorded by the Dashboard API.
171///
172
173#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
174pub struct IcCanisterUpgrade {
175 /// Proposal execution time as raw Unix seconds.
176 pub executed_timestamp_seconds: u64,
177 /// Wasm module hash as raw lowercase hexadecimal text.
178 pub module_hash: String,
179 /// NNS proposal that installed this module.
180 pub proposal_id: u64,
181}
182
183///
184/// IcDashboardReportProvenance
185///
186/// Shared off-chain provenance and authority guarantees for Dashboard reports.
187///
188
189#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
190pub struct IcDashboardReportProvenance {
191 /// Report schema version.
192 pub schema_version: u32,
193 /// Network represented by the official Dashboard API.
194 pub network: String,
195 /// Authority that supplied the report fields.
196 pub authority: String,
197 /// Dashboard API base endpoint queried by the source.
198 pub source_endpoint: String,
199 /// Time this report was collected.
200 pub fetched_at: String,
201 /// Collector identity.
202 pub fetched_by: String,
203 /// Whether the API response is cryptographically certified IC state.
204 pub certified: bool,
205 /// Whether every returned value is guaranteed to describe one point in time.
206 pub point_in_time_guaranteed: bool,
207}
208
209///
210/// IcCanisterReport
211///
212/// One live canister metadata report from the official Dashboard API.
213///
214
215#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
216pub struct IcCanisterReport {
217 /// Shared Dashboard provenance, flattened in serialized report JSON.
218 #[serde(flatten)]
219 pub provenance: IcDashboardReportProvenance,
220 /// Canonical canister principal.
221 pub canister_id: String,
222 /// Dashboard database row identifier.
223 pub dashboard_id: u64,
224 /// Raw optional Dashboard canister classification.
225 pub canister_type: Option<String>,
226 /// Raw Dashboard canister name; an empty string means no name was recorded.
227 pub name: String,
228 /// Canonical Subnet principal recorded by the Dashboard.
229 pub subnet_id: String,
230 /// Canonically ordered controller principals recorded by the Dashboard.
231 pub controllers: Vec<String>,
232 /// Raw Dashboard language label; an empty string means no language was recorded.
233 pub language: String,
234 /// Raw current module hash; an empty string means no hash was recorded.
235 pub module_hash: String,
236 /// Raw Dashboard row update timestamp.
237 pub dashboard_updated_at: String,
238 /// Number of proposal-linked upgrades when history is available.
239 pub upgrade_count: Option<usize>,
240 /// Proposal-linked upgrade history, or `None` when the Dashboard returned `null`.
241 pub upgrades: Option<Vec<IcCanisterUpgrade>>,
242}
243
244///
245/// IcCanisterCountReport
246///
247/// One filtered canister count from the official Dashboard API.
248///
249
250#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
251pub struct IcCanisterCountReport {
252 /// Shared Dashboard provenance, flattened in serialized report JSON.
253 #[serde(flatten)]
254 pub provenance: IcDashboardReportProvenance,
255 /// Filters applied by the Dashboard.
256 pub filters: IcCanisterFilters,
257 /// Number of matching Dashboard canister records.
258 pub total: u64,
259}
260
261///
262/// IcCanisterPageController
263///
264/// One controller entry returned by the Dashboard canister collection API.
265///
266
267#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
268pub struct IcCanisterPageController {
269 /// Canonical controller principal.
270 pub principal_id: String,
271 /// Raw optional Dashboard metadata associated with the controller.
272 pub raw_metadata: Option<String>,
273}
274
275///
276/// IcCanisterPageRow
277///
278/// One discovery row from a bounded Dashboard canister page.
279///
280
281#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
282pub struct IcCanisterPageRow {
283 /// Canonical canister principal.
284 pub canister_id: String,
285 /// Dashboard database row identifier.
286 pub dashboard_id: u64,
287 /// Raw optional Dashboard canister classification.
288 pub canister_type: Option<String>,
289 /// Raw Dashboard canister name.
290 pub name: String,
291 /// Canonical Subnet principal recorded by the Dashboard.
292 pub subnet_id: String,
293 /// Canonically ordered controller entries recorded by the Dashboard.
294 pub controllers: Vec<IcCanisterPageController>,
295 /// Raw Dashboard language label.
296 pub language: String,
297 /// Raw current module hash.
298 pub module_hash: String,
299 /// Raw Dashboard row update timestamp.
300 pub dashboard_updated_at: String,
301}
302
303///
304/// IcCanisterPageReport
305///
306/// One explicitly bounded page from the official Dashboard canister collection.
307///
308
309#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
310pub struct IcCanisterPageReport {
311 /// Shared Dashboard provenance, flattened in serialized report JSON.
312 #[serde(flatten)]
313 pub provenance: IcDashboardReportProvenance,
314 /// Filters applied by the Dashboard.
315 pub filters: IcCanisterFilters,
316 /// Maximum rows requested from the API.
317 pub requested_limit: u16,
318 /// Number of rows returned in this report.
319 pub returned_count: usize,
320 /// Exclusive forward cursor supplied to this request.
321 pub after: Option<String>,
322 /// Exclusive backward cursor supplied to this request.
323 pub before: Option<String>,
324 /// Cursor for an explicit request for the preceding page.
325 pub previous_cursor: Option<String>,
326 /// Cursor for an explicit request for the following page.
327 pub next_cursor: Option<String>,
328 /// Canister discovery rows in Dashboard canister-id order.
329 pub rows: Vec<IcCanisterPageRow>,
330}
331
332///
333/// IcSourceRequest
334///
335/// Shared endpoint and collection provenance for IC Dashboard source calls and results.
336///
337
338#[cfg(feature = "host")]
339#[derive(Clone, Debug, Eq, PartialEq)]
340pub struct IcSourceRequest {
341 /// Dashboard API base endpoint.
342 pub endpoint: String,
343 /// Collection timestamp in UTC.
344 pub fetched_at: String,
345 /// Collector identity recorded in report provenance.
346 pub fetched_by: String,
347}
348
349#[cfg(feature = "host")]
350impl IcSourceRequest {
351 /// Construct source-call provenance.
352 #[must_use]
353 pub fn new(
354 endpoint: impl Into<String>,
355 fetched_at: impl Into<String>,
356 fetched_by: impl Into<String>,
357 ) -> Self {
358 Self {
359 endpoint: endpoint.into(),
360 fetched_at: fetched_at.into(),
361 fetched_by: fetched_by.into(),
362 }
363 }
364}
365
366///
367/// IcCanisterSourceData
368///
369/// Raw canister metadata and provenance returned by an IC Dashboard source.
370///
371
372#[cfg(feature = "host")]
373#[derive(Clone, Debug, Eq, PartialEq)]
374pub struct IcCanisterSourceData {
375 /// Source request and provenance preserved by the source.
376 pub source: IcSourceRequest,
377 /// Canister principal returned by the Dashboard.
378 pub canister_id: String,
379 /// Dashboard database row identifier.
380 pub dashboard_id: u64,
381 /// Raw optional Dashboard canister classification.
382 pub canister_type: Option<String>,
383 /// Raw Dashboard canister name.
384 pub name: String,
385 /// Subnet principal returned by the Dashboard.
386 pub subnet_id: String,
387 /// Controller principals returned by the Dashboard.
388 pub controllers: Vec<String>,
389 /// Raw Dashboard language label.
390 pub language: String,
391 /// Raw current module hash.
392 pub module_hash: String,
393 /// Raw Dashboard row update timestamp.
394 pub dashboard_updated_at: String,
395 /// Proposal-linked upgrades, or `None` when the Dashboard returned `null`.
396 pub upgrades: Option<Vec<IcCanisterUpgrade>>,
397}
398
399///
400/// IcCanisterCountSourceData
401///
402/// Raw filtered count and provenance returned by a Dashboard source.
403///
404
405#[cfg(feature = "host")]
406#[derive(Clone, Debug, Eq, PartialEq)]
407pub struct IcCanisterCountSourceData {
408 /// Source request and provenance preserved by the source.
409 pub source: IcSourceRequest,
410 /// Filters applied by the source.
411 pub filters: IcCanisterFilters,
412 /// Number of matching Dashboard canister records.
413 pub total: u64,
414}
415
416///
417/// IcCanisterPageSourceData
418///
419/// Raw bounded canister page and provenance returned by a Dashboard source.
420///
421
422#[cfg(feature = "host")]
423#[derive(Clone, Debug, Eq, PartialEq)]
424pub struct IcCanisterPageSourceData {
425 /// Source request and provenance preserved by the source.
426 pub source: IcSourceRequest,
427 /// Filters applied by the source.
428 pub filters: IcCanisterFilters,
429 /// Maximum rows requested from the source.
430 pub requested_limit: u16,
431 /// Exclusive forward cursor supplied to the source.
432 pub after: Option<String>,
433 /// Exclusive backward cursor supplied to the source.
434 pub before: Option<String>,
435 /// Cursor for an explicit request for the preceding page.
436 pub previous_cursor: Option<String>,
437 /// Cursor for an explicit request for the following page.
438 pub next_cursor: Option<String>,
439 /// Canister discovery rows returned by the source.
440 pub rows: Vec<IcCanisterPageRow>,
441}
442
443///
444/// IcHostError
445///
446/// Typed error returned by IC Dashboard report builders and live sources.
447///
448
449#[cfg(feature = "host")]
450#[derive(Debug, ThisError)]
451pub enum IcHostError {
452 /// The synchronous adapter could not create its local async runtime.
453 #[error("failed to run IC Dashboard query: {0}")]
454 Runtime(#[from] RuntimeError),
455
456 /// A request supplied an invalid canister principal.
457 #[error("invalid {field}: {reason}")]
458 InvalidPrincipal {
459 /// Principal field being validated.
460 field: &'static str,
461 /// Principal parser diagnostic.
462 reason: String,
463 },
464
465 /// A request violates the bounded Dashboard query contract.
466 #[error("invalid {field}: {reason}")]
467 InvalidRequest {
468 /// Request field being validated.
469 field: &'static str,
470 /// Deterministic validation diagnostic.
471 reason: String,
472 },
473
474 /// The Dashboard API base endpoint is malformed or unsupported.
475 #[error("invalid IC Dashboard endpoint {endpoint}: {reason}")]
476 InvalidEndpoint {
477 /// Rejected endpoint.
478 endpoint: String,
479 /// URL validation diagnostic.
480 reason: String,
481 },
482
483 /// The HTTP client could not be constructed.
484 #[error("failed to build IC Dashboard HTTP client: {reason}")]
485 HttpClientBuild {
486 /// HTTP client construction diagnostic.
487 reason: String,
488 },
489
490 /// The live Dashboard request failed before a response was received.
491 #[error("IC Dashboard request to {url} failed: {reason}")]
492 HttpRequest {
493 /// Fully resolved request URL.
494 url: String,
495 /// HTTP transport error.
496 reason: String,
497 },
498
499 /// The Dashboard returned a non-success HTTP status.
500 #[error("IC Dashboard request to {url} returned HTTP status {status}")]
501 HttpStatus {
502 /// Fully resolved request URL.
503 url: String,
504 /// Numeric HTTP status.
505 status: u16,
506 },
507
508 /// The Dashboard response did not match the expected JSON shape.
509 #[error("failed to decode IC Dashboard response from {url}: {reason}")]
510 JsonDecode {
511 /// Fully resolved request URL.
512 url: String,
513 /// JSON response decoding error.
514 reason: String,
515 },
516
517 /// A source capability returned data that violates its public result contract.
518 #[error("invalid IC Dashboard canister source data: {reason}")]
519 InvalidSourceData {
520 /// Deterministic invariant failure.
521 reason: String,
522 },
523}