theater_cli/templates/
mod.rs

1
2use std::collections::HashMap;
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use tracing::{debug, info, warn};
7
8/// Template metadata
9#[derive(Debug, Clone)]
10#[allow(dead_code)]
11pub struct Template {
12    pub name: String,
13    pub description: String,
14    pub files: HashMap<&'static str, &'static str>,
15}
16
17/// Available templates for creating new actors
18pub fn available_templates() -> HashMap<String, Template> {
19    let mut templates = HashMap::new();
20
21    // Basic actor template
22    templates.insert(
23        "basic".to_string(),
24        Template {
25            name: "basic".to_string(),
26            description: "A simple Theater actor with basic functionality".to_string(),
27            files: basic_template_files(),
28        },
29    );
30
31    // HTTP actor template
32    templates.insert(
33        "http".to_string(),
34        Template {
35            name: "http".to_string(),
36            description: "A Theater actor with HTTP server capabilities".to_string(),
37            files: http_template_files(),
38        },
39    );
40
41    templates
42}
43
44/// Basic actor template files
45fn basic_template_files() -> HashMap<&'static str, &'static str> {
46    let mut files = HashMap::new();
47
48    // Add Cargo.toml
49    files.insert("Cargo.toml", r#"[package]
50name = "{{project_name}}"
51version = "0.1.0"
52edition = "2021"
53
54[lib]
55crate-type = ["cdylib"]
56
57[dependencies]
58serde = { version = "1.0", features = ["derive"] }
59serde_json = "1.0"
60wit-bindgen-rt = { version = "0.39.0", features = ["bitflags"] }
61"#);
62
63    // Add manifest.toml
64    files.insert("manifest.toml", r#"name = "{{project_name}}"
65version = "0.1.0"
66description = "A basic Theater actor"
67component_path = "not yet build"
68save_chain = true
69
70[interface]
71implements = "theater:simple/actor"
72requires = []
73
74[[handlers]]
75type = "runtime"
76config = {}
77"#);
78
79    // Add src/lib.rs
80    files.insert("src/lib.rs", r#"mod bindings;
81
82use crate::bindings::exports::ntwk::theater::actor::Guest;
83use crate::bindings::ntwk::theater::runtime::{log, shutdown};
84
85use serde::{Deserialize, Serialize};
86
87#[derive(Serialize, Deserialize)]
88struct State {
89    messages: Vec<String>,
90}
91
92struct Actor;
93impl Guest for Actor {
94    fn init(
95        _init_state_bytes: Option<Vec<u8>>,
96        params: (String,),
97    ) -> Result<(Option<Vec<u8>>,), String> {
98        log("Initializing {{project_name}} actor");
99        let (self_id,) = params;
100        log(&format!("Actor ID: {}", &self_id));
101        log("Hello from {{project_name}} actor!");
102
103        shutdown("{{project_name}} actor shutting down");
104
105        Ok((None,))
106    }
107}
108
109bindings::export!(Actor with_types_in bindings);
110"#);
111
112    // Add README.md
113    files.insert("README.md", r#"# {{project_name}}
114
115A basic Theater actor created from the template.
116
117## Building
118
119To build the actor:
120
121```bash
122theater build
123```
124
125## Running
126
127To run the actor with Theater:
128
129```bash
130theater start manifest.toml
131```
132
133## Features
134
135This basic actor supports:
136
137- Storing and retrieving state
138- Handling simple messages
139- Incrementing a counter
140- Storing text messages
141
142## API
143
144You can interact with this actor using the following commands:
145
146- `count` - Get the current count
147- `messages` - Get all stored messages
148- `increment` - Increment the counter
149- Any other text - Store as a message
150
151## Example
152
153```bash
154# Send a request to get the current count
155theater message {{project_name}} count
156```
157"#);
158
159    files
160}
161
162/// HTTP actor template files
163fn http_template_files() -> HashMap<&'static str, &'static str> {
164    let mut files = HashMap::new();
165
166    // Add Cargo.toml
167    files.insert("Cargo.toml", r#"[package]
168name = "{{project_name}}"
169version = "0.1.0"
170edition = "2021"
171
172[lib]
173crate-type = ["cdylib"]
174
175[dependencies]
176serde = { version = "1.0", features = ["derive"] }
177serde_json = "1.0"
178wit-bindgen-rt = { version = "0.39.0", features = ["bitflags"] }
179"#);
180
181    // Add manifest.toml
182    files.insert("manifest.toml", r#"name = "{{project_name}}"
183version = "0.1.0"
184description = "An HTTP server Theater actor"
185component_path = "target/wasm32-unknown-unknown/release/{{project_name_snake}}.wasm"
186
187[interface]
188implements = "theater:simple/actor"
189requires = []
190
191[[handlers]]
192type = "runtime"
193config = {}
194
195[[handlers]]
196type = "http-framework"
197config = {}
198"#);
199
200    // Add src/lib.rs
201    files.insert("src/lib.rs", r#"mod bindings;
202
203use crate::bindings::exports::ntwk::theater::actor::Guest;
204use crate::bindings::exports::ntwk::theater::http_handlers::Guest as HttpHandlers;
205use crate::bindings::exports::ntwk::theater::message_server_client::Guest as MessageServerClient;
206use crate::bindings::ntwk::theater::http_framework::{
207    add_middleware, add_route, create_server, enable_websocket, register_handler, start_server,
208    ServerConfig,
209};
210use crate::bindings::ntwk::theater::http_types::{HttpRequest, HttpResponse, MiddlewareResult};
211use crate::bindings::ntwk::theater::runtime::log;
212use crate::bindings::ntwk::theater::types::State;
213use crate::bindings::ntwk::theater::websocket_types::{MessageType, WebsocketMessage};
214
215use serde::{Deserialize, Serialize};
216
217#[derive(Serialize, Deserialize)]
218struct AppState {
219    count: u32,
220    messages: Vec<String>,
221}
222
223impl Default for AppState {
224    fn default() -> Self {
225        Self {
226            count: 0,
227            messages: Vec::new(),
228        }
229    }
230}
231
232struct Actor;
233impl Guest for Actor {
234    fn init(_state: State, params: (String,)) -> Result<(State,), String> {
235        log("Initializing HTTP Actor");
236        let (param,) = params;
237        log(&format!("Init parameter: {}", param));
238
239        let app_state = AppState::default();
240        log("Created default app state");
241        let state_bytes = serde_json::to_vec(&app_state).map_err(|e| e.to_string())?;
242
243        // Create the initial state
244        let new_state = Some(state_bytes);
245
246        // Set up the HTTP server
247        log("Setting up HTTP server...");
248        setup_http_server().map_err(|e| e.to_string())?;
249        log("HTTP server set up successfully");
250
251        Ok((new_state,))
252    }
253}
254
255// Setup the HTTP server and return the server ID
256fn setup_http_server() -> Result<u64, String> {
257    log("Setting up HTTP server");
258
259    // Create server configuration
260    let config = ServerConfig {
261        port: Some(8080),
262        host: Some("0.0.0.0".to_string()),
263        tls_config: None,
264    };
265
266    // Create a new HTTP server
267    let server_id = create_server(&config)?;
268    log(&format!("Created server with ID: {}", server_id));
269
270    // Register handlers
271    let api_handler_id = register_handler("handle_api")?;
272    let middleware_handler_id = register_handler("auth_middleware")?;
273    let ws_handler_id = register_handler("handle_websocket")?;
274
275    log(&format!(
276        "Registered handlers - API: {}, Middleware: {}, WebSocket: {}",
277        api_handler_id, middleware_handler_id, ws_handler_id
278    ));
279
280    // Add middleware
281    add_middleware(server_id, "/api", middleware_handler_id)?;
282
283    // Add routes
284    add_route(server_id, "/api/count", "GET", api_handler_id)?;
285    add_route(server_id, "/api/count", "POST", api_handler_id)?;
286    add_route(server_id, "/api/messages", "GET", api_handler_id)?;
287    add_route(server_id, "/api/messages", "POST", api_handler_id)?;
288
289    // Enable WebSocket
290    enable_websocket(
291        server_id,
292        "/ws",
293        Some(ws_handler_id), // Connect handler
294        ws_handler_id,       // Message handler
295        Some(ws_handler_id), // Disconnect handler
296    )?;
297
298    // Start the server
299    let port = start_server(server_id)?;
300    log(&format!("Server started on port {}", port));
301
302    Ok(server_id)
303}
304
305impl HttpHandlers for Actor {
306    // HTTP Request Handler
307    fn handle_request(
308        state: State,
309        params: (u64, HttpRequest),
310    ) -> Result<(State, (HttpResponse,)), String> {
311        let (handler_id, request) = params;
312        log(&format!(
313            "Handling HTTP request with handler ID: {}",
314            handler_id
315        ));
316        log(&format!(
317            "  Method: {}, Path: {}",
318            request.method, request.uri
319        ));
320
321        // Parse the current state
322        let state_bytes = state.unwrap_or_default();
323        let mut app_state: AppState = if !state_bytes.is_empty() {
324            log("Deserializing existing state");
325            let app_state: AppState =
326                serde_json::from_slice(&state_bytes).map_err(|e| e.to_string())?;
327            log(&format!(
328                "Current state: count={}, messages={}",
329                app_state.count,
330                app_state.messages.len()
331            ));
332            app_state
333        } else {
334            log("Creating default state");
335            AppState::default()
336        };
337
338        // Process the request based on the path and method
339        log(&format!(
340            "Processing request: {} {}",
341            request.method, request.uri
342        ));
343        let response = match (request.uri.as_str(), request.method.as_str()) {
344            ("/api/count", "GET") => {
345                log("Handling GET /api/count");
346                // Return the current count
347                let data = serde_json::json!({ "count": app_state.count });
348                let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
349                log(&format!("Returning count: {}", app_state.count));
350
351                HttpResponse {
352                    status: 200,
353                    headers: vec![("content-type".to_string(), "application/json".to_string())],
354                    body: Some(body),
355                }
356            }
357            ("/api/count", "POST") => {
358                log("Handling POST /api/count");
359                // Increment the count
360                app_state.count += 1;
361                log(&format!("Incremented count to: {}", app_state.count));
362
363                // Return the new count
364                let data = serde_json::json!({ "count": app_state.count });
365                let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
366
367                HttpResponse {
368                    status: 200,
369                    headers: vec![("content-type".to_string(), "application/json".to_string())],
370                    body: Some(body),
371                }
372            }
373            ("/api/messages", "GET") => {
374                log("Handling GET /api/messages");
375                // Return all messages
376                let data = serde_json::json!({ "messages": app_state.messages });
377                let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
378                log(&format!("Returning {} messages", app_state.messages.len()));
379
380                HttpResponse {
381                    status: 200,
382                    headers: vec![("content-type".to_string(), "application/json".to_string())],
383                    body: Some(body),
384                }
385            }
386            ("/api/messages", "POST") => {
387                log("Handling POST /api/messages");
388                // Parse the message from the request body
389                if let Some(body) = &request.body {
390                    log(&format!("Received request body of {} bytes", body.len()));
391                    // Attempt to parse the body as a JSON object with a message field
392                    if let Ok(json) = serde_json::from_slice::<serde_json::Value>(body) {
393                        log("Successfully parsed JSON body");
394                        if let Some(message) = json.get("message").and_then(|m| m.as_str()) {
395                            log(&format!("Adding message: {}", message));
396                            // Add the message to our state
397                            app_state.messages.push(message.to_string());
398
399                            // Return success
400                            let data = serde_json::json!({
401                                "success": true,
402                                "message": "Message added successfully"
403                            });
404                            let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
405                            log("Message added successfully");
406
407                            HttpResponse {
408                                status: 200,
409                                headers: vec![(
410                                    "content-type".to_string(),
411                                    "application/json".to_string(),
412                                )],
413                                body: Some(body),
414                            }
415                        } else {
416                            // No message field found
417                            log("Error: No message field found in request");
418                            let data = serde_json::json!({
419                                "success": false,
420                                "error": "No message field found in request"
421                            });
422                            let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
423
424                            HttpResponse {
425                                status: 400,
426                                headers: vec![(
427                                    "content-type".to_string(),
428                                    "application/json".to_string(),
429                                )],
430                                body: Some(body),
431                            }
432                        }
433                    } else {
434                        // Invalid JSON
435                        log("Error: Invalid JSON in request body");
436                        let data = serde_json::json!({
437                            "success": false,
438                            "error": "Invalid JSON in request body"
439                        });
440                        let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
441
442                        HttpResponse {
443                            status: 400,
444                            headers: vec![(
445                                "content-type".to_string(),
446                                "application/json".to_string(),
447                            )],
448                            body: Some(body),
449                        }
450                    }
451                } else {
452                    // No body provided
453                    log("Error: No request body provided");
454                    let data = serde_json::json!({
455                        "success": false,
456                        "error": "No request body provided"
457                    });
458                    let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
459
460                    HttpResponse {
461                        status: 400,
462                        headers: vec![("content-type".to_string(), "application/json".to_string())],
463                        body: Some(body),
464                    }
465                }
466            }
467            _ => {
468                // Path not found
469                log(&format!(
470                    "Error: Path not found - {} {}",
471                    request.method, request.uri
472                ));
473                let data = serde_json::json!({
474                    "success": false,
475                    "error": "Not found"
476                });
477                let body = serde_json::to_vec(&data).map_err(|e| e.to_string())?;
478
479                HttpResponse {
480                    status: 404,
481                    headers: vec![("content-type".to_string(), "application/json".to_string())],
482                    body: Some(body),
483                }
484            }
485        };
486
487        // Save the updated state
488        log(&format!(
489            "Saving updated state: count={}, messages={}",
490            app_state.count,
491            app_state.messages.len()
492        ));
493        let updated_state_bytes = serde_json::to_vec(&app_state).map_err(|e| e.to_string())?;
494        let updated_state = Some(updated_state_bytes);
495
496        Ok((updated_state, (response,)))
497    }
498
499    // Middleware Handler
500    fn handle_middleware(
501        state: State,
502        params: (u64, HttpRequest),
503    ) -> Result<(State, (MiddlewareResult,)), String> {
504        let (handler_id, request) = params;
505        log(&format!(
506            "Handling middleware with handler ID: {}",
507            handler_id
508        ));
509
510        // Check for an API key header
511        log("Checking for API key header");
512        let auth_header = request
513            .headers
514            .iter()
515            .find(|(name, _)| name.to_lowercase() == "x-api-key");
516
517        if let Some((_, value)) = auth_header {
518            log(&format!("Found API key header: {}", value));
519            // Check if the API key is valid
520            if value == "theater-demo-key" {
521                // Allow the request to proceed
522                log("API key is valid, allowing request to proceed");
523                Ok((
524                    state,
525                    (MiddlewareResult {
526                        proceed: true,
527                        request,
528                    },),
529                ))
530            } else {
531                // Invalid API key
532                log(&format!("Invalid API key: {}", value));
533                Ok((
534                    state,
535                    (MiddlewareResult {
536                        proceed: false,
537                        request,
538                    },),
539                ))
540            }
541        } else {
542            // No API key provided
543            log("No API key provided");
544            Ok((
545                state,
546                (MiddlewareResult {
547                    proceed: false,
548                    request,
549                },),
550            ))
551        }
552    }
553
554    // WebSocket Connect Handler
555    fn handle_websocket_connect(
556        state: State,
557        params: (u64, u64, String, Option<String>),
558    ) -> Result<(State,), String> {
559        let (handler_id, connection_id, path, query) = params;
560        log(&format!(
561            "WebSocket connected - Handler: {}, Connection: {}, Path: {}",
562            handler_id, connection_id, path
563        ));
564
565        if let Some(q) = query {
566            log(&format!("  Query parameters: {}", q));
567        }
568
569        Ok((state,))
570    }
571
572    // WebSocket Message Handler
573    fn handle_websocket_message(
574        state: State,
575        params: (u64, u64, WebsocketMessage),
576    ) -> Result<(State, (Vec<WebsocketMessage>,)), String> {
577        let (handler_id, connection_id, message) = params;
578        log(&format!(
579            "WebSocket message received - Handler: {}, Connection: {}",
580            handler_id, connection_id
581        ));
582
583        // Parse the current state
584        let state_bytes = state.unwrap_or_default();
585        let mut app_state: AppState = if !state_bytes.is_empty() {
586            log("WebSocket: Deserializing existing state");
587            let app_state: AppState =
588                serde_json::from_slice(&state_bytes).map_err(|e| e.to_string())?;
589            app_state
590        } else {
591            log("WebSocket: Creating default state");
592            AppState::default()
593        };
594
595        let responses = match message.ty {
596            MessageType::Text => {
597                // Echo the message back
598                if let Some(text) = message.text {
599                    log(&format!("  Text message: {}", text));
600
601                    // Add the message to our state
602                    app_state.messages.push(text.clone());
603
604                    // Echo back the message
605                    let echo_message = WebsocketMessage {
606                        ty: MessageType::Text,
607                        data: None,
608                        text: Some(format!("Echo: {}", text)),
609                    };
610
611                    // Also send the current count
612                    let count_message = WebsocketMessage {
613                        ty: MessageType::Text,
614                        data: None,
615                        text: Some(format!("Current count: {}", app_state.count)),
616                    };
617
618                    vec![echo_message, count_message]
619                } else {
620                    vec![]
621                }
622            }
623            MessageType::Binary => {
624                // Echo binary data
625                if let Some(data) = message.data {
626                    log(&format!("  Binary message: {} bytes", data.len()));
627
628                    // Just echo it back
629                    vec![WebsocketMessage {
630                        ty: MessageType::Binary,
631                        data: Some(data),
632                        text: None,
633                    }]
634                } else {
635                    vec![]
636                }
637            }
638            MessageType::Ping => {
639                // Respond to ping with pong
640                log("  Ping received");
641                vec![WebsocketMessage {
642                    ty: MessageType::Pong,
643                    data: None,
644                    text: None,
645                }]
646            }
647            _ => {
648                // Other message types
649                log("  Other message type");
650                vec![]
651            }
652        };
653
654        // Save the updated state
655        let updated_state_bytes = serde_json::to_vec(&app_state).map_err(|e| e.to_string())?;
656        let updated_state = Some(updated_state_bytes);
657
658        Ok((updated_state, (responses,)))
659    }
660
661    // WebSocket Disconnect Handler
662    fn handle_websocket_disconnect(state: State, params: (u64, u64)) -> Result<(State,), String> {
663        let (handler_id, connection_id) = params;
664        log(&format!(
665            "WebSocket disconnected - Handler: {}, Connection: {}",
666            handler_id, connection_id
667        ));
668
669        Ok((state,))
670    }
671}
672
673impl MessageServerClient for Actor {
674    fn handle_send(
675        state: Option<Vec<u8>>,
676        _params: (Vec<u8>,),
677    ) -> Result<(Option<Vec<u8>>,), String> {
678        Ok((state,))
679    }
680
681    fn handle_request(
682        state: Option<Vec<u8>>,
683        _params: (Vec<u8>,),
684    ) -> Result<(Option<Vec<u8>>, (Vec<u8>,)), String> {
685        Ok((state, (vec![],)))
686    }
687
688    fn handle_channel_open(
689        state: Option<bindings::exports::ntwk::theater::message_server_client::Json>,
690        params: (bindings::exports::ntwk::theater::message_server_client::Json,),
691    ) -> Result<
692        (
693            Option<bindings::exports::ntwk::theater::message_server_client::Json>,
694            (bindings::exports::ntwk::theater::message_server_client::ChannelAccept,),
695        ),
696        String,
697    > {
698        Ok((
699            state,
700            (
701                bindings::exports::ntwk::theater::message_server_client::ChannelAccept {
702                    accepted: true,
703                    message: None,
704                },
705            ),
706        ))
707    }
708
709    fn handle_channel_close(
710        state: Option<bindings::exports::ntwk::theater::message_server_client::Json>,
711        params: (String,),
712    ) -> Result<(Option<bindings::exports::ntwk::theater::message_server_client::Json>,), String>
713    {
714        Ok((state,))
715    }
716
717    fn handle_channel_message(
718        state: Option<bindings::exports::ntwk::theater::message_server_client::Json>,
719        params: (
720            String,
721            bindings::exports::ntwk::theater::message_server_client::Json,
722        ),
723    ) -> Result<(Option<bindings::exports::ntwk::theater::message_server_client::Json>,), String>
724    {
725        log("runtime-content-fs: Received channel message");
726        Ok((state,))
727    }
728}
729
730bindings::export!(Actor with_types_in bindings);
731"#);
732
733    // Add README.md
734    files.insert("README.md", r#"# {{project_name}}
735
736An HTTP server Theater actor created from the template.
737
738## Building
739
740To build the actor:
741
742```bash
743cargo build --target wasm32-unknown-unknown --release
744```
745
746## Running
747
748To run the actor with Theater:
749
750```bash
751theater start manifest.toml
752```
753
754## Features
755
756This HTTP actor provides:
757
758- RESTful API endpoints
759- WebSocket support
760- Middleware for authentication
761- State management
762- Message handling
763
764## API Endpoints
765
766The actor exposes the following HTTP endpoints:
767
768- `GET /api/count` - Get the current count
769- `POST /api/count` - Increment the count
770- `GET /api/messages` - Get all stored messages
771- `POST /api/messages` - Add a new message
772
773## WebSocket
774
775The actor also supports WebSocket connections at `/ws`. The WebSocket interface:
776
777- Echoes back text messages
778- Returns the current count
779- Stores new messages in the actor state
780
781## Authentication
782
783API endpoints under `/api/*` are protected with a simple API key middleware.
784Include the header `X-API-Key: theater-demo-key` in your requests.
785
786## Example Usage
787
788```bash
789# Get the current count
790curl -H "X-API-Key: theater-demo-key" http://localhost:8080/api/count
791
792# Add a new message
793curl -X POST -H "Content-Type: application/json" \
794     -H "X-API-Key: theater-demo-key" \
795     -d '{"message":"Hello, Theater!"}' \
796     http://localhost:8080/api/messages
797```
798
799For WebSocket testing, you can use a tool like websocat:
800
801```bash
802websocat ws://localhost:8080/ws
803```
804"#);
805
806    files
807}
808
809/// Create a new actor project from a template
810pub fn create_project(
811    template_name: &str,
812    project_name: &str,
813    target_dir: &Path,
814) -> Result<(), io::Error> {
815    let templates = available_templates();
816    let template = templates
817        .get(template_name)
818        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Template not found"))?;
819
820    info!(
821        "Creating new {} project '{}' in {}",
822        template_name,
823        project_name,
824        target_dir.display()
825    );
826
827    // Create the target directory
828    fs::create_dir_all(target_dir)?;
829
830    // Create all template files
831    for (relative_path, content) in &template.files {
832        let file_path = target_dir.join(relative_path);
833
834        // Create parent directories if they don't exist
835        if let Some(parent) = file_path.parent() {
836            if !parent.exists() {
837                fs::create_dir_all(parent)?;
838            }
839        }
840
841        // Replace template variables
842        let processed_content = content.replace("{{project_name}}", project_name);
843        
844        // Also handle snake_case version for file paths
845        let project_name_snake = project_name.replace('-', "_");
846        let processed_content = processed_content.replace("{{project_name_snake}}", &project_name_snake);
847
848        debug!(
849            "Creating file: {} ({} bytes)",
850            file_path.display(),
851            processed_content.len()
852        );
853
854        // Write the file
855        fs::write(&file_path, processed_content)?;
856    }
857
858    info!("Project '{}' created successfully!", project_name);
859    Ok(())
860}
861
862/// List all available templates
863pub fn list_templates() {
864    let templates = available_templates();
865    
866    println!("Available templates:");
867    for (name, template) in templates {
868        println!("  {}: {}", name, template.description);
869    }
870}