xtalforge_agent/
session.rs1use std::collections::BTreeMap;
9use std::path::Path;
10
11use serde_json::Value;
12use xtalforge_commands::Document;
13use xtalforge_core::{Limits, Profile, Structure};
14
15use crate::Error;
16use crate::answers::{Answer, DocumentSummary};
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct SessionOptions {
21 pub limits: Limits,
23}
24
25impl Default for SessionOptions {
26 fn default() -> Self {
27 Self {
28 limits: Limits::new(Profile::Standard),
29 }
30 }
31}
32
33#[derive(Debug)]
34pub(crate) struct Entry {
35 document: Document,
36 source: Option<String>,
37}
38
39#[derive(Debug)]
41pub struct Session {
42 documents: BTreeMap<String, Entry>,
43 active: Option<String>,
44 options: SessionOptions,
45}
46
47impl Default for Session {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl Session {
54 #[must_use]
56 pub fn new() -> Self {
57 Self::with_options(SessionOptions::default())
58 }
59
60 #[must_use]
62 pub fn with_options(options: SessionOptions) -> Self {
63 Self {
64 documents: BTreeMap::new(),
65 active: None,
66 options,
67 }
68 }
69
70 #[must_use]
72 pub fn options(&self) -> &SessionOptions {
73 &self.options
74 }
75
76 #[must_use]
78 pub fn active_name(&self) -> Option<&str> {
79 self.active.as_deref()
80 }
81
82 pub fn structure(&self, name: Option<&str>) -> Result<&Structure, Error> {
88 Ok(self.document(name)?.structure())
89 }
90
91 #[must_use]
93 pub fn active(&self) -> Option<&Document> {
94 self.active_name()
95 .and_then(|name| self.documents.get(name))
96 .map(|entry| &entry.document)
97 }
98
99 #[must_use]
101 pub fn contains(&self, name: &str) -> bool {
102 self.documents.contains_key(name)
103 }
104
105 #[must_use]
107 pub fn len(&self) -> usize {
108 self.documents.len()
109 }
110
111 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.documents.is_empty()
115 }
116
117 pub(crate) fn resolve(&self, name: Option<&str>) -> Result<String, Error> {
123 match name {
124 Some(name) => {
125 if self.documents.contains_key(name) {
126 Ok(name.to_owned())
127 } else {
128 Err(Error::UnknownDocument(name.to_owned()))
129 }
130 }
131 None => self
132 .active_name()
133 .map(str::to_owned)
134 .ok_or(Error::NoDocument),
135 }
136 }
137
138 pub(crate) fn document(&self, name: Option<&str>) -> Result<&Document, Error> {
139 let name = self.resolve(name)?;
140 self.documents
141 .get(&name)
142 .map(|entry| &entry.document)
143 .ok_or(Error::UnknownDocument(name))
144 }
145
146 pub(crate) fn document_mut(&mut self, name: Option<&str>) -> Result<&mut Document, Error> {
147 let name = self.resolve(name)?;
148 self.documents
149 .get_mut(&name)
150 .map(|entry| &mut entry.document)
151 .ok_or(Error::UnknownDocument(name))
152 }
153
154 pub(crate) fn source(&self, name: &str) -> Option<&str> {
155 self.documents
156 .get(name)
157 .and_then(|entry| entry.source.as_deref())
158 }
159
160 pub(crate) fn insert(
162 &mut self,
163 name: impl Into<String>,
164 structure: Structure,
165 source: Option<String>,
166 ) -> String {
167 let name = name.into();
168 self.documents.insert(
169 name.clone(),
170 Entry {
171 document: Document::new(structure),
172 source,
173 },
174 );
175 self.active = Some(name.clone());
176 name
177 }
178
179 pub(crate) fn available_name(&self, path: Option<&str>, fallback: &str) -> String {
181 let stem = path
182 .and_then(|path| Path::new(path).file_stem())
183 .and_then(|stem| stem.to_str())
184 .filter(|stem| !stem.is_empty())
185 .unwrap_or(fallback);
186 if !self.documents.contains_key(stem) {
187 return stem.to_owned();
188 }
189 let mut index = 2;
190 loop {
191 let candidate = format!("{stem}-{index}");
192 if !self.documents.contains_key(&candidate) {
193 return candidate;
194 }
195 index += 1;
196 }
197 }
198
199 pub(crate) fn remove(&mut self, name: &str) -> bool {
200 let removed = self.documents.remove(name).is_some();
201 if removed && self.active.as_deref() == Some(name) {
202 self.active = self.documents.keys().next().cloned();
203 }
204 removed
205 }
206
207 #[must_use]
209 pub fn summaries(&self) -> Vec<DocumentSummary> {
210 self.documents
211 .iter()
212 .filter_map(|(name, entry)| summary(name, entry).ok())
213 .collect()
214 }
215
216 #[must_use]
218 pub fn documents_answer(&self) -> Answer {
219 Answer::Documents {
220 documents: self.summaries(),
221 active: self.active.clone(),
222 }
223 }
224
225 pub fn run(&mut self, verb: &str, arguments: &Value) -> Result<Answer, Error> {
232 if crate::verbs::lookup(verb).is_none() {
233 return Err(Error::UnknownVerb(verb.to_owned()));
234 }
235 crate::run::run(self, verb, arguments)
236 }
237}
238
239pub(crate) fn summary(name: &str, entry: &Entry) -> Result<DocumentSummary, Error> {
240 let structure = entry.document.structure();
241 let summary = structure.summary()?;
242 Ok(DocumentSummary {
243 name: name.to_owned(),
244 title: summary.name,
245 formula: summary.formula,
246 n_sites: summary.n_sites,
247 volume: summary.volume,
248 density: summary.density,
249 source: entry.source.clone(),
250 dirty: entry.document.dirty(),
251 })
252}
253
254pub(crate) fn entry_summary(
255 name: &str,
256 document: &Document,
257 source: Option<&str>,
258) -> Result<DocumentSummary, Error> {
259 let summary = document.structure().summary()?;
260 Ok(DocumentSummary {
261 name: name.to_owned(),
262 title: summary.name,
263 formula: summary.formula,
264 n_sites: summary.n_sites,
265 volume: summary.volume,
266 density: summary.density,
267 source: source.map(str::to_owned),
268 dirty: document.dirty(),
269 })
270}