Skip to main content

systemprompt_extension/
error.rs

1//! Typed error enums raised by extension registration and configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum LoaderError {
10    #[error("Extension '{extension}' requires dependency '{dependency}' which is not registered")]
11    MissingDependency {
12        extension: String,
13        dependency: String,
14    },
15
16    #[error("Extension with ID '{0}' is already registered")]
17    DuplicateExtension(String),
18
19    #[error("Failed to initialize extension '{extension}': {message}")]
20    InitializationFailed { extension: String, message: String },
21
22    #[error("Failed to install schema for extension '{extension}': {message}")]
23    SchemaInstallationFailed { extension: String, message: String },
24
25    #[error("Migration failed for extension '{extension}': {message}")]
26    MigrationFailed { extension: String, message: String },
27
28    #[error(
29        "Migration {version} for extension '{extension}' is not reversible (no down SQL provided)"
30    )]
31    MigrationNotReversible { extension: String, version: u32 },
32
33    #[error(
34        "Extension '{extension}' migration slot {version} was applied as '{stored_name}' but the \
35         tree now names it '{current_name}'. A migration file was deleted and its number reused; \
36         established databases have already spent that slot, so the new SQL would be skipped, \
37         never executed. Renumber it above every used slot and leave a \
38         `{version:03}_{stored_name}.tombstone` behind so the number cannot be claimed again."
39    )]
40    MigrationSlotReused {
41        extension: String,
42        version: u32,
43        stored_name: String,
44        current_name: String,
45    },
46
47    #[error("Configuration validation failed for extension '{extension}': {message}")]
48    ConfigValidationFailed { extension: String, message: String },
49
50    #[error("Extension '{extension}' uses reserved API path '{path}'")]
51    ReservedPathCollision { extension: String, path: String },
52
53    #[error("Extension '{extension}' has invalid base path '{path}': must start with /api/")]
54    InvalidBasePath { extension: String, path: String },
55
56    #[error("Dependency cycle detected while ordering extensions: {chain}")]
57    DependencyCycle { chain: String },
58
59    #[error(
60        "Extension '{extension}' migration ALTERs table '{table}' but does not create it in its \
61         schemas() nor declare it in cross_extension_tables(); cross-extension table mutations \
62         must be declared explicitly"
63    )]
64    CrossExtensionAlterUndeclared { extension: String, table: String },
65
66    #[error(
67        "Table '{table}' is created by both extension '{extension_a}' and '{extension_b}'; every \
68         table must be declared by exactly one extension"
69    )]
70    DuplicateTableOwner {
71        table: String,
72        extension_a: String,
73        extension_b: String,
74    },
75
76    #[error(
77        "Extension '{extension}' declares cross_extension_tables() entry '{table}', which is not \
78         a table created by any other loaded extension"
79    )]
80    CrossExtensionTableNotOwned { extension: String, table: String },
81
82    #[error(
83        "Extension '{extension}' seed '{seed}' contains forbidden statement '{statement}'; seeds \
84         may only contain INSERT … ON CONFLICT, UPDATE, MERGE, or WITH … INSERT"
85    )]
86    InvalidSeedStatement {
87        extension: String,
88        seed: String,
89        statement: String,
90    },
91
92    #[error(
93        "Extension '{extension}' seed '{seed}' contains a bare INSERT with no ON CONFLICT clause; \
94         seeds run on every boot and must be idempotent — add ON CONFLICT … DO NOTHING/UPDATE"
95    )]
96    SeedInsertNotIdempotent { extension: String, seed: String },
97
98    #[error("Extension '{extension}' seed '{seed}' failed to parse or apply: {message}")]
99    SeedFailed {
100        extension: String,
101        seed: String,
102        message: String,
103    },
104}
105
106#[derive(Debug, Error)]
107pub enum ConfigError {
108    #[error("Configuration key '{0}' not found")]
109    NotFound(String),
110
111    #[error("Invalid configuration value for '{key}': {message}")]
112    InvalidValue { key: String, message: String },
113
114    #[error("Failed to parse configuration: {message}")]
115    ParseError { message: String },
116
117    #[error("Schema validation failed: {0}")]
118    SchemaValidation(String),
119}