1use std::fs;
2use std::path::PathBuf;
3
4use serde_json::Value;
5use thiserror::Error;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct RawFallbackImportSpec {
9 pub initialize_inline: Option<String>,
10 pub initialize_file: Option<PathBuf>,
11 pub tools_inline: Option<String>,
12 pub tools_file: Option<PathBuf>,
13}
14
15impl RawFallbackImportSpec {
16 pub fn has_any(&self) -> bool {
17 self.initialize_inline.is_some()
18 || self.initialize_file.is_some()
19 || self.tools_inline.is_some()
20 || self.tools_file.is_some()
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ImportSource {
26 Inline,
27 File(PathBuf),
28}
29
30impl std::fmt::Display for ImportSource {
31 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::Inline => formatter.write_str("inline"),
34 Self::File(path) => write!(formatter, "{}", path.display()),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct LoadedJson {
41 pub json: String,
42 pub source: ImportSource,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct LoadedFallbackJson {
47 pub initialize: LoadedJson,
48 pub tools: LoadedJson,
49}
50
51#[derive(Debug, Error)]
52pub enum LoadError {
53 #[error("fallback import must include both initialize and tools metadata")]
54 IncompletePair,
55 #[error("both inline and file sources were provided for {kind}")]
56 ConflictingSources { kind: &'static str },
57 #[error("failed to read {kind} fallback file {path}: {source}")]
58 ReadFile {
59 kind: &'static str,
60 path: PathBuf,
61 #[source]
62 source: std::io::Error,
63 },
64 #[error("invalid JSON syntax for {kind} fallback from {source_name}: {source}")]
65 InvalidJsonSyntax {
66 kind: &'static str,
67 source_name: String,
68 #[source]
69 source: serde_json::Error,
70 },
71}
72
73fn load_one(
74 kind: &'static str,
75 inline: &Option<String>,
76 file: &Option<PathBuf>,
77) -> Result<Option<LoadedJson>, LoadError> {
78 let loaded = match (inline, file) {
79 (Some(_), Some(_)) => return Err(LoadError::ConflictingSources { kind }),
80 (None, None) => return Ok(None),
81 (Some(json), None) => LoadedJson {
82 json: json.clone(),
83 source: ImportSource::Inline,
84 },
85 (None, Some(path)) => {
86 let json = fs::read_to_string(path).map_err(|source| LoadError::ReadFile {
87 kind,
88 path: path.clone(),
89 source,
90 })?;
91 LoadedJson {
92 json,
93 source: ImportSource::File(path.clone()),
94 }
95 }
96 };
97
98 serde_json::from_str::<Value>(&loaded.json).map_err(|source| {
99 let source_name = loaded.source.to_string();
100 LoadError::InvalidJsonSyntax {
101 kind,
102 source_name,
103 source,
104 }
105 })?;
106
107 Ok(Some(loaded))
108}
109
110pub fn try_load_fallback(
111 spec: &RawFallbackImportSpec,
112) -> Result<Option<LoadedFallbackJson>, LoadError> {
113 if !spec.has_any() {
114 return Ok(None);
115 }
116
117 let initialize = load_one("initialize", &spec.initialize_inline, &spec.initialize_file)?;
118 let tools = load_one("tools", &spec.tools_inline, &spec.tools_file)?;
119
120 match (initialize, tools) {
121 (Some(initialize), Some(tools)) => Ok(Some(LoadedFallbackJson { initialize, tools })),
122 _ => Err(LoadError::IncompletePair),
123 }
124}