Creating Plugins
Extend Xberg with custom extractors, post-processors, OCR backends, and validators registered globally for use across all extraction calls.
Plugin Types
Section titled “Plugin Types”| Type | Purpose | Use case |
|---|---|---|
| DocumentExtractor | Extract content from file formats | New format support, override built-in extractors |
| PostProcessor | Transform extraction results | Metadata enrichment, content filtering, text normalization |
| OcrBackend | Perform OCR on images | Cloud OCR services, custom OCR engines |
| Validator | Validate extraction quality | Minimum content length, quality score thresholds |
| EmbeddingBackend | Generate embedding vectors | Custom embedding models, RAG pipelines |
| RerankerBackend | Score query/document pairs | Cross-encoder reranking of retrieved chunks |
| TokenizerBackend | Count tokens for chunk boundaries | Model-specific tokenizers, token-aware chunk sizing |
| Renderer | Convert results to output formats | Custom Markdown, HTML, Djot, or plain-text renderers |
All plugins must be thread-safe (Send + Sync in Rust, thread-safe in Python) and
implement initialize() / shutdown() lifecycle methods.
Document Extractors
Section titled “Document Extractors”Implementation
Section titled “Implementation”use xberg::plugins::{DocumentExtractor, Plugin};use xberg::{Result, ExtractedDocument, ExtractionConfig, Metadata};use async_trait::async_trait;use std::path::Path;
struct CustomJsonExtractor;
impl Plugin for CustomJsonExtractor { fn name(&self) -> &str { "custom-json-extractor" } fn version(&self) -> String { "1.0.0".to_string() } fn initialize(&self) -> Result<()> { Ok(()) } fn shutdown(&self) -> Result<()> { Ok(()) }}
#[async_trait]impl DocumentExtractor for CustomJsonExtractor { async fn extract( &self, content: &[u8], _mime_type: &str, _config: &ExtractionConfig, ) -> Result<ExtractedDocument> { let json: serde_json::Value = serde_json::from_slice(content)?; let text = extract_text_from_json(&json);
Ok(ExtractedDocument { content: text, mime_type: "application/json".to_string(), metadata: Metadata::default(), tables: vec![], detected_languages: None, chunks: None, images: None, }) }
fn supported_mime_types(&self) -> &[&str] { &["application/json", "text/json"] }
fn priority(&self) -> i32 { 50 }}
fn extract_text_from_json(value: &serde_json::Value) -> String { match value { serde_json::Value::String(s) => format!("{}\n", s), serde_json::Value::Array(arr) => arr.iter().map(extract_text_from_json).collect(), serde_json::Value::Object(obj) => obj.values().map(extract_text_from_json).collect(), _ => String::new(), }}from xberg import register_document_extractor, ExtractInput, ExtractionConfigimport json
class CustomJsonExtractor: def name(self) -> str: return "custom-json-extractor"
def version(self) -> str: return "1.0.0"
def supported_mime_types(self) -> list[str]: return ["application/json"]
def priority(self) -> int: return 50
def extract(self, input: ExtractInput, config: ExtractionConfig) -> dict: data: dict = json.loads(input.bytes) text: str = self._extract_text(data) return {"content": text, "mime_type": "application/json"}
def _extract_text(self, obj: object) -> str: if isinstance(obj, str): return f"{obj}\n" if isinstance(obj, list): return "".join(self._extract_text(item) for item in obj) if isinstance(obj, dict): return "".join(self._extract_text(v) for v in obj.values()) return ""
def initialize(self) -> None: pass
def shutdown(self) -> None: pass
extractor: CustomJsonExtractor = CustomJsonExtractor()register_document_extractor(extractor)Registration
Section titled “Registration”from xberg import register_document_extractor, ExtractInput, ExtractionConfig, ExtractedDocument
class CustomExtractor: def name(self) -> str: return "custom"
def version(self) -> str: return "1.0.0"
def supported_mime_types(self) -> list[str]: return ["application/x-custom"]
def extract(self, input: ExtractInput, config: ExtractionConfig) -> dict: content = input.bytes.decode("utf-8") if input.bytes else "" return {"content": content, "mime_type": "application/x-custom"}
extractor = CustomExtractor()register_document_extractor(extractor)print("Extractor registered")import { listDocumentExtractors, registerDocumentExtractor, unregisterDocumentExtractor, clearDocumentExtractors, type DocumentExtractor, type ExtractedDocument,} from "@xberg-io/xberg";
// Custom document extractors are supported: implement `DocumentExtractor`// and register it. See `plugin_extractor.md` for a complete example.const customExtractor: DocumentExtractor = { name: () => "custom-text-extractor", supportedMimeTypes: () => ["text/x-custom"], priority: () => 60, async extract(): Promise<ExtractedDocument> { return { content: "custom extraction result", mimeType: "text/x-custom" }; },};registerDocumentExtractor(customExtractor);
// List all registered document extractorsconst extractors = listDocumentExtractors();console.log("Available extractors:", extractors);
// Unregister a specific extractor (use with caution)unregisterDocumentExtractor("custom-text-extractor");
// Clear all extractors (use with extreme caution)// clearDocumentExtractors();use xberg::plugins::registry::get_document_extractor_registry;use std::sync::Arc;
fn register_custom_extractor() -> xberg::Result<()> { let extractor = Arc::new(CustomJsonExtractor); let registry = get_document_extractor_registry(); registry.write().unwrap().register(extractor)?; Ok(())}package main
import ( "log"
"github.com/xberg-io/xberg/packages/go")
func main() { // Register custom extractor with priority 50 if err := xberg.RegisterDocumentExtractor("custom-json-extractor", 50); err != nil { log.Fatalf("register extractor failed: %v", err) }
input := xberg.ExtractInputFromURI("document.json") result, err := xberg.Extract(*input, xberg.ExtractionConfig{}) if err != nil { log.Fatalf("extract failed: %v", err) } log.Printf("Extracted content length: %d", len(result.Results[0].Content))}import io.xberg.Xberg;import io.xberg.ExtractInputKind;import io.xberg.ExtractionResult;import io.xberg.ExtractedDocument;import io.xberg.ExtractInput;import io.xberg.ExtractionConfig;import io.xberg.XbergException;import java.io.IOException;
public class CustomExtractorExample { public static void main(String[] args) { try { ExtractionResult output = Xberg.extract( ExtractInput.builder().withKind(ExtractInputKind.Uri).withUri("document.json").build(), ExtractionConfig.builder().build() ); ExtractedDocument result = output.results().get(0); System.out.println("Extracted content length: " + result.content().length()); } catch (IOException | XbergException e) { e.printStackTrace(); } }}using Xberg;using System;using System.Collections.Generic;
var extractor = new CustomExtractor();DocumentExtractorRegistry.RegisterDocumentExtractor(extractor);Console.WriteLine("Extractor registered");
public class CustomExtractor : IDocumentExtractor{ public string Name => "custom"; public string Version => "1.0.0"; public int Priority => 50; public List<string> SupportedMimeTypes => new() { "application/x-custom" };
public void Initialize() { } public void Shutdown() { }
public bool CanHandle(string path, string mimeType) => mimeType == "application/x-custom";
public ExtractedDocument Extract(ExtractInput input, ExtractionConfig config) { return new ExtractedDocument { Content = "Extracted content", MimeType = "application/x-custom", Metadata = new Metadata(), }; }}require 'xberg'
# Register custom extractor with priority 50Xberg.register_document_extractor( name: "custom-json-extractor", extractor: ->(content, mime_type, config) { JSON.parse(content.to_s) }, priority: 50)
input = Xberg::ExtractInput.new(uri: "document.json")config = Xberg::ExtractionConfig.newresult = Xberg.extract(input, config)puts "Extracted content length: #{result.results.first.content.length}"Priority System
Section titled “Priority System”When multiple extractors support the same MIME type, the highest priority wins:
| Range | Level |
|---|---|
| 0–25 | Fallback / low-quality |
| 26–49 | Alternative |
| 50 | Default (built-in) |
| 51–75 | Enhanced / premium |
| 76–100 | Specialized / high-priority |
Post-Processors
Section titled “Post-Processors”Processors execute in three stages:
- Early — Foundational: language detection, quality scoring, text normalization
- Middle — Transformation: keyword extraction, token reduction, summarization
- Late — Final: custom metadata, analytics, output formatting
Implementation
Section titled “Implementation”use xberg::plugins::{Plugin, PostProcessor, ProcessingStage};use xberg::{Result, ExtractedDocument, ExtractionConfig};use async_trait::async_trait;
struct WordCountProcessor;
impl Plugin for WordCountProcessor { fn name(&self) -> &str { "word-count" } fn version(&self) -> String { "1.0.0".to_string() } fn initialize(&self) -> Result<()> { Ok(()) } fn shutdown(&self) -> Result<()> { Ok(()) }}
#[async_trait]impl PostProcessor for WordCountProcessor { async fn process( &self, result: &mut ExtractedDocument, _config: &ExtractionConfig ) -> Result<()> { let word_count = result.content.split_whitespace().count();
result.processing_warnings.push(ProcessingWarning { source: "word-count".to_string(), message: format!("Processed with word count: {}", word_count) });
Ok(()) }
fn processing_stage(&self) -> ProcessingStage { ProcessingStage::Early }
fn should_process( &self, result: &ExtractedDocument, _config: &ExtractionConfig ) -> bool { !result.content.is_empty() }}import loggingfrom xberg import register_post_processor, ExtractedDocument, ExtractionConfig
logger = logging.getLogger(__name__)
class WordCountProcessor: def name(self) -> str: return "word_count"
def version(self) -> str: return "1.0.0"
def processing_stage(self) -> str: return "early"
def process(self, result: ExtractedDocument, config: ExtractionConfig) -> None: word_count: int = len(result.content.split()) logger.info(f"Word count: {word_count}")
def should_process(self, result: ExtractedDocument, config: ExtractionConfig) -> bool: return bool(result.content)
def initialize(self) -> None: pass
def shutdown(self) -> None: pass
processor: WordCountProcessor = WordCountProcessor()register_post_processor(processor)Conditional Processing
Section titled “Conditional Processing”from xberg import ExtractedDocument, ExtractionConfig, register_post_processor
class PdfOnlyProcessor: def name(self) -> str: return "pdf-only-processor"
def version(self) -> str: return "1.0.0"
def processing_stage(self) -> str: return "early"
def process(self, result: ExtractedDocument, config: ExtractionConfig) -> None: pass
def should_process(self, result: ExtractedDocument, config: ExtractionConfig) -> bool: return result.mime_type == "application/pdf"
processor: PdfOnlyProcessor = PdfOnlyProcessor()register_post_processor(processor)impl PostProcessor for PdfOnlyProcessor { async fn process( &self, result: &mut ExtractedDocument, _config: &ExtractionConfig ) -> Result<()> { Ok(()) }
fn processing_stage(&self) -> ProcessingStage { ProcessingStage::Middle }
fn should_process( &self, result: &ExtractedDocument, _config: &ExtractionConfig ) -> bool { result.mime_type == "application/pdf" }}package main
import ( "encoding/json" "log" "unsafe"
"github.com/xberg-io/xberg/packages/go")
/*#cgo CFLAGS: -I${SRCDIR}/../../../crates/xberg-ffi#cgo LDFLAGS: -L${SRCDIR}/../../../target/release -L${SRCDIR}/../../../target/debug -lxberg_ffi#include "../../../crates/xberg-ffi/xberg.h"#include <stdlib.h>*/import "C"
// pdfOnlyProcessor applies PDF-specific processing logic only to PDF documents//export pdfOnlyProcessorfunc pdfOnlyProcessor(resultJSON *C.char) *C.char { jsonStr := C.GoString(resultJSON) var result map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { return C.CString("{\"error\":\"Failed to parse result JSON\"}") }
// Check MIME type - only process PDFs mimeType, ok := result["mime_type"].(string) if !ok || mimeType != "application/pdf" { // Return unchanged for non-PDF documents outputJSON, err := json.Marshal(result) if err != nil { return C.CString("{\"error\":\"Failed to serialize result\"}") } return C.CString(string(outputJSON)) }
// Perform PDF-specific processing metadata, ok := result["metadata"].(map[string]interface{}) if !ok { metadata = make(map[string]interface{}) }
// Example PDF-specific processing: // - Extract tables as structured data // - Handle PDF-specific formatting // - Preserve document hierarchy
metadata["pdf_specific_processing"] = true metadata["processor_type"] = "pdf_only"
// Check for tables in PDF if tablesJSON, ok := result["tables_json"].(string); ok && tablesJSON != "" { var tables []interface{} if err := json.Unmarshal([]byte(tablesJSON), &tables); err == nil { metadata["table_count"] = len(tables) } }
result["metadata"] = metadata
// Serialize back to JSON outputJSON, err := json.Marshal(result) if err != nil { return C.CString("{\"error\":\"Failed to serialize result\"}") }
return C.CString(string(outputJSON))}
func main() { // Register the post-processor with priority 70 if err := xberg.RegisterPostProcessor("pdf_only_processor", 70, (C.PostProcessorCallback)(C.pdfOnlyProcessor)); err != nil { log.Fatalf("failed to register post-processor: %v", err) } defer func() { if err := xberg.UnregisterPostProcessor("pdf_only_processor"); err != nil { log.Printf("warning: failed to unregister post-processor: %v", err) } }()
// Process multiple documents - processor will only affect PDFs files := []string{ "document.pdf", "image.jpg", "spreadsheet.xlsx", }
for _, file := range files { input := xberg.ExtractInputFromURI(file) result, err := xberg.Extract(*input, xberg.ExtractionConfig{}) if err != nil { log.Printf("Warning: extraction failed for %s: %v", file, err) continue }
// Parse metadata to check if PDF processing occurred var metadata map[string]interface{} if metaJSON, ok := result.Results[0].MetadataJSON.(string); ok { if err := json.Unmarshal([]byte(metaJSON), &metadata); err == nil { if pdfProcessing, ok := metadata["pdf_specific_processing"].(bool); ok && pdfProcessing { log.Printf("PDF-specific processing applied to: %s", file) if tableCount, ok := metadata["table_count"].(float64); ok { log.Printf(" Tables found: %.0f", tableCount) } } else { log.Printf("Skipped PDF processor for: %s (MIME: %s)", file, result.Results[0].MimeType) } } } }}import io.xberg.PostProcessor;import java.util.HashMap;import java.util.Map;
PostProcessor pdfOnly = result -> { if (!result.getMimeType().equals("application/pdf")) { return result; }
Map<String, Object> metadata = new HashMap<>(result.getMetadata()); metadata.put("pdf_processed", true);
return result;};using Xberg;
public class PdfOnlyProcessor : IPostProcessor{ public string Name => "pdf-only-processor"; public string Version => "1.0.0"; public int Priority => 50; public ProcessingStage ProcessingStage => ProcessingStage.Middle;
public void Initialize() { } public void Shutdown() { }
public ulong EstimatedDurationMs(ExtractedDocument result) => 1;
public void Process(ExtractedDocument result, ExtractionConfig config) { }
public bool ShouldProcess(ExtractedDocument result, ExtractionConfig config) => result.MimeType == "application/pdf";}
class Program{ static void Main() { var processor = new PdfOnlyProcessor(); PostProcessorRegistry.RegisterPostProcessor(processor); }}OCR Backends
Section titled “OCR Backends”Implementation
Section titled “Implementation”use xberg::plugins::{Plugin, OcrBackend, OcrBackendType};use xberg::{Result, ExtractedDocument, OcrConfig, Metadata};use async_trait::async_trait;use std::path::Path;
struct CloudOcrBackend { api_key: String, supported_langs: Vec<String>,}
impl Plugin for CloudOcrBackend { fn name(&self) -> &str { "cloud-ocr" } fn version(&self) -> String { "1.0.0".to_string() } fn initialize(&self) -> Result<()> { Ok(()) } fn shutdown(&self) -> Result<()> { Ok(()) }}
#[async_trait]impl OcrBackend for CloudOcrBackend { async fn process_image( &self, image_bytes: &[u8], config: &OcrConfig, ) -> Result<ExtractedDocument> { let text = self.call_cloud_api(image_bytes, &config.language).await?;
Ok(ExtractedDocument { content: text, mime_type: "text/plain".to_string(), metadata: Metadata::default(), tables: vec![], detected_languages: None, chunks: None, images: None, }) }
fn supports_language(&self, lang: &str) -> bool { self.supported_langs.iter().any(|l| l == lang) }
fn backend_type(&self) -> OcrBackendType { OcrBackendType::Custom }
fn supported_languages(&self) -> Vec<String> { self.supported_langs.clone() }}
impl CloudOcrBackend { async fn call_cloud_api( &self, image: &[u8], language: &str ) -> Result<String> { Ok("Extracted text".to_string()) }}from xberg import register_ocr_backend, ExtractedDocument, OcrBackendType, OcrConfig, Metadataimport httpx
class CloudOcrBackend: def __init__(self, api_key: str): self.api_key: str = api_key self.langs: list[str] = ["eng", "deu", "fra"]
def name(self) -> str: return "cloud-ocr"
def version(self) -> str: return "1.0.0"
def supported_languages(self) -> list[str]: return self.langs
def supports_language(self, lang: str) -> bool: return lang in self.langs
def backend_type(self) -> OcrBackendType: return OcrBackendType.CUSTOM
def supports_table_detection(self) -> bool: return False
def supports_document_processing(self) -> bool: return False
def emits_structured_markdown(self) -> bool: return False
def process_image(self, image_bytes: bytes, config: OcrConfig) -> ExtractedDocument: with httpx.Client() as client: response = client.post( "https://api.example.com/ocr", files={"image": image_bytes}, json={"language": config.language[0] if config.language else "eng"}, ) text: str = response.json()["text"] return ExtractedDocument( content=text, mime_type="text/plain", metadata=Metadata(), )
def process_image_file(self, path: str, config: OcrConfig) -> ExtractedDocument: with open(path, "rb") as f: return self.process_image(f.read(), config)
def process_document(self, path: str, config: OcrConfig) -> ExtractedDocument: return self.process_image_file(path, config)
def initialize(self) -> None: pass
def shutdown(self) -> None: pass
backend: CloudOcrBackend = CloudOcrBackend(api_key="your-api-key")register_ocr_backend(backend)import io.xberg.*;import io.xberg.ExtractInputKind;import java.lang.foreign.Arena;import java.lang.foreign.MemorySegment;import java.lang.foreign.ValueLayout;import java.net.http.*;import java.net.URI;
public class CloudOcrExample { public static void main(String[] args) { Arena callbackArena = Arena.ofAuto(); String apiKey = "your-api-key"; OcrBackend cloudOcr = (imageBytes, imageLength, configJson) -> { try { // Read image bytes from native memory byte[] image = imageBytes.reinterpret(imageLength) .toArray(ValueLayout.JAVA_BYTE); // Read config JSON String config = configJson.reinterpret(Long.MAX_VALUE) .getString(0); // Call cloud OCR API HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/ocr")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofByteArray(image)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); String text = parseTextFromResponse(response.body()); // Return result as C string return callbackArena.allocateFrom(text); } catch (Exception e) { return MemorySegment.NULL; } }; try (Arena arena = Arena.ofConfined()) { Xberg.registerOcrBackend("cloud-ocr", cloudOcr, arena); // Use custom OCR backend in extraction // Note: Requires ExtractionConfig with OCR enabled var resultOutput = Xberg.extract( io.xberg.ExtractInput.builder() .withKind(io.xberg.ExtractInputKind.Uri) .withUri("scanned.pdf") .build(), io.xberg.ExtractionConfig.builder().build() ); ExtractedDocument result = resultOutput.results().get(0); } catch (Exception e) { e.printStackTrace(); } }
private static String parseTextFromResponse(String json) { // Parse JSON response and extract text field return json; // Simplified }}using Xberg;using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks;
public class CloudOcrBackend : IOcrBackend{ private readonly string _apiKey; private readonly List<string> _langs = new() { "eng", "deu", "fra" }; private readonly HttpClient _httpClient = new();
public CloudOcrBackend(string apiKey) { _apiKey = apiKey; }
public string Name => "cloud-ocr"; public string Version => "1.0.0"; public OcrBackendType BackendType => OcrBackendType.Custom; public List<string> SupportedLanguages => _langs; public bool SupportsTableDetection => false; public bool SupportsDocumentProcessing => false; public bool EmitsStructuredMarkdown => false;
public void Initialize() { } public void Shutdown() => _httpClient.Dispose();
public bool SupportsLanguage(string lang) => _langs.Contains(lang);
public ExtractedDocument ProcessImage(byte[] imageBytes, OcrConfig config) { using var form = new MultipartFormDataContent(); form.Add(new ByteArrayContent(imageBytes), "image"); var lang = config.Language.Count > 0 ? config.Language[0] : "eng"; form.Add(new StringContent(lang), "language");
var response = _httpClient.PostAsync("https://api.example.com/ocr", form).Result; var json = response.Content.ReadAsStringAsync().Result; var doc = JsonDocument.Parse(json); var text = doc.RootElement.GetProperty("text").GetString() ?? "";
return new ExtractedDocument { Content = text, MimeType = "text/plain", Metadata = new Metadata(), }; }
public ExtractedDocument ProcessImageFile(string path, OcrConfig config) => ProcessImage(System.IO.File.ReadAllBytes(path), config);
public ExtractedDocument ProcessDocument(string path, OcrConfig config) => throw new OcrException("cloud-ocr does not support whole-document processing");}
class Program{ static void Main() { var backend = new CloudOcrBackend(apiKey: "your-api-key"); OcrBackendRegistry.RegisterOcrBackend(backend); }}require 'xberg'require 'net/http'
class CloudOcrBackend def name 'cloud-ocr' end
def supported_languages %w[eng fra deu] end
def process_image(image_data, language) uri = URI('https://api.example.com/ocr') req = Net::HTTP::Post.new(uri) req['Authorization'] = "Bearer #{ENV['OCR_API_KEY']}" req.body = image_data res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) } raise StandardError, res.message unless res.is_a?(Net::HTTPSuccess) { content: JSON.parse(res.body)['text'] } rescue StandardError => e raise StandardError, e.message endend
Xberg.register_ocr_backend(CloudOcrBackend.new)config = Xberg::ExtractionConfig.new( ocr: Xberg::OcrConfig.new(backend: 'cloud-ocr'))input = Xberg::ExtractInput.new(uri: 'doc.pdf')Xberg.extract(input, config)Registration
Section titled “Registration”Register the backend and set its name in OcrConfig:
from xberg import register_ocr_backend, unregister_ocr_backend
backend = CloudOcrBackend(api_key="your-api-key")register_ocr_backend(backend)
from xberg import extract, ExtractionConfig, OcrConfig
config = ExtractionConfig(ocr=OcrConfig(backend="cloud-ocr", language=["eng"]))result = extract("scanned.pdf", config=config)
unregister_ocr_backend("cloud-ocr")Validators
Section titled “Validators”use xberg::plugins::{Plugin, Validator};use xberg::{Result, ExtractedDocument, ExtractionConfig, XbergError};use async_trait::async_trait;
struct MinLengthValidator { min_length: usize,}
impl Plugin for MinLengthValidator { fn name(&self) -> &str { "min-length-validator" } fn version(&self) -> String { "1.0.0".to_string() } fn initialize(&self) -> Result<()> { Ok(()) } fn shutdown(&self) -> Result<()> { Ok(()) }}
#[async_trait]impl Validator for MinLengthValidator { async fn validate( &self, result: &ExtractedDocument, _config: &ExtractionConfig, ) -> Result<()> { if result.content.len() < self.min_length { return Err(XbergError::validation(format!( "Content too short: {} < {} characters", result.content.len(), self.min_length ))); } Ok(()) }
fn priority(&self) -> i32 { 100 }}from xberg import register_validator, ExtractedDocument, ExtractionConfig, ValidationError
class MinLengthValidator: def __init__(self, min_length: int = 100): self.min_length: int = min_length
def name(self) -> str: return "min_length_validator"
def version(self) -> str: return "1.0.0"
def priority(self) -> int: return 100
def validate(self, result: ExtractedDocument, config: ExtractionConfig) -> None: content_len: int = len(result.content) if content_len < self.min_length: raise ValidationError(f"Content too short: {content_len}")
def should_validate(self, result: ExtractedDocument, config: ExtractionConfig) -> bool: return True
def initialize(self) -> None: pass
def shutdown(self) -> None: pass
validator: MinLengthValidator = MinLengthValidator(min_length=100)register_validator(validator)import io.xberg.Xberg;import io.xberg.ExtractInputKind;import io.xberg.ExtractionResult;import io.xberg.ExtractedDocument;import io.xberg.ExtractInput;import io.xberg.ExtractionConfig;import io.xberg.Validator;import io.xberg.ValidationException;import io.xberg.XbergException;import java.io.IOException;
public class MinLengthValidatorExample { public static void main(String[] args) { int minLength = 100; Validator minLengthValidator = result -> { if (result.content().length() < minLength) { throw new ValidationException( "Content too short: " + result.content().length() + " < " + minLength ); } }; try { Xberg.registerValidator("min-length", minLengthValidator, 100); ExtractionResult output = Xberg.extract( ExtractInput.builder().withKind(ExtractInputKind.Uri).withUri("document.pdf").build(), ExtractionConfig.builder().build() ); ExtractedDocument result = output.results().get(0); System.out.println("Validation passed!"); } catch (ValidationException e) { System.err.println("Validation failed: " + e.getMessage()); } catch (IOException | XbergException e) { e.printStackTrace(); } }}using Xberg;
var validator = new MinLengthValidator(minLength: 100);ValidatorRegistry.RegisterValidator(validator);
public class MinLengthValidator : IValidator{ private readonly int _minLength;
public MinLengthValidator(int minLength = 100) { _minLength = minLength; }
public string Name => "min_length_validator"; public string Version => "1.0.0"; public int Priority => 100;
public void Validate(ExtractedDocument result, ExtractionConfig config) { var contentLength = result.Content.Length; if (contentLength < _minLength) throw new ValidationException($"Content too short: {contentLength}"); }
public bool ShouldValidate(ExtractedDocument result, ExtractionConfig config) => true; public void Initialize() { } public void Shutdown() { }}Quality Score Validator
Section titled “Quality Score Validator”#[async_trait]impl Validator for QualityValidator { async fn validate( &self, result: &ExtractedDocument, _config: &ExtractionConfig, ) -> Result<()> { let score = result.metadata .additional .get("quality_score") .and_then(|v| v.as_f64()) .unwrap_or(0.0);
if score < 0.5 { return Err(XbergError::validation(format!( "Quality score too low: {:.2} < 0.50", score ))); }
Ok(()) }}from xberg import ExtractedDocument, ExtractionConfig, ValidationError, register_validator
class QualityValidator: def name(self) -> str: return "quality-validator"
def version(self) -> str: return "1.0.0"
def validate(self, result: ExtractedDocument, config: ExtractionConfig) -> None: score: float = result.quality_score or 0.0 if score < 0.5: raise ValidationError( f"Quality score too low: {score:.2f}" )
validator: QualityValidator = QualityValidator()register_validator(validator)Validator qualityValidator = result -> { double score = result.getQualityScore() != null ? result.getQualityScore() : 0.0;
if (score < 0.5) { throw new ValidationException( String.format("Quality score too low: %.2f < 0.50", score) ); }};using Xberg;
public class QualityValidator : IValidator{ public string Name => "quality-validator"; public string Version => "1.0.0"; public int Priority => 100;
public void Validate(ExtractedDocument result, ExtractionConfig config) { var score = result.QualityScore ?? 0.0;
if (score < 0.5) throw new ValidationException($"Quality score too low: {score:F2}"); }
public bool ShouldValidate(ExtractedDocument result, ExtractionConfig config) => result.QualityScore.HasValue; public void Initialize() { } public void Shutdown() { }}
class Program{ static void Main() { var validator = new QualityValidator(); ValidatorRegistry.RegisterValidator(validator); }}Plugin Management
Section titled “Plugin Management”Listing
Section titled “Listing”from xberg import ( list_document_extractors, list_post_processors, list_ocr_backends, list_validators,)
extractors: list[str] = list_document_extractors()processors: list[str] = list_post_processors()ocr_backends: list[str] = list_ocr_backends()validators: list[str] = list_validators()
print(f"Extractors: {extractors}")print(f"Processors: {processors}")print(f"OCR backends: {ocr_backends}")print(f"Validators: {validators}")use xberg::plugins::registry::*;
let registry = get_document_extractor_registry();let extractors = registry.list()?;println!("Registered extractors: {:?}", extractors);
let registry = get_post_processor_registry();let processors = registry.list()?;println!("Registered processors: {:?}", processors);
let registry = get_ocr_backend_registry();let backends = registry.list()?;println!("Registered OCR backends: {:?}", backends);
let registry = get_validator_registry();let validators = registry.list()?;println!("Registered validators: {:?}", validators);// Java does not provide plugin listing functionality in v4.0.0// Plugins are registered and managed through the FFI layerusing Xberg;using System;
var extractors = XbergConverter.ListDocumentExtractors();var processors = XbergConverter.ListPostProcessors();var ocrBackends = XbergConverter.ListOcrBackends();var validators = XbergConverter.ListValidators();
Console.WriteLine($"Extractors: {string.Join(", ", extractors)}");Console.WriteLine($"Processors: {string.Join(", ", processors)}");Console.WriteLine($"OCR backends: {string.Join(", ", ocrBackends)}");Console.WriteLine($"Validators: {string.Join(", ", validators)}");Unregistering
Section titled “Unregistering”from xberg import ( unregister_document_extractor, unregister_post_processor, unregister_ocr_backend, unregister_validator,)
names: list[str] = [ "custom-json-extractor", "word_count", "cloud-ocr", "min_length_validator",]
unregister_document_extractor(names[0])unregister_post_processor(names[1])unregister_ocr_backend(names[2])unregister_validator(names[3])use xberg::plugins::registry::get_document_extractor_registry;
let registry = get_document_extractor_registry();registry.remove("custom-json-extractor")?;import io.xberg.Xberg;
try { // Unregister specific plugins Xberg.unregisterPostProcessor("word-count"); Xberg.unregisterValidator("min-length");} catch (XbergException e) { System.err.println("Failed to unregister: " + e.getMessage());}using Xberg;using System.Collections.Generic;
var names = new List<string>{ "custom-json-extractor", "word_count", "cloud-ocr", "min_length_validator"};
DocumentExtractorRegistry.Unregister(names[0]);PostProcessorRegistry.Unregister(names[1]);OcrBackendRegistry.Unregister(names[2]);ValidatorRegistry.Unregister(names[3]);Clearing All
Section titled “Clearing All”from xberg import ( clear_document_extractors, clear_post_processors, clear_ocr_backends, clear_validators,)
clear_post_processors()clear_validators()clear_ocr_backends()clear_document_extractors()
print("All plugins cleared")use xberg::{clear_document_extractors, clear_post_processors, clear_ocr_backends, clear_validators};
fn main() { clear_document_extractors(); clear_post_processors(); clear_ocr_backends(); clear_validators();
println!("All plugins cleared");}// Java does not provide bulk clearing functionality in v4.0.0// Unregister plugins individually using unregisterPostProcessor() and unregisterValidator()using Xberg;using System;
PostProcessorRegistry.Clear();ValidatorRegistry.Clear();OcrBackendRegistry.Clear();DocumentExtractorRegistry.Clear();
Console.WriteLine("All plugins cleared");Thread Safety
Section titled “Thread Safety”use std::sync::{Arc, Mutex};use std::sync::atomic::{AtomicUsize, Ordering};use xberg::XbergError;
struct StatefulPlugin { call_count: AtomicUsize, cache: Mutex<HashMap<String, String>>,}
impl Plugin for StatefulPlugin { fn name(&self) -> &str { "stateful-plugin" } fn version(&self) -> String { "1.0.0".to_string() }
fn initialize(&self) -> Result<()> { self.call_count.store(0, Ordering::Release); Ok(()) }
fn shutdown(&self) -> Result<()> { let count = self.call_count.load(Ordering::Acquire); println!("Plugin called {} times", count); Ok(()) }}
#[async_trait]impl PostProcessor for StatefulPlugin { async fn process( &self, result: &mut ExtractedDocument, _config: &ExtractionConfig ) -> Result<()> { self.call_count.fetch_add(1, Ordering::AcqRel);
let mut cache = self.cache.lock() .map_err(|_| XbergError::plugin("Cache lock poisoned"))?; cache.insert("last_mime".to_string(), result.mime_type.clone());
Ok(()) }
fn processing_stage(&self) -> ProcessingStage { ProcessingStage::Middle }}import threadingfrom xberg import ExtractedDocument, ExtractionConfig
class StatefulPlugin: def __init__(self): self.lock: threading.Lock = threading.Lock() self.call_count: int = 0 self.cache: dict = {}
def name(self) -> str: return "stateful-plugin"
def version(self) -> str: return "1.0.0"
def processing_stage(self) -> str: return "early"
def process(self, result: ExtractedDocument, config: ExtractionConfig) -> None: with self.lock: self.call_count += 1 self.cache["last_mime"] = result.mime_type
def initialize(self) -> None: pass
def shutdown(self) -> None: passimport io.xberg.ExtractedDocument;import io.xberg.PostProcessor;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicInteger;
class StatefulPlugin implements PostProcessor { // Use atomic types for simple counters private final AtomicInteger callCount = new AtomicInteger(0);
// Use concurrent collections for complex state private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
@Override public ExtractedDocument process(ExtractedDocument result) { // Increment counter atomically callCount.incrementAndGet();
// Update cache (thread-safe) cache.put("last_mime", result.mimeType());
return result; }
public int getCallCount() { return callCount.get(); }}using Xberg;using System;using System.Collections.Concurrent;using System.Text.Json;
public class StatefulPostProcessor : IPostProcessor{ private readonly object _lock = new(); private int _callCount = 0; private readonly ConcurrentDictionary<string, string> _cache = new();
public string Name => "stateful-plugin"; public string Version => "1.0.0"; public int Priority => 50; public ProcessingStage ProcessingStage => ProcessingStage.Middle;
public void Initialize() { } public void Shutdown() { }
public bool ShouldProcess(ExtractedDocument result, ExtractionConfig config) => true; public ulong EstimatedDurationMs(ExtractedDocument result) => 5;
public void Process(ExtractedDocument result, ExtractionConfig config) { lock (_lock) { _callCount++; _cache["last_mime"] = result.MimeType; } result.Metadata.Additional["call_count"] = JsonSerializer.SerializeToElement(_callCount); }}Best Practices
Section titled “Best Practices”Naming: Use kebab-case (my-custom-plugin), lowercase only, no spaces or special characters.
Logging
Section titled “Logging”import loggingfrom xberg import ExtractInput, ExtractionConfig
logger = logging.getLogger(__name__)
class MyPlugin: def name(self) -> str: return "my-plugin"
def version(self) -> str: return "1.0.0"
def supported_mime_types(self) -> list[str]: return ["application/x-custom"]
def initialize(self) -> None: logger.info(f"Initializing plugin: {self.name()}")
def shutdown(self) -> None: logger.info(f"Shutting down plugin: {self.name()}")
def extract(self, input: ExtractInput, config: ExtractionConfig) -> dict: logger.info(f"Extracting {input.mime_type} ({len(input.bytes or b'')} bytes)") result: dict = {"content": "", "mime_type": input.mime_type or "application/x-custom"} if not result["content"]: logger.warning("Extraction resulted in empty content") return resultuse log::{info, warn, error};
impl Plugin for MyPlugin { fn initialize(&self) -> Result<()> { info!("Initializing plugin: {}", self.name()); Ok(()) }
fn shutdown(&self) -> Result<()> { info!("Shutting down plugin: {}", self.name()); Ok(()) }}
#[async_trait]impl DocumentExtractor for MyPlugin { async fn extract( &self, content: &[u8], mime_type: &str, _config: &ExtractionConfig, ) -> Result<ExtractedDocument> { info!("Extracting {} ({} bytes)", mime_type, content.len());
let result = ExtractedDocument::default();
if result.content.is_empty() { warn!("Extraction resulted in empty content"); }
Ok(result) }}import io.xberg.ExtractedDocument;import io.xberg.PostProcessor;import java.util.logging.Logger;import java.util.logging.Level;
class MyPlugin implements PostProcessor { private static final Logger logger = Logger.getLogger(MyPlugin.class.getName());
@Override public ExtractedDocument process(ExtractedDocument result) { logger.info("Processing " + result.mimeType() + " (" + result.content().length() + " bytes)");
// Processing...
if (result.content().isEmpty()) { logger.warning("Processing resulted in empty content"); }
return result; }}using Xberg;using Microsoft.Extensions.Logging;using System.Collections.Generic;
public class MyExtractorPlugin : IDocumentExtractor{ private readonly ILogger _logger;
public MyExtractorPlugin(ILogger logger) { _logger = logger; }
public string Name => "my-plugin"; public string Version => "1.0.0"; public int Priority => 50; public List<string> SupportedMimeTypes => new() { "text/plain" };
public void Initialize() { _logger.LogInformation($"Initializing plugin: {Name}"); }
public void Shutdown() { _logger.LogInformation($"Shutting down plugin: {Name}"); }
public bool CanHandle(string path, string mimeType) => mimeType == "text/plain";
public ExtractedDocument Extract(ExtractInput input, ExtractionConfig config) { _logger.LogInformation($"Extracting {input.MimeType} ({input.Bytes?.Length ?? 0} bytes)"); var content = input.Bytes is null ? "" : System.Text.Encoding.UTF8.GetString(input.Bytes); if (string.IsNullOrEmpty(content)) { _logger.LogWarning("Extraction resulted in empty content"); } return new ExtractedDocument { Content = content, MimeType = input.MimeType ?? "text/plain", Metadata = new Metadata(), }; }}Testing
Section titled “Testing”from xberg import ExtractInput, ExtractionConfig
def test_custom_extractor() -> None: extractor = CustomJsonExtractor() json_data: bytes = b'{"message": "Hello, world!"}' input = ExtractInput(kind="bytes", bytes=json_data, mime_type="application/json") config = ExtractionConfig() result: dict = extractor.extract(input, config) assert "Hello, world!" in result["content"] assert result["mime_type"] == "application/json"#[cfg(test)]mod tests { use super::*;
#[tokio::test] async fn test_custom_extractor() { let extractor = CustomJsonExtractor;
let json_data = br#"{"message": "Hello, world!"}"#; let config = ExtractionConfig::default();
let result = extractor .extract(json_data, "application/json", &config) .await .expect("Extraction failed");
assert!(result.content.contains("Hello, world!")); assert_eq!(result.mime_type, "application/json"); }}import io.xberg.ExtractedDocument;import io.xberg.PostProcessor;import org.junit.jupiter.api.Test;import java.util.HashMap;import java.util.Map;import static org.junit.jupiter.api.Assertions.*;
class PostProcessorTest { @Test void testWordCountProcessor() { PostProcessor processor = result -> { long count = result.getContent().split("\\s+").length;
Map<String, Object> metadata = new HashMap<>(result.getMetadata()); metadata.put("word_count", count);
return result; };
ExtractedDocument input = new ExtractedDocument( "Hello world test", "text/plain", new HashMap<>(), java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of(), true );
ExtractedDocument output = processor.process(input);
assertEquals(3, output.getMetadata().get("word_count")); }}using Xberg;using Xunit;
public class CustomExtractorTests{ [Fact] public void TestCustomExtractor() { var extractor = new CustomJsonExtractor(); var jsonData = System.Text.Encoding.UTF8.GetBytes(@"{""message"": ""Hello, world!""}"); var input = ExtractInput.FromBytes(jsonData, "application/json", null); var config = ExtractionConfig.Default();
var result = extractor.Extract(input, config);
Assert.Contains("Hello, world!", result.Content); Assert.Equal("application/json", result.MimeType); }}Complete Example: PDF Metadata Extractor
Section titled “Complete Example: PDF Metadata Extractor”from xberg import register_post_processor, ExtractedDocument, ExtractionConfigimport logging
logger = logging.getLogger(__name__)
class PdfMetadataExtractor: def __init__(self): self.processed_count: int = 0
def name(self) -> str: return "pdf_metadata_extractor"
def version(self) -> str: return "1.0.0"
def description(self) -> str: return "Logs PDF processing activity"
def processing_stage(self) -> str: return "early"
def should_process(self, result: ExtractedDocument, config: ExtractionConfig) -> bool: return result.mime_type == "application/pdf"
def process(self, result: ExtractedDocument, config: ExtractionConfig) -> None: self.processed_count += 1 logger.info(f"Processed PDF #{self.processed_count}")
def initialize(self) -> None: logger.info("PDF metadata extractor initialized")
def shutdown(self) -> None: logger.info(f"Processed {self.processed_count} PDFs")
processor: PdfMetadataExtractor = PdfMetadataExtractor()register_post_processor(processor)package main
import ( "encoding/json" "log" "sync/atomic" "unsafe"
"github.com/xberg-io/xberg/packages/go")
/*#cgo CFLAGS: -I${SRCDIR}/../../../crates/xberg-ffi#cgo LDFLAGS: -L${SRCDIR}/../../../target/release -L${SRCDIR}/../../../target/debug -lxberg_ffi#include "../../../crates/xberg-ffi/xberg.h"#include <stdlib.h>*/import "C"
// pdfMetadataState tracks statistics about PDF processingvar pdfMetadataState = struct { processedCount int64}{ processedCount: 0,}
// pdfMetadataExtractor enriches PDF extraction results with additional metadata//export pdfMetadataExtractorfunc pdfMetadataExtractor(resultJSON *C.char) *C.char { jsonStr := C.GoString(resultJSON) var result map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { return C.CString("{\"error\":\"Failed to parse result JSON\"}") }
// Only process PDFs mimeType, ok := result["mime_type"].(string) if !ok || mimeType != "application/pdf" { // Return unchanged for non-PDF documents outputJSON, err := json.Marshal(result) if err != nil { return C.CString("{\"error\":\"Failed to serialize result\"}") } return C.CString(string(outputJSON)) }
// Process PDF-specific metadata metadata, ok := result["metadata"].(map[string]interface{}) if !ok { metadata = make(map[string]interface{}) }
// Mark as processed by this processor metadata["pdf_processed"] = true
// Add content statistics content, ok := result["content"].(string) if ok { metadata["content_length"] = len(content) }
// Increment processed count atomically atomic.AddInt64(&pdfMetadataState.processedCount, 1) metadata["pdf_processor_version"] = "1.0.0"
result["metadata"] = metadata
// Serialize back to JSON outputJSON, err := json.Marshal(result) if err != nil { return C.CString("{\"error\":\"Failed to serialize result\"}") }
return C.CString(string(outputJSON))}
func main() { // Register the post-processor with priority 80, early stage if err := xberg.RegisterPostProcessor("pdf_metadata_extractor", 80, (C.PostProcessorCallback)(C.pdfMetadataExtractor)); err != nil { log.Fatalf("failed to register post-processor: %v", err) } defer func() { if err := xberg.UnregisterPostProcessor("pdf_metadata_extractor"); err != nil { log.Printf("warning: failed to unregister post-processor: %v", err) }
log.Printf("Total PDFs processed: %d", atomic.LoadInt64(&pdfMetadataState.processedCount)) }()
// Extract PDF document input := xberg.ExtractInputFromURI("document.pdf") result, err := xberg.Extract(*input, xberg.ExtractionConfig{}) if err != nil { log.Fatalf("extraction failed: %v", err) }
log.Printf("PDF MIME type: %s", result.Results[0].MimeType)
// Parse and display metadata var metadata map[string]interface{} if metaJSON, ok := result.Results[0].MetadataJSON.(string); ok { if err := json.Unmarshal([]byte(metaJSON), &metadata); err == nil { if pdfProcessed, ok := metadata["pdf_processed"].(bool); ok && pdfProcessed { log.Printf("PDF metadata extracted successfully") if contentLen, ok := metadata["content_length"].(float64); ok { log.Printf("Content length: %.0f bytes", contentLen) } } } }}import io.xberg.Xberg;import io.xberg.ExtractInputKind;import io.xberg.ExtractionResult;import io.xberg.ExtractedDocument;import io.xberg.ExtractInput;import io.xberg.ExtractionConfig;import io.xberg.PostProcessor;import io.xberg.XbergException;import java.io.IOException;import java.util.HashMap;import java.util.Map;import java.util.concurrent.atomic.AtomicInteger;import java.util.logging.Logger;
public class PdfMetadataExtractorExample { private static final Logger logger = Logger.getLogger( PdfMetadataExtractorExample.class.getName() ); public static void main(String[] args) { AtomicInteger processedCount = new AtomicInteger(0); PostProcessor pdfMetadata = result -> { if (!result.mimeType().equals("application/pdf")) { return result; } processedCount.incrementAndGet(); Map<String, Object> metadata = new HashMap<>(result.metadata()); metadata.put("pdf_processed", true); metadata.put("processing_timestamp", System.currentTimeMillis()); logger.info("Processed PDF: " + processedCount.get()); return result; }; try { Xberg.registerPostProcessor("pdf-metadata-extractor", pdfMetadata, 50); logger.info("PDF metadata extractor initialized"); ExtractionResult output = Xberg.extract( ExtractInput.builder().withKind(ExtractInputKind.Uri).withUri("document.pdf").build(), ExtractionConfig.builder().build() ); ExtractedDocument result = output.results().get(0); System.out.println("PDF processed: " + result.metadata().get("pdf_processed")); logger.info("Processed " + processedCount.get() + " PDFs"); } catch (IOException | XbergException e) { e.printStackTrace(); } }}using Xberg;using System;
var processor = new PdfMetadataExtractor();PostProcessorRegistry.RegisterPostProcessor(processor);
public class PdfMetadataExtractor : IPostProcessor{ private int _processedCount = 0;
public string Name => "pdf_metadata_extractor"; public string Version => "1.0.0"; public int Priority => 50; public ProcessingStage ProcessingStage => ProcessingStage.Early;
public bool ShouldProcess(ExtractedDocument result, ExtractionConfig config) => result.MimeType == "application/pdf";
public ulong EstimatedDurationMs(ExtractedDocument result) => 1;
public void Process(ExtractedDocument result, ExtractionConfig config) { _processedCount++; }
public void Initialize() { Console.WriteLine("PDF metadata extractor initialized"); }
public void Shutdown() { Console.WriteLine($"Processed {_processedCount} PDFs"); }}require 'xberg'
class PdfMetadataExtractor def initialize @count = 0 end
def call(result) return result unless result['mime_type'] == 'application/pdf' @count += 1 result['metadata'] ||= {} result['metadata']['pdf_order'] = @count result endend
extractor = PdfMetadataExtractor.newXberg.register_post_processor('pdf_metadata', extractor)
config = Xberg::ExtractionConfig.new( postprocessor: { enabled: true })
input = Xberg::ExtractInput.new(uri: 'report.pdf')result = Xberg.extract(input, config)puts "Metadata: #{result.results.first.metadata.inspect}"