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)
142 .map_err(|_| format!("ERR:LOAD 连接 kind 非法: {kind}(仅 mongodb/mysql/pg/sqlite/local)"))?;
143 Ok(match ds.backend() {
144 Some(Backend::Postgres) => 2,
145 _ => 1,
146 })
147}
148
149pub fn locate(rel_under_db: &str, source: &str, db: &str, depth: u8) -> Result<Location, String> {
156 let segs: Vec<&str> = rel_under_db
157 .split(['/', '\\'])
158 .filter(|s| !s.is_empty())
159 .collect();
160 if segs.is_empty() {
161 return Err(format!("ERR:LOAD 定义路径非法(缺文件名): {rel_under_db}"));
162 }
163 let dirs = &segs[..segs.len() - 1];
165 let schema = if depth >= 2 && !dirs.is_empty() {
166 Some(dirs[0].to_string())
167 } else {
168 None
169 };
170 Ok(Location {
171 source: source.to_string(),
172 database: Some(db.to_string()),
173 schema,
174 })
175}
176
177pub fn plan_load(
186 cfg: &LoadConfig,
187 files: &[(String, Value)],
188) -> Result<Vec<(Value, Location)>, String> {
189 let mut items: Vec<(Value, Location)> = Vec::with_capacity(files.len());
191 for (rel, defn) in files {
192 let segs: Vec<&str> = rel.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
193 if segs.is_empty() {
194 return Err(format!("ERR:LOAD 定义路径非法: {rel}"));
195 }
196 let db = segs[0];
197 let (decl, depth) = cfg.source_of_db(db)?;
198 let rel_under_db = segs[1..].join("/");
199 let loc = locate(&rel_under_db, &decl.name, db, depth)?;
200 items.push((defn.clone(), loc));
201 }
202
203 let class = classify_batch(&items).map_err(|e| batch_err_to_load(e, files))?;
205
206 let mut names: Vec<&String> = class.order.iter().collect();
208 names.sort();
209
210 let mut quads: Vec<(String, String, Location)> = Vec::new();
212 for name in &names {
213 if let Some((_, loc, coll)) = class.primaries.get(*name) {
214 quads.push(((*name).clone(), coll.clone(), loc.clone()));
215 }
216 if let Some(rs) = class.replicas.get(*name) {
217 for (defn, loc) in rs {
218 let coll = defn
219 .get("name")
220 .and_then(|v| v.as_str())
221 .unwrap_or(name)
222 .to_string();
223 quads.push(((*name).clone(), coll, loc.clone()));
224 }
225 }
226 }
227 if let Some(c) = detect_location_conflict(&quads) {
228 return Err(format!(
229 "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
230 c.collection, c.source, c.database, c.schema, c.prev, c.name
231 ));
232 }
233
234 let mut out: Vec<(Value, Location)> = Vec::with_capacity(items.len());
235 for name in names {
236 let Some((defn, loc, _coll)) = class.primaries.get(name) else {
237 return Err(format!(
238 "ERR:LOAD 主定义缺失: {name}(同名项均为 replica:true,须恰好一份主)"
239 ));
240 };
241 out.push((defn.clone(), loc.clone()));
242 if let Some(rs) = class.replicas.get(name) {
243 for (rdefn, rloc) in rs {
244 out.push((rdefn.clone(), rloc.clone()));
245 }
246 }
247 }
248 Ok(out)
249}
250
251fn batch_err_to_load(e: BatchError, files: &[(String, Value)]) -> String {
253 match e {
254 BatchError::Parse(m) => format!("ERR:LOAD {m}"),
255 BatchError::PrimaryDuplicate { name, indices } => {
256 let rels: Vec<String> = indices
257 .iter()
258 .map(|i| {
259 files
260 .get(*i)
261 .map(|(r, _)| r.clone())
262 .unwrap_or_else(|| format!("#{i}"))
263 })
264 .collect();
265 format!(
266 "ERR:LOAD 主定义重复: {name}({})——同名只允许一份主,其余须 replica:true",
267 rels.join(" 与 ")
268 )
269 }
270 BatchError::LocationConflict(c) => format!(
271 "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
272 c.collection, c.source, c.database, c.schema, c.prev, c.name
273 ),
274 }
275}