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(|| "ERR:LOAD 缺 sources(须为 { name: { kind, databases } } 对象)".to_string())?;
49 if sources_obj.is_empty() {
50 return Err("ERR:LOAD sources 不得为空".to_string());
51 }
52
53 let mut sources = Vec::new();
54 for (name, decl) in sources_obj {
55 let d = decl
56 .as_object()
57 .ok_or_else(|| format!("ERR:LOAD 连接 {name} 声明必须是对象"))?;
58 let raw_kind = d
59 .get("kind")
60 .and_then(|k| k.as_str())
61 .filter(|s| !s.is_empty())
62 .ok_or_else(|| format!("ERR:LOAD 连接 {name} 缺 kind"))?;
63 DataSource::from_kind(raw_kind).map_err(|_| {
65 format!("ERR:LOAD 连接 {name} 的 kind 非法: {raw_kind}(仅 mongodb/mysql/pg/sqlite)")
66 })?;
67 let databases: Vec<String> = d
68 .get("databases")
69 .and_then(|ds| ds.as_array())
70 .map(|arr| {
71 arr.iter()
72 .filter_map(|x| x.as_str())
73 .filter(|s| !s.is_empty())
74 .map(String::from)
75 .collect()
76 })
77 .unwrap_or_default();
78 if databases.is_empty() {
79 return Err(format!("ERR:LOAD 连接 {name} 的 databases 须为非空字符串数组"));
80 }
81 sources.push(SourceDecl {
82 name: name.clone(),
83 kind: raw_kind.to_lowercase(),
84 databases,
85 });
86 }
87
88 let defs: Vec<String> = obj
89 .get("defs")
90 .and_then(|ds| ds.as_array())
91 .map(|arr| {
92 arr.iter()
93 .filter_map(|x| x.as_str())
94 .filter(|s| !s.is_empty())
95 .map(String::from)
96 .collect()
97 })
98 .unwrap_or_default();
99 if defs.is_empty() {
100 return Err("ERR:LOAD defs 须为非空字符串数组(定义根)".to_string());
101 }
102
103 Ok(LoadConfig { sources, defs })
104 }
105
106 pub fn source_of_db(&self, db: &str) -> Result<(&SourceDecl, u8), String> {
110 let mut hit: Option<&SourceDecl> = None;
111 for s in &self.sources {
112 if s.databases.iter().any(|d| d == db) {
113 if let Some(prev) = hit {
114 return Err(format!(
115 "ERR:LOAD 库归属冲突: {db} 同时声明在连接 {} 与 {}(同一 db 名只能挂一个连接)",
116 prev.name, s.name
117 ));
118 }
119 hit = Some(s);
120 }
121 }
122 let decl = hit.ok_or_else(|| {
123 format!("ERR:LOAD 库目录未声明: {db}(须在 store.config.json 的某连接 databases 中列出)")
124 })?;
125 let depth = depth_layered(&decl.kind)?;
126 Ok((decl, depth))
127 }
128}
129
130pub fn depth_layered(kind: &str) -> Result<u8, String> {
133 let ds = DataSource::from_kind(kind).map_err(|_| {
134 format!("ERR:LOAD 连接 kind 非法: {kind}(仅 mongodb/mysql/pg/sqlite)")
135 })?;
136 Ok(match ds.backend() {
137 Some(Backend::Postgres) => 2,
138 _ => 1,
139 })
140}
141
142pub fn locate(rel_under_db: &str, source: &str, db: &str, depth: u8) -> Result<Location, String> {
149 let segs: Vec<&str> = rel_under_db
150 .split(['/', '\\'])
151 .filter(|s| !s.is_empty())
152 .collect();
153 if segs.is_empty() {
154 return Err(format!("ERR:LOAD 定义路径非法(缺文件名): {rel_under_db}"));
155 }
156 let dirs = &segs[..segs.len() - 1];
158 let schema = if depth >= 2 && !dirs.is_empty() {
159 Some(dirs[0].to_string())
160 } else {
161 None
162 };
163 Ok(Location {
164 source: source.to_string(),
165 database: Some(db.to_string()),
166 schema,
167 })
168}
169
170pub fn plan_load(
179 cfg: &LoadConfig,
180 files: &[(String, Value)],
181) -> Result<Vec<(Value, Location)>, String> {
182 let mut items: Vec<(Value, Location)> = Vec::with_capacity(files.len());
184 for (rel, defn) in files {
185 let segs: Vec<&str> = rel.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
186 if segs.is_empty() {
187 return Err(format!("ERR:LOAD 定义路径非法: {rel}"));
188 }
189 let db = segs[0];
190 let (decl, depth) = cfg.source_of_db(db)?;
191 let rel_under_db = segs[1..].join("/");
192 let loc = locate(&rel_under_db, &decl.name, db, depth)?;
193 items.push((defn.clone(), loc));
194 }
195
196 let class = classify_batch(&items).map_err(|e| batch_err_to_load(e, files))?;
198
199 let mut names: Vec<&String> = class.order.iter().collect();
201 names.sort();
202
203 let mut quads: Vec<(String, String, Location)> = Vec::new();
205 for name in &names {
206 if let Some((_, loc, coll)) = class.primaries.get(*name) {
207 quads.push(((*name).clone(), coll.clone(), loc.clone()));
208 }
209 if let Some(rs) = class.replicas.get(*name) {
210 for (defn, loc) in rs {
211 let coll = defn
212 .get("name")
213 .and_then(|v| v.as_str())
214 .unwrap_or(name)
215 .to_string();
216 quads.push(((*name).clone(), coll, loc.clone()));
217 }
218 }
219 }
220 if let Some(c) = detect_location_conflict(&quads) {
221 return Err(format!(
222 "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
223 c.collection, c.source, c.database, c.schema, c.prev, c.name
224 ));
225 }
226
227 let mut out: Vec<(Value, Location)> = Vec::with_capacity(items.len());
228 for name in names {
229 let Some((defn, loc, _coll)) = class.primaries.get(name) else {
230 return Err(format!(
231 "ERR:LOAD 主定义缺失: {name}(同名项均为 replica:true,须恰好一份主)"
232 ));
233 };
234 out.push((defn.clone(), loc.clone()));
235 if let Some(rs) = class.replicas.get(name) {
236 for (rdefn, rloc) in rs {
237 out.push((rdefn.clone(), rloc.clone()));
238 }
239 }
240 }
241 Ok(out)
242}
243
244fn batch_err_to_load(e: BatchError, files: &[(String, Value)]) -> String {
246 match e {
247 BatchError::Parse(m) => format!("ERR:LOAD {m}"),
248 BatchError::PrimaryDuplicate { name, indices } => {
249 let rels: Vec<String> = indices
250 .iter()
251 .map(|i| {
252 files
253 .get(*i)
254 .map(|(r, _)| r.clone())
255 .unwrap_or_else(|| format!("#{i}"))
256 })
257 .collect();
258 format!(
259 "ERR:LOAD 主定义重复: {name}({})——同名只允许一份主,其余须 replica:true",
260 rels.join(" 与 ")
261 )
262 }
263 BatchError::LocationConflict {
264 collection,
265 prev,
266 name,
267 source,
268 database,
269 schema,
270 } => format!(
271 "ERR:LOAD 落点冲突: {collection} 同时映射 ({source}, {database:?}, {schema:?})(同一落点只能有一个 collection;冲突名 `{prev}` 与 `{name}`)"
272 ),
273 }
274}