verdigris 0.2.0

Browser application to explore, learn and debug CoAP
use yew::worker::*;

use crate::coapwsmessage::{CoAPWSMessageR, CoAPWSMessageW};

use coap_message::{MinimalWritableMessage, ReadableMessage};
use coap_handler_implementations::option_processing::OptionsExt;

pub enum Input {
    Initialize { uri: String },
    Message(CoAPWSMessageW),
}

#[derive(Debug)]
/// Event generated by a Connection
///
/// Most applications will want to match for Connected while waiting for the Go to send, and
/// Message later on, and treat anything else as an error that can be Debug-displayed. A connection
/// being closed by the peer, no matter how orderly, is expressed as an error (but without any
/// outstanding or later requests to send, servers will typically discard them anyway).
///
/// The regular sequence of messages produced is a Connected, followed by any sequence of Message
/// and SignalingInfo events. An error can be produced at any time. It is final; no more events
/// will be reported, and any input sent is ignored.
///
/// ```text
///               /---[ Connected ] ----- [ Message or SignalingInfo ]--\
///              /                    /                                 |
///   <start>---<                     \_________________________________/\
///              \________________________________________________________>---[ Error(...) ]
/// ```
pub enum Output {
    /// Connection is now ready. Emitted once, and before any Received events. Does not yet contain
    /// the CSM; that will be reported later (and, if the peer behaves correctly but that's not
    /// checked for here, immediately) as the next event.
    Connected,

    /// A (non-signaling) CoAP messager was received
    Message(CoAPWSMessageR<Box<[u8]>>),

    /// A signaling message was received and handled by the Connection. The application does not
    /// need to process this any further (but may display it for debugging purposes or draw
    /// additional conclusions from it). Most applications can treat this as a no-op (which is
    /// needs to be sent anyway as the construction of this inside the yew ecosystem means that
    /// every message from the socket must result in exactly one message to the application).
    ///
    /// This includes Release messages.
    SignalingInfo(CoAPWSMessageR<Box<[u8]>>),

    Error(OutputError)
}

#[derive(Debug)]
#[non_exhaustive]
/// As errors could be added later, they are expressed in a non-exhaustive fashion.
pub enum OutputError {
    /// The connection can't even be created
    ConnectError(String),
    /// A malformed message was received
    ParseError(&'static str),
    /// A text message was received (that's a protocol error too, but it may help to show the text
    /// for debugging purposes).
    // It's admittedly a bit inconsistent to strip the variable-lengthness off the box but not of
    // the error; this is primarily because Box<str> is an unfamiliar idiom but Box<[u8]> is more
    // common, and because TextReceived is a corner case anyway (where optimization does not matter
    // a lot) while CoAPWSMessageR<Box<[u8]>> is the regular case.
    TextReceived(String),
    /// An unknown signaling message was received, aborting the connection.
    ///
    /// This includes Abort messages.
    SignalingError(CoAPWSMessageR<Box<[u8]>>),
    /// The connection was closed
    Closed,
    /// An error was reported by the WebSocket
    ///
    /// This includes errors of yew conversion that are considered highly unlikely and don't
    /// warrant individual passing on.
    SocketError,
}

impl From<OutputError> for Output {
    fn from(e: OutputError) -> Output {
        Output::Error(e)
    }
}

/// A pool of CoAP-over-WS connections that is usable inside yew.
///
/// (This being a pool is not really essential to the functionality, but a consequence of the way
/// actors are designed or bridges are implemented).
///
/// This is only responsible for opening up a connection, parsing/serializing in- and outgoing
/// messages, and handling signalling messages. It does not take tasks like managing tokens or
/// request/response correlation, and only passes on messages. The only active part in the protocol
/// it takes is determining low-level protocol errors, handling signaling messages and responding
/// to them by closing the connection.
///
/// As this does not handle tokens on its own, each bridge (handler) requesting a connection will
/// get its own connection. A 
pub struct Connection {
    tasks: std::collections::HashMap<HandlerId, yew::services::websocket::WebSocketTask>,
    link: AgentLink<Self>,
}

impl Agent for Connection {
    type Reach = Context<Self>;
    type Message = (HandlerId, WSMsg);
    type Input = Input;
    type Output = Output;

    fn create(
        link: AgentLink<Self>,
    ) -> Self {
        let tasks =  Default::default();
        Connection {
            tasks,
            link,
        }
    }

    fn disconnected(&mut self, id: HandlerId) {
        // It's OK if that is None -- then either there was no connect, or there was an error
        drop(self.tasks.remove(&id));
    }

