1use serde_json::Value;
11
12use crate::datasource::DataSource;
13use crate::dialect::Backend;
14
15use super::definition::Location;
16use super::registry::{classify_batch, detect_location_conflict, BatchError};
17
18#[derive(Debug, Clone)]
20pub struct SourceDecl {
21 pub name: String,
22 pub kind: String,
24 pub databases: Vec<String>,
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct LoadConfig {
31 pub sources: Vec<SourceDecl>,
33 pub defs: Vec<String>,
34}
35
36impl LoadConfig {
37 pub fn from_json(v: &Value) -> Result<Self, String> {
41 let obj = v
42 .as_object()
43 .ok_or_else(|| "ERR:LOAD store.config.json 必须是对象".to_string())?;
44
45 let sources_obj = obj
46 .get("sources")
47 .and_then(|s| s.as_object())
48 .ok_or_else(|| {
49 "ERR:LOAD 缺 sources(须为 { name: { kind, databases } } 对象)".to_string()
50 })?;
51 if sources_obj.is_empty() {
52 return Err("ERR:LOAD sources 不得为空".to_string());
53 }
54
55 let mut sources = Vec::new();
56 for (name, decl) in sources_obj {
57 let d = decl
58 .as_object()
59 .ok_or_else(|| format!("ERR:LOAD 连接 {name} 声明必须是对象"))?;
60 let raw_kind = d
61 .get("kind")
62 .and_then(|k| k.as_str())
63 .filter(|s| !s.is_empty())
64 .ok_or_else(|| format!("ERR:LOAD 连接 {name} 缺 kind"))?;
65 DataSource::from_kind(raw_kind).map_err(|_| {
67 format!(
68 "ERR:LOAD 连接 {name} 的 kind 非法: {raw_kind}(仅 mongodb/mysql/pg/sqlite/local)"
69 )
70 })?;
71 let databases: Vec<String> = d
72 .get("databases")
73 .and_then(|ds| ds.as_array())
74 .map(|arr| {
75 arr.iter()
76 .filter_map(|x| x.as_str())
77 .filter(|s| !s.is_empty())
78 .map(String::from)
79 .collect()
80 })
81 .unwrap_or_default();
82 if databases.is_empty() {
83 return Err(format!(
84 "ERR:LOAD 连接 {name} 的 databases 须为非空字符串数组"
85 ));
86 }
87 sources.push(SourceDecl {
88 name: name.clone(),
89 kind: raw_kind.to_lowercase(),
90 databases,
91 });
92 }
93
94 let defs: Vec<String> = obj
95 .get("defs")
96 .and_then(|ds| ds.as_array())
97 .map(|arr| {
98 arr.iter()
99 .filter_map(|x| x.as_str())
100 .filter(|s| !s.is_empty())
101 .map(String::from)
102 .collect()
103 })
104 .unwrap_or_default();
105 if defs.is_empty() {
106 return Err("ERR:LOAD defs 须为非空字符串数组(定义根)".to_string());
107 }
108
109 Ok(LoadConfig { sources, defs })
110 }
111
112 pub fn source_of_db(&self, db: &str) -> Result<(&SourceDecl, u8), String> {
116 let mut hit: Option<&SourceDecl> = None;
117 for s in &self.sources {
118 if s.databases.iter().any(|d| d == db) {
119 if let Some(prev) = hit {
120 return Err(format!(
121 "ERR:LOAD 库归属冲突: {db} 同时声明在连接 {} 与 {}(同一 db 名只能挂一个连接)",
122 prev.name, s.name
123 ));
124 }
125 hit = Some(s);
126 }
127 }
128 let decl = hit.ok_or_else(|| {
129 format!(
130 "ERR:LOAD 库目录未声明: {db}(须在 store.config.json 的某连接 databases 中列出)"
131 )
132 })?;
133 let depth = depth_layered(&decl.kind)?;
134 Ok((decl, depth))
135 }
136}
137
138pub fn depth_layered(kind: &str) -> Result<u8, String> {
141 let ds = DataSource::from_kind(kind).map_err(|_| {
142 format!("ERR:LOAD 连接 kind 非法: {kind}(仅 mongodb/mysql/pg/sqlite/local)")
143 })?;
144 Ok(match ds.backend() {
145 Some(Backend::Postgres) => 2,
146 _ => 1,
147 })
148}
149
150pub fn locate(rel_under_db: &str, source: &str, db: &str, depth: u8) -> Result<Location, String> {
157 let segs: Vec<&str> = rel_under_db
158 .split(['/', '\\'])
159 .filter(|s| !s.is_empty())
160 .collect();
161 if segs.is_empty() {
162 return Err(format!("ERR:LOAD 定义路径非法(缺文件名): {rel_under_db}"));
163 }
164 let dirs = &segs[..segs.len() - 1];
166 let schema = if depth >= 2 && !dirs.is_empty() {
167 Some(dirs[0].to_string())
168 } else {
169 None
170 };
171 Ok(Location {
172 source: source.to_string(),
173 database: Some(db.to_string()),
174 schema,
175 })
176}
177
178pub fn plan_load(
187 cfg: &LoadConfig,
188 files: &[(String, Value)],
189) -> Result<Vec<(Value, Location)>, String> {
190 let mut items: Vec<(Value, Location)> = Vec::with_capacity(files.len());
192 for (rel, defn) in files {
193 let segs: Vec<&str> = rel.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
194 if segs.is_empty() {
195 return Err(format!("ERR:LOAD 定义路径非法: {rel}"));
196 }
197 let db = segs[0];
198 let (decl, depth) = cfg.source_of_db(db)?;
199 let rel_under_db = segs[1..].join("/");
200 let loc = locate(&rel_under_db, &decl.name, db, depth)?;
201 items.push((defn.clone(), loc));
202 }
203
204 let class = classify_batch(&items).map_err(|e| batch_err_to_load(e, files))?;
206
207 let mut names: Vec<&String> = class.order.iter().collect();
209 names.sort();
210
211 let mut quads: Vec<(String, String, Location)> = Vec::new();
213 for name in &names {
214 if let Some((_, loc, coll)) = class.primaries.get(*name) {
215 quads.push(((*name).clone(), coll.clone(), loc.clone()));
216 }
217 if let Some(rs) = class.replicas.get(*name) {
218 for (defn, loc) in rs {
219 let coll = defn
220 .get("name")
221 .and_then(|v| v.as_str())
222 .unwrap_or(name)
223 .to_string();
224 quads.push(((*name).clone(), coll, loc.clone()));
225 }
226 }
227 }
228 if let Some(c) = detect_location_conflict(&quads) {
229 return Err(format!(
230 "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
231 c.collection, c.source, c.database, c.schema, c.prev, c.name
232 ));
233 }
234
235 let mut out: Vec<(Value, Location)> = Vec::with_capacity(items.len());
236 for name in names {
237 let Some((defn, loc, _coll)) = class.primaries.get(name) else {
238 return Err(format!(
239 "ERR:LOAD 主定义缺失: {name}(同名项均为 replica:true,须恰好一份主)"
240 ));
241 };
242 out.push((defn.clone(), loc.clone()));
243 if let Some(rs) = class.replicas.get(name) {
244 for (rdefn, rloc) in rs {
245 out.push((rdefn.clone(), rloc.clone()));
246 }
247 }
248 }
249 Ok(out)
250}
251
252fn batch_err_to_load(e: BatchError, files: &[(String, Value)]) -> String {
254 match e {
255 BatchError::Parse(m) => format!("ERR:LOAD {m}"),
256 BatchError::PrimaryDuplicate { name, indices } => {
257 let rels: Vec<String> = indices
258 .iter()
259 .map(|i| {
260 files
261 .get(*i)
262 .map(|(r, _)| r.clone())
263 .unwrap_or_else(|| format!("#{i}"))
264 })
265 .collect();
266 format!(
267 "ERR:LOAD 主定义重复: {name}({})——同名只允许一份主,其余须 replica:true",
268 rels.join(" 与 ")
269 )
270 }
271 BatchError::LocationConflict(c) => format!(
272 "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
273 c.collection, c.source, c.database, c.schema, c.prev, c.name
274 ),
275 }
276}