...
This commit is contained in:
@@ -124,3 +124,48 @@ fn test_export_without_includes() {
|
||||
exported := os.read_file('${export_path}/test_col2/page1.md')!
|
||||
assert exported.contains('!!include')
|
||||
}
|
||||
|
||||
fn test_error_deduplication() {
|
||||
mut a := new(name: 'test')!
|
||||
mut col := a.new_collection(name: 'test', path: test_base)!
|
||||
|
||||
// Report same error twice
|
||||
col.error(
|
||||
category: .missing_include
|
||||
page_key: 'test:page1'
|
||||
message: 'Test error'
|
||||
)
|
||||
|
||||
col.error(
|
||||
category: .missing_include
|
||||
page_key: 'test:page1'
|
||||
message: 'Test error' // Same hash, should be deduplicated
|
||||
)
|
||||
|
||||
assert col.errors.len == 1
|
||||
|
||||
// Different page_key = different hash
|
||||
col.error(
|
||||
category: .missing_include
|
||||
page_key: 'test:page2'
|
||||
message: 'Test error'
|
||||
)
|
||||
|
||||
assert col.errors.len == 2
|
||||
}
|
||||
|
||||
fn test_error_hash() {
|
||||
err1 := CollectionError{
|
||||
category: .missing_include
|
||||
page_key: 'col:page1'
|
||||
message: 'Error message'
|
||||
}
|
||||
|
||||
err2 := CollectionError{
|
||||
category: .missing_include
|
||||
page_key: 'col:page1'
|
||||
message: 'Different message' // Hash is same!
|
||||
}
|
||||
|
||||
assert err1.hash() == err2.hash()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ module atlas
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.core.base
|
||||
import incubaid.herolib.ui.console
|
||||
import os
|
||||
|
||||
@[heap]
|
||||
@@ -15,6 +16,7 @@ pub mut:
|
||||
files map[string]&File
|
||||
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
|
||||
errors []CollectionError
|
||||
error_cache map[string]bool // Track error hashes to avoid duplicates
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -33,6 +35,7 @@ fn (mut self Atlas) new_collection(args CollectionNewArgs) !Collection {
|
||||
name: name
|
||||
path: path
|
||||
atlas: &self // Set atlas reference
|
||||
error_cache: map[string]bool{}
|
||||
}
|
||||
|
||||
return col
|
||||
@@ -208,3 +211,87 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
|
||||
redis.hset('atlas:path', c.name, col_dir.path)!
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CollectionErrorArgs {
|
||||
pub mut:
|
||||
category CollectionErrorCategory @[required]
|
||||
message string @[required]
|
||||
page_key string
|
||||
file string
|
||||
show_console bool // Show error in console immediately
|
||||
log_error bool = true // Log to errors array (default: true)
|
||||
}
|
||||
|
||||
// Report an error, avoiding duplicates based on hash
|
||||
pub fn (mut c Collection) error(args CollectionErrorArgs) {
|
||||
// Create error struct
|
||||
err := CollectionError{
|
||||
category: args.category
|
||||
page_key: args.page_key
|
||||
message: args.message
|
||||
file: args.file
|
||||
}
|
||||
|
||||
// Calculate hash for deduplication
|
||||
hash := err.hash()
|
||||
|
||||
// Check if this error was already reported
|
||||
if hash in c.error_cache {
|
||||
return // Skip duplicate
|
||||
}
|
||||
|
||||
// Mark this error as reported
|
||||
c.error_cache[hash] = true
|
||||
|
||||
// Log to errors array if requested
|
||||
if args.log_error {
|
||||
c.errors << err
|
||||
}
|
||||
|
||||
// Show in console if requested
|
||||
if args.show_console {
|
||||
console.print_stderr('[${c.name}] ${err.str()}')
|
||||
}
|
||||
}
|
||||
|
||||
// Get all errors
|
||||
pub fn (c Collection) get_errors() []CollectionError {
|
||||
return c.errors
|
||||
}
|
||||
|
||||
// Check if collection has errors
|
||||
pub fn (c Collection) has_errors() bool {
|
||||
return c.errors.len > 0
|
||||
}
|
||||
|
||||
// Clear all errors
|
||||
pub fn (mut c Collection) clear_errors() {
|
||||
c.errors = []CollectionError{}
|
||||
c.error_cache = map[string]bool{}
|
||||
}
|
||||
|
||||
// Get error summary by category
|
||||
pub fn (c Collection) error_summary() map[CollectionErrorCategory]int {
|
||||
mut summary := map[CollectionErrorCategory]int{}
|
||||
|
||||
for err in c.errors {
|
||||
summary[err.category] = summary[err.category] + 1
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// Print all errors to console
|
||||
pub fn (c Collection) print_errors() {
|
||||
if c.errors.len == 0 {
|
||||
console.print_green('Collection ${c.name}: No errors')
|
||||
return
|
||||
}
|
||||
|
||||
console.print_header('Collection ${c.name} - Errors (${c.errors.len})')
|
||||
|
||||
for err in c.errors {
|
||||
console.print_stderr(' ${err.str()}')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
module atlas
|
||||
|
||||
import crypto.md5
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
pub enum CollectionErrorCategory {
|
||||
circular_include
|
||||
missing_include
|
||||
include_syntax_error
|
||||
circular_include
|
||||
page_not_found
|
||||
invalid_page_reference
|
||||
file_not_found
|
||||
collection_not_found
|
||||
other
|
||||
invalid_collection
|
||||
general_error
|
||||
}
|
||||
|
||||
pub struct CollectionError {
|
||||
pub:
|
||||
page_key string // "collection:page_name" if applicable
|
||||
message string
|
||||
pub mut:
|
||||
category CollectionErrorCategory
|
||||
page_key string // Format: "collection:page" or just collection name
|
||||
message string
|
||||
file string // Optional: specific file path if relevant
|
||||
}
|
||||
|
||||
pub fn (e CollectionError) markdown() string {
|
||||
return 'ERROR [${e.category.str()}]: ${e.message}' + (if e.page_key != '' { ' (Page: `${e.page_key}`)' } else { '' })
|
||||
// Generate MD5 hash for error deduplication
|
||||
// Hash is based on category + page_key (or file if page_key is empty)
|
||||
pub fn (e CollectionError) hash() string {
|
||||
mut hash_input := '${e.category}'
|
||||
|
||||
if e.page_key != '' {
|
||||
hash_input += ':${e.page_key}'
|
||||
} else if e.file != '' {
|
||||
hash_input += ':${e.file}'
|
||||
}
|
||||
|
||||
return md5.hexhash(hash_input)
|
||||
}
|
||||
|
||||
// Get human-readable error message
|
||||
pub fn (e CollectionError) str() string {
|
||||
mut location := ''
|
||||
if e.page_key != '' {
|
||||
location = ' [${e.page_key}]'
|
||||
} else if e.file != '' {
|
||||
location = ' [${e.file}]'
|
||||
}
|
||||
|
||||
return '[${e.category}]${location}: ${e.message}'
|
||||
}
|
||||
|
||||
// Get category as string
|
||||
pub fn (e CollectionError) category_str() string {
|
||||
return match e.category {
|
||||
.circular_include { 'Circular Include' }
|
||||
.missing_include { 'Missing Include' }
|
||||
.include_syntax_error { 'Include Syntax Error' }
|
||||
.invalid_page_reference { 'Invalid Page Reference' }
|
||||
.file_not_found { 'File Not Found' }
|
||||
.invalid_collection { 'Invalid Collection' }
|
||||
.general_error { 'General Error' }
|
||||
}
|
||||
}
|
||||
@@ -26,5 +26,10 @@ pub fn (mut a Atlas) export(args ExportArgs) ! {
|
||||
include: args.include
|
||||
redis: args.redis
|
||||
)!
|
||||
|
||||
// Print errors for this collection if any
|
||||
if col.has_errors() {
|
||||
col.print_errors()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ module atlas
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.data.atlas.collection_error { CollectionError, CollectionErrorCategory }
|
||||
|
||||
@[heap]
|
||||
pub struct Page {
|
||||
@@ -59,12 +58,13 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
|
||||
// Prevent circular includes
|
||||
page_key := p.key()
|
||||
if page_key in visited {
|
||||
p.collection.errors << CollectionError{
|
||||
page_key: page_key
|
||||
message: 'Circular include detected for page `${page_key}`.'
|
||||
p.collection.error(
|
||||
category: .circular_include
|
||||
}
|
||||
return '' // Return empty string for circular includes
|
||||
page_key: page_key
|
||||
message: 'Circular include detected for page `${page_key}`'
|
||||
show_console: false // Don't show immediately, collect for later
|
||||
)
|
||||
return ''
|
||||
}
|
||||
visited[page_key] = true
|
||||
|
||||
@@ -90,11 +90,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
|
||||
target_collection = texttools.name_fix(parts[0])
|
||||
target_page = texttools.name_fix(parts[1])
|
||||
} else {
|
||||
p.collection.errors << CollectionError{
|
||||
page_key: page_key
|
||||
message: 'Invalid include format: `${include_ref}`.'
|
||||
p.collection.error(
|
||||
category: .include_syntax_error
|
||||
}
|
||||
page_key: page_key
|
||||
message: 'Invalid include format: `${include_ref}`'
|
||||
show_console: false
|
||||
)
|
||||
processed_lines << '<!-- Invalid include format: ${include_ref} -->'
|
||||
continue
|
||||
}
|
||||
@@ -112,12 +113,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
|
||||
|
||||
// Get the referenced page from atlas
|
||||
mut include_page := atlas.page_get(page_ref) or {
|
||||
p.collection.errors << CollectionError{
|
||||
page_key: page_key
|
||||
message: 'Included page `${page_ref}` not found.'
|
||||
p.collection.error(
|
||||
category: .missing_include
|
||||
}
|
||||
// If page not found, keep original line as comment
|
||||
page_key: page_key
|
||||
message: 'Included page `${page_ref}` not found'
|
||||
show_console: false
|
||||
)
|
||||
processed_lines << '<!-- Include not found: ${page_ref} -->'
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ fn (mut a Atlas) scan_directory(mut dir pathlib.Path) ! {
|
||||
}
|
||||
|
||||
// Scan subdirectories
|
||||
entries := dir.list(recursive: false)!
|
||||
for entry in entries.paths {
|
||||
mut entries := dir.list(recursive: false)!
|
||||
for mut entry in entries.paths {
|
||||
if !entry.is_dir() || should_skip_dir(entry) {
|
||||
continue
|
||||
}
|
||||
@@ -69,9 +69,9 @@ fn (mut c Collection) scan() ! {
|
||||
}
|
||||
|
||||
fn (mut c Collection) scan_path(mut dir pathlib.Path) ! {
|
||||
entries := dir.list(recursive: false)!
|
||||
mut entries := dir.list(recursive: false)!
|
||||
|
||||
for entry in entries.paths {
|
||||
for mut entry in entries.paths {
|
||||
// Skip hidden files/dirs
|
||||
if entry.name().starts_with('.') || entry.name().starts_with('_') {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user