    // Data coming from the socket
    fn update(&mut self, (handler, msg): (HandlerId, WSMsg)) {
        let task = self.tasks.get_mut(&handler);

        let task = match task {
            Some(t) => t,
            // Not complaining loudly -- the message could have been enqueued before we decided to
            // drop the task in the last error.
            None => return,
        };

        match msg {
            WSMsg::Notif(yew::services::websocket::WebSocketStatus::Opened) => {
                let mut empty_csm = CoAPWSMessageW::new(&[]);
                empty_csm.set_code(coap_numbers::code::CSM);

                task.send_binary(Ok(empty_csm.serialize()));

                self.link.respond(handler, Output::Connected);
            }
            WSMsg::Binary(data) => {
                // Making the Vec into a Box primarily because we'd never use its variability any
                // more anyway. (Why does yew even make it one?)
                let msg = CoAPWSMessageR::new(data.into_boxed_slice());
                let msg = match msg {
                    Err(e) => {
                        self.link.respond(handler, OutputError::ParseError(e).into());
                        self.tasks.remove(&handler);
                        return;
                    }
                    Ok(m) => m,
                };

                if coap_numbers::code::classify(msg.code()) == coap_numbers::code::Range::Signaling {
                    match msg.code() {
                        coap_numbers::code::CSM | coap_numbers::code::RELEASE => {
                            let opts = msg.options().ignore_elective_others();
                            if opts.is_err() {
                                self.link.respond(handler, OutputError::SignalingError(msg).into());
                                self.tasks.remove(&handler);
                                return;
                            }
                        },
                        coap_numbers::code::PING => {
                            let mut pong = CoAPWSMessageW::new(msg.token());
                            pong.set_code(coap_numbers::code::PONG);

                            // When custody is implemented, this here would be the time to
                            // set custody on the response
                            let opts = msg.options().ignore_elective_others();
                            if opts.is_err() {
                                self.link.respond(handler, OutputError::SignalingError(msg).into());
                                self.tasks.remove(&handler);
                                return;
                            }
                            task.send_binary(Ok(pong.serialize()));
                        }
                        // including ABORT
                        _ => {
                            self.link.respond(handler, OutputError::SignalingError(msg).into());
                            self.tasks.remove(&handler);
                            return;
                        },
                    }

                    self.link.respond(handler, Output::SignalingInfo(msg));

                    return;
                }

                self.link.respond(handler, Output::Message(msg));
            }
            WSMsg::Text(data) => {
                self.link.respond(handler, OutputError::TextReceived(data).into());
                self.tasks.remove(&handler);
            }
            WSMsg::Notif(yew::services::websocket::WebSocketStatus::Closed) => {
                self.link.respond(handler, OutputError::Closed.into());
                self.tasks.remove(&handler);
            }
            WSMsg::Notif(yew::services::websocket::WebSocketStatus::Error) | WSMsg::YewBinaryError(_) | WSMsg::YewTextError(_) => {
                self.link.respond(handler, OutputError::SocketError.into());
                self.tasks.remove(&handler);
            }
        }
    }

    // Data coming from a user
    fn handle_input(&mut self, input: Input, handler: HandlerId) {
        match input {
            Input::Initialize { uri } => {
                let task = yew::services::websocket::WebSocketService::connect(
                        &uri,
                        self.link.callback(move |m: WSMsg| (handler, m)),
                        self.link.callback(move |n| (handler, WSMsg::Notif(n))),
                        );
                let task = match task {
                    Err(e) => {
                        self.link.respond(handler, OutputError::ConnectError(e.to_string()).into());
                        return;
                    }
                    Ok(t) => t
                };
                let old = self.tasks.insert(handler, task);
                debug_assert!(old.is_none(), "Double initialization message");
            }
            Input::Message(msg) => {
                let task = self.tasks.get_mut(&handler);
                let task = match task {
                    // Usage error -- but not complaining, maybe the message was racing against our
                    // error notification?
                    None => return,
                    Some(x) => x,
                };

                task.send_binary(Ok(msg.serialize()));
            }
        }
    }
}

/// Just a single message to represent all that can come in from yew
#[derive(Debug)]
// FIXME Can this be hidden even more effectively? Just because it's in my type doesn't mean
// anything about its internals should be public.
#[doc(hidden)]
pub enum WSMsg {
    Notif(yew::services::websocket::WebSocketStatus),
    Binary(Vec<u8>),
    Text(String),
    // Maybe as the WebSocket API is improved these can go away
    YewBinaryError(anyhow::Error),
    YewTextError(anyhow::Error),
}

impl From<yew::format::Binary> for WSMsg {
    fn from(input: yew::format::Binary) -> Self {
        input.map(|b| WSMsg::Binary(b)).unwrap_or_else(WSMsg::YewBinaryError)
    }
}

impl From<yew::format::Text> for WSMsg {
    fn from(input: yew::format::Text) -> Self {
        input.map(|s| WSMsg::Text(s)).unwrap_or_else(|e| WSMsg::YewTextError(e))
    }
}