spikard 0.3.6 → 0.5.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -4,6 +4,7 @@ use crate::parameters::ParameterValidator;
4
4
  use crate::schema_registry::SchemaRegistry;
5
5
  use crate::validation::SchemaValidator;
6
6
  use crate::{CorsConfig, Method, RouteMetadata};
7
+ use serde::{Deserialize, Serialize};
7
8
  use serde_json::Value;
8
9
  use std::collections::HashMap;
9
10
  use std::sync::Arc;
@@ -11,10 +12,69 @@ use std::sync::Arc;
11
12
  /// Handler function type (placeholder - will be enhanced with Python callbacks)
12
13
  pub type RouteHandler = Arc<dyn Fn() -> String + Send + Sync>;
13
14
 
15
+ /// JSON-RPC method metadata for routes that support JSON-RPC
16
+ ///
17
+ /// This struct captures the metadata needed to expose HTTP routes as JSON-RPC methods,
18
+ /// enabling discovery and documentation of RPC-compatible endpoints.
19
+ ///
20
+ /// # Examples
21
+ ///
22
+ /// ```ignore
23
+ /// use spikard_core::router::JsonRpcMethodInfo;
24
+ /// use serde_json::json;
25
+ ///
26
+ /// let rpc_info = JsonRpcMethodInfo {
27
+ /// method_name: "user.create".to_string(),
28
+ /// description: Some("Creates a new user".to_string()),
29
+ /// params_schema: Some(json!({
30
+ /// "type": "object",
31
+ /// "properties": {
32
+ /// "name": {"type": "string"}
33
+ /// }
34
+ /// })),
35
+ /// result_schema: Some(json!({
36
+ /// "type": "object",
37
+ /// "properties": {
38
+ /// "id": {"type": "integer"}
39
+ /// }
40
+ /// })),
41
+ /// deprecated: false,
42
+ /// tags: vec!["users".to_string()],
43
+ /// };
44
+ /// ```
45
+ #[derive(Debug, Clone, Serialize, Deserialize)]
46
+ pub struct JsonRpcMethodInfo {
47
+ /// The JSON-RPC method name (e.g., "user.create")
48
+ pub method_name: String,
49
+
50
+ /// Optional description of what the method does
51
+ #[serde(skip_serializing_if = "Option::is_none")]
52
+ pub description: Option<String>,
53
+
54
+ /// Optional JSON Schema for method parameters
55
+ #[serde(skip_serializing_if = "Option::is_none")]
56
+ pub params_schema: Option<Value>,
57
+
58
+ /// Optional JSON Schema for the result
59
+ #[serde(skip_serializing_if = "Option::is_none")]
60
+ pub result_schema: Option<Value>,
61
+
62
+ /// Whether this method is deprecated
63
+ #[serde(default)]
64
+ pub deprecated: bool,
65
+
66
+ /// Tags for categorizing and grouping methods
67
+ #[serde(default)]
68
+ pub tags: Vec<String>,
69
+ }
70
+
14
71
  /// Route definition with compiled validators
15
72
  ///
16
73
  /// Validators are Arc-wrapped to enable cheap cloning across route instances
17
74
  /// and to support schema deduplication via SchemaRegistry.
75
+ ///
76
+ /// The `jsonrpc_method` field is optional and has zero overhead when None,
77
+ /// enabling routes to optionally expose themselves as JSON-RPC methods.
18
78
  #[derive(Clone)]
