1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! One response turned into keyed rows.
use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_api_pool::resilient::TransportEvent;
use vantage_core::error;
use vantage_dataset::traits::Result;
use vantage_expressions::Expression;
use vantage_table::table::retain_matching;
use vantage_types::Record;
use super::{Order, ResponseShape, RestApi};
use crate::row_keys::insert_row;
/// One response's rows, see [`RestApi::fetch_page`].
pub(super) struct Page {
pub(super) rows: IndexMap<String, Record<CborValue>>,
pub(super) total: Option<i64>,
pub(super) server_rows: usize,
pub(super) lossy: bool,
}
impl RestApi {
/// One request: the page's rows keyed by id, the envelope total, and how
/// many rows the server sent — more than `rows` holds when client filters
/// dropped some (`lossy`). Two rows sharing an id within this one
/// response are an error.
///
/// Client-side filters go through vantage-table's
/// [`retain_matching`]: a row lacking the filtered field fails only when
/// the field is in `row_fields`.
pub(super) async fn fetch_page(
&self,
table_name: &str,
id_field: Option<&str>,
row_fields: &[String],
window: Option<(i64, i64)>,
order: Order<'_>,
conditions: &[&Expression<CborValue>],
) -> Result<Page> {
let (body, client_filters) = self
.fetch_raw_body(table_name, window, order, conditions.iter().copied())
.await?;
let total = self
.total_key
.as_deref()
.and_then(|key| body.get(key))
.and_then(|v| v.as_i64());
let data = self.extract_array(&body, table_name)?;
// Where this page starts in the server's result, so an id-less row
// is keyed by its absolute position (see `insert_row`).
let offset = match window {
Some((offset, _)) if !self.no_pagination => offset.max(0) as usize,
_ => 0,
};
let mut records = IndexMap::new();
for (row_idx, item) in data.iter().enumerate() {
let obj = item
.as_object()
.ok_or_else(|| error!("API data item is not an object", index = row_idx))?;
// The configured id field when it holds a string or number;
// otherwise the row is keyed by its position.
let id = id_field
.and_then(|field| obj.get(field))
.and_then(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
});
// The one JSON-to-CBOR boundary: Table and Vista speak CBOR.
let mut record: Record<CborValue> = Record::new();
for (k, v) in obj {
record.insert(k.clone(), vantage_types::json_to_cbor(v.clone()));
}
insert_row(&mut records, id, offset + row_idx, record)?;
}
if !client_filters.is_empty() {
let filters: Vec<(&str, &str)> = client_filters
.iter()
.map(|(field, want)| (field.as_str(), want.as_str()))
.collect();
retain_matching(&mut records, &filters, row_fields, |value, want| {
crate::cbor_to_query_string(value).as_deref() == Some(want)
});
}
// Counts the rows this call hands back, so a client-side filter shows
// up as fewer rows pulled than the server sent.
self.client
.report(TransportEvent::rows_pulled(records.len()));
// The envelope counted what the SERVER matched, before any rows were
// dropped above — reporting that total now would size a grid to rows it
// will never be given. No total is better than a wrong one.
let total = if client_filters.is_empty() {
total
} else {
None
};
Ok(Page {
lossy: records.len() < data.len(),
server_rows: data.len(),
rows: records,
total,
})
}
/// Pull the row array out of the response body, according to the
/// configured `ResponseShape`. A bare-array API answers a single-record
/// endpoint (`activities/{id}`) with one object: that reads as one row.
fn extract_array<'a>(
&self,
body: &'a serde_json::Value,
table_name: &str,
) -> Result<&'a [serde_json::Value]> {
match &self.response_shape {
ResponseShape::BareArray => match body {
serde_json::Value::Array(rows) => Ok(rows),
serde_json::Value::Object(_) => Ok(std::slice::from_ref(body)),
_ => Err(error!(
"Expected response body to be a JSON array or object (BareArray shape)"
)),
},
ResponseShape::Wrapped { array_key } => body[array_key]
.as_array()
.map(Vec::as_slice)
.ok_or_else(|| {
error!(
"Response missing array under wrapper key",
array_key = array_key
)
}),
ResponseShape::WrappedByTableName => body[table_name]
.as_array()
.map(Vec::as_slice)
.ok_or_else(|| {
error!(
"Response missing array under table-name key",
table_name = table_name
)
}),
}
}
}