1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
4#[serde(transparent)]
5pub struct DiagnosticCode(pub String);
6
7impl DiagnosticCode {
8 pub fn new(value: impl Into<String>) -> Self {
9 Self(value.into())
10 }
11
12 pub fn as_str(&self) -> &str {
13 &self.0
14 }
15}
16
17impl From<&str> for DiagnosticCode {
18 fn from(value: &str) -> Self {
19 Self(value.to_string())
20 }
21}
22
23impl From<String> for DiagnosticCode {
24 fn from(value: String) -> Self {
25 Self(value)
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "camelCase")]
31pub struct Diagnostic {
32 pub severity: DiagnosticSeverity,
33 pub code: DiagnosticCode,
34 pub message: String,
35 pub source: Option<String>,
36 pub help: Option<String>,
37}
38
39impl Diagnostic {
40 pub fn new(
41 severity: DiagnosticSeverity,
42 code: impl Into<DiagnosticCode>,
43 message: impl Into<String>,
44 ) -> Self {
45 Self {
46 severity,
47 code: code.into(),
48 message: message.into(),
49 source: None,
50 help: None,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "camelCase")]
57pub enum DiagnosticSeverity {
58 Info,
59 Warning,
60 Error,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "camelCase")]
65pub struct RuntimeCapabilities {
66 pub native: bool,
67 pub server: bool,
68 pub wasm: bool,
69 pub mobile: MobileCapability,
70 pub requirements: Vec<RuntimeRequirement>,
71 pub max_recommended_input_bytes: Option<u64>,
72}
73
74impl RuntimeCapabilities {
75 pub fn pure_rust() -> Self {
76 Self {
77 native: true,
78 server: true,
79 wasm: true,
80 mobile: MobileCapability::Wasm,
81 requirements: Vec::new(),
82 max_recommended_input_bytes: None,
83 }
84 }
85
86 pub fn with_max_recommended_input_bytes(mut self, bytes: u64) -> Self {
87 self.max_recommended_input_bytes = Some(bytes);
88 self
89 }
90
91 pub fn with_requirement(
92 mut self,
93 name: impl Into<String>,
94 description: impl Into<String>,
95 required: bool,
96 ) -> Self {
97 self.requirements.push(RuntimeRequirement {
98 name: name.into(),
99 description: Some(description.into()),
100 required,
101 });
102 self
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "camelCase")]
108pub enum MobileCapability {
109 Native,
110 Wasm,
111 ApiOnly,
112 Unsupported,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(rename_all = "camelCase")]
117pub struct RuntimeRequirement {
118 pub name: String,
119 pub description: Option<String>,
120 pub required: bool,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
124#[serde(transparent)]
125pub struct OperationId(pub String);
126
127impl OperationId {
128 pub fn new(value: impl Into<String>) -> Self {
129 Self(value.into())
130 }
131
132 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135}
136
137impl From<&str> for OperationId {
138 fn from(value: &str) -> Self {
139 Self(value.to_string())
140 }
141}
142
143impl From<String> for OperationId {
144 fn from(value: String) -> Self {
145 Self(value)
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150#[serde(rename_all = "camelCase")]
151pub struct OperationMetadata {
152 pub id: OperationId,
153 pub name: String,
154 pub description: Option<String>,
155 pub version: String,
156 pub capabilities: RuntimeCapabilities,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
160#[serde(rename_all = "camelCase")]
161pub struct PackageSurface {
162 pub library: String,
163 pub version: String,
164 pub operations: Vec<SurfaceOperation>,
165 pub capabilities: RuntimeCapabilities,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169#[serde(rename_all = "camelCase")]
170pub struct SurfaceOperation {
171 pub id: OperationId,
172 pub name: String,
173 pub description: Option<String>,
174 pub input_schema: serde_json::Value,
175 pub output_schema: serde_json::Value,
176 pub example_request: serde_json::Value,
177 pub wasm_supported: bool,
178 pub server_supported: bool,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182#[serde(rename_all = "camelCase")]
183pub struct SurfaceRequest {
184 pub operation: OperationId,
185 pub input: serde_json::Value,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189#[serde(rename_all = "camelCase")]
190pub struct SurfaceResponse {
191 pub operation: OperationId,
192 pub value: serde_json::Value,
193 pub diagnostics: Vec<Diagnostic>,
194 pub artifacts: Vec<serde_json::Value>,
195}
196
197pub fn surface_operation(
200 id: impl Into<String>,
201 name: impl Into<String>,
202 description: impl Into<String>,
203 example_request: serde_json::Value,
204) -> SurfaceOperation {
205 SurfaceOperation {
206 id: OperationId::new(id),
207 name: name.into(),
208 description: Some(description.into()),
209 input_schema: serde_json::json!({"type": "object", "additionalProperties": true}),
210 output_schema: serde_json::json!({"type": "object"}),
211 example_request,
212 wasm_supported: true,
213 server_supported: true,
214 }
215}
216
217pub fn describe_surface_response(
220 surface: &PackageSurface,
221 request: SurfaceRequest,
222) -> SurfaceResponse {
223 let result = serde_json::json!({
224 "library": &surface.library,
225 "version": &surface.version,
226 "operationCount": surface.operations.len(),
227 "operations": surface
228 .operations
229 .iter()
230 .map(|operation| operation.id.as_str())
231 .collect::<Vec<_>>(),
232 "input": request.input
233 });
234 structured_surface_response(
235 request.operation,
236 "Package surface metadata",
237 format!(
238 "{} exposes {} package-surface operations.",
239 surface.library,
240 surface.operations.len()
241 ),
242 serde_json::json!({
243 "operationCount": surface.operations.len(),
244 "runtime": {
245 "wasm": surface.capabilities.wasm,
246 "server": surface.capabilities.server,
247 "native": surface.capabilities.native
248 }
249 }),
250 result,
251 )
252}
253
254pub fn surface_response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
256 let title = operation.as_str().to_string();
257 let message = format!("Ran package-surface operation `{}`.", operation.as_str());
258 let value = ensure_structured_surface_value(&operation, title, message, value);
259 SurfaceResponse {
260 operation,
261 value,
262 diagnostics: Vec::new(),
263 artifacts: Vec::new(),
264 }
265}
266
267pub fn structured_surface_value(
270 operation: &OperationId,
271 title: impl Into<String>,
272 message: impl Into<String>,
273 summary: serde_json::Value,
274 result: serde_json::Value,
275) -> serde_json::Value {
276 let mut object = match &result {
277 serde_json::Value::Object(map) => map.clone(),
278 _ => serde_json::Map::new(),
279 };
280 object.insert("title".to_string(), serde_json::Value::String(title.into()));
281 object.insert(
282 "operation".to_string(),
283 serde_json::Value::String(operation.as_str().to_string()),
284 );
285 object.insert(
286 "message".to_string(),
287 serde_json::Value::String(message.into()),
288 );
289 object.insert("summary".to_string(), summary);
290 object.insert("result".to_string(), result);
291 serde_json::Value::Object(object)
292}
293
294pub fn ensure_structured_surface_value(
297 operation: &OperationId,
298 title: impl Into<String>,
299 message: impl Into<String>,
300 value: serde_json::Value,
301) -> serde_json::Value {
302 let result = value.clone();
303 let mut object = match value {
304 serde_json::Value::Object(map) => map,
305 _ => serde_json::Map::new(),
306 };
307 object
308 .entry("operation".to_string())
309 .or_insert_with(|| serde_json::Value::String(operation.as_str().to_string()));
310 object
311 .entry("title".to_string())
312 .or_insert_with(|| serde_json::Value::String(title.into()));
313 object
314 .entry("message".to_string())
315 .or_insert_with(|| serde_json::Value::String(message.into()));
316 object
317 .entry("summary".to_string())
318 .or_insert_with(|| operation_summary(&result));
319 object.entry("result".to_string()).or_insert(result);
320 serde_json::Value::Object(object)
321}
322
323pub fn structured_surface_response(
325 operation: OperationId,
326 title: impl Into<String>,
327 message: impl Into<String>,
328 summary: serde_json::Value,
329 result: serde_json::Value,
330) -> SurfaceResponse {
331 let value = structured_surface_value(&operation, title, message, summary, result);
332 surface_response(operation, value)
333}
334
335pub fn structured_operation_response(
341 surface: &PackageSurface,
342 operation: OperationId,
343 result: serde_json::Value,
344) -> SurfaceResponse {
345 let metadata = surface
346 .operations
347 .iter()
348 .find(|candidate| candidate.id.as_str() == operation.as_str());
349 let title = metadata
350 .map(|operation| operation.name.clone())
351 .unwrap_or_else(|| operation.as_str().to_string());
352 let message = metadata
353 .and_then(|operation| operation.description.clone())
354 .unwrap_or_else(|| format!("Ran package-surface operation `{}`.", operation.as_str()));
355 let summary = operation_summary(&result);
356 structured_surface_response(operation, title, message, summary, result)
357}
358
359fn operation_summary(result: &serde_json::Value) -> serde_json::Value {
360 match result {
361 serde_json::Value::Object(object) => {
362 let mut summary = serde_json::Map::new();
363 summary.insert("status".to_string(), serde_json::json!("ok"));
364 for key in [
365 "count",
366 "width",
367 "height",
368 "format",
369 "pixelFormat",
370 "dimensions",
371 "operationCount",
372 ] {
373 if let Some(value) = object.get(key) {
374 summary.insert(key.to_string(), value.clone());
375 }
376 }
377 if let Some((key, value)) = object
378 .iter()
379 .find(|(_, value)| matches!(value, serde_json::Value::Array(_)))
380 {
381 summary.insert(
382 format!("{key}Count"),
383 serde_json::json!(value.as_array().map(Vec::len).unwrap_or(0)),
384 );
385 }
386 serde_json::Value::Object(summary)
387 }
388 serde_json::Value::Array(values) => {
389 serde_json::json!({"status": "ok", "count": values.len()})
390 }
391 _ => serde_json::json!({"status": "ok"}),
392 }
393}
394
395pub mod cli {
397 use std::fs;
398 use std::io::{self, Read};
399
400 use super::{
401 ensure_structured_surface_value, OperationId, PackageSurface, SurfaceRequest,
402 SurfaceResponse,
403 };
404
405 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
407 pub struct CliAdapterMetadata {
408 pub library_crate: &'static str,
409 pub surface_kind: &'static str,
410 pub library_import: &'static str,
411 pub server_package: &'static str,
412 pub app_package: &'static str,
413 pub wasm_package: &'static str,
414 }
415
416 pub fn package_metadata_json(metadata: CliAdapterMetadata, surface: PackageSurface) -> String {
418 serde_json::json!({
419 "package": format!("{}-cli", metadata.library_crate),
420 "surface": metadata.surface_kind,
421 "library": metadata.library_crate,
422 "libraryImport": metadata.library_import,
423 "serverPackage": metadata.server_package,
424 "appPackage": metadata.app_package,
425 "wasmPackage": metadata.wasm_package,
426 "operations": surface.operations
427 })
428 .to_string()
429 }
430
431 pub fn command_schema_json() -> String {
433 serde_json::json!({
434 "commands": [
435 {"name": "info", "description": "Print package and adapter metadata."},
436 {"name": "schema", "description": "Print the CLI command schema."},
437 {"name": "operations", "description": "Print library operations."},
438 {"name": "run", "description": "Run one library-owned operation."}
439 ]
440 })
441 .to_string()
442 }
443
444 pub fn read_json_input(
446 json: Option<String>,
447 file: Option<String>,
448 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
449 let input = if let Some(json) = json {
450 json
451 } else if let Some(file) = file {
452 fs::read_to_string(file)?
453 } else {
454 let mut buffer = String::new();
455 io::stdin().read_to_string(&mut buffer)?;
456 if buffer.trim().is_empty() {
457 "{}".to_string()
458 } else {
459 buffer
460 }
461 };
462 Ok(serde_json::from_str(&input)?)
463 }
464
465 pub fn run_wrapped_operation(
468 operation: &str,
469 input: serde_json::Value,
470 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
471 ) -> Result<SurfaceResponse, String> {
472 let mut response = runner(SurfaceRequest {
473 operation: OperationId::new(operation),
474 input,
475 })?;
476 let value = std::mem::take(&mut response.value);
477 response.value = ensure_structured_surface_value(
478 &response.operation,
479 operation.to_string(),
480 format!("Ran package-surface operation `{}`.", operation),
481 value,
482 );
483 Ok(response)
484 }
485}
486
487pub mod server {
489 use std::io::{self, BufRead, BufReader, Read, Write};
490 use std::net::{TcpListener, TcpStream};
491
492 use super::{
493 Diagnostic, DiagnosticSeverity, OperationId, PackageSurface, SurfaceRequest,
494 SurfaceResponse,
495 };
496
497 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
499 pub struct ServerAdapterMetadata {
500 pub library_crate: &'static str,
501 pub surface_kind: &'static str,
502 pub library_import: &'static str,
503 pub cli_package: &'static str,
504 pub app_package: &'static str,
505 pub wasm_package: &'static str,
506 }
507
508 #[derive(Debug, Clone, PartialEq, Eq)]
509 pub struct HttpResponse {
510 pub status_code: u16,
511 pub reason: &'static str,
512 pub content_type: &'static str,
513 pub body: String,
514 }
515
516 pub fn serve(
518 addr: &str,
519 metadata: ServerAdapterMetadata,
520 surface_provider: fn() -> PackageSurface,
521 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
522 ) -> io::Result<()> {
523 let listener = TcpListener::bind(addr)?;
524 for stream in listener.incoming() {
525 handle_stream(stream?, metadata, surface_provider, runner)?;
526 }
527 Ok(())
528 }
529
530 pub fn response_for(
532 method: &str,
533 path: &str,
534 body: &str,
535 metadata: ServerAdapterMetadata,
536 surface_provider: fn() -> PackageSurface,
537 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
538 ) -> HttpResponse {
539 match (method, path) {
540 ("OPTIONS", _) => HttpResponse {
541 status_code: 204,
542 reason: "No Content",
543 content_type: "application/json",
544 body: String::new(),
545 },
546 ("GET", "/health") => json_response(
547 200,
548 "OK",
549 serde_json::json!({
550 "ok": true,
551 "package": format!("{}-server", metadata.library_crate),
552 "library": metadata.library_crate
553 }),
554 ),
555 ("GET", "/api/package") => json_response(
556 200,
557 "OK",
558 package_metadata_value(metadata, surface_provider()),
559 ),
560 ("GET", "/api/schema") => {
561 json_response(200, "OK", schema_value(metadata, surface_provider()))
562 }
563 ("GET", "/api/operations") => {
564 json_response(200, "OK", serde_json::json!(surface_provider().operations))
565 }
566 ("POST", "/api/run") => run_response(body, metadata, runner),
567 ("POST", path) if path.starts_with("/api/") => {
568 let operation = path.trim_start_matches("/api/");
569 run_request(
570 SurfaceRequest {
571 operation: OperationId::new(operation),
572 input: parse_json_or_empty(body),
573 },
574 metadata,
575 runner,
576 )
577 }
578 _ => json_response(
579 404,
580 "Not Found",
581 serde_json::json!({
582 "error": "not found",
583 "path": path
584 }),
585 ),
586 }
587 }
588
589 pub fn package_metadata_json(
591 metadata: ServerAdapterMetadata,
592 surface: PackageSurface,
593 ) -> String {
594 package_metadata_value(metadata, surface).to_string()
595 }
596
597 fn package_metadata_value(
598 metadata: ServerAdapterMetadata,
599 surface: PackageSurface,
600 ) -> serde_json::Value {
601 serde_json::json!({
602 "package": format!("{}-server", metadata.library_crate),
603 "surface": metadata.surface_kind,
604 "library": metadata.library_crate,
605 "libraryImport": metadata.library_import,
606 "cliPackage": metadata.cli_package,
607 "appPackage": metadata.app_package,
608 "wasmPackage": metadata.wasm_package,
609 "endpoints": [
610 "GET /health",
611 "GET /api/package",
612 "GET /api/schema",
613 "GET /api/operations",
614 "POST /api/run",
615 "POST /api/<operation-id>"
616 ],
617 "operations": surface.operations
618 })
619 }
620
621 fn schema_value(metadata: ServerAdapterMetadata, surface: PackageSurface) -> serde_json::Value {
622 let operations = surface
623 .operations
624 .into_iter()
625 .map(|operation| {
626 let path = format!("/api/{}", operation.id.as_str());
627 (
628 path,
629 serde_json::json!({
630 "post": {
631 "summary": operation.name,
632 "description": operation.description,
633 "requestBody": operation.input_schema,
634 "responses": {"200": operation.output_schema}
635 }
636 }),
637 )
638 })
639 .collect::<serde_json::Map<_, _>>();
640
641 serde_json::json!({
642 "openapi": "3.1.0",
643 "info": {
644 "title": format!("{} API", metadata.library_crate),
645 "version": env!("CARGO_PKG_VERSION")
646 },
647 "paths": operations
648 })
649 }
650
651 fn run_response(
652 body: &str,
653 metadata: ServerAdapterMetadata,
654 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
655 ) -> HttpResponse {
656 let payload = match serde_json::from_str::<serde_json::Value>(body) {
657 Ok(value) => value,
658 Err(error) => {
659 return diagnostic_response(
660 400,
661 "Bad Request",
662 "invalid_request",
663 &format!("invalid JSON: {error}"),
664 metadata,
665 );
666 }
667 };
668 let operation = payload
669 .get("operation")
670 .and_then(serde_json::Value::as_str)
671 .unwrap_or("describe")
672 .to_string();
673 let input = payload
674 .get("input")
675 .cloned()
676 .unwrap_or_else(|| payload.clone());
677 run_request(
678 SurfaceRequest {
679 operation: OperationId::new(operation),
680 input,
681 },
682 metadata,
683 runner,
684 )
685 }
686
687 fn run_request(
688 request: SurfaceRequest,
689 metadata: ServerAdapterMetadata,
690 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
691 ) -> HttpResponse {
692 match runner(request) {
693 Ok(response) => json_response(200, "OK", serde_json::json!(response)),
694 Err(error) => {
695 diagnostic_response(400, "Bad Request", "operation_failed", &error, metadata)
696 }
697 }
698 }
699
700 fn diagnostic_response(
701 status_code: u16,
702 reason: &'static str,
703 code: &str,
704 message: &str,
705 metadata: ServerAdapterMetadata,
706 ) -> HttpResponse {
707 json_response(
708 status_code,
709 reason,
710 serde_json::json!({
711 "diagnostics": [Diagnostic {
712 severity: DiagnosticSeverity::Error,
713 code: code.into(),
714 message: message.to_string(),
715 source: Some(format!("{}-server", metadata.library_crate)),
716 help: None,
717 }]
718 }),
719 )
720 }
721
722 fn parse_json_or_empty(body: &str) -> serde_json::Value {
723 if body.trim().is_empty() {
724 serde_json::json!({})
725 } else {
726 serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({"raw": body}))
727 }
728 }
729
730 fn handle_stream(
731 mut stream: TcpStream,
732 metadata: ServerAdapterMetadata,
733 surface_provider: fn() -> PackageSurface,
734 runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
735 ) -> io::Result<()> {
736 let mut reader = BufReader::new(stream.try_clone()?);
737 let mut request_line = String::new();
738 reader.read_line(&mut request_line)?;
739
740 let mut content_length = 0usize;
741 loop {
742 let mut header = String::new();
743 reader.read_line(&mut header)?;
744 let trimmed = header.trim_end();
745 if trimmed.is_empty() {
746 break;
747 }
748 if let Some((name, value)) = trimmed.split_once(':') {
749 if name.eq_ignore_ascii_case("content-length") {
750 content_length = value.trim().parse().unwrap_or(0);
751 }
752 }
753 }
754
755 let mut body = vec![0; content_length];
756 if content_length > 0 {
757 reader.read_exact(&mut body)?;
758 }
759 let body = String::from_utf8_lossy(&body);
760
761 let mut parts = request_line.split_whitespace();
762 let method = parts.next().unwrap_or("GET");
763 let path = parts.next().unwrap_or("/");
764 let response = response_for(method, path, &body, metadata, surface_provider, runner);
765 write_response(&mut stream, response)
766 }
767
768 fn json_response(
769 status_code: u16,
770 reason: &'static str,
771 value: serde_json::Value,
772 ) -> HttpResponse {
773 HttpResponse {
774 status_code,
775 reason,
776 content_type: "application/json",
777 body: value.to_string(),
778 }
779 }
780
781 fn write_response(stream: &mut TcpStream, response: HttpResponse) -> io::Result<()> {
782 write!(
783 stream,
784 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers: content-type\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nConnection: close\r\n\r\n{}",
785 response.status_code,
786 response.reason,
787 response.content_type,
788 response.body.len(),
789 response.body
790 )
791 }
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
795#[serde(transparent)]
796pub struct JobId(pub String);
797
798impl JobId {
799 pub fn new(value: impl Into<String>) -> Self {
800 Self(value.into())
801 }
802
803 pub fn as_str(&self) -> &str {
804 &self.0
805 }
806}
807
808impl From<&str> for JobId {
809 fn from(value: &str) -> Self {
810 Self(value.to_string())
811 }
812}
813
814impl From<String> for JobId {
815 fn from(value: String) -> Self {
816 Self(value)
817 }
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
821#[serde(transparent)]
822pub struct ArtifactId(pub String);
823
824impl ArtifactId {
825 pub fn new(value: impl Into<String>) -> Self {
826 Self(value.into())
827 }
828
829 pub fn as_str(&self) -> &str {
830 &self.0
831 }
832}
833
834impl From<&str> for ArtifactId {
835 fn from(value: &str) -> Self {
836 Self(value.to_string())
837 }
838}
839
840impl From<String> for ArtifactId {
841 fn from(value: String) -> Self {
842 Self(value)
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn diagnostic_uses_camel_case_json() {
852 let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "demo.warning", "check");
853 let json = serde_json::to_string(&diagnostic).expect("serialize diagnostic");
854
855 assert!(json.contains("\"severity\":\"warning\""));
856 assert!(json.contains("\"code\":\"demo.warning\""));
857 }
858
859 #[test]
860 fn pure_rust_capabilities_allow_wasm_and_server() {
861 let capabilities = RuntimeCapabilities::pure_rust();
862
863 assert!(capabilities.native);
864 assert!(capabilities.server);
865 assert!(capabilities.wasm);
866 assert_eq!(capabilities.mobile, MobileCapability::Wasm);
867 }
868
869 #[test]
870 fn capability_builders_preserve_pure_rust_defaults() {
871 let capabilities = RuntimeCapabilities::pure_rust()
872 .with_max_recommended_input_bytes(1024)
873 .with_requirement("fixture", "test fixture input", false);
874
875 assert!(capabilities.native);
876 assert!(capabilities.server);
877 assert!(capabilities.wasm);
878 assert_eq!(capabilities.max_recommended_input_bytes, Some(1024));
879 assert_eq!(capabilities.requirements[0].name, "fixture");
880 assert!(!capabilities.requirements[0].required);
881 }
882
883 #[test]
884 fn package_surface_uses_camel_case_json() {
885 let surface = PackageSurface {
886 library: "demo-core".to_string(),
887 version: "0.1.0".to_string(),
888 capabilities: RuntimeCapabilities::pure_rust(),
889 operations: vec![SurfaceOperation {
890 id: OperationId::new("describe"),
891 name: "Describe".to_string(),
892 description: Some("Describe package surface".to_string()),
893 input_schema: serde_json::json!({"type": "object"}),
894 output_schema: serde_json::json!({"type": "object"}),
895 example_request: serde_json::json!({}),
896 wasm_supported: true,
897 server_supported: true,
898 }],
899 };
900
901 let json = serde_json::to_string(&surface).expect("serialize surface");
902
903 assert!(json.contains("\"inputSchema\""));
904 assert!(json.contains("\"exampleRequest\""));
905 assert!(json.contains("\"wasmSupported\":true"));
906 }
907
908 #[test]
909 fn surface_helpers_preserve_standard_response_shape() {
910 let surface = PackageSurface {
911 library: "demo".to_string(),
912 version: "0.1.0".to_string(),
913 capabilities: RuntimeCapabilities::pure_rust(),
914 operations: vec![surface_operation(
915 "describe",
916 "Describe",
917 "Describe demo package",
918 serde_json::json!({"includeOperations": true}),
919 )],
920 };
921 let response = describe_surface_response(
922 &surface,
923 SurfaceRequest {
924 operation: OperationId::new("describe"),
925 input: serde_json::json!({"includeOperations": true}),
926 },
927 );
928
929 assert_eq!(response.operation.as_str(), "describe");
930 assert_eq!(response.value["library"], "demo");
931 assert_eq!(response.value["operationCount"], 1);
932 assert_eq!(response.diagnostics, Vec::new());
933 assert_eq!(response.artifacts, Vec::<serde_json::Value>::new());
934 }
935}