Skip to main content

a2ui_gallery/
sample_loader.rs

1//! Sample loader — reads A2UI sample JSON files.
2//!
3//! By default the specification tree is embedded into the binary at compile
4//! time ([`SPEC_DIR`] + [`load_samples`]), so the gallery is fully
5//! self-contained and distributable. The legacy filesystem reader
6//! [`load_samples_from_dir`] is retained for the `A2UI_SPEC_DIR` dev override.
7
8use std::fs;
9use std::path::Path;
10
11use include_dir::{include_dir, Dir};
12
13use a2ui_base::message_processor::MessageProcessor;
14use a2ui_base::protocol::server_to_client::A2uiMessage;
15
16/// The full A2UI specification tree, embedded at compile time.
17///
18/// Makes the binary self-contained: no on-disk spec directory is required at
19/// runtime. Paths inside are relative to the spec root, e.g.
20/// `v1_0/catalogs/minimal/examples`.
21pub static SPEC_DIR: Dir<'static> =
22    include_dir!("$CARGO_MANIFEST_DIR/a2ui/specification");
23
24/// A loaded sample with metadata and parsed messages.
25pub struct Sample {
26    /// Display name (from the sample JSON).
27    pub name: String,
28    /// Description (from the sample JSON).
29    pub description: String,
30    /// File path for display purposes.
31    pub file_path: String,
32    /// Parsed A2UI messages.
33    pub messages: Vec<A2uiMessage>,
34}
35
36/// Load all `.json` sample files from the given directory.
37///
38/// Files are sorted by filename (they are numbered `1_*.json`, `2_*.json`, etc.).
39/// Files that fail to parse are skipped silently.
40pub fn load_samples_from_dir(dir: &str) -> Vec<Sample> {
41    let path = Path::new(dir);
42    if !path.is_dir() {
43        return Vec::new();
44    }
45
46    let dir_entries = match fs::read_dir(path) {
47        Ok(de) => de,
48        Err(e) => {
49            eprintln!("Warning: cannot read sample directory {:?}: {}", dir, e);
50            return Vec::new();
51        }
52    };
53
54    let mut entries: Vec<String> = dir_entries
55        .filter_map(|entry| {
56            let entry = entry.ok()?;
57            let file_name = entry.file_name().to_string_lossy().to_string();
58            if file_name.ends_with(".json") {
59                Some(file_name)
60            } else {
61                None
62            }
63        })
64        .collect();
65
66    // Sort by filename — the numbering prefix ensures correct ordering.
67    entries.sort();
68
69    let mut samples = Vec::new();
70
71    for file_name in &entries {
72        let full_path = path.join(file_name);
73        let content = match fs::read_to_string(&full_path) {
74            Ok(c) => c,
75            Err(e) => {
76                eprintln!("Warning: cannot read {:?}: {}", full_path, e);
77                continue;
78            }
79        };
80
81        match MessageProcessor::load_sample(&content) {
82            Ok((name, description, messages)) => {
83                samples.push(Sample {
84                    name,
85                    description,
86                    file_path: file_name.clone(),
87                    messages,
88                });
89            }
90            Err(e) => {
91                eprintln!(
92                    "Warning: failed to parse sample {:?}: {}",
93                    full_path, e
94                );
95            }
96        }
97    }
98
99    samples
100}
101
102/// Load all `.json` samples from an embedded subdirectory of [`SPEC_DIR`].
103///
104/// `subpath` is relative to the spec root, e.g.
105/// `"v1_0/catalogs/minimal/examples"`. Direct children only (not recursive).
106/// Files are sorted by filename; files that fail to parse are skipped silently.
107pub fn load_samples(subpath: &str) -> Vec<Sample> {
108    let dir = match SPEC_DIR.get_dir(subpath) {
109        Some(d) => d,
110        None => {
111            eprintln!("Warning: embedded sample directory not found: {subpath:?}");
112            return Vec::new();
113        }
114    };
115
116    let mut files: Vec<&include_dir::File> = dir.files().collect();
117    files.sort_by_key(|f| f.path().to_string_lossy().to_string());
118    files.retain(|f| f.path().extension().is_some_and(|ext| ext == "json"));
119
120    let mut samples = Vec::new();
121    for file in files {
122        let file_name = file
123            .path()
124            .file_name()
125            .map(|n| n.to_string_lossy().to_string())
126            .unwrap_or_default();
127        let content = match std::str::from_utf8(file.contents()) {
128            Ok(s) => s,
129            Err(e) => {
130                eprintln!("Warning: sample {file_name:?} is not valid UTF-8: {e}");
131                continue;
132            }
133        };
134        match MessageProcessor::load_sample(content) {
135            Ok((name, description, messages)) => samples.push(Sample {
136                name,
137                description,
138                file_path: file_name,
139                messages,
140            }),
141            Err(e) => {
142                eprintln!("Warning: failed to parse sample {file_name:?}: {e}");
143            }
144        }
145    }
146    samples
147}