spikard 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/ext/spikard_rb/Cargo.lock +6 -6
  3. data/ext/spikard_rb/Cargo.toml +1 -1
  4. data/lib/spikard/version.rb +1 -1
  5. data/vendor/crates/spikard-bindings-shared/Cargo.toml +9 -1
  6. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +61 -23
  7. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +16 -0
  8. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +1 -1
  9. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +22 -19
  10. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
  11. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +15 -6
  12. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +6 -0
  13. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +42 -36
  14. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +6 -1
  15. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +18 -6
  16. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
  17. data/vendor/crates/spikard-core/Cargo.toml +9 -1
  18. data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
  19. data/vendor/crates/spikard-core/src/debug.rs +2 -2
  20. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  21. data/vendor/crates/spikard-core/src/di/error.rs +1 -1
  22. data/vendor/crates/spikard-core/src/di/factory.rs +7 -3
  23. data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
  24. data/vendor/crates/spikard-core/src/di/resolved.rs +23 -0
  25. data/vendor/crates/spikard-core/src/di/value.rs +1 -0
  26. data/vendor/crates/spikard-core/src/errors.rs +3 -0
  27. data/vendor/crates/spikard-core/src/http.rs +19 -18
  28. data/vendor/crates/spikard-core/src/lifecycle.rs +42 -18
  29. data/vendor/crates/spikard-core/src/parameters.rs +61 -35
  30. data/vendor/crates/spikard-core/src/problem.rs +18 -4
  31. data/vendor/crates/spikard-core/src/request_data.rs +9 -8
  32. data/vendor/crates/spikard-core/src/router.rs +20 -6
  33. data/vendor/crates/spikard-core/src/schema_registry.rs +23 -8
  34. data/vendor/crates/spikard-core/src/type_hints.rs +11 -5
  35. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +29 -15
  36. data/vendor/crates/spikard-core/src/validation/mod.rs +45 -32
  37. data/vendor/crates/spikard-http/Cargo.toml +8 -1
  38. data/vendor/crates/spikard-rb/Cargo.toml +9 -1
  39. data/vendor/crates/spikard-rb/build.rs +1 -0
  40. data/vendor/crates/spikard-rb/src/lib.rs +58 -0
  41. data/vendor/crates/spikard-rb/src/lifecycle.rs +2 -2
  42. data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
  43. data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
  44. metadata +1 -1
@@ -82,7 +82,8 @@ impl ProblemDetails {
82
82
  /// Standard type URI for bad request (400)
83
83
  pub const TYPE_BAD_REQUEST: &'static str = "https://spikard.dev/errors/bad-request";
84
84
 
85
- /// Create a new ProblemDetails with required fields
85
+ /// Create a new `ProblemDetails` with required fields
86
+ #[must_use]
86
87
  pub fn new(type_uri: impl Into<String>, title: impl Into<String>, status: StatusCode) -> Self {
87
88
  Self {
88
89
  type_uri: type_uri.into(),
@@ -95,24 +96,29 @@ impl ProblemDetails {
95
96
  }
96
97
 
97
98
  /// Set the detail field
99
+ #[must_use]
98
100
  pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
99
101
  self.detail = Some(detail.into());
100
102
  self
101
103
  }
102
104
 
103
105
  /// Set the instance field
106
+ #[must_use]
104
107
  pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
105
108
  self.instance = Some(instance.into());
106
109
  self
107
110
  }
108
111
 
109
112
  /// Add an extension field
113
+ #[must_use]
110
114
  pub fn with_extension(mut self, key: impl Into<String>, value: Value) -> Self {
111
115
  self.extensions.insert(key.into(), value);
112
116
  self
113
117
  }
114
118
 
115
119
  /// Add all extensions from a JSON object
120
+ #[must_use]
121
+ #[allow(clippy::needless_pass_by_value)]
116
122
  pub fn with_extensions(mut self, extensions: Value) -> Self {
117
123
  if let Some(obj) = extensions.as_object() {
118
124
  for (key, value) in obj {
@@ -122,20 +128,21 @@ impl ProblemDetails {
122
128
  self
123
129
  }
124
130
 
125
- /// Create a validation error Problem Details from ValidationError
131
+ /// Create a validation error Problem Details from `ValidationError`
126
132
  ///
127
133
  /// This converts the FastAPI-style validation errors to RFC 9457 format:
128
- /// - `type`: "https://spikard.dev/errors/validation-error"
134
+ /// - `type`: <https://spikard.dev/errors/validation-error>
129
135
  /// - `title`: "Request Validation Failed"
130
136
  /// - `status`: 422
131
137
  /// - `detail`: Summary of error count
132
138
  /// - `errors`: Array of validation error details (as extension field)
139
+ #[must_use]
133
140
  pub fn from_validation_error(error: &ValidationError) -> Self {
134
141
  let error_count = error.errors.len();
135
142
  let detail = if error_count == 1 {
136
143
  "1 validation error in request".to_string()
137
144
  } else {
138
- format!("{} validation errors in request", error_count)
145
+ format!("{error_count} validation errors in request")
139
146
  };
140
147
 
141
148
  let errors_json = serde_json::to_value(&error.errors).unwrap_or_else(|_| serde_json::Value::Array(vec![]));
@@ -201,16 +208,23 @@ impl ProblemDetails {
201
208
  }
202
209
 
203
210
  /// Get the HTTP status code
211
+ #[must_use]
204
212
  pub fn status_code(&self) -> StatusCode {
205
213
  StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
206
214
  }
207
215
 
208
216
  /// Serialize to JSON string
217
+ ///
218
+ /// # Errors
219
+ /// Returns an error if the serialization fails.
209
220
  pub fn to_json(&self) -> Result<String, serde_json::Error> {
210
221
  serde_json::to_string(self)
211
222
  }
212
223
 
213
224
  /// Serialize to pretty JSON string
225
+ ///
226
+ /// # Errors
227
+ /// Returns an error if the serialization fails.
214
228
  pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
215
229
  serde_json::to_string_pretty(self)
216
230
  }
@@ -17,19 +17,19 @@ use bytes::Bytes;
17
17
  ///
18
18
  /// This is the language-agnostic representation passed to handlers.
19
19
  ///
20
- /// Uses Arc for HashMaps to enable cheap cloning without duplicating data.
21
- /// When RequestData is cloned, only the Arc pointers are cloned, not the underlying data.
20
+ /// Uses `Arc` for `HashMap`s to enable cheap cloning without duplicating data.
21
+ /// When `RequestData` is cloned, only the `Arc` pointers are cloned, not the underlying data.
22
22
  ///
23
- /// Performance optimization: raw_body stores the unparsed request body bytes.
24
- /// Language bindings should use raw_body when possible to avoid double-parsing.
25
- /// The body field is lazily parsed only when needed for validation.
23
+ /// Performance optimization: `raw_body` stores the unparsed request body bytes.
24
+ /// Language bindings should use `raw_body` when possible to avoid double-parsing.
25
+ /// The `body` field is lazily parsed only when needed for validation.
26
26
  #[derive(Debug, Clone)]
27
27
  pub struct RequestData {
28
28
  /// Path parameters extracted from the URL path
29
29
  pub path_params: Arc<HashMap<String, String>>,
30
30
  /// Query parameters parsed as JSON
31
31
  pub query_params: Value,
32
- /// Validated parameters produced by ParameterValidator (query/path/header/cookie combined).
32
+ /// Validated parameters produced by `ParameterValidator` (query/path/header/cookie combined).
33
33
  pub validated_params: Option<Value>,
34
34
  /// Raw query parameters as key-value pairs
35
35
  pub raw_query_params: Arc<HashMap<String, Vec<String>>>,
@@ -66,7 +66,7 @@ impl Serialize for RequestData {
66
66
  state.serialize_field("raw_query_params", &*self.raw_query_params)?;
67
67
  state.serialize_field("body", &self.body)?;
68
68
  #[cfg(feature = "di")]
69
- state.serialize_field("raw_body", &self.raw_body.as_ref().map(|b| b.as_ref()))?;
69
+ state.serialize_field("raw_body", &self.raw_body.as_ref().map(AsRef::as_ref))?;
70
70
  #[cfg(not(feature = "di"))]
71
71
  state.serialize_field("raw_body", &self.raw_body)?;
72
72
  state.serialize_field("headers", &*self.headers)?;
@@ -78,6 +78,7 @@ impl Serialize for RequestData {
78
78
  }
79
79
 
80
80
  impl<'de> Deserialize<'de> for RequestData {
81
+ #[allow(clippy::too_many_lines)]
81
82
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82
83
  where
83
84
  D: serde::Deserializer<'de>,
@@ -933,7 +934,7 @@ mod tests {
933
934
  "float": 3.2,
934
935
  "negative": -100,
935
936
  "zero": 0,
936
- "large": 9223372036854775807i64
937
+ "large": 9_223_372_036_854_775_807i64
937
938
  });
938
939
 
939
940
  let data = create_request_data(
@@ -70,8 +70,8 @@ pub struct JsonRpcMethodInfo {
70
70
 
71
71
  /// Route definition with compiled validators
72
72
  ///
73
- /// Validators are Arc-wrapped to enable cheap cloning across route instances
74
- /// and to support schema deduplication via SchemaRegistry.
73
+ /// Validators are `Arc`-wrapped to enable cheap cloning across route instances
74
+ /// and to support schema deduplication via `SchemaRegistry`.
75
75
  ///
76
76
  /// The `jsonrpc_method` field is optional and has zero overhead when None,
77
77
  /// enabling routes to optionally expose themselves as JSON-RPC methods.
@@ -102,10 +102,14 @@ impl Route {
102
102
  ///
103
103
  /// Auto-generates parameter schema from type hints in the path if no explicit schema provided.
104
104
  /// Type hints like `/items/{id:uuid}` generate appropriate JSON Schema validation.
105
- /// Explicit parameter_schema overrides auto-generated schemas.
105
+ /// Explicit `parameter_schema` overrides auto-generated schemas.
106
+ ///
107
+ /// # Errors
108
+ /// Returns an error if the schema compilation fails or metadata is invalid.
106
109
  ///
107
110
  /// The schema registry ensures each unique schema is compiled only once, improving
108
111
  /// startup performance and memory usage for applications with many routes.
112
+ #[allow(clippy::items_after_statements)]
109
113
  pub fn from_metadata(metadata: RouteMetadata, registry: &SchemaRegistry) -> Result<Self, String> {
110
114
  let method = metadata.method.parse()?;
111
115
 
@@ -135,7 +139,10 @@ impl Route {
135
139
  if is_empty_schema(&explicit_schema) {
136
140
  Some(auto_schema)
137
141
  } else {
138
- Some(crate::type_hints::merge_parameter_schemas(auto_schema, explicit_schema))
142
+ Some(crate::type_hints::merge_parameter_schemas(
143
+ &auto_schema,
144
+ &explicit_schema,
145
+ ))
139
146
  }
140
147
  }
141
148
  (Some(auto_schema), None) => Some(auto_schema),
@@ -187,17 +194,20 @@ impl Route {
187
194
  /// tags: vec!["users".to_string()],
188
195
  /// });
189
196
  /// ```
197
+ #[must_use]
190
198
  pub fn with_jsonrpc_method(mut self, info: JsonRpcMethodInfo) -> Self {
191
199
  self.jsonrpc_method = Some(info);
192
200
  self
193
201
  }
194
202
 
195
203
  /// Check if this route has JSON-RPC metadata
196
- pub fn is_jsonrpc_method(&self) -> bool {
204
+ #[must_use]
205
+ pub const fn is_jsonrpc_method(&self) -> bool {
197
206
  self.jsonrpc_method.is_some()
198
207
  }
199
208
 
200
209
  /// Get the JSON-RPC method name if present
210
+ #[must_use]
201
211
  pub fn jsonrpc_method_name(&self) -> Option<&str> {
202
212
  self.jsonrpc_method.as_ref().map(|m| m.method_name.as_str())
203
213
  }
@@ -210,6 +220,7 @@ pub struct Router {
210
220
 
211
221
  impl Router {
212
222
  /// Create a new router
223
+ #[must_use]
213
224
  pub fn new() -> Self {
214
225
  Self { routes: HashMap::new() }
215
226
  }
@@ -221,18 +232,21 @@ impl Router {
221
232
  }
222
233
 
223
234
  /// Find a route by method and path
235
+ #[must_use]
224
236
  pub fn find_route(&self, method: &Method, path: &str) -> Option<&Route> {
225
237
  self.routes.get(path)?.get(method)
226
238
  }
227
239
 
228
240
  /// Get all routes
241
+ #[must_use]
229
242
  pub fn routes(&self) -> Vec<&Route> {
230
243
  self.routes.values().flat_map(|methods| methods.values()).collect()
231
244
  }
232
245
 
233
246
  /// Get route count
247
+ #[must_use]
234
248
  pub fn route_count(&self) -> usize {
235
- self.routes.values().map(|m| m.len()).sum()
249
+ self.routes.values().map(std::collections::HashMap::len).sum()
236
250
  }
237
251
  }
238
252
 
@@ -1,9 +1,9 @@
1
- //! Schema registry for deduplication and OpenAPI generation
1
+ //! Schema registry for deduplication and `OpenAPI` generation
2
2
  //!
3
3
  //! This module provides a global registry that compiles JSON schemas once at application
4
4
  //! startup and reuses them across all routes. This enables:
5
5
  //! - Schema deduplication (same schema used by multiple routes)
6
- //! - OpenAPI spec generation (access to all schemas)
6
+ //! - `OpenAPI` spec generation (access to all schemas)
7
7
  //! - Memory efficiency (one compiled validator per unique schema)
8
8
 
9
9
  use crate::validation::SchemaValidator;
@@ -14,7 +14,7 @@ use std::sync::{Arc, RwLock};
14
14
  /// Global schema registry for compiled validators
15
15
  ///
16
16
  /// Thread-safe registry that ensures each unique schema is compiled exactly once.
17
- /// Uses RwLock for concurrent read access with occasional writes during startup.
17
+ /// Uses `RwLock` for concurrent read access with occasional writes during startup.
18
18
  pub struct SchemaRegistry {
19
19
  /// Map from schema JSON string to compiled validator
20
20
  schemas: RwLock<HashMap<String, Arc<SchemaValidator>>>,
@@ -22,13 +22,14 @@ pub struct SchemaRegistry {
22
22
 
23
23
  impl SchemaRegistry {
24
24
  /// Create a new empty schema registry
25
+ #[must_use]
25
26
  pub fn new() -> Self {
26
27
  Self {
27
28
  schemas: RwLock::new(HashMap::new()),
28
29
  }
29
30
  }
30
31
 
31
- /// Get or compile a schema, returning Arc to the compiled validator
32
+ /// Get or compile a schema, returning `Arc` to the compiled validator
32
33
  ///
33
34
  /// This method is thread-safe and uses a double-check pattern:
34
35
  /// 1. Fast path: Read lock to check if schema exists
@@ -38,9 +39,15 @@ impl SchemaRegistry {
38
39
  /// * `schema` - The JSON schema to compile
39
40
  ///
40
41
  /// # Returns
41
- /// Arc-wrapped compiled validator that can be cheaply cloned
42
+ /// `Arc`-wrapped compiled validator that can be cheaply cloned
43
+ ///
44
+ /// # Errors
45
+ /// Returns an error if schema serialization or compilation fails.
46
+ ///
47
+ /// # Panics
48
+ /// Panics if the read or write lock is poisoned.
42
49
  pub fn get_or_compile(&self, schema: &Value) -> Result<Arc<SchemaValidator>, String> {
43
- let key = serde_json::to_string(schema).map_err(|e| format!("Failed to serialize schema: {}", e))?;
50
+ let key = serde_json::to_string(schema).map_err(|e| format!("Failed to serialize schema: {e}"))?;
44
51
 
45
52
  {
46
53
  let schemas = self.schemas.read().unwrap();
@@ -62,10 +69,14 @@ impl SchemaRegistry {
62
69
  Ok(validator)
63
70
  }
64
71
 
65
- /// Get all registered schemas (for OpenAPI generation)
72
+ /// Get all registered schemas (for `OpenAPI` generation)
66
73
  ///
67
74
  /// Returns a snapshot of all compiled validators.
68
- /// Useful for generating OpenAPI specifications from runtime schema information.
75
+ /// Useful for generating `OpenAPI` specifications from runtime schema information.
76
+ ///
77
+ /// # Panics
78
+ /// Panics if the read lock is poisoned.
79
+ #[must_use]
69
80
  pub fn all_schemas(&self) -> Vec<Arc<SchemaValidator>> {
70
81
  let schemas = self.schemas.read().unwrap();
71
82
  schemas.values().cloned().collect()
@@ -74,6 +85,10 @@ impl SchemaRegistry {
74
85
  /// Get the number of unique schemas registered
75
86
  ///
76
87
  /// Useful for diagnostics and understanding schema deduplication effectiveness.
88
+ ///
89
+ /// # Panics
90
+ /// Panics if the read lock is poisoned.
91
+ #[must_use]
77
92
  pub fn schema_count(&self) -> usize {
78
93
  let schemas = self.schemas.read().unwrap();
79
94
  schemas.len()
@@ -39,6 +39,10 @@ fn path_type_regex() -> &'static Regex {
39
39
  /// assert_eq!(hints.get("id"), Some(&"uuid".to_string()));
40
40
  /// assert_eq!(hints.get("tag_id"), Some(&"int".to_string()));
41
41
  /// ```
42
+ ///
43
+ /// # Panics
44
+ /// Panics if regex capture groups don't contain expected indices.
45
+ #[must_use]
42
46
  pub fn parse_type_hints(route_path: &str) -> HashMap<String, String> {
43
47
  let mut hints = HashMap::new();
44
48
  let re = type_hint_regex();
@@ -66,6 +70,7 @@ pub fn parse_type_hints(route_path: &str) -> HashMap<String, String> {
66
70
  /// assert_eq!(strip_type_hints("/items/{id:uuid}"), "/items/{id}");
67
71
  /// assert_eq!(strip_type_hints("/files/{path:path}"), "/files/{*path}");
68
72
  /// ```
73
+ #[must_use]
69
74
  pub fn strip_type_hints(route_path: &str) -> String {
70
75
  let path_re = path_type_regex();
71
76
  let route_path = path_re.replace_all(route_path, "{*$1}");
@@ -86,6 +91,8 @@ pub fn strip_type_hints(route_path: &str) -> String {
86
91
  /// - `date` → `{"type": "string", "format": "date"}`
87
92
  /// - `datetime` → `{"type": "string", "format": "date-time"}`
88
93
  /// - `path` → `{"type": "string"}` (wildcard capture)
94
+ #[must_use]
95
+ #[allow(clippy::match_same_arms)]
89
96
  pub fn type_hint_to_schema(type_hint: &str) -> Value {
90
97
  match type_hint {
91
98
  "uuid" => json!({
@@ -95,7 +102,7 @@ pub fn type_hint_to_schema(type_hint: &str) -> Value {
95
102
  "int" | "integer" => json!({
96
103
  "type": "integer"
97
104
  }),
98
- "str" | "string" => json!({
105
+ "str" | "string" | "path" => json!({
99
106
  "type": "string"
100
107
  }),
101
108
  "float" | "number" => json!({
@@ -112,9 +119,6 @@ pub fn type_hint_to_schema(type_hint: &str) -> Value {
112
119
  "type": "string",
113
120
  "format": "date-time"
114
121
  }),
115
- "path" => json!({
116
- "type": "string"
117
- }),
118
122
  _ => json!({
119
123
  "type": "string"
120
124
  }),
@@ -145,6 +149,7 @@ pub fn type_hint_to_schema(type_hint: &str) -> Value {
145
149
  /// "required": ["id"]
146
150
  /// })));
147
151
  /// ```
152
+ #[must_use]
148
153
  pub fn auto_generate_parameter_schema(route_path: &str) -> Option<Value> {
149
154
  let type_hints = parse_type_hints(route_path);
150
155
 
@@ -204,7 +209,8 @@ pub fn auto_generate_parameter_schema(route_path: &str) -> Option<Value> {
204
209
  /// let merged = merge_parameter_schemas(auto_schema, explicit_schema);
205
210
  /// // Result: auto-generated id + explicit count with constraints
206
211
  /// ```
207
- pub fn merge_parameter_schemas(auto_schema: Value, explicit_schema: Value) -> Value {
212
+ #[must_use]
213
+ pub fn merge_parameter_schemas(auto_schema: &Value, explicit_schema: &Value) -> Value {
208
214
  let mut result = auto_schema.clone();
209
215
 
210
216
  let auto_props = result.get_mut("properties").and_then(|v| v.as_object_mut());
@@ -51,31 +51,35 @@ pub enum ErrorCondition {
51
51
 
52
52
  impl ErrorCondition {
53
53
  /// Determine the error condition from schema path and error message
54
+ #[must_use]
55
+ #[allow(clippy::ignored_unit_patterns)]
54
56
  pub fn from_schema_error(schema_path_str: &str, error_msg: &str) -> Self {
55
57
  match () {
56
- _ if schema_path_str.contains("minLength") => Self::StringTooShort { min_length: None },
57
- _ if schema_path_str.contains("maxLength") => Self::StringTooLong { max_length: None },
58
- _ if schema_path_str.contains("exclusiveMinimum")
58
+ () if schema_path_str.contains("minLength") => Self::StringTooShort { min_length: None },
59
+ () if schema_path_str.contains("maxLength") => Self::StringTooLong { max_length: None },
60
+ () if schema_path_str.contains("exclusiveMinimum")
59
61
  || (error_msg.contains("less than or equal to") && error_msg.contains("minimum")) =>
60
62
  {
61
63
  Self::GreaterThan { value: None }
62
64
  }
63
- _ if schema_path_str.contains("minimum") || error_msg.contains("less than the minimum") => {
65
+ () if schema_path_str.contains("minimum") || error_msg.contains("less than the minimum") => {
64
66
  Self::GreaterThanEqual { value: None }
65
67
  }
66
- _ if schema_path_str.contains("exclusiveMaximum")
68
+ () if schema_path_str.contains("exclusiveMaximum")
67
69
  || (error_msg.contains("greater than or equal to") && error_msg.contains("maximum")) =>
68
70
  {
69
71
  Self::LessThan { value: None }
70
72
  }
71
- _ if schema_path_str.contains("maximum") || error_msg.contains("greater than the maximum") => {
73
+ () if schema_path_str.contains("maximum") || error_msg.contains("greater than the maximum") => {
72
74
  Self::LessThanEqual { value: None }
73
75
  }
74
- _ if schema_path_str.contains("enum") || error_msg.contains("is not one of") => Self::Enum { values: None },
75
- _ if schema_path_str.contains("pattern") || error_msg.contains("does not match") => {
76
+ () if schema_path_str.contains("enum") || error_msg.contains("is not one of") => {
77
+ Self::Enum { values: None }
78
+ }
79
+ () if schema_path_str.contains("pattern") || error_msg.contains("does not match") => {
76
80
  Self::StringPatternMismatch { pattern: None }
77
81
  }
78
- _ if schema_path_str.contains("format") => {
82
+ () if schema_path_str.contains("format") => {
79
83
  if error_msg.contains("email") {
80
84
  Self::EmailFormat
81
85
  } else if error_msg.contains("uuid") {
@@ -104,7 +108,8 @@ impl ErrorCondition {
104
108
  }
105
109
 
106
110
  /// Get the error type code for this condition
107
- pub fn error_type(&self) -> &'static str {
111
+ #[must_use]
112
+ pub const fn error_type(&self) -> &'static str {
108
113
  match self {
109
114
  Self::StringTooShort { .. } => "string_too_short",
110
115
  Self::StringTooLong { .. } => "string_too_long",
@@ -113,23 +118,22 @@ impl ErrorCondition {
113
118
  Self::LessThan { .. } => "less_than",
114
119
  Self::LessThanEqual { .. } => "less_than_equal",
115
120
  Self::Enum { .. } => "enum",
116
- Self::StringPatternMismatch { .. } => "string_pattern_mismatch",
117
- Self::EmailFormat => "string_pattern_mismatch",
121
+ Self::StringPatternMismatch { .. } | Self::EmailFormat => "string_pattern_mismatch",
118
122
  Self::UuidFormat => "uuid_parsing",
119
123
  Self::DatetimeFormat => "datetime_parsing",
120
124
  Self::DateFormat => "date_parsing",
121
125
  Self::FormatError => "format_error",
122
126
  Self::TypeMismatch { .. } => "type_error",
123
127
  Self::Missing => "missing",
124
- Self::AdditionalProperties { .. } => "validation_error",
128
+ Self::AdditionalProperties { .. } | Self::ValidationError => "validation_error",
125
129
  Self::TooFewItems { .. } => "too_short",
126
130
  Self::TooManyItems => "too_long",
127
- Self::ValidationError => "validation_error",
128
131
  }
129
132
  }
130
133
 
131
134
  /// Get default message for this error condition
132
- pub fn default_message(&self) -> &'static str {
135
+ #[must_use]
136
+ pub const fn default_message(&self) -> &'static str {
133
137
  match self {
134
138
  Self::StringTooShort { .. } => "String is too short",
135
139
  Self::StringTooLong { .. } => "String is too long",
@@ -159,6 +163,16 @@ pub struct ErrorMapper;
159
163
 
160
164
  impl ErrorMapper {
161
165
  /// Map an error condition to its type, message, and context
166
+ ///
167
+ /// # Panics
168
+ /// Panics if accessing `.last()` on an empty vector for enum values extraction.
169
+ #[must_use]
170
+ #[allow(
171
+ clippy::too_many_lines,
172
+ clippy::option_if_let_else,
173
+ clippy::redundant_closure_for_method_calls,
174
+ clippy::uninlined_format_args
175
+ )]
162
176
  pub fn map_error(
163
177
  condition: &ErrorCondition,
164
178
  schema: &Value,
@@ -18,6 +18,9 @@ pub struct SchemaValidator {
18
18
 
19
19
  impl SchemaValidator {
20
20
  /// Create a new validator from a JSON Schema
21
+ ///
22
+ /// # Errors
23
+ /// Returns an error if the schema is invalid or compilation fails.
21
24
  pub fn new(schema: Value) -> Result<Self, String> {
22
25
  let compiled = jsonschema::options()
23
26
  .with_draft(jsonschema::Draft::Draft202012)
@@ -26,7 +29,7 @@ impl SchemaValidator {
26
29
  .build(&schema)
27
30
  .map_err(|e| {
28
31
  anyhow::anyhow!("Invalid JSON Schema")
29
- .context(format!("Schema compilation failed: {}", e))
32
+ .context(format!("Schema compilation failed: {e}"))
30
33
  .to_string()
31
34
  })?;
32
35
 
@@ -37,16 +40,17 @@ impl SchemaValidator {
37
40
  }
38
41
 
39
42
  /// Get the underlying JSON Schema
40
- pub fn schema(&self) -> &Value {
43
+ #[must_use]
44
+ pub const fn schema(&self) -> &Value {
41
45
  &self.schema
42
46
  }
43
47
 
44
- /// Pre-process data to convert file objects to strings for format: "binary" validation
48
+ /// Pre-process data to convert file objects to strings for format: `binary` validation
45
49
  ///
46
50
  /// Files uploaded via multipart are converted to objects like:
47
- /// {"filename": "...", "size": N, "content": "...", "content_type": "..."}
51
+ /// `{"filename": "...", "size": N, "content": "...", "content_type": "..."}`
48
52
  ///
49
- /// But schemas define them as: {"type": "string", "format": "binary"}
53
+ /// But schemas define them as: `{"type": "string", "format": "binary"}`
50
54
  ///
51
55
  /// This method recursively processes the data and converts file objects to their content strings
52
56
  /// so that validation passes, while preserving the original structure for handlers to use.
@@ -54,7 +58,7 @@ impl SchemaValidator {
54
58
  self.preprocess_value_with_schema(data, &self.schema)
55
59
  }
56
60
 
57
- #[allow(clippy::only_used_in_recursion)]
61
+ #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
58
62
  fn preprocess_value_with_schema(&self, data: &Value, schema: &Value) -> Value {
59
63
  if let Some(schema_obj) = schema.as_object() {
60
64
  let is_string_type = schema_obj.get("type").and_then(|t| t.as_str()) == Some("string");
@@ -110,6 +114,13 @@ impl SchemaValidator {
110
114
  }
111
115
 
112
116
  /// Validate JSON data against the schema
117
+ ///
118
+ /// # Errors
119
+ /// Returns a `ValidationError` if the data does not conform to the schema.
120
+ ///
121
+ /// # Too Many Lines
122
+ /// This function is complex due to error mapping logic.
123
+ #[allow(clippy::option_if_let_else, clippy::uninlined_format_args, clippy::too_many_lines)]
113
124
  pub fn validate(&self, data: &Value) -> Result<(), ValidationError> {
114
125
  let processed_data = self.preprocess_binary_fields(data);
115
126
 
@@ -131,41 +142,39 @@ impl SchemaValidator {
131
142
  if let Some(end) = error_msg[start + 1..].find('"') {
132
143
  error_msg[start + 1..start + 1 + end].to_string()
133
144
  } else {
134
- "".to_string()
145
+ String::new()
135
146
  }
136
147
  } else {
137
- "".to_string()
148
+ String::new()
138
149
  };
139
150
 
140
- if !instance_path.is_empty() && instance_path.starts_with('/') && instance_path.len() > 1 {
151
+ if instance_path.starts_with('/') && instance_path.len() > 1 {
141
152
  let base_path = &instance_path[1..];
142
- if !field_name.is_empty() {
143
- format!("{}/{}", base_path, field_name)
144
- } else {
153
+ if field_name.is_empty() {
145
154
  base_path.to_string()
155
+ } else {
156
+ format!("{base_path}/{field_name}")
146
157
  }
147
- } else if !field_name.is_empty() {
148
- field_name
149
- } else {
158
+ } else if field_name.is_empty() {
150
159
  "body".to_string()
160
+ } else {
161
+ field_name
151
162
  }
152
163
  } else if schema_path_str.contains("/additionalProperties") {
153
164
  if let Some(start) = error_msg.find('(') {
154
165
  if let Some(quote_start) = error_msg[start..].find('\'') {
155
166
  let abs_start = start + quote_start + 1;
156
- if let Some(quote_end) = error_msg[abs_start..].find('\'') {
157
- let property_name = error_msg[abs_start..abs_start + quote_end].to_string();
158
- if !instance_path.is_empty()
159
- && instance_path.starts_with('/')
160
- && instance_path.len() > 1
161
- {
162
- format!("{}/{}", &instance_path[1..], property_name)
163
- } else {
164
- property_name
165
- }
166
- } else {
167
- instance_path[1..].to_string()
168
- }
167
+ error_msg[abs_start..].find('\'').map_or_else(
168
+ || instance_path[1..].to_string(),
169
+ |quote_end| {
170
+ let property_name = error_msg[abs_start..abs_start + quote_end].to_string();
171
+ if instance_path.starts_with('/') && instance_path.len() > 1 {
172
+ format!("{}/{property_name}", &instance_path[1..])
173
+ } else {
174
+ property_name
175
+ }
176
+ },
177
+ )
169
178
  } else {
170
179
  instance_path[1..].to_string()
171
180
  }
@@ -184,7 +193,7 @@ impl SchemaValidator {
184
193
 
185
194
  let loc_parts: Vec<String> = if param_name.contains('/') {
186
195
  let mut parts = vec!["body".to_string()];
187
- parts.extend(param_name.split('/').map(|s| s.to_string()));
196
+ parts.extend(param_name.split('/').map(ToString::to_string));
188
197
  parts
189
198
  } else if param_name == "body" {
190
199
  vec!["body".to_string()]
@@ -201,7 +210,7 @@ impl SchemaValidator {
201
210
  let schema_prop_path = if param_name.contains('/') {
202
211
  format!("/properties/{}", param_name.replace('/', "/properties/"))
203
212
  } else {
204
- format!("/properties/{}", param_name)
213
+ format!("/properties/{param_name}")
205
214
  };
206
215
 
207
216
  let mut error_condition = ErrorCondition::from_schema_error(schema_path_str, &error_msg);
@@ -210,13 +219,14 @@ impl SchemaValidator {
210
219
  ErrorCondition::TypeMismatch { .. } => {
211
220
  let expected_type = self
212
221
  .schema
213
- .pointer(&format!("{}/type", schema_prop_path))
222
+ .pointer(&format!("{schema_prop_path}/type"))
214
223
  .and_then(|v| v.as_str())
215
224
  .unwrap_or("unknown")
216
225
  .to_string();
217
226
  ErrorCondition::TypeMismatch { expected_type }
218
227
  }
219
228
  ErrorCondition::AdditionalProperties { .. } => {
229
+ #[allow(clippy::redundant_clone)]
220
230
  let unexpected_field = if param_name.contains('/') {
221
231
  param_name.split('/').next_back().unwrap_or(&param_name).to_string()
222
232
  } else {
@@ -268,12 +278,15 @@ impl SchemaValidator {
268
278
  }
269
279
 
270
280
  /// Validate and parse JSON bytes
281
+ ///
282
+ /// # Errors
283
+ /// Returns a validation error if the JSON is invalid or fails validation against the schema.
271
284
  pub fn validate_json(&self, json_bytes: &[u8]) -> Result<Value, ValidationError> {
272
285
  let value: Value = serde_json::from_slice(json_bytes).map_err(|e| ValidationError {
273
286
  errors: vec![ValidationErrorDetail {
274
287
  error_type: "json_parse_error".to_string(),
275
288
  loc: vec!["body".to_string()],
276
- msg: format!("Invalid JSON: {}", e),
289
+ msg: format!("Invalid JSON: {e}"),
277
290
  input: Value::Null,
278
291
  ctx: None,
279
292
  }],