Skip to main content

runtime_core/
lib.rs

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
197/// Builds the standard package-surface operation metadata used by library
198/// crates and transport adapters.
199pub 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
217/// Builds the standard `describe` response without changing the shared
218/// `SurfaceResponse` JSON shape.
219pub 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
254/// Builds a successful surface response with empty diagnostics and artifacts.
255pub 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
267/// Builds a package-surface value with standard human-readable metadata while
268/// preserving object fields from the concrete operation result at the top level.
269pub 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
294/// Adds the common package-surface UI fields to a result value when they are
295/// missing, while preserving every existing top-level domain field.
296pub 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
323/// Builds a successful package-surface response using `structured_surface_value`.
324pub 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
335/// Builds a structured response for an operation listed in a package surface.
336///
337/// This keeps the concrete operation result at the top level for compatibility,
338/// while adding the common `title`, `message`, `summary`, and `result` fields
339/// expected by package-surface UIs.
340pub 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
395/// Shared helpers for thin package-surface CLI adapters.
396pub 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    /// Static package metadata for one CLI adapter.
406    #[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    /// Builds the standard CLI adapter metadata payload.
417    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    /// Builds the standard CLI command schema payload.
432    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    /// Reads a JSON request from `--json`, `--file`, or stdin.
445    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    /// Runs an operation through a library-owned surface and adds standard
466    /// package-surface value fields if an older surface omitted them.
467    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
487/// Shared helpers for local package-surface HTTP adapters.
488pub 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    /// Static package metadata for one server adapter.
498    #[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    /// Serves the standard local package-surface HTTP API.
517    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    /// Returns the standard response for one HTTP request.
531    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    /// Builds the standard server adapter metadata payload.
590    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}