19
79
  pub struct Route {
20
80
  pub method: Method,
@@ -32,6 +92,9 @@ pub struct Route {
32
92
  /// List of dependency keys this handler requires (for DI)
33
93
  #[cfg(feature = "di")]
34
94
  pub handler_dependencies: Vec<String>,
95
+ /// Optional JSON-RPC method information
96
+ /// When present, this route can be exposed as a JSON-RPC method
97
+ pub jsonrpc_method: Option<JsonRpcMethodInfo>,
35
98
  }
36
99
 
37
100
  impl Route {
@@ -46,15 +109,21 @@ impl Route {
46
109
  pub fn from_metadata(metadata: RouteMetadata, registry: &SchemaRegistry) -> Result<Self, String> {
47
110
  let method = metadata.method.parse()?;
48
111
 
112
+ fn is_empty_schema(schema: &Value) -> bool {
113
+ matches!(schema, Value::Object(map) if map.is_empty())
114
+ }
115
+
49
116
  let request_validator = metadata
50
117
  .request_schema
51
118
  .as_ref()
119
+ .filter(|schema| !is_empty_schema(schema))
52
120
  .map(|schema| registry.get_or_compile(schema))
53
121
  .transpose()?;
54
122
 
55
123
  let response_validator = metadata
56
124
  .response_schema
57
125
  .as_ref()
126
+ .filter(|schema| !is_empty_schema(schema))
58
127
  .map(|schema| registry.get_or_compile(schema))
59
128
  .transpose()?;
60
129
 
@@ -63,10 +132,14 @@ impl Route {
63
132
  metadata.parameter_schema,
64
133
  ) {
65
134
  (Some(auto_schema), Some(explicit_schema)) => {
66
- Some(crate::type_hints::merge_parameter_schemas(auto_schema, explicit_schema))
135
+ if is_empty_schema(&explicit_schema) {
136
+ Some(auto_schema)
137
+ } else {
138
+ Some(crate::type_hints::merge_parameter_schemas(auto_schema, explicit_schema))
139
+ }
67
140
  }
68
141
  (Some(auto_schema), None) => Some(auto_schema),
69
- (None, Some(explicit_schema)) => Some(explicit_schema),
142
+ (None, Some(explicit_schema)) => (!is_empty_schema(&explicit_schema)).then_some(explicit_schema),
70
143
  (None, None) => None,
71
144
  };
72
145
 
@@ -74,6 +147,11 @@ impl Route {
74
147
 
75
148
  let expects_json_body = request_validator.is_some();
76
149
 
150
+ let jsonrpc_method = metadata
151
+ .jsonrpc_method
152
+ .as_ref()
153
+ .and_then(|json_value| serde_json::from_value(json_value.clone()).ok());
154
+
77
155
  Ok(Self {
78
156
  method,
79
157
  path: metadata.path,
@@ -87,8 +165,42 @@ impl Route {
87
165
  expects_json_body,
88
166
  #[cfg(feature = "di")]
89
167
  handler_dependencies: metadata.handler_dependencies.unwrap_or_default(),
168
+ jsonrpc_method,
90
169
  })
91
170
  }
171
+
172
+ /// Builder method to attach JSON-RPC method info to a route
173
+ ///
174
+ /// This is a convenient way to add JSON-RPC metadata after route creation.
175
+ /// It consumes the route and returns a new route with the metadata attached.
176
+ ///
177
+ /// # Examples
178
+ ///
179
+ /// ```ignore
180
+ /// let route = Route::from_metadata(metadata, &registry)?
181
+ /// .with_jsonrpc_method(JsonRpcMethodInfo {
182
+ /// method_name: "user.create".to_string(),
183
+ /// description: Some("Creates a new user".to_string()),
184
+ /// params_schema: Some(request_schema),
185
+ /// result_schema: Some(response_schema),
186
+ /// deprecated: false,
187
+ /// tags: vec!["users".to_string()],
188
+ /// });
189
+ /// ```
190
+ pub fn with_jsonrpc_method(mut self, info: JsonRpcMethodInfo) -> Self {
191
+ self.jsonrpc_method = Some(info);
192
+ self
193
+ }
194
+
195
+ /// Check if this route has JSON-RPC metadata
196
+ pub fn is_jsonrpc_method(&self) -> bool {
197
+ self.jsonrpc_method.is_some()
198
+ }
199
+
200
+ /// Get the JSON-RPC method name if present
201
+ pub fn jsonrpc_method_name(&self) -> Option<&str> {
202
+ self.jsonrpc_method.as_ref().map(|m| m.method_name.as_str())
203
+ }
92
204
  }
93
205
 
94
206
  /// Router that manages routes
@@ -151,6 +263,7 @@ mod tests {
151
263
  is_async: true,
152
264
  cors: None,
153
265
  body_param_name: None,
266
+ jsonrpc_method: None,
154
267
  #[cfg(feature = "di")]
155
268
  handler_dependencies: None,
156
269
  };
@@ -184,6 +297,7 @@ mod tests {
184
297
  is_async: true,
185
298
  cors: None,
186
299
  body_param_name: None,
300
+ jsonrpc_method: None,
187
301
  #[cfg(feature = "di")]
188
302
  handler_dependencies: None,
189
303
  };
@@ -215,6 +329,7 @@ mod tests {
215
329
  is_async: true,
216
330
  cors: None,
217
331
  body_param_name: None,
332
+ jsonrpc_method: None,
218
333
  #[cfg(feature = "di")]
219
334
  handler_dependencies: None,
220
335
  };
@@ -230,6 +345,7 @@ mod tests {
230
345
  is_async: true,
231
346
  cors: None,
232
347
  body_param_name: None,
348
+ jsonrpc_method: None,
233
349
  #[cfg(feature = "di")]
234
350
  handler_dependencies: None,
235
351
  };
@@ -246,4 +362,149 @@ mod tests {
246
362
 
247
363
  assert_eq!(registry.schema_count(), 1);
248
364
  }
365
+
366
+ #[test]
367
+ fn test_jsonrpc_method_info() {
368
+ let rpc_info = JsonRpcMethodInfo {
369
+ method_name: "user.create".to_string(),
370
+ description: Some("Creates a new user account".to_string()),
371
+ params_schema: Some(json!({
372
+ "type": "object",
373
+ "properties": {
374
+ "name": {"type": "string"},
375
+ "email": {"type": "string"}
376
+ },
377
+ "required": ["name", "email"]
378
+ })),
379
+ result_schema: Some(json!({
380
+ "type": "object",
381
+ "properties": {
382
+ "id": {"type": "integer"},
383
+ "name": {"type": "string"},
384
+ "email": {"type": "string"}
385
+ }
386
+ })),
387
+ deprecated: false,
388
+ tags: vec!["users".to_string(), "admin".to_string()],
389
+ };
390
+
391
+ assert_eq!(rpc_info.method_name, "user.create");
392
+ assert_eq!(rpc_info.description.as_ref().unwrap(), "Creates a new user account");
393
+ assert!(rpc_info.params_schema.is_some());
394
+ assert!(rpc_info.result_schema.is_some());
395
+ assert!(!rpc_info.deprecated);
396
+ assert_eq!(rpc_info.tags.len(), 2);
397
+ assert!(rpc_info.tags.contains(&"users".to_string()));
398
+ }
399
+
400
+ #[test]
401
+ fn test_route_with_jsonrpc_method() {
402
+ let registry = SchemaRegistry::new();
403
+
404
+ let metadata = RouteMetadata {
405
+ method: "POST".to_string(),
406
+ path: "/user/create".to_string(),
407
+ handler_name: "create_user".to_string(),
408
+ request_schema: Some(json!({
409
+ "type": "object",
410
+ "properties": {
411
+ "name": {"type": "string"}
412
+ },
413
+ "required": ["name"]
414
+ })),
415
+ response_schema: Some(json!({
416
+ "type": "object",
417
+ "properties": {
418
+ "id": {"type": "integer"}
419
+ }
420
+ })),
421
+ parameter_schema: None,
422
+ file_params: None,
423
+ is_async: true,
424
+ cors: None,
425
+ body_param_name: None,
426
+ jsonrpc_method: None,
427
+ #[cfg(feature = "di")]
428
+ handler_dependencies: None,
429
+ };
430
+
431
+ let rpc_info = JsonRpcMethodInfo {
432
+ method_name: "user.create".to_string(),
433
+ description: Some("Creates a new user".to_string()),
434
+ params_schema: Some(json!({
435
+ "type": "object",
436
+ "properties": {
437
+ "name": {"type": "string"}
438
+ }
439
+ })),
440
+ result_schema: Some(json!({
441
+ "type": "object",
442
+ "properties": {
443
+ "id": {"type": "integer"}
444
+ }
445
+ })),
446
+ deprecated: false,
447
+ tags: vec!["users".to_string()],
448
+ };
449
+
450
+ let route = Route::from_metadata(metadata, &registry)
451
+ .unwrap()
452
+ .with_jsonrpc_method(rpc_info);
453
+
454
+ assert!(route.is_jsonrpc_method());
455
+ assert_eq!(route.jsonrpc_method_name(), Some("user.create"));
456
+ assert!(route.jsonrpc_method.is_some());
457
+
458
+ let rpc = route.jsonrpc_method.as_ref().unwrap();
459
+ assert_eq!(rpc.method_name, "user.create");
460
+ assert_eq!(rpc.description.as_ref().unwrap(), "Creates a new user");
461
+ assert!(!rpc.deprecated);
462
+ }
463
+
464
+ #[test]
465
+ fn test_jsonrpc_method_serialization() {
466
+ let rpc_info = JsonRpcMethodInfo {
467
+ method_name: "test.method".to_string(),
468
+ description: Some("Test method".to_string()),
469
+ params_schema: Some(json!({"type": "object"})),
470
+ result_schema: Some(json!({"type": "string"})),
471
+ deprecated: false,
472
+ tags: vec!["test".to_string()],
473
+ };
474
+
475
+ let json = serde_json::to_value(&rpc_info).unwrap();
476
+ assert_eq!(json["method_name"], "test.method");
477
+ assert_eq!(json["description"], "Test method");
478
+
479
+ let deserialized: JsonRpcMethodInfo = serde_json::from_value(json).unwrap();
480
+ assert_eq!(deserialized.method_name, rpc_info.method_name);
481
+ assert_eq!(deserialized.description, rpc_info.description);
482
+ }
483
+
484
+ #[test]
485
+ fn test_route_without_jsonrpc_method_has_zero_overhead() {
486
+ let registry = SchemaRegistry::new();
487
+
488
+ let metadata = RouteMetadata {
489
+ method: "GET".to_string(),
490
+ path: "/status".to_string(),
491
+ handler_name: "status".to_string(),
492
+ request_schema: None,
493
+ response_schema: None,
494
+ parameter_schema: None,
495
+ file_params: None,
496
+ is_async: false,
497
+ cors: None,
498
+ body_param_name: None,
499
+ jsonrpc_method: None,
500
+ #[cfg(feature = "di")]
501
+ handler_dependencies: None,
502
+ };
503
+
504
+ let route = Route::from_metadata(metadata, &registry).unwrap();
505
+
506
+ assert!(!route.is_jsonrpc_method());
507
+ assert_eq!(route.jsonrpc_method_name(), None);
508
+ assert!(route.jsonrpc_method.is_none());
509
+ }
249
510
  }