1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[derive(Default)]
17pub enum ChunkGranularity {
18 Function,
20 Type,
22 #[default]
24 Module,
25 Package,
27 Project,
29}
30
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35#[derive(Default)]
36pub enum ChunkCategory {
37 #[default]
39 Logic,
40 Data,
42 Utility,
44 Api,
46 Config,
48 Test,
50 Docs,
52 Build,
54 Ui,
56 Backend,
58 Database,
60 Custom(String),
62}
63
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PlatformConstraint {
68 #[serde(default = "default_any")]
70 pub os: String,
71 #[serde(default = "default_any")]
73 pub arch: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub runtime: Option<String>,
77 #[serde(default)]
79 pub min_versions: HashMap<String, String>,
80}
81
82fn default_any() -> String {
83 "any".to_string()
84}
85
86impl Default for PlatformConstraint {
87 fn default() -> Self {
88 Self {
89 os: "any".to_string(),
90 arch: "any".to_string(),
91 runtime: None,
92 min_versions: HashMap::new(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ChunkAlias {
100 pub path: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub namespace: Option<String>,
105 #[serde(default)]
107 pub primary: bool,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub registered_at: Option<String>,
111}
112
113impl ChunkAlias {
114 pub fn new(path: impl Into<String>) -> Self {
116 Self {
117 path: path.into(),
118 namespace: None,
119 primary: true,
120 registered_at: Some(chrono::Utc::now().to_rfc3339()),
121 }
122 }
123
124 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
126 self.namespace = Some(namespace.into());
127 self
128 }
129
130 pub fn full_path(&self) -> String {
132 match &self.namespace {
133 Some(ns) => format!("{}/{}", ns, self.path),
134 None => self.path.clone(),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ChunkReference {
142 pub chunk_id: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub alias: Option<String>,
147 #[serde(default = "default_true")]
149 pub required: bool,
150 #[serde(default)]
152 pub imports: Vec<String>,
153}
154
155fn default_true() -> bool {
156 true
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ChunkComposition {
162 #[serde(default)]
164 pub composed_of: Vec<ChunkReference>,
165 #[serde(default)]
167 pub composed_by: Vec<String>,
168 #[serde(default = "default_true")]
170 pub is_atomic: bool,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub composition_strategy: Option<String>,
174}
175
176impl Default for ChunkComposition {
177 fn default() -> Self {
178 Self {
179 composed_of: Vec::new(),
180 composed_by: Vec::new(),
181 is_atomic: true,
182 composition_strategy: None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct ChunkMetrics {
190 #[serde(default)]
192 pub loc: usize,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub complexity: Option<f32>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub reusability_score: Option<f32>,
199 #[serde(default)]
201 pub export_count: usize,
202 #[serde(default)]
204 pub dependency_count: usize,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub coupling: Option<f32>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct SourceLocation {
213 pub file: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub start_line: Option<usize>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub end_line: Option<usize>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub start_col: Option<usize>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub end_col: Option<usize>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct AtomicChunk {
232 pub chunk_id: String,
234
235 #[serde(default)]
237 pub aliases: Vec<ChunkAlias>,
238
239 pub name: String,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub description: Option<String>,
245
246 pub language: String,
248
249 #[serde(default)]
251 pub granularity: ChunkGranularity,
252
253 #[serde(default)]
255 pub categories: Vec<ChunkCategory>,
256
257 #[serde(default)]
259 pub tags: Vec<String>,
260
261 #[serde(default)]
263 pub concepts: Vec<String>,
264
265 #[serde(default)]
267 pub provides: Vec<String>,
268
269 #[serde(default)]
271 pub requires: Vec<String>,
272
273 #[serde(default)]
275 pub platform: PlatformConstraint,
276
277 #[serde(default)]
279 pub composition: ChunkComposition,
280
281 #[serde(default)]
283 pub metrics: ChunkMetrics,
284
285 #[serde(default)]
287 pub sources: Vec<SourceLocation>,
288
289 pub content_hash: String,
291
292 pub size: usize,
294
295 #[serde(default = "default_license")]
297 pub license: String,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub created_at: Option<String>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub version: Option<String>,
306}
307
308fn default_license() -> String {
309 "MIT".to_string()
310}
311
312impl AtomicChunk {
313 pub fn new(
315 chunk_id: String,
316 name: String,
317 language: String,
318 content_hash: String,
319 size: usize,
320 ) -> Self {
321 Self {
322 chunk_id,
323 aliases: Vec::new(),
324 name,
325 description: None,
326 language,
327 granularity: ChunkGranularity::default(),
328 categories: Vec::new(),
329 tags: Vec::new(),
330 concepts: Vec::new(),
331 provides: Vec::new(),
332 requires: Vec::new(),
333 platform: PlatformConstraint::default(),
334 composition: ChunkComposition::default(),
335 metrics: ChunkMetrics::default(),
336 sources: Vec::new(),
337 content_hash,
338 size,
339 license: "MIT".to_string(),
340 created_at: Some(chrono::Utc::now().to_rfc3339()),
341 version: None,
342 }
343 }
344
345 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
347 self.aliases.push(ChunkAlias::new(alias));
348 self
349 }
350
351 pub fn with_aliases(mut self, aliases: Vec<String>) -> Self {
353 for (i, alias) in aliases.into_iter().enumerate() {
354 let mut a = ChunkAlias::new(alias);
355 a.primary = i == 0;
356 self.aliases.push(a);
357 }
358 self
359 }
360
361 pub fn with_categories(mut self, categories: Vec<ChunkCategory>) -> Self {
363 self.categories = categories;
364 self
365 }
366
367 pub fn with_granularity(mut self, granularity: ChunkGranularity) -> Self {
369 self.granularity = granularity;
370 self
371 }
372
373 pub fn with_concepts(mut self, concepts: Vec<String>) -> Self {
375 self.concepts = concepts;
376 self
377 }
378
379 pub fn composed_of(mut self, chunks: Vec<ChunkReference>) -> Self {
381 let is_empty = chunks.is_empty();
382 self.composition.composed_of = chunks;
383 self.composition.is_atomic = is_empty;
384 self
385 }
386
387 pub fn primary_alias(&self) -> Option<&ChunkAlias> {
389 self.aliases.iter().find(|a| a.primary)
390 }
391
392 pub fn display_name(&self) -> String {
394 self.primary_alias()
395 .map(|a| a.full_path())
396 .unwrap_or_else(|| self.name.clone())
397 }
398
399 pub fn is_atomic(&self) -> bool {
401 self.composition.is_atomic && self.composition.composed_of.is_empty()
402 }
403}
404
405#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407pub struct AliasRegistry {
408 pub aliases: HashMap<String, String>,
410 pub chunks: HashMap<String, Vec<String>>,
412 #[serde(default)]
414 pub reserved: Vec<String>,
415}
416
417impl AliasRegistry {
418 pub fn new() -> Self {
420 Self::default()
421 }
422
423 pub fn is_available(&self, alias: &str) -> bool {
425 !self.aliases.contains_key(alias) && !self.reserved.contains(&alias.to_string())
426 }
427
428 pub fn register(&mut self, alias: impl Into<String>, chunk_id: impl Into<String>) -> bool {
430 let alias = alias.into();
431 let chunk_id = chunk_id.into();
432
433 if !self.is_available(&alias) {
434 return false;
435 }
436
437 self.aliases.insert(alias.clone(), chunk_id.clone());
438 self.chunks
439 .entry(chunk_id)
440 .or_default()
441 .push(alias);
442
443 true
444 }
445
446 pub fn resolve(&self, alias: &str) -> Option<&String> {
448 self.aliases.get(alias)
449 }
450
451 pub fn get_aliases(&self, chunk_id: &str) -> Option<&Vec<String>> {
453 self.chunks.get(chunk_id)
454 }
455
456 pub fn generate_unique(&self, base: &str) -> String {
458 if self.is_available(base) {
459 return base.to_string();
460 }
461
462 let mut counter = 1;
463 loop {
464 let candidate = format!("{}-{}", base, counter);
465 if self.is_available(&candidate) {
466 return candidate;
467 }
468 counter += 1;
469 }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_chunk_alias() {
479 let alias = ChunkAlias::new("utils/string-helpers")
480 .with_namespace("my-org");
481
482 assert_eq!(alias.full_path(), "my-org/utils/string-helpers");
483 assert!(alias.primary);
484 }
485
486 #[test]
487 fn test_alias_registry() {
488 let mut registry = AliasRegistry::new();
489
490 assert!(registry.is_available("test/chunk"));
491 assert!(registry.register("test/chunk", "chunk:sha256:abc123"));
492 assert!(!registry.is_available("test/chunk"));
493
494 let resolved = registry.resolve("test/chunk");
495 assert_eq!(resolved, Some(&"chunk:sha256:abc123".to_string()));
496 }
497
498 #[test]
499 fn test_atomic_chunk() {
500 let chunk = AtomicChunk::new(
501 "chunk:sha256:abc123".to_string(),
502 "string-helpers".to_string(),
503 "rust".to_string(),
504 "abc123".to_string(),
505 1024,
506 )
507 .with_alias("utils/string-helpers")
508 .with_granularity(ChunkGranularity::Module)
509 .with_categories(vec![ChunkCategory::Utility]);
510
511 assert!(ChunkComposition::default().is_atomic);
513
514 assert!(chunk.is_atomic());
515 assert_eq!(chunk.display_name(), "utils/string-helpers");
516 }
517}