Skip to main content

tablo_test/protocol/
widgets.rs

1//! Reads a page's widget grid, its stat tiles, its charts and its record tables: `[data-widget]`
2//! names each widget the grid renders, `[data-stat]` marks each tile, whose first two paragraphs
3//! hold its label and value, `[data-chart]` marks each chart, whose `[data-chart-data]` table
4//! holds its values, and `[data-record-table]` marks each record table by its resource's slug.
5
6use super::dom::{Row, attr_value, columns, elements, rows, strip_text, tags_where};
7
8/// The names of the widgets a page's grid renders, in order: `RecentOrdersWidget` is
9/// `recent-orders`.
10pub fn widgets(html: &str) -> Vec<String> {
11    tags_where(html, |_, attrs| attr_value(attrs, "data-widget").is_some())
12        .into_iter()
13        .filter_map(|(_, attrs, _)| attr_value(&attrs, "data-widget"))
14        .collect()
15}
16
17/// One stat tile.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct StatTile {
20    /// What the figure counts.
21    pub label: String,
22    /// The figure.
23    pub value: String,
24    /// Where the tile links, if it does.
25    pub href: Option<String>,
26}
27
28/// The stat tiles `html` renders, in document order.
29pub fn stats(html: &str) -> Vec<StatTile> {
30    tags_where(html, |_, attrs| {
31        attrs.iter().any(|(name, _)| name == "data-stat")
32    })
33    .into_iter()
34    .map(|(tag, attrs, inner)| {
35        let mut paragraphs = elements(&inner, "p")
36            .into_iter()
37            .map(|(_, _, p)| strip_text(&p));
38        StatTile {
39            label: paragraphs.next().unwrap_or_default(),
40            value: paragraphs.next().unwrap_or_default(),
41            href: (tag == "a").then(|| attr_value(&attrs, "href")).flatten(),
42        }
43    })
44    .collect()
45}
46
47/// One chart, as its data table reads.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ChartData {
50    /// How it draws: `line` or `bar`.
51    pub kind: String,
52    /// Its heading, if it has one.
53    pub heading: Option<String>,
54    /// The labels along its bottom.
55    pub labels: Vec<String>,
56    /// Its series, in the order the chart draws them.
57    pub series: Vec<ChartSeries>,
58}
59
60/// One series of a [`ChartData`].
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ChartSeries {
63    /// What the series measures.
64    pub label: String,
65    /// A value per label of the chart, as the chart spells it: empty where it has none.
66    pub values: Vec<String>,
67}
68
69/// The charts `html` renders, in document order.
70pub fn charts(html: &str) -> Vec<ChartData> {
71    tags_where(html, |_, attrs| attr_value(attrs, "data-chart").is_some())
72        .into_iter()
73        .map(|(_, attrs, inner)| {
74            let table = tags_where(&inner, |tag, attrs| {
75                tag == "table" && attrs.iter().any(|(name, _)| name == "data-chart-data")
76            })
77            .into_iter()
78            .next()
79            .map(|(_, _, table)| table)
80            .unwrap_or_default();
81            let text = |element: &str| -> Vec<String> {
82                elements(&table, element)
83                    .into_iter()
84                    .map(|(_, _, inner)| strip_text(&inner))
85                    .collect()
86            };
87            let head = elements(&table, "thead")
88                .into_iter()
89                .next()
90                .map(|(_, _, head)| head)
91                .unwrap_or_default();
92            let series = elements(&table, "tbody")
93                .into_iter()
94                .next()
95                .map(|(_, _, body)| elements(&body, "tr"))
96                .unwrap_or_default()
97                .into_iter()
98                .map(|(_, _, row)| ChartSeries {
99                    label: elements(&row, "th")
100                        .into_iter()
101                        .next()
102                        .map(|(_, _, th)| strip_text(&th))
103                        .unwrap_or_default(),
104                    values: elements(&row, "td")
105                        .into_iter()
106                        .map(|(_, _, td)| strip_text(&td))
107                        .collect(),
108                })
109                .collect();
110            ChartData {
111                kind: attr_value(&attrs, "data-chart").unwrap_or_default(),
112                heading: text("caption").into_iter().next(),
113                labels: elements(&head, "th")
114                    .into_iter()
115                    .map(|(_, _, th)| strip_text(&th))
116                    .collect(),
117                series,
118            }
119        })
120        .collect()
121}
122
123/// One record table, as its markup reads.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct RecordTableData {
126    /// The slug of the resource it lists.
127    pub resource: String,
128    /// Its heading.
129    pub heading: String,
130    /// The names of the columns it shows, in order.
131    pub columns: Vec<String>,
132    /// Its rows, in order, as [`rows`](super::rows) reads a list's.
133    pub rows: Vec<Row>,
134    /// Where its link to the full list points.
135    pub list: Option<String>,
136}
137
138/// The record tables `html` renders, in document order.
139pub fn record_tables(html: &str) -> Vec<RecordTableData> {
140    tags_where(html, |_, attrs| {
141        attr_value(attrs, "data-record-table").is_some()
142    })
143    .into_iter()
144    .map(|(_, attrs, inner)| RecordTableData {
145        resource: attr_value(&attrs, "data-record-table").unwrap_or_default(),
146        heading: elements(&inner, "h2")
147            .into_iter()
148            .next()
149            .map(|(_, _, h2)| strip_text(&h2))
150            .unwrap_or_default(),
151        columns: columns(&inner),
152        rows: rows(&inner),
153        list: tags_where(&inner, |tag, attrs| {
154            tag == "a"
155                && attrs
156                    .iter()
157                    .any(|(name, _)| name == "data-record-table-list")
158        })
159        .into_iter()
160        .next()
161        .and_then(|(_, attrs, _)| attr_value(&attrs, "href")),
162    })
163    .collect()
164}