1
2use std::collections::HashMap;
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use tracing::{debug, info, warn};
7
8#[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
17pub fn available_templates() -> HashMap<String, Template> {
19 let mut templates = HashMap::new();
20
21 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 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
44fn basic_template_files() -> HashMap<&'static str, &'static str> {
46 let mut files = HashMap::new();
47
48 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 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 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 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
162fn http_template_files() -> HashMap<&'static str, &'static str> {
164 let mut files = HashMap::new();
165
166 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 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 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 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
809pub 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 fs::create_dir_all(target_dir)?;
829
830 for (relative_path, content) in &template.files {
832 let file_path = target_dir.join(relative_path);
833
834 if let Some(parent) = file_path.parent() {
836 if !parent.exists() {
837 fs::create_dir_all(parent)?;
838 }
839 }
840
841 let processed_content = content.replace("{{project_name}}", project_name);
843
844 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 fs::write(&file_path, processed_content)?;
856 }
857
858 info!("Project '{}' created successfully!", project_name);
859 Ok(())
860}
861
862pub 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}