flo_scene 0.2.0

Entity-messaging system for composing large programs from small programs
Documentation
use crate::host::input_stream::*;
use crate::host::programs::*;
use crate::host::scene_context::*;
use crate::host::scene_message::*;
use super::error::*;
use super::fn_command::*;
use super::list_commands::*;
use super::describe_command::*;
use super::run_command::*;

use futures::prelude::*;
use futures::future::{BoxFuture};

use std::borrow::{Cow};
use std::collections::{HashMap};
use std::marker::{PhantomData};
use std::sync::*;

///
/// A command launcher will respond to `RunCommand<TParameter, Result<TResponse, CommandError>>` requests by
/// spawning a task using a function. This is the typical way that a group of command queries is declared.
///
/// The launcher will also respond to the `::list_commands` command with a list of responses converted from
/// the `ListCommandResponse` struture.
///
pub struct CommandLauncher<TParameter, TResponse> {
    /// The commands are invoked as a subtask when a `RunCommand<TParameter, Result<TResponse, CommandError>>` request is made
    commands: HashMap<String, Arc<dyn Send + Sync + Fn(&TParameter, SceneContext) -> BoxFuture<'static, ()>>>,

    /// The summaries are one-line descriptions of what the command does
    summaries: HashMap<String, String>,

    /// The help provides markdown help for the commands managed by this launcher
    help: HashMap<String, Cow<'static, str>>,

    /// The response is used for the output of the commands run by this launcher
    response: PhantomData<TResponse>,
}

impl<TParameter, TResponse> CommandLauncher<TParameter, TResponse>
where
    TParameter: 'static + Unpin + Send + Sync + Clone,
    TParameter: TryInto<DescribeCommandRequest>,
    TResponse:  'static + Unpin + Send + SceneMessage,
    TResponse:  From<ListCommandResponse>,
    TResponse:  From<CommandError>,
    TResponse:  From<DescribeCommandResponse>,
{
    ///
    /// Creates a new command launcher, with no built in commands
    ///
    pub fn empty() -> Self {
        CommandLauncher {
            commands:   HashMap::new(),
            summaries:  HashMap::new(),
            help:       HashMap::new(),
            response:   PhantomData
        }
    }

    ///
    /// Returns this launcher modified with a new command. The command can send its results to the `TResponse` output stream in the context
    ///
    pub fn with_command<TFuture>(mut self, command_name: impl Into<String>, command: impl 'static + Send + Sync + Fn(&TParameter, SceneContext) -> TFuture) -> Self
    where
        TFuture: 'static + Send + Future<Output=()>,
    {
        self.commands.insert(command_name.into(), Arc::new(move |parameter, context| command(parameter, context).boxed()));

        self
    }

    ///
    /// Returns this launcher modified with some summary text for a command (used when providing help)
    ///
    pub fn with_summary(mut self, command_name: impl Into<String>, summary: impl Into<String>) -> Self {
        self.summaries.insert(command_name.into(), summary.into());

        self
    }

    ///
    /// Returns this launcher modified with some markdown text for a command (used when providing help)
    ///
    pub fn with_help(mut self, command_name: impl Into<String>, help: impl Into<Cow<'static, str>>) -> Self {
        self.help.insert(command_name.into(), help.into());

        self
    }

    ///
    /// Converts this launcher to a subprogram that can be added to a scene to respond to the run command requests
    ///
    pub fn to_subprogram(self) -> impl 'static + Send + FnOnce(InputStream<RunCommand<TParameter, TResponse>>, SceneContext) -> BoxFuture<'static, ()> {
        move |input, context| async move {
            let mut input = input;

            // Read run command requests from the input
            while let Some(run_request) = input.next().await {
                if run_request.name() == LIST_COMMANDS {
                    // List the commands in the launcher
                    let command_target  = run_request.target();
                    let response        = ListCommandResponse(self.commands.iter()
                        .map(|(name, _)| CommandDescription { name: name.clone() })
                        .chain([CommandDescription { name: LIST_COMMANDS.into() }])
                        .collect::<Vec<_>>());

                    let list_commands_response  = QueryResponse::with_data(response.into());
                    let response_stream         = context.send::<QueryResponse<TResponse>>(command_target);
                    
                    if let Ok(mut response_stream) = response_stream {
                        response_stream.send(list_commands_response).await.ok();
                    }
                } else if run_request.name() == DESCRIBE_COMMAND {
                    let command_target  = run_request.target();

                    // Fetch the description for this command
                    let request = if let Ok(request) = run_request.parameter().clone().try_into() { request } else { continue; };

                    // Create the response by looking up the details of this command
                    let response = DescribeCommandResponse {
                        summary:    self.summaries.get(&request.0).cloned().unwrap_or_else(|| "".into()),
                        help:       self.help.get(&request.0).cloned().unwrap_or_else(|| format!("# {}\n\nExecutes the `{}` command\n", request.0, request.0).into()),
                    };

                    // Send the response
                    let description_command_response    = QueryResponse::with_data(response.into());
                    let response_stream                 = context.send::<QueryResponse<TResponse>>(command_target);

                    if let Ok(mut response_stream) = response_stream {
                        response_stream.send(description_command_response).await.ok();
                    }
                } else if let Some(command) = self.commands.get(run_request.name()).cloned() {
                    // Run the command
                    let command_target = run_request.target();
                    let command_output = context.spawn_command(FnCommand::<(), TResponse>::new(move |_, context| {
                        let command = Arc::clone(&command);
                        let future = (*command)(run_request.parameter(), context);

                        async move {
                            future.await
                        }
                    }), stream::empty());

                    if let Ok(command_output) = command_output {
                        // Send the output to the target
                        let response = context.send::<QueryResponse<TResponse>>(command_target);
                        
                        if let Ok(mut response) = response {
                            response.send(QueryResponse::with_stream(command_output)).await.ok();
                        }
                    }
                } else {
                    // Send an error saying the command is not known
                    let command_target  = run_request.target();
                    let command_name    = run_request.name().to_string();
                    let response        = context.send::<QueryResponse<TResponse>>(command_target);
                    
                    if let Ok(mut response) = response {
                        response.send(QueryResponse::with_iterator([TResponse::from(CommandError::CommandNotFound(command_name))])).await.ok();
                    }
                }
            }
        }.boxed()
    }
}