<commit_before>use rlua::{FromLua as _, Function as LuaFunction, Lua, StdLib as LuaStdLib, Value as LuaValue};
use serde::Serialize;
use serde_json::Value as JsonValue;

use gerritbot_gerrit as gerrit;

const DEFAULT_FORMAT_SCRIPT: &str = include_str!("../../scripts/format.lua");
const LUA_FORMAT_COMMENT_ADDED: &str = "format_comment_added";
const LUA_FORMAT_REVIEWER_ADDED: &str = "format_reviewer_added";
const LUA_FORMAT_FUNCTIONS: &[&str] = &[LUA_FORMAT_COMMENT_ADDED, LUA_FORMAT_REVIEWER_ADDED];

pub struct Formatter {
    lua: Lua,
}

impl Default for Formatter {
    fn default() -> Self {
        Self {
            lua: load_format_script(DEFAULT_FORMAT_SCRIPT).unwrap(),
        }
    }
}

fn load_format_script(script_source: &str) -> Result<Lua, String> {
    let lua_std_lib = LuaStdLib::BASE | LuaStdLib::STRING | LuaStdLib::TABLE;
    let lua = Lua::new_with(lua_std_lib);
    lua.context(|context| -> Result<(), String> {
        let globals = context.globals();
        context
            .load(script_source)
            .exec()
            .map_err(|err| format!("syntax error: {}", err))?;

        // check that the required functions are present
        for function_name in LUA_FORMAT_FUNCTIONS {
            let _: LuaFunction = globals
                .get(*function_name)
                .map_err(|_| format!("{} function missing", function_name))?;
        }

        Ok(())
    })?;
    Ok(lua)
}

fn json_to_lua<'lua>(json: &JsonValue, lua: rlua::Context<'lua>) -> rlua::Result<LuaValue<'lua>> {
    Ok(match json {
        JsonValue::Null => LuaValue::Nil,
        JsonValue::Bool(b) => LuaValue::Boolean(*b),
        JsonValue::Number(n) => {
            if let Some(n) = n.as_i64() {
                LuaValue::Integer(n)
            } else if let Some(n) = n.as_f64() {
                LuaValue::Number(n)
            } else {
                Err(rlua::Error::ToLuaConversionError {
                    from: "serde_json::Number",
                    to: "Integer",
                    message: Some(format!("value {} too large", n)),
                })?
            }
        }
        JsonValue::String(s) => lua.create_string(s).map(LuaValue::String)?,
        JsonValue::Array(values) => {
            let table = lua.create_table()?;

            for (i, value) in values.iter().enumerate() {
                table.set(LuaValue::Integer(i as i64 + 1), json_to_lua(value, lua)?)?;
            }

            LuaValue::Table(table)
        }
        JsonValue::Object(items) => {
            let table = lua.create_table()?;

            for (key, value) in items {
                let key = lua.create_string(key)?;
                let value = json_to_lua(value, lua)?;
                table.set(key, value)?;
            }

            LuaValue::Table(table)
        }
    })
}

fn to_lua_via_json<'lua, T: Serialize>(
    value: T,
    lua: rlua::Context<'lua>,
) -> Result<LuaValue<'lua>, Box<dyn std::error::Error>> {
    let json_value = serde_json::to_value(value)?;
    let lua_value = json_to_lua(&json_value, lua)?;
    Ok(lua_value)
}

impl Formatter {
    pub fn format_comment_added(
        &self,
        event: &gerrit::CommentAddedEvent,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        self.lua.context(|context| {
            let globals = context.globals();

            let lua_format_comment_added: LuaFunction = globals
                .get(LUA_FORMAT_COMMENT_ADDED)
                .map_err(|_| "format_approval function missing".to_string())?;
            let lua_event = to_lua_via_json(event, context)
                .map_err(|e| format!("failed to serialize event: {}", e))?;
            let lua_result = lua_format_comment_added
                .call::<_, LuaValue>((lua_event, is_human))
                .map_err(|err| format!("lua formatting function failed: {}", err))?;

            match lua_result {
                LuaValue::Nil => Ok(None),
                _ => Ok(Some(String::from_lua(lua_result, context).map_err(
                    |e| format!("failed to convert formatting result to string: {}", e),
                )?)),
            }
        })
    }

    pub fn format_reviewer_added(
        &self,
        event: &gerrit::ReviewerAddedEvent,
    ) -> Result<String, String> {
        self.lua.context(|context| {
            let globals = context.globals();

            let lua_format_reviewer_added: LuaFunction = globals
                .get(LUA_FORMAT_REVIEWER_ADDED)
                .map_err(|_| format!("{} function missing", LUA_FORMAT_REVIEWER_ADDED))?;
            let lua_event = to_lua_via_json(event, context)
                .map_err(|e| format!("failed to serialize event: {}", e))?;
            let lua_result = lua_format_reviewer_added
                .call::<_, LuaValue>(lua_event)
                .map_err(|err| format!("lua formatting function failed: {}", err))?;

            Ok(String::from_lua(lua_result, context)
                .map_err(|e| format!("failed to convert formatting result to string: {}", e))?)
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const EVENT_JSON : &'static str = r#"
{"author":{"name":"Approver","username":"approver","email":"approver@approvers.com"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"-1"}],"comment":"Patch Set 1: Code-Review+2\n\nJust a buggy script. FAILURE\n\nAnd more problems. FAILURE","patchSet":{"number":1,"revision":"49a65998c02eda928559f2d0b586c20bc8e37b10","parents":["fb1909b4eda306985d2bbce769310e5a50a98cf5"],"ref":"refs/changes/42/42/1","uploader":{"name":"Author","email":"author@example.com","username":"Author"},"createdOn":1494165142,"author":{"name":"Author","email":"author@example.com","username":"Author"},"isDraft":false,"kind":"REWORK","sizeInsertions":0,"sizeDeletions":0},"change":{"project":"demo-project","branch":"master","id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14","number":49,"subject":"Some review.","owner":{"name":"Author","email":"author@example.com","username":"author"},"url":"http://localhost/42","commitMessage":"Some review.\n\nChange-Id: Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14\n","status":"NEW"},"project":"demo-project","refName":"refs/heads/master","changeKey":{"id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14"},"type":"comment-added","eventCreatedOn":1499190282}"#;

    const CHANGE_JSON_WITH_COMMENTS : &'static str = r#"
{"project":"gerritbot-rs","branch":"master","id":"If70442f674c595a59f3e44280570e760ba3584c4","number":1,"subject":"Bump version to 0.6.0","owner":{"name":"Administrator","email":"admin@example.com","username":"admin"},"url":"http://localhost:8080/1","commitMessage":"Bump version to 0.6.0\n\nChange-Id: If70442f674c595a59f3e44280570e760ba3584c4\n","createdOn":1524584729,"lastUpdated":1524584975,"open":true,"status":"NEW","comments":[{"timestamp":1524584729,"reviewer":{"name":"Administrator","email":"admin@example.com","username":"admin"},"message":"Uploaded patch set 1."},{"timestamp":1524584975,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"Patch Set 1:\n\n(1 comment)"}]}"#;

    const PATCHSET_JSON_WITH_COMMENTS : &'static str = r#"{"number":1,"revision":"3f58af760fc1e39fcc4a85b8ab6a6be032cf2ae2","parents":["578bc1e684098d2ac597e030442c3472f15ac3ad"],"ref":"refs/changes/01/1/1","uploader":{"name":"Administrator","email":"admin@example.com","username":"admin"},"createdOn":1524584729,"author":{"name":"jdoe","email":"jdoe@example.com","username":""},"isDraft":false,"kind":"REWORK","comments":[{"file":"/COMMIT_MSG","line":1,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"This is a multiline\ncomment\non some change."}],"sizeInsertions":2,"sizeDeletions":-2}"#;

    fn get_event() -> gerrit::CommentAddedEvent {
        let event: Result<gerrit::Event, _> = serde_json::from_str(EVENT_JSON);
        match event.expect("failed to decode event") {
            gerrit::Event::CommentAdded(event) => event,
            event => panic!("wrong type of event: {:?}", event),
        }
    }

    fn get_change_with_comments() -> (gerrit::Change, gerrit::Patchset) {
        let change: Result<gerrit::Change, _> = serde_json::from_str(CHANGE_JSON_WITH_COMMENTS);
        assert!(change.is_ok());
        let patchset: Result<gerrit::Patchset, _> =
            serde_json::from_str(PATCHSET_JSON_WITH_COMMENTS);
        assert!(patchset.is_ok());
        (change.unwrap(), patchset.unwrap())
    }

    #[test]
    fn test_format_approval() {
        let event = get_event();
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_unknown_labels() {
        let mut event = get_event();
        event.approvals[0].approval_type = String::from("Some-New-Type");
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Some-New-Type) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_multiple_labels() {
        let mut event = get_event();
        event.approvals.push(gerrit::Approval {
            approval_type: "Verified".to_string(),
            description: "Verified".to_string(),
            value: "1".to_string(),
            old_value: None,
        });
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review), ✔ +1 (Verified) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_no_approvals() {
        let mut event = get_event();
        event.approvals.clear();
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(res, Ok(None));
    }

    #[test]
    fn test_format_comments() {
        let mut event = get_event();
        let (change, patchset) = get_change_with_comments();
        event.comment = "(1 comment)".to_string();
        event.change = change;
        event.patchset = patchset;

        let res = Formatter::default()
            .format_comment_added(&event, true)
            .expect("format failed")
            .expect("no comments");

        assert!(res.ends_with("`/COMMIT_MSG`\n\n> [Line 1](http://localhost:8080/#/c/1/1//COMMIT_MSG@1) by [jdoe](http://localhost:8080/q/reviewer:john.doe@localhost+status:open): This is a multiline\n> comment\n> on some change.\n"), "no inline comments: {:?}", res);
    }
}
<commit_msg>unify lua call logic<commit_after>use std::convert::identity;

use rlua::{
    FromLua, Function as LuaFunction, Lua, StdLib as LuaStdLib, ToLuaMulti, Value as LuaValue,
};
use serde::Serialize;
use serde_json::Value as JsonValue;

use gerritbot_gerrit as gerrit;

const DEFAULT_FORMAT_SCRIPT: &str = include_str!("../../scripts/format.lua");
const LUA_FORMAT_COMMENT_ADDED: &str = "format_comment_added";
const LUA_FORMAT_REVIEWER_ADDED: &str = "format_reviewer_added";
const LUA_FORMAT_FUNCTIONS: &[&str] = &[LUA_FORMAT_COMMENT_ADDED, LUA_FORMAT_REVIEWER_ADDED];

pub struct Formatter {
    lua: Lua,
}

impl Default for Formatter {
    fn default() -> Self {
        Self {
            lua: load_format_script(DEFAULT_FORMAT_SCRIPT).unwrap(),
        }
    }
}

fn load_format_script(script_source: &str) -> Result<Lua, String> {
    let lua_std_lib = LuaStdLib::BASE | LuaStdLib::STRING | LuaStdLib::TABLE;
    let lua = Lua::new_with(lua_std_lib);
    lua.context(|context| -> Result<(), String> {
        let globals = context.globals();
        context
            .load(script_source)
            .exec()
            .map_err(|err| format!("syntax error: {}", err))?;

        // check that the required functions are present
        for function_name in LUA_FORMAT_FUNCTIONS {
            let _: LuaFunction = globals
                .get(*function_name)
                .map_err(|_| format!("{} function missing", function_name))?;
        }

        Ok(())
    })?;
    Ok(lua)
}

fn json_to_lua<'lua>(json: &JsonValue, lua: rlua::Context<'lua>) -> rlua::Result<LuaValue<'lua>> {
    Ok(match json {
        JsonValue::Null => LuaValue::Nil,
        JsonValue::Bool(b) => LuaValue::Boolean(*b),
        JsonValue::Number(n) => {
            if let Some(n) = n.as_i64() {
                LuaValue::Integer(n)
            } else if let Some(n) = n.as_f64() {
                LuaValue::Number(n)
            } else {
                Err(rlua::Error::ToLuaConversionError {
                    from: "serde_json::Number",
                    to: "Integer",
                    message: Some(format!("value {} too large", n)),
                })?
            }
        }
        JsonValue::String(s) => lua.create_string(s).map(LuaValue::String)?,
        JsonValue::Array(values) => {
            let table = lua.create_table()?;

            for (i, value) in values.iter().enumerate() {
                table.set(LuaValue::Integer(i as i64 + 1), json_to_lua(value, lua)?)?;
            }

            LuaValue::Table(table)
        }
        JsonValue::Object(items) => {
            let table = lua.create_table()?;

            for (key, value) in items {
                let key = lua.create_string(key)?;
                let value = json_to_lua(value, lua)?;
                table.set(key, value)?;
            }

            LuaValue::Table(table)
        }
    })
}

fn to_lua_via_json<'lua, T: Serialize>(
    value: T,
    lua: rlua::Context<'lua>,
) -> Result<LuaValue<'lua>, Box<dyn std::error::Error>> {
    let json_value = serde_json::to_value(value)?;
    let lua_value = json_to_lua(&json_value, lua)?;
    Ok(lua_value)
}

impl Formatter {
    fn format_lua<'lua, T, E, F, A>(
        lua: rlua::Context<'lua>,
        function_name: &str,
        event: E,
        prepare_args: F,
    ) -> Result<T, String>
    where
        T: FromLua<'lua>,
        E: Serialize,
        F: FnOnce(LuaValue<'lua>) -> A,
        A: ToLuaMulti<'lua>,
    {
        let globals = lua.globals();

        let format_function: LuaFunction = globals
            .get(function_name)
            .map_err(|_| format!("{} function missing", function_name))?;
        let event =
            to_lua_via_json(event, lua).map_err(|e| format!("failed to serialize event: {}", e))?;
        let args = prepare_args(event);
        let result = format_function
            .call::<_, LuaValue>(args)
            .map_err(|err| format!("lua formatting function failed: {}", err))?;

        T::from_lua(result, lua).map_err(|e| format!("failed to convert formatting result: {}", e))
    }

    pub fn format_comment_added(
        &self,
        event: &gerrit::CommentAddedEvent,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        self.lua.context(|context| {
            Formatter::format_lua(context, LUA_FORMAT_COMMENT_ADDED, event, |event| {
                (event, is_human)
            })
        })
    }

    pub fn format_reviewer_added(
        &self,
        event: &gerrit::ReviewerAddedEvent,
    ) -> Result<String, String> {
        self.lua.context(|context| {
            Formatter::format_lua(context, LUA_FORMAT_REVIEWER_ADDED, event, identity)
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const EVENT_JSON : &'static str = r#"
{"author":{"name":"Approver","username":"approver","email":"approver@approvers.com"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"-1"}],"comment":"Patch Set 1: Code-Review+2\n\nJust a buggy script. FAILURE\n\nAnd more problems. FAILURE","patchSet":{"number":1,"revision":"49a65998c02eda928559f2d0b586c20bc8e37b10","parents":["fb1909b4eda306985d2bbce769310e5a50a98cf5"],"ref":"refs/changes/42/42/1","uploader":{"name":"Author","email":"author@example.com","username":"Author"},"createdOn":1494165142,"author":{"name":"Author","email":"author@example.com","username":"Author"},"isDraft":false,"kind":"REWORK","sizeInsertions":0,"sizeDeletions":0},"change":{"project":"demo-project","branch":"master","id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14","number":49,"subject":"Some review.","owner":{"name":"Author","email":"author@example.com","username":"author"},"url":"http://localhost/42","commitMessage":"Some review.\n\nChange-Id: Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14\n","status":"NEW"},"project":"demo-project","refName":"refs/heads/master","changeKey":{"id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14"},"type":"comment-added","eventCreatedOn":1499190282}"#;

    const CHANGE_JSON_WITH_COMMENTS : &'static str = r#"
{"project":"gerritbot-rs","branch":"master","id":"If70442f674c595a59f3e44280570e760ba3584c4","number":1,"subject":"Bump version to 0.6.0","owner":{"name":"Administrator","email":"admin@example.com","username":"admin"},"url":"http://localhost:8080/1","commitMessage":"Bump version to 0.6.0\n\nChange-Id: If70442f674c595a59f3e44280570e760ba3584c4\n","createdOn":1524584729,"lastUpdated":1524584975,"open":true,"status":"NEW","comments":[{"timestamp":1524584729,"reviewer":{"name":"Administrator","email":"admin@example.com","username":"admin"},"message":"Uploaded patch set 1."},{"timestamp":1524584975,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"Patch Set 1:\n\n(1 comment)"}]}"#;

    const PATCHSET_JSON_WITH_COMMENTS : &'static str = r#"{"number":1,"revision":"3f58af760fc1e39fcc4a85b8ab6a6be032cf2ae2","parents":["578bc1e684098d2ac597e030442c3472f15ac3ad"],"ref":"refs/changes/01/1/1","uploader":{"name":"Administrator","email":"admin@example.com","username":"admin"},"createdOn":1524584729,"author":{"name":"jdoe","email":"jdoe@example.com","username":""},"isDraft":false,"kind":"REWORK","comments":[{"file":"/COMMIT_MSG","line":1,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"This is a multiline\ncomment\non some change."}],"sizeInsertions":2,"sizeDeletions":-2}"#;

    fn get_event() -> gerrit::CommentAddedEvent {
        let event: Result<gerrit::Event, _> = serde_json::from_str(EVENT_JSON);
        match event.expect("failed to decode event") {
            gerrit::Event::CommentAdded(event) => event,
            event => panic!("wrong type of event: {:?}", event),
        }
    }

    fn get_change_with_comments() -> (gerrit::Change, gerrit::Patchset) {
        let change: Result<gerrit::Change, _> = serde_json::from_str(CHANGE_JSON_WITH_COMMENTS);
        assert!(change.is_ok());
        let patchset: Result<gerrit::Patchset, _> =
            serde_json::from_str(PATCHSET_JSON_WITH_COMMENTS);
        assert!(patchset.is_ok());
        (change.unwrap(), patchset.unwrap())
    }

    #[test]
    fn test_format_approval() {
        let event = get_event();
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_unknown_labels() {
        let mut event = get_event();
        event.approvals[0].approval_type = String::from("Some-New-Type");
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Some-New-Type) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_multiple_labels() {
        let mut event = get_event();
        event.approvals.push(gerrit::Approval {
            approval_type: "Verified".to_string(),
            description: "Verified".to_string(),
            value: "1".to_string(),
            old_value: None,
        });
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review), ✔ +1 (Verified) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE\n\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_no_approvals() {
        let mut event = get_event();
        event.approvals.clear();
        let res = Formatter::default().format_comment_added(&event, true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(res, Ok(None));
    }

    #[test]
    fn test_format_comments() {
        let mut event = get_event();
        let (change, patchset) = get_change_with_comments();
        event.comment = "(1 comment)".to_string();
        event.change = change;
        event.patchset = patchset;

        let res = Formatter::default()
            .format_comment_added(&event, true)
            .expect("format failed")
            .expect("no comments");

        assert!(res.ends_with("`/COMMIT_MSG`\n\n> [Line 1](http://localhost:8080/#/c/1/1//COMMIT_MSG@1) by [jdoe](http://localhost:8080/q/reviewer:john.doe@localhost+status:open): This is a multiline\n> comment\n> on some change.\n"), "no inline comments: {:?}", res);
    }
}
<|endoftext|>


<commit_before>use itertools::Itertools as _;

use rlua::{FromLua as _, Function as LuaFunction, Lua, Value as LuaValue};
use serde::Serialize;
use serde_json::Value as JsonValue;

use gerritbot_gerrit as gerrit;

const UNKNOWN_USER: &str = "<unknown user>";
const DEFAULT_FORMAT_SCRIPT: &str = include_str!("../../scripts/format.lua");

pub struct Formatter {
    lua: Lua,
}

impl Default for Formatter {
    fn default() -> Self {
        Self {
            lua: load_format_script(DEFAULT_FORMAT_SCRIPT).unwrap(),
        }
    }
}

fn load_format_script(script_source: &str) -> Result<Lua, String> {
    let lua = Lua::new();
    lua.context(|context| -> Result<(), String> {
        let globals = context.globals();
        context
            .load(script_source)
            .exec()
            .map_err(|err| format!("syntax error: {}", err))?;
        let _: LuaFunction = globals
            .get("format_approval")
            .map_err(|_| "format_approval function missing")?;
        Ok(())
    })?;
    Ok(lua)
}

fn json_to_lua<'lua>(json: &JsonValue, lua: rlua::Context<'lua>) -> rlua::Result<LuaValue<'lua>> {
    Ok(match json {
        JsonValue::Null => LuaValue::Nil,
        JsonValue::Bool(b) => LuaValue::Boolean(*b),
        JsonValue::Number(n) => {
            if let Some(n) = n.as_i64() {
                LuaValue::Integer(n)
            } else if let Some(n) = n.as_f64() {
                LuaValue::Number(n)
            } else {
                Err(rlua::Error::ToLuaConversionError {
                    from: "serde_json::Number",
                    to: "Integer",
                    message: Some(format!("value {} too large", n)),
                })?
            }
        }
        JsonValue::String(s) => lua.create_string(s).map(LuaValue::String)?,
        JsonValue::Array(values) => {
            let table = lua.create_table()?;

            for (i, value) in values.iter().enumerate() {
                table.set(LuaValue::Integer(i as i64 + 1), json_to_lua(value, lua)?)?;
            }

            LuaValue::Table(table)
        }
        JsonValue::Object(items) => {
            let table = lua.create_table()?;

            for (key, value) in items {
                let key = lua.create_string(key)?;
                let value = json_to_lua(value, lua)?;
                table.set(key, value)?;
            }

            LuaValue::Table(table)
        }
    })
}

fn to_lua_via_json<'lua, T: Serialize>(
    value: T,
    lua: rlua::Context<'lua>,
) -> Result<LuaValue<'lua>, Box<dyn std::error::Error>> {
    let json_value = serde_json::to_value(value)?;
    let lua_value = json_to_lua(&json_value, lua)?;
    Ok(lua_value)
}

impl Formatter {
    pub fn format_approval(
        &self,
        event: &gerrit::CommentAddedEvent,
        approval: &gerrit::Approval,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        self.lua
            .context(|context| -> Result<Option<String>, String> {
                let globals = context.globals();

                let lua_format_approval: LuaFunction = globals
                    .get("format_approval")
                    .map_err(|_| "format_approval function missing".to_string())?;
                let lua_event = to_lua_via_json(event, context)
                    .map_err(|e| format!("failed to serialize event: {}", e))?;
                let lua_approval = to_lua_via_json(approval, context)
                    .map_err(|e| format!("failed to serialize approval: {}", e))?;
                let lua_result = lua_format_approval
                    .call::<_, LuaValue>((lua_event, lua_approval, is_human))
                    .map_err(|err| format!("lua formatting function failed: {}", err))?;

                match lua_result {
                    LuaValue::Nil => Ok(None),
                    _ => Ok(Some(String::from_lua(lua_result, context).map_err(
                        |e| format!("failed to convert formatting result to string: {}", e),
                    )?)),
                }
            })
    }

    pub fn format_comment_added(
        &self,
        event: &gerrit::CommentAddedEvent,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        let messages: Vec<String> = event
            .approvals
            .iter()
            .filter(|approval| {
                approval.old_value.as_ref() != Some(&approval.value) && approval.value != "0"
            })
            .filter_map(|approval| self.format_approval(event, approval, is_human).transpose())
            .collect::<Result<_, _>>()?;

        let inline_comments = self.format_inline_comments(&event.change, &event.patchset);

        let message = if !messages.is_empty() {
            // We got some approvals
            messages.join("\n\n")
        } else if is_human && inline_comments.is_some() {
            // We did not get any approvals, but we got inline comments from a human.
            event.comment.clone()
        } else {
            return Ok(None);
        };

        Ok(Some(
            inline_comments
                .map(|c| format!("{}\n\n{}", message, c))
                .unwrap_or(message),
        ))
    }

    pub fn format_reviewer_added(
        &self,
        event: &gerrit::ReviewerAddedEvent,
    ) -> Result<String, String> {
        Ok(format!(
            "[{}]({}) ({}) 👓 Added as reviewer",
            event.change.subject,
            event.change.url,
            event
                .change
                .owner
                .username
                .as_ref()
                .map(String::as_str)
                .unwrap_or(UNKNOWN_USER)
        ))
    }

    fn format_inline_comments(
        &self,
        change: &gerrit::Change,
        patchset: &gerrit::Patchset,
    ) -> Option<String> {
        let change_number = change.number;
        let base_url = {
            let last_slash = change.url.rfind('/').unwrap();
            &change.url[..last_slash]
        };
        let patch_set_number = patchset.number;

        patchset
            .comments
            .as_ref()
            .cloned()
            .map(|mut comments| {
                comments.sort_by(|a, b| a.file.cmp(&b.file));
                comments
                    .into_iter()
                    .group_by(|c| c.file.clone())
                    .into_iter()
                    .map(|(file, comments)| -> String {
                        let line_comments = comments
                            .map(|comment| {
                                let mut lines = comment.message.split('\n');
                                let url = format!(
                                    "{}/#/c/{}/{}/{}@{}",
                                    base_url,
                                    change_number,
                                    patch_set_number,
                                    comment.file,
                                    comment.line
                                );
                                let first_line = lines.next().unwrap_or("");
                                let first_line = format!(
                                    "> [Line {}]({}) by {}: {}",
                                    comment.line,
                                    url,
                                    comment
                                        .reviewer
                                        .username
                                        .as_ref()
                                        .map(String::as_str)
                                        .unwrap_or(UNKNOWN_USER),
                                    first_line
                                );
                                let tail = lines
                                    .map(|l| format!("> {}", l))
                                    .intersperse("\n".into())
                                    .collect::<Vec<_>>()
                                    .concat();
                                format!("{}\n{}", first_line, tail)
                            })
                            .intersperse("\n".into())
                            .collect::<Vec<_>>()
                            .concat();
                        format!("`{}`\n\n{}", file, line_comments)
                    })
                    .intersperse("\n\n".into())
                    .collect::<Vec<_>>()
                    .concat()
            })
            .filter(|s| !s.is_empty())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const EVENT_JSON : &'static str = r#"
{"author":{"name":"Approver","username":"approver","email":"approver@approvers.com"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"-1"}],"comment":"Patch Set 1: Code-Review+2\n\nJust a buggy script. FAILURE\n\nAnd more problems. FAILURE","patchSet":{"number":1,"revision":"49a65998c02eda928559f2d0b586c20bc8e37b10","parents":["fb1909b4eda306985d2bbce769310e5a50a98cf5"],"ref":"refs/changes/42/42/1","uploader":{"name":"Author","email":"author@example.com","username":"Author"},"createdOn":1494165142,"author":{"name":"Author","email":"author@example.com","username":"Author"},"isDraft":false,"kind":"REWORK","sizeInsertions":0,"sizeDeletions":0},"change":{"project":"demo-project","branch":"master","id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14","number":49,"subject":"Some review.","owner":{"name":"Author","email":"author@example.com","username":"author"},"url":"http://localhost/42","commitMessage":"Some review.\n\nChange-Id: Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14\n","status":"NEW"},"project":"demo-project","refName":"refs/heads/master","changeKey":{"id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14"},"type":"comment-added","eventCreatedOn":1499190282}"#;

    const CHANGE_JSON_WITH_COMMENTS : &'static str = r#"
{"project":"gerritbot-rs","branch":"master","id":"If70442f674c595a59f3e44280570e760ba3584c4","number":1,"subject":"Bump version to 0.6.0","owner":{"name":"Administrator","email":"admin@example.com","username":"admin"},"url":"http://localhost:8080/1","commitMessage":"Bump version to 0.6.0\n\nChange-Id: If70442f674c595a59f3e44280570e760ba3584c4\n","createdOn":1524584729,"lastUpdated":1524584975,"open":true,"status":"NEW","comments":[{"timestamp":1524584729,"reviewer":{"name":"Administrator","email":"admin@example.com","username":"admin"},"message":"Uploaded patch set 1."},{"timestamp":1524584975,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"Patch Set 1:\n\n(1 comment)"}]}"#;

    const PATCHSET_JSON_WITH_COMMENTS : &'static str = r#"{"number":1,"revision":"3f58af760fc1e39fcc4a85b8ab6a6be032cf2ae2","parents":["578bc1e684098d2ac597e030442c3472f15ac3ad"],"ref":"refs/changes/01/1/1","uploader":{"name":"Administrator","email":"admin@example.com","username":"admin"},"createdOn":1524584729,"author":{"name":"jdoe","email":"jdoe@example.com","username":""},"isDraft":false,"kind":"REWORK","comments":[{"file":"/COMMIT_MSG","line":1,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"This is a multiline\ncomment\non some change."}],"sizeInsertions":2,"sizeDeletions":-2}"#;

    fn get_event() -> gerrit::CommentAddedEvent {
        let event: Result<gerrit::Event, _> = serde_json::from_str(EVENT_JSON);
        match event.expect("failed to decode event") {
            gerrit::Event::CommentAdded(event) => event,
            event => panic!("wrong type of event: {:?}", event),
        }
    }

    fn get_change_with_comments() -> (gerrit::Change, gerrit::Patchset) {
        let change: Result<gerrit::Change, _> = serde_json::from_str(CHANGE_JSON_WITH_COMMENTS);
        assert!(change.is_ok());
        let patchset: Result<gerrit::Patchset, _> =
            serde_json::from_str(PATCHSET_JSON_WITH_COMMENTS);
        assert!(patchset.is_ok());
        (change.unwrap(), patchset.unwrap())
    }

    #[test]
    fn test_format_approval() {
        let event = get_event();
        let res = Formatter::default().format_approval(&event, &event.approvals[0], true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE<br>\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_filters_specific_messages() {
        let mut event = get_event();
        event.approvals[0].approval_type = String::from("Some new type");
        let res = Formatter::default().format_approval(&event, &event.approvals[0], true);
        assert_eq!(res, Ok(None));
    }

    #[test]
    fn test_format_comments() {
        let (change, mut patchset) = get_change_with_comments();
        patchset.comments = None;
        assert_eq!(
            Formatter::default().format_inline_comments(&change, &patchset),
            None
        );

        let (change, patchset) = get_change_with_comments();
        assert_eq!(Formatter::default().format_inline_comments(&change, &patchset),
                   Some("`/COMMIT_MSG`\n\n> [Line 1](http://localhost:8080/#/c/1/1//COMMIT_MSG@1) by jdoe: This is a multiline\n> comment\n> on some change.".into()));
    }
}
<commit_msg>only expose minimal set of modules to format script<commit_after>use itertools::Itertools as _;

use rlua::{FromLua as _, Function as LuaFunction, Lua, StdLib as LuaStdLib, Value as LuaValue};
use serde::Serialize;
use serde_json::Value as JsonValue;

use gerritbot_gerrit as gerrit;

const UNKNOWN_USER: &str = "<unknown user>";
const DEFAULT_FORMAT_SCRIPT: &str = include_str!("../../scripts/format.lua");

pub struct Formatter {
    lua: Lua,
}

impl Default for Formatter {
    fn default() -> Self {
        Self {
            lua: load_format_script(DEFAULT_FORMAT_SCRIPT).unwrap(),
        }
    }
}

fn load_format_script(script_source: &str) -> Result<Lua, String> {
    let lua_std_lib = LuaStdLib::BASE | LuaStdLib::STRING | LuaStdLib::TABLE;
    let lua = Lua::new_with(lua_std_lib);
    lua.context(|context| -> Result<(), String> {
        let globals = context.globals();
        context
            .load(script_source)
            .exec()
            .map_err(|err| format!("syntax error: {}", err))?;
        let _: LuaFunction = globals
            .get("format_approval")
            .map_err(|_| "format_approval function missing")?;
        Ok(())
    })?;
    Ok(lua)
}

fn json_to_lua<'lua>(json: &JsonValue, lua: rlua::Context<'lua>) -> rlua::Result<LuaValue<'lua>> {
    Ok(match json {
        JsonValue::Null => LuaValue::Nil,
        JsonValue::Bool(b) => LuaValue::Boolean(*b),
        JsonValue::Number(n) => {
            if let Some(n) = n.as_i64() {
                LuaValue::Integer(n)
            } else if let Some(n) = n.as_f64() {
                LuaValue::Number(n)
            } else {
                Err(rlua::Error::ToLuaConversionError {
                    from: "serde_json::Number",
                    to: "Integer",
                    message: Some(format!("value {} too large", n)),
                })?
            }
        }
        JsonValue::String(s) => lua.create_string(s).map(LuaValue::String)?,
        JsonValue::Array(values) => {
            let table = lua.create_table()?;

            for (i, value) in values.iter().enumerate() {
                table.set(LuaValue::Integer(i as i64 + 1), json_to_lua(value, lua)?)?;
            }

            LuaValue::Table(table)
        }
        JsonValue::Object(items) => {
            let table = lua.create_table()?;

            for (key, value) in items {
                let key = lua.create_string(key)?;
                let value = json_to_lua(value, lua)?;
                table.set(key, value)?;
            }

            LuaValue::Table(table)
        }
    })
}

fn to_lua_via_json<'lua, T: Serialize>(
    value: T,
    lua: rlua::Context<'lua>,
) -> Result<LuaValue<'lua>, Box<dyn std::error::Error>> {
    let json_value = serde_json::to_value(value)?;
    let lua_value = json_to_lua(&json_value, lua)?;
    Ok(lua_value)
}

impl Formatter {
    pub fn format_approval(
        &self,
        event: &gerrit::CommentAddedEvent,
        approval: &gerrit::Approval,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        self.lua
            .context(|context| -> Result<Option<String>, String> {
                let globals = context.globals();

                let lua_format_approval: LuaFunction = globals
                    .get("format_approval")
                    .map_err(|_| "format_approval function missing".to_string())?;
                let lua_event = to_lua_via_json(event, context)
                    .map_err(|e| format!("failed to serialize event: {}", e))?;
                let lua_approval = to_lua_via_json(approval, context)
                    .map_err(|e| format!("failed to serialize approval: {}", e))?;
                let lua_result = lua_format_approval
                    .call::<_, LuaValue>((lua_event, lua_approval, is_human))
                    .map_err(|err| format!("lua formatting function failed: {}", err))?;

                match lua_result {
                    LuaValue::Nil => Ok(None),
                    _ => Ok(Some(String::from_lua(lua_result, context).map_err(
                        |e| format!("failed to convert formatting result to string: {}", e),
                    )?)),
                }
            })
    }

    pub fn format_comment_added(
        &self,
        event: &gerrit::CommentAddedEvent,
        is_human: bool,
    ) -> Result<Option<String>, String> {
        let messages: Vec<String> = event
            .approvals
            .iter()
            .filter(|approval| {
                approval.old_value.as_ref() != Some(&approval.value) && approval.value != "0"
            })
            .filter_map(|approval| self.format_approval(event, approval, is_human).transpose())
            .collect::<Result<_, _>>()?;

        let inline_comments = self.format_inline_comments(&event.change, &event.patchset);

        let message = if !messages.is_empty() {
            // We got some approvals
            messages.join("\n\n")
        } else if is_human && inline_comments.is_some() {
            // We did not get any approvals, but we got inline comments from a human.
            event.comment.clone()
        } else {
            return Ok(None);
        };

        Ok(Some(
            inline_comments
                .map(|c| format!("{}\n\n{}", message, c))
                .unwrap_or(message),
        ))
    }

    pub fn format_reviewer_added(
        &self,
        event: &gerrit::ReviewerAddedEvent,
    ) -> Result<String, String> {
        Ok(format!(
            "[{}]({}) ({}) 👓 Added as reviewer",
            event.change.subject,
            event.change.url,
            event
                .change
                .owner
                .username
                .as_ref()
                .map(String::as_str)
                .unwrap_or(UNKNOWN_USER)
        ))
    }

    fn format_inline_comments(
        &self,
        change: &gerrit::Change,
        patchset: &gerrit::Patchset,
    ) -> Option<String> {
        let change_number = change.number;
        let base_url = {
            let last_slash = change.url.rfind('/').unwrap();
            &change.url[..last_slash]
        };
        let patch_set_number = patchset.number;

        patchset
            .comments
            .as_ref()
            .cloned()
            .map(|mut comments| {
                comments.sort_by(|a, b| a.file.cmp(&b.file));
                comments
                    .into_iter()
                    .group_by(|c| c.file.clone())
                    .into_iter()
                    .map(|(file, comments)| -> String {
                        let line_comments = comments
                            .map(|comment| {
                                let mut lines = comment.message.split('\n');
                                let url = format!(
                                    "{}/#/c/{}/{}/{}@{}",
                                    base_url,
                                    change_number,
                                    patch_set_number,
                                    comment.file,
                                    comment.line
                                );
                                let first_line = lines.next().unwrap_or("");
                                let first_line = format!(
                                    "> [Line {}]({}) by {}: {}",
                                    comment.line,
                                    url,
                                    comment
                                        .reviewer
                                        .username
                                        .as_ref()
                                        .map(String::as_str)
                                        .unwrap_or(UNKNOWN_USER),
                                    first_line
                                );
                                let tail = lines
                                    .map(|l| format!("> {}", l))
                                    .intersperse("\n".into())
                                    .collect::<Vec<_>>()
                                    .concat();
                                format!("{}\n{}", first_line, tail)
                            })
                            .intersperse("\n".into())
                            .collect::<Vec<_>>()
                            .concat();
                        format!("`{}`\n\n{}", file, line_comments)
                    })
                    .intersperse("\n\n".into())
                    .collect::<Vec<_>>()
                    .concat()
            })
            .filter(|s| !s.is_empty())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const EVENT_JSON : &'static str = r#"
{"author":{"name":"Approver","username":"approver","email":"approver@approvers.com"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"-1"}],"comment":"Patch Set 1: Code-Review+2\n\nJust a buggy script. FAILURE\n\nAnd more problems. FAILURE","patchSet":{"number":1,"revision":"49a65998c02eda928559f2d0b586c20bc8e37b10","parents":["fb1909b4eda306985d2bbce769310e5a50a98cf5"],"ref":"refs/changes/42/42/1","uploader":{"name":"Author","email":"author@example.com","username":"Author"},"createdOn":1494165142,"author":{"name":"Author","email":"author@example.com","username":"Author"},"isDraft":false,"kind":"REWORK","sizeInsertions":0,"sizeDeletions":0},"change":{"project":"demo-project","branch":"master","id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14","number":49,"subject":"Some review.","owner":{"name":"Author","email":"author@example.com","username":"author"},"url":"http://localhost/42","commitMessage":"Some review.\n\nChange-Id: Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14\n","status":"NEW"},"project":"demo-project","refName":"refs/heads/master","changeKey":{"id":"Ic160fa37fca005fec17a2434aadf0d9dcfbb7b14"},"type":"comment-added","eventCreatedOn":1499190282}"#;

    const CHANGE_JSON_WITH_COMMENTS : &'static str = r#"
{"project":"gerritbot-rs","branch":"master","id":"If70442f674c595a59f3e44280570e760ba3584c4","number":1,"subject":"Bump version to 0.6.0","owner":{"name":"Administrator","email":"admin@example.com","username":"admin"},"url":"http://localhost:8080/1","commitMessage":"Bump version to 0.6.0\n\nChange-Id: If70442f674c595a59f3e44280570e760ba3584c4\n","createdOn":1524584729,"lastUpdated":1524584975,"open":true,"status":"NEW","comments":[{"timestamp":1524584729,"reviewer":{"name":"Administrator","email":"admin@example.com","username":"admin"},"message":"Uploaded patch set 1."},{"timestamp":1524584975,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"Patch Set 1:\n\n(1 comment)"}]}"#;

    const PATCHSET_JSON_WITH_COMMENTS : &'static str = r#"{"number":1,"revision":"3f58af760fc1e39fcc4a85b8ab6a6be032cf2ae2","parents":["578bc1e684098d2ac597e030442c3472f15ac3ad"],"ref":"refs/changes/01/1/1","uploader":{"name":"Administrator","email":"admin@example.com","username":"admin"},"createdOn":1524584729,"author":{"name":"jdoe","email":"jdoe@example.com","username":""},"isDraft":false,"kind":"REWORK","comments":[{"file":"/COMMIT_MSG","line":1,"reviewer":{"name":"jdoe","email":"john.doe@localhost","username":"jdoe"},"message":"This is a multiline\ncomment\non some change."}],"sizeInsertions":2,"sizeDeletions":-2}"#;

    fn get_event() -> gerrit::CommentAddedEvent {
        let event: Result<gerrit::Event, _> = serde_json::from_str(EVENT_JSON);
        match event.expect("failed to decode event") {
            gerrit::Event::CommentAdded(event) => event,
            event => panic!("wrong type of event: {:?}", event),
        }
    }

    fn get_change_with_comments() -> (gerrit::Change, gerrit::Patchset) {
        let change: Result<gerrit::Change, _> = serde_json::from_str(CHANGE_JSON_WITH_COMMENTS);
        assert!(change.is_ok());
        let patchset: Result<gerrit::Patchset, _> =
            serde_json::from_str(PATCHSET_JSON_WITH_COMMENTS);
        assert!(patchset.is_ok());
        (change.unwrap(), patchset.unwrap())
    }

    #[test]
    fn test_format_approval() {
        let event = get_event();
        let res = Formatter::default().format_approval(&event, &event.approvals[0], true);
        // Result<Option<String>, _> -> Result<Option<&str>, _>
        let res = res.as_ref().map(|o| o.as_ref().map(String::as_str));
        assert_eq!(
            res,
            Ok(Some("[Some review.](http://localhost/42) ([demo-project](http://localhost/q/project:demo-project+status:open)) 👍 +2 (Code-Review) from [Approver](http://localhost/q/reviewer:approver@approvers.com+status:open)\n\n> Just a buggy script. FAILURE<br>\n> And more problems. FAILURE"))
        );
    }

    #[test]
    fn format_approval_filters_specific_messages() {
        let mut event = get_event();
        event.approvals[0].approval_type = String::from("Some new type");
        let res = Formatter::default().format_approval(&event, &event.approvals[0], true);
        assert_eq!(res, Ok(None));
    }

    #[test]
    fn test_format_comments() {
        let (change, mut patchset) = get_change_with_comments();
        patchset.comments = None;
        assert_eq!(
            Formatter::default().format_inline_comments(&change, &patchset),
            None
        );

        let (change, patchset) = get_change_with_comments();
        assert_eq!(Formatter::default().format_inline_comments(&change, &patchset),
                   Some("`/COMMIT_MSG`\n\n> [Line 1](http://localhost:8080/#/c/1/1//COMMIT_MSG@1) by jdoe: This is a multiline\n> comment\n> on some change.".into()));
    }
}
<|endoftext|>


<commit_before>extern crate ncurses;

use std::option::Option;
use self::ncurses::*;
use cpu::ExecState;
use instruction;

struct CpuWin {
    win:        WINDOW,
    winner:     WINDOW,
    wsidebar:   WINDOW,
}

const SIDEBAR_CELL_HEIGHT: i32 = 2;
const SIDEBAR_WIDTH: i32 = 6;
const CPUWIN_HEIGHT: i32 = (SIDEBAR_CELL_HEIGHT + 1) * 4 + 1;
const CPUWIN_WIDTH: i32 = CPUWIN_HEIGHT*2 + SIDEBAR_WIDTH;
const SIDEBAR_X: i32 = CPUWIN_WIDTH - 1 - SIDEBAR_WIDTH - 1;

impl CpuWin {
    fn new(posx: i32, posy: i32) -> CpuWin {

        let win = newwin(CPUWIN_HEIGHT, CPUWIN_WIDTH, posy, posx);
        let winner = derwin(win, CPUWIN_HEIGHT-2, CPUWIN_WIDTH-2, 1, 1);
        let wsidebar = derwin(winner, CPUWIN_HEIGHT-2, SIDEBAR_WIDTH, 0, SIDEBAR_X);

        /* Border */
        box_(win, 0, 0);

        /* Left size of sidebar */
        mvwaddch(win, 0, SIDEBAR_X, ACS_TTEE());
        mvwvline(win, 1, SIDEBAR_X, ACS_VLINE(), CPUWIN_HEIGHT - 2);
        mvwaddch(win, CPUWIN_HEIGHT-1, SIDEBAR_X, ACS_BTEE());

        let mut cpuwin = CpuWin {
            win:        win,
            winner:     winner,
            wsidebar:   wsidebar,
        };
        cpuwin.cell_label(0, "ACC");
        cpuwin.cell_label(1, "BAK");
        cpuwin.cell_label(2, "LAST");
        cpuwin.cell_label(3, "MODE");

        cpuwin.cell_divider(1);
        cpuwin.cell_divider(2);
        cpuwin.cell_divider(3);

        cpuwin.set_values(0, 10, None, ExecState::EXEC);

        cpuwin
    }

    fn cell_label(&mut self, cell: i32, l: &str) {
        mvwprintw(self.wsidebar, Self::cell_y(cell), 1, l);
    }

    fn cell_val(&mut self, cell: i32, l: &str) {
        mvwprintw(self.wsidebar, Self::cell_y(cell)+1, 1, l);
    }

    fn cell_divider(&mut self, cell: i32) {
        mvwaddch(self.win, Self::cell_y(cell), SIDEBAR_X, ACS_LTEE());
        mvwhline(self.win, Self::cell_y(cell), SIDEBAR_X+1, ACS_HLINE(), SIDEBAR_WIDTH);
        mvwaddch(self.win, Self::cell_y(cell), SIDEBAR_X+SIDEBAR_WIDTH+1, ACS_RTEE());
    }

    fn cell_y(cell: i32) -> i32 {
        (SIDEBAR_CELL_HEIGHT + 1) * cell
    }

    fn refresh(&mut self) {
        wrefresh(self.win);
    }

    fn set_values(&mut self, acc: i32, bak: i32, last: Option<instruction::Port>, mode: ExecState) {
        let acc = format!("{:^4}", acc);
        let bak = format!("{:^4}", bak);
        let last = last.map_or_else(|| "N/A".to_string(), |p| p.to_string());
        let mode = mode.to_string();
        self.cell_val(0, &acc);
        self.cell_val(1, &bak);
        self.cell_val(2, &last);
        self.cell_val(3, &mode);
    }
}

impl Drop for CpuWin {
    fn drop(&mut self) {
        delwin(self.wsidebar);
        delwin(self.winner);
        wclear(self.win);
        wrefresh(self.win);
        delwin(self.win);
    }
}

pub fn gui() {
    initscr();
    refresh();

    let left_margin = 10;
    let inner_margin = 4;

    let mut cpuwins: Vec<Vec<CpuWin>> = (0..4).map(|x| {
        (0..3).map(|y| (x, y)).map(|(x, y)| {
            CpuWin::new(x*(CPUWIN_WIDTH + inner_margin) + left_margin,
                        y*(CPUWIN_HEIGHT + inner_margin/2) + left_margin/2)
        }).collect::<Vec<_>>()
    }).collect();

    refresh();
    getch();

    for y in cpuwins.iter_mut() {
        for cpu in y.iter_mut() {
            cpu.refresh();
        }
    }

    getch();

    refresh();
    getch();

    endwin();
}
<commit_msg>gui_ncurses: handle resize<commit_after>extern crate ncurses;

use std::option::Option;
use self::ncurses::*;
use cpu::ExecState;
use instruction;

struct CpuWin {
    win:        WINDOW,
    winner:     WINDOW,
    wsidebar:   WINDOW,
}

const SIDEBAR_CELL_HEIGHT: i32 = 2;
const SIDEBAR_WIDTH: i32 = 6;
const CPUWIN_HEIGHT: i32 = (SIDEBAR_CELL_HEIGHT + 1) * 4 + 1;
const CPUWIN_WIDTH: i32 = CPUWIN_HEIGHT*2 + SIDEBAR_WIDTH;
const SIDEBAR_X: i32 = CPUWIN_WIDTH - 1 - SIDEBAR_WIDTH - 1;

impl CpuWin {
    fn new(posx: i32, posy: i32) -> CpuWin {

        let win = newwin(CPUWIN_HEIGHT, CPUWIN_WIDTH, posy, posx);
        let winner = derwin(win, CPUWIN_HEIGHT-2, CPUWIN_WIDTH-2, 1, 1);
        let wsidebar = derwin(winner, CPUWIN_HEIGHT-2, SIDEBAR_WIDTH, 0, SIDEBAR_X);

        /* Border */
        box_(win, 0, 0);

        /* Left size of sidebar */
        mvwaddch(win, 0, SIDEBAR_X, ACS_TTEE());
        mvwvline(win, 1, SIDEBAR_X, ACS_VLINE(), CPUWIN_HEIGHT - 2);
        mvwaddch(win, CPUWIN_HEIGHT-1, SIDEBAR_X, ACS_BTEE());

        let mut cpuwin = CpuWin {
            win:        win,
            winner:     winner,
            wsidebar:   wsidebar,
        };
        cpuwin.cell_label(0, "ACC");
        cpuwin.cell_label(1, "BAK");
        cpuwin.cell_label(2, "LAST");
        cpuwin.cell_label(3, "MODE");

        cpuwin.cell_divider(1);
        cpuwin.cell_divider(2);
        cpuwin.cell_divider(3);

        cpuwin.set_values(0, 10, None, ExecState::EXEC);

        cpuwin
    }

    fn cell_label(&mut self, cell: i32, l: &str) {
        mvwprintw(self.wsidebar, Self::cell_y(cell), 1, l);
    }

    fn cell_val(&mut self, cell: i32, l: &str) {
        mvwprintw(self.wsidebar, Self::cell_y(cell)+1, 1, l);
    }

    fn cell_divider(&mut self, cell: i32) {
        mvwaddch(self.win, Self::cell_y(cell), SIDEBAR_X, ACS_LTEE());
        mvwhline(self.win, Self::cell_y(cell), SIDEBAR_X+1, ACS_HLINE(), SIDEBAR_WIDTH);
        mvwaddch(self.win, Self::cell_y(cell), SIDEBAR_X+SIDEBAR_WIDTH+1, ACS_RTEE());
    }

    fn cell_y(cell: i32) -> i32 {
        (SIDEBAR_CELL_HEIGHT + 1) * cell
    }

    fn refresh(&mut self) {
        wrefresh(self.win);
    }

    fn set_values(&mut self, acc: i32, bak: i32, last: Option<instruction::Port>, mode: ExecState) {
        let acc = format!("{:^4}", acc);
        let bak = format!("{:^4}", bak);
        let last = last.map_or_else(|| "N/A".to_string(), |p| p.to_string());
        let mode = mode.to_string();
        self.cell_val(0, &acc);
        self.cell_val(1, &bak);
        self.cell_val(2, &last);
        self.cell_val(3, &mode);
    }
}

impl Drop for CpuWin {
    fn drop(&mut self) {
        delwin(self.wsidebar);
        delwin(self.winner);
        wclear(self.win);
        //wrefresh(self.win);
        delwin(self.win);
    }
}

fn create_cpu_wins() -> Vec<Vec<CpuWin>> {
    let left_margin = 10;
    let inner_margin = 4;

    let mut cpuwins: Vec<Vec<CpuWin>> = (0..4).map(|x| {
        (0..3).map(|y| (x, y)).map(|(x, y)| {
            CpuWin::new(x*(CPUWIN_WIDTH + inner_margin) + left_margin,
                        y*(CPUWIN_HEIGHT + inner_margin/2) + left_margin/2)
        }).collect::<Vec<_>>()
    }).collect();

    for y in cpuwins.iter_mut() {
        for cpu in y.iter_mut() {
            cpu.refresh();
        }
    }

    cpuwins
}

pub fn gui() {
    initscr();
    refresh();

    let mut cpuwins = create_cpu_wins();

    loop {
        let c = getch();
        if c == b'q' as i32 {
            break;
        } else if c == KEY_RESIZE {
            drop(cpuwins);
            clear();
            refresh();
            cpuwins = create_cpu_wins();
        }
    }

    drop(cpuwins);
    endwin();
}
<|endoftext|>


<commit_before>// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

#[macro_use]
extern crate glib;
#[macro_use]
extern crate gstreamer as gst;
extern crate gstreamer_base as gst_base;
extern crate gstreamer_video as gst_video;
#[macro_use]
extern crate lazy_static;

mod cdgdec;
mod cdgparse;
mod constants;

use constants::{CDG_COMMAND, CDG_MASK, CDG_PACKET_PERIOD, CDG_PACKET_SIZE};
use gst::{Caps, TypeFind, TypeFindProbability};
use std::cmp;

const NB_WINDOWS: u64 = 8;
const TYPEFIND_SEARCH_WINDOW_SEC: i64 = 4;
const TYPEFIND_SEARCH_WINDOW: i64 =
    TYPEFIND_SEARCH_WINDOW_SEC * (CDG_PACKET_SIZE as i64 * CDG_PACKET_PERIOD as i64); /* in bytes */

/* Return the percentage of CDG packets in the first @len bytes of @typefind */
fn cdg_packets_ratio(typefind: &mut TypeFind, start: i64, len: i64) -> i64 {
    let mut count = 0;
    let total = len / CDG_PACKET_SIZE as i64;

    for offset in (0..len).step_by(CDG_PACKET_SIZE as usize) {
        match typefind.peek(start + offset, CDG_PACKET_SIZE as u32) {
            Some(data) => {
                if data[0] & CDG_MASK == CDG_COMMAND {
                    count += 1;
                }
            }
            None => break,
        }
    }
    (count * 100) / total
}

/* Some CDG files starts drawing right away and then pause for a while
 * (typically because of the song intro) while other wait for a few
 * seconds before starting to draw.
 * In order to support all variants, scan through all the file per block
 * of size TYPEFIND_SEARCH_WINDOW and keep the highest ratio of CDG packets
 * detected. */
fn compute_probability(typefind: &mut TypeFind) -> TypeFindProbability {
    let mut best = TypeFindProbability::None;
    // Try looking at the start of the file if its length isn't available
    let len = typefind
        .get_length()
        .unwrap_or(TYPEFIND_SEARCH_WINDOW as u64 * NB_WINDOWS);
    let step = len / NB_WINDOWS;

    for offset in (0..len).step_by(step as usize) {
        let proba = match cdg_packets_ratio(typefind, offset as i64, TYPEFIND_SEARCH_WINDOW) {
            0..=5 => TypeFindProbability::None,
            6..=10 => TypeFindProbability::Possible,
            _ => TypeFindProbability::Likely,
        };

        if proba == TypeFindProbability::Likely {
            return proba;
        }

        best = cmp::max(best, proba);
    }

    best
}

fn typefind_register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
    TypeFind::register(
        Some(plugin),
        "cdg_typefind",
        gst::Rank::None,
        Some("cdg"),
        Some(&Caps::new_simple("video/x-cdg", &[])),
        |mut typefind| {
            let proba = compute_probability(&mut typefind);

            if proba != gst::TypeFindProbability::None {
                typefind.suggest(proba, &Caps::new_simple("video/x-cdg", &[]));
            }
        },
    )
}

fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
    cdgdec::register(plugin)?;
    cdgparse::register(plugin)?;
    typefind_register(plugin)?;
    Ok(())
}

gst_plugin_define!(
    cdg,
    env!("CARGO_PKG_DESCRIPTION"),
    plugin_init,
    concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
    "MIT/X11",
    env!("CARGO_PKG_NAME"),
    env!("CARGO_PKG_NAME"),
    env!("CARGO_PKG_REPOSITORY"),
    env!("BUILD_REL_DATE")
);
<commit_msg>cdg: Fix assertion failure while typefinding with too short file<commit_after>// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

#[macro_use]
extern crate glib;
#[macro_use]
extern crate gstreamer as gst;
extern crate gstreamer_base as gst_base;
extern crate gstreamer_video as gst_video;
#[macro_use]
extern crate lazy_static;

mod cdgdec;
mod cdgparse;
mod constants;

use constants::{CDG_COMMAND, CDG_MASK, CDG_PACKET_PERIOD, CDG_PACKET_SIZE};
use gst::{Caps, TypeFind, TypeFindProbability};
use std::cmp;

const NB_WINDOWS: u64 = 8;
const TYPEFIND_SEARCH_WINDOW_SEC: i64 = 4;
const TYPEFIND_SEARCH_WINDOW: i64 =
    TYPEFIND_SEARCH_WINDOW_SEC * (CDG_PACKET_SIZE as i64 * CDG_PACKET_PERIOD as i64); /* in bytes */

/* Return the percentage of CDG packets in the first @len bytes of @typefind */
fn cdg_packets_ratio(typefind: &mut TypeFind, start: i64, len: i64) -> i64 {
    let mut count = 0;
    let total = len / CDG_PACKET_SIZE as i64;

    for offset in (0..len).step_by(CDG_PACKET_SIZE as usize) {
        match typefind.peek(start + offset, CDG_PACKET_SIZE as u32) {
            Some(data) => {
                if data[0] & CDG_MASK == CDG_COMMAND {
                    count += 1;
                }
            }
            None => break,
        }
    }
    (count * 100) / total
}

/* Some CDG files starts drawing right away and then pause for a while
 * (typically because of the song intro) while other wait for a few
 * seconds before starting to draw.
 * In order to support all variants, scan through all the file per block
 * of size TYPEFIND_SEARCH_WINDOW and keep the highest ratio of CDG packets
 * detected. */
fn compute_probability(typefind: &mut TypeFind) -> TypeFindProbability {
    let mut best = TypeFindProbability::None;
    // Try looking at the start of the file if its length isn't available
    let len = typefind
        .get_length()
        .unwrap_or(TYPEFIND_SEARCH_WINDOW as u64 * NB_WINDOWS);
    let step = len / NB_WINDOWS;

    // Too short file
    if step == 0 {
        return TypeFindProbability::None;
    }

    for offset in (0..len).step_by(step as usize) {
        let proba = match cdg_packets_ratio(typefind, offset as i64, TYPEFIND_SEARCH_WINDOW) {
            0..=5 => TypeFindProbability::None,
            6..=10 => TypeFindProbability::Possible,
            _ => TypeFindProbability::Likely,
        };

        if proba == TypeFindProbability::Likely {
            return proba;
        }

        best = cmp::max(best, proba);
    }

    best
}

fn typefind_register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
    TypeFind::register(
        Some(plugin),
        "cdg_typefind",
        gst::Rank::None,
        Some("cdg"),
        Some(&Caps::new_simple("video/x-cdg", &[])),
        |mut typefind| {
            let proba = compute_probability(&mut typefind);

            if proba != gst::TypeFindProbability::None {
                typefind.suggest(proba, &Caps::new_simple("video/x-cdg", &[]));
            }
        },
    )
}

fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
    cdgdec::register(plugin)?;
    cdgparse::register(plugin)?;
    typefind_register(plugin)?;
    Ok(())
}

gst_plugin_define!(
    cdg,
    env!("CARGO_PKG_DESCRIPTION"),
    plugin_init,
    concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
    "MIT/X11",
    env!("CARGO_PKG_NAME"),
    env!("CARGO_PKG_NAME"),
    env!("CARGO_PKG_REPOSITORY"),
    env!("BUILD_REL_DATE")
);
<|endoftext|>


<commit_before>extern crate libc;
use std::ptr;
use std::str;
use std::ffi::CStr;
use std::fmt::{Display, Debug, Formatter, Error};
use ffi::cs_free;

/// Representation of the array of instructions returned by disasm
pub struct Instructions {
    ptr: *const Insn,
    len: isize,
}

impl Instructions {
    pub unsafe fn from_raw_parts(ptr: *const Insn, len: isize) -> Instructions {
        Instructions {
            ptr: ptr,
            len: len,
        }
    }

    pub fn len(&self) -> isize {
        self.len
    }

    pub fn iter(&self) -> InstructionIterator {
        InstructionIterator {
            insns: &self,
            cur: 0,
        }
    }
}

impl Drop for Instructions {
    fn drop(&mut self) {
        unsafe {
            cs_free(self.ptr, self.len as libc::size_t);
        }
    }
}

/// An iterator over the instructions returned by disasm
///
/// This is currently the only supported interface for reading them.
pub struct InstructionIterator<'a> {
    insns: &'a Instructions,
    cur: isize,
}

impl<'a> Iterator for InstructionIterator<'a> {
    type Item = Insn;

    fn next(&mut self) -> Option<Insn> {
        if self.cur == self.insns.len {
            None
        } else {
            let obj = unsafe { self.insns.ptr.offset(self.cur) };
            self.cur += 1;
            Some(unsafe { ptr::read(obj) })
        }
    }
}

#[repr(C)]
/// A logical instruction disassembled by capstone
pub struct Insn {
    id: ::libc::c_uint,
    pub address: u64,
    pub size: u16,
    pub bytes: [u8; 16usize],
    pub mnemonic: [::libc::c_char; 32usize],
    pub op_str: [::libc::c_char; 160usize],
    detail: *mut libc::c_void, // Opaque cs_detail
}

impl Insn {
    /// The mnemonic for the instruction
    pub fn mnemonic(&self) -> Option<&str> {
        let cstr = unsafe { CStr::from_ptr(self.mnemonic.as_ptr()) };
        str::from_utf8(cstr.to_bytes()).ok()
    }

    /// The operand string associated with the instruction
    pub fn op_str(&self) -> Option<&str> {
        let cstr = unsafe { CStr::from_ptr(self.op_str.as_ptr()) };
        str::from_utf8(cstr.to_bytes()).ok()
    }
}

impl Debug for Insn {
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        fmt.debug_struct("Insn")
           .field("address", &self.address)
           .field("size", &self.size)
           .field("mnemonic", &self.mnemonic())
           .field("op_str", &self.op_str())
           .finish()
    }
}

impl Display for Insn {
    #[allow(unused_must_use)]
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        write!(fmt, "{:#x}: ", self.address);
        if let Some(mnemonic) = self.mnemonic() {
            write!(fmt, "{} ", mnemonic);
            if let Some(op_str) = self.op_str() {
                write!(fmt, "{}", op_str);
            }
        }
        Ok(())
    }
}
<commit_msg>Hide private fields on Insn<commit_after>extern crate libc;
use std::ptr;
use std::str;
use std::ffi::CStr;
use std::fmt::{Display, Debug, Formatter, Error};
use ffi::cs_free;

/// Representation of the array of instructions returned by disasm
pub struct Instructions {
    ptr: *const Insn,
    len: isize,
}

impl Instructions {
    pub unsafe fn from_raw_parts(ptr: *const Insn, len: isize) -> Instructions {
        Instructions {
            ptr: ptr,
            len: len,
        }
    }

    pub fn len(&self) -> isize {
        self.len
    }

    pub fn iter(&self) -> InstructionIterator {
        InstructionIterator {
            insns: &self,
            cur: 0,
        }
    }
}

impl Drop for Instructions {
    fn drop(&mut self) {
        unsafe {
            cs_free(self.ptr, self.len as libc::size_t);
        }
    }
}

/// An iterator over the instructions returned by disasm
///
/// This is currently the only supported interface for reading them.
pub struct InstructionIterator<'a> {
    insns: &'a Instructions,
    cur: isize,
}

impl<'a> Iterator for InstructionIterator<'a> {
    type Item = Insn;

    fn next(&mut self) -> Option<Insn> {
        if self.cur == self.insns.len {
            None
        } else {
            let obj = unsafe { self.insns.ptr.offset(self.cur) };
            self.cur += 1;
            Some(unsafe { ptr::read(obj) })
        }
    }
}

#[repr(C)]
/// A logical instruction disassembled by capstone
pub struct Insn {
    id: ::libc::c_uint,
    pub address: u64,
    pub size: u16,
    bytes: [u8; 16usize],
    mnemonic: [::libc::c_char; 32usize],
    op_str: [::libc::c_char; 160usize],
    detail: *mut libc::c_void, // Opaque cs_detail
}

impl Insn {
    /// The mnemonic for the instruction
    pub fn mnemonic(&self) -> Option<&str> {
        let cstr = unsafe { CStr::from_ptr(self.mnemonic.as_ptr()) };
        str::from_utf8(cstr.to_bytes()).ok()
    }

    /// The operand string associated with the instruction
    pub fn op_str(&self) -> Option<&str> {
        let cstr = unsafe { CStr::from_ptr(self.op_str.as_ptr()) };
        str::from_utf8(cstr.to_bytes()).ok()
    }
}

impl Debug for Insn {
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        fmt.debug_struct("Insn")
           .field("address", &self.address)
           .field("size", &self.size)
           .field("mnemonic", &self.mnemonic())
           .field("op_str", &self.op_str())
           .finish()
    }
}

impl Display for Insn {
    #[allow(unused_must_use)]
    fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
        write!(fmt, "{:#x}: ", self.address);
        if let Some(mnemonic) = self.mnemonic() {
            write!(fmt, "{} ", mnemonic);
            if let Some(op_str) = self.op_str() {
                write!(fmt, "{}", op_str);
            }
        }
        Ok(())
    }
}
<|endoftext|>


<commit_before>use ast::*;
use std::collections::HashMap;
use std::io;
use std::io::prelude::*;

#[derive(Debug, PartialEq, Clone)]
pub enum Value {
    Nil,
    Boolean(bool),
    Number(f64),
    String(String),
}

impl Value {
    fn is_true(&self) -> bool {
        match *self {
            Value::Nil => false,
            Value::Boolean(b) => b,
            _ => true,
        }
    }

    fn to_string(&self) -> String {
        match *self {
            Value::Nil => "nil".into(),
            Value::Boolean(ref b) => b.to_string(),
            Value::Number(ref n) => n.to_string(),
            Value::String(ref s) => s.clone(),
        }
    }
}

struct Environment {
    values: Vec<HashMap<Identifier, Value>>,
}

impl Environment {
    fn new() -> Environment {
        Environment { values: vec![HashMap::new()] }
    }

    fn push(&mut self) {
        self.values.push(HashMap::new());
    }

    fn pop(&mut self) {
        self.values.pop();
    }

    fn top(&self) -> usize {
        self.values.len() - 1
    }

    fn define(&mut self, identifier: Identifier, value: Value) {
        // NOTE that this allow for variable redefinition. See the chapter.
        let index = self.top();
        self.values[index].insert(identifier, value);
    }

    fn try_set(&mut self, identifier: Identifier, value: Value) -> bool {
        for index in (0..self.values.len()).rev() {
            if self.values[index].contains_key(&identifier) {
                self.values[index].insert(identifier, value);
                return true;
            }
        }
        false
    }

    fn get(&self, identifier: &Identifier) -> Option<&Value> {
        for index in (0..self.values.len()).rev() {
            if let Some(value) = self.values[index].get(identifier) {
                return Some(value);
            }
        }
        None
    }
}

pub trait Interpreter {
    fn execute(&mut self, statement: &Statement) -> Result<Option<Value>, RuntimeError>;
}

pub struct StatementInterpreter {
    environment: Environment,
}

impl StatementInterpreter {
    pub fn new() -> StatementInterpreter {
        StatementInterpreter { environment: Environment::new() }
    }
}
impl Interpreter for StatementInterpreter {
    fn execute(&mut self, statement: &Statement) -> Result<Option<Value>, RuntimeError> {
        statement.execute(&mut self.environment)
    }
}

#[derive(Debug)]
pub enum RuntimeError {
    UnaryMinusTypeMismatch(Value),
    BinaryOperatorTypeMismatch(BinaryOperator, Value, Value),
    UndefinedIdentifier(Identifier),
}

trait Interpret {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError>;
}

trait Execute {
    fn execute(&self, environment: &mut Environment) -> Result<Option<Value>, RuntimeError>;
}

impl Interpret for Expr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        match *self {
            Expr::Literal(ref l) => l.interpret(environment),
            Expr::Unary(ref u) => u.interpret(environment),
            Expr::Binary(ref b) => b.interpret(environment),
            Expr::Grouping(ref g) => g.interpret(environment),
            Expr::Identifier(ref i) => i.interpret(environment),
            Expr::Assignment(ref a) => a.interpret(environment),
        }
    }
}

impl Interpret for Literal {
    fn interpret(&self, _: &mut Environment) -> Result<Value, RuntimeError> {
        match *self {
            Literal::NilLiteral => Ok(Value::Nil),
            Literal::BoolLiteral(b) => Ok(Value::Boolean(b)),
            Literal::StringLiteral(ref s) => Ok(Value::String(s.clone())),
            Literal::NumberLiteral(n) => Ok(Value::Number(n)),
        }
    }
}

impl Interpret for Identifier {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        match environment.get(self) {
            Some(value) => Ok(value.clone()),
            None => Err(RuntimeError::UndefinedIdentifier(self.clone())),
        }
    }
}

impl Interpret for Assignment {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let target = match self.lvalue {
            Target::Identifier(ref i) => Identifier { name: i.name.clone() },
        };
        match self.rvalue.interpret(environment) {
            Ok(value) => {
                if environment.try_set(target.clone(), value.clone()) {
                    Ok(value.clone())
                } else {
                    Err(RuntimeError::UndefinedIdentifier(target))
                }
            }
            Err(error) => Err(error),
        }
    }
}

impl Interpret for Grouping {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        self.expr.interpret(environment)
    }
}

impl Interpret for UnaryExpr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let value = try!(self.right.interpret(environment));
        match self.operator {
            UnaryOperator::Bang => Ok(Value::Boolean(!value.is_true())),
            UnaryOperator::Minus => {
                match value {
                    Value::Number(n) => Ok(Value::Number(-n)),
                    _ => Err(RuntimeError::UnaryMinusTypeMismatch(value)),
                }
            }
        }
    }
}

impl Interpret for BinaryExpr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let left = try!(self.left.interpret(environment));
        let right = try!(self.right.interpret(environment));
        match (&self.operator, &left, &right) {
            (&BinaryOperator::Minus, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l - r))
            }
            (&BinaryOperator::Slash, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l / r))
            }
            (&BinaryOperator::Star, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l * r))
            }
            (&BinaryOperator::Plus, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l + r))
            }
            (&BinaryOperator::Plus, &Value::String(ref l), &Value::String(ref r)) => {
                let mut result = String::new();
                result.push_str(l);
                result.push_str(r);
                Ok(Value::String(result))
            }
            (&BinaryOperator::Greater, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l > r))
            }
            (&BinaryOperator::Less, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l < r))
            }
            (&BinaryOperator::GreaterEqual, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l >= r))
            }
            (&BinaryOperator::LessEqual, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l <= r))
            }
            (&BinaryOperator::NotEqual, l, r) => Ok(Value::Boolean(l != r)),
            (&BinaryOperator::Equal, l, r) => Ok(Value::Boolean(l == r)),
            _ => {
                Err(RuntimeError::BinaryOperatorTypeMismatch(self.operator,
                                                             left.clone(),
                                                             right.clone()))
            }
        }
    }
}

impl Execute for Statement {
    fn execute(&self, mut environment: &mut Environment) -> Result<Option<Value>, RuntimeError> {
        match *self {
            Statement::Expression(ref e) => e.interpret(environment).map(Some),
            Statement::Print(ref e) => {
                e.interpret(environment)
                    .map(|value| {
                             println!("{}", value.to_string());
                             let _ = io::stdout().flush(); //TODO: is this okay?
                             None
                         })
            }
            Statement::VariableDefinition(ref identifier) => {
                let identifier = Identifier { name: identifier.name.clone() };
                environment.define(identifier, Value::Nil);
                Ok(None)
            }
            Statement::VariableDefinitionWithInitalizer(ref identifier, ref expression) => {
                let identifier = Identifier { name: identifier.name.clone() };
                expression
                    .interpret(environment)
                    .map(|initializer| {
                             environment.define(identifier, initializer);
                             None
                         })
            }
            Statement::Block(ref b) => {
                environment.push();
                let mut result = Ok(None);
                for statement in &b.statements {
                    result = statement.execute(environment);
                }
                environment.pop();
                result
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use ast::*;
    use interpreter::{Interpret, Execute, Environment, Value};

    #[test]
    fn literal() {
        let mut environment = Environment::new();
        let string = String::from("abc");
        let expr = Expr::Literal(Literal::StringLiteral(string.clone()));
        assert_eq!(Value::String(string),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn grouping() {
        let mut environment = Environment::new();
        let expr = Grouping { expr: Expr::Literal(Literal::NumberLiteral(45.67f64)) };
        assert_eq!(Value::Number(45.67f64),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn unary() {
        let mut environment = Environment::new();
        let expr = UnaryExpr {
            operator: UnaryOperator::Bang,
            right: Expr::Literal(Literal::BoolLiteral(false)),
        };
        assert_eq!(Value::Boolean(true),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn binary() {
        let mut environment = Environment::new();
        let expr = BinaryExpr {
            operator: BinaryOperator::Plus,
            left: Expr::Literal(Literal::NumberLiteral(1.0f64)),
            right: Expr::Literal(Literal::NumberLiteral(1.0f64)),
        };
        assert_eq!(Value::Number(2.0f64),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn expression_statement() {
        let mut environment = Environment::new();
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statement = Statement::Expression(expr);
        assert_eq!(Some(Value::Number(1.0f64)),
                   statement.execute(&mut environment).unwrap());
    }

    #[test]
    fn variable_definition() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let statement = Statement::VariableDefinition(identifier.clone());
        assert_eq!(None, statement.execute(&mut environment).unwrap());
        assert_eq!(&Value::Nil, environment.get(&identifier).unwrap());
    }

    #[test]
    fn variable_definition_with_initializer() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statement = Statement::VariableDefinitionWithInitalizer(identifier.clone(), expr);
        assert_eq!(None, statement.execute(&mut environment).unwrap());
        assert_eq!(&Value::Number(1.0f64),
                   environment.get(&identifier).unwrap());
    }

    #[test]
    fn block_affects_outer_scope() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let outer_statement = Statement::VariableDefinitionWithInitalizer(identifier.clone(), expr);
        assert_eq!(None, outer_statement.execute(&mut environment).unwrap());

        let statements = vec![
            Statement::Expression(
                Expr::Assignment(Box::new(Assignment{
                    lvalue: Target::Identifier(Identifier{name: "x".into()}),
                    rvalue: Expr::Literal(Literal::BoolLiteral(false))})))];
        let block = Statement::Block(Box::new(Block { statements: statements }));
        assert!(block.execute(&mut environment).is_ok());
        assert_eq!(&Value::Boolean(false),
                   environment.get(&identifier).unwrap());
    }
    #[test]
    fn block_variable_dont_escape_scope() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statements = vec![Statement::VariableDefinitionWithInitalizer(identifier.clone(),
                                                                          expr)];
        let block = Statement::Block(Box::new(Block { statements: statements }));
        assert_eq!(None, block.execute(&mut environment).unwrap());
        // The variable declaration gets lost when we exit the scope
        assert_eq!(None, environment.get(&identifier));
    }

}
<commit_msg>Test for assigning and accessing undeclared variables<commit_after>use ast::*;
use std::collections::HashMap;
use std::io;
use std::io::prelude::*;

#[derive(Debug, PartialEq, Clone)]
pub enum Value {
    Nil,
    Boolean(bool),
    Number(f64),
    String(String),
}

impl Value {
    fn is_true(&self) -> bool {
        match *self {
            Value::Nil => false,
            Value::Boolean(b) => b,
            _ => true,
        }
    }

    fn to_string(&self) -> String {
        match *self {
            Value::Nil => "nil".into(),
            Value::Boolean(ref b) => b.to_string(),
            Value::Number(ref n) => n.to_string(),
            Value::String(ref s) => s.clone(),
        }
    }
}

struct Environment {
    values: Vec<HashMap<Identifier, Value>>,
}

impl Environment {
    fn new() -> Environment {
        Environment { values: vec![HashMap::new()] }
    }

    fn push(&mut self) {
        self.values.push(HashMap::new());
    }

    fn pop(&mut self) {
        self.values.pop();
    }

    fn top(&self) -> usize {
        self.values.len() - 1
    }

    fn define(&mut self, identifier: Identifier, value: Value) {
        // NOTE that this allow for variable redefinition. See the chapter.
        let index = self.top();
        self.values[index].insert(identifier, value);
    }

    fn try_set(&mut self, identifier: Identifier, value: Value) -> bool {
        for index in (0..self.values.len()).rev() {
            if self.values[index].contains_key(&identifier) {
                self.values[index].insert(identifier, value);
                return true;
            }
        }
        false
    }

    fn get(&self, identifier: &Identifier) -> Option<&Value> {
        for index in (0..self.values.len()).rev() {
            if let Some(value) = self.values[index].get(identifier) {
                return Some(value);
            }
        }
        None
    }
}

pub trait Interpreter {
    fn execute(&mut self, statement: &Statement) -> Result<Option<Value>, RuntimeError>;
}

pub struct StatementInterpreter {
    environment: Environment,
}

impl StatementInterpreter {
    pub fn new() -> StatementInterpreter {
        StatementInterpreter { environment: Environment::new() }
    }
}
impl Interpreter for StatementInterpreter {
    fn execute(&mut self, statement: &Statement) -> Result<Option<Value>, RuntimeError> {
        statement.execute(&mut self.environment)
    }
}

#[derive(Debug)]
pub enum RuntimeError {
    UnaryMinusTypeMismatch(Value),
    BinaryOperatorTypeMismatch(BinaryOperator, Value, Value),
    UndefinedIdentifier(Identifier),
}

trait Interpret {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError>;
}

trait Execute {
    fn execute(&self, environment: &mut Environment) -> Result<Option<Value>, RuntimeError>;
}

impl Interpret for Expr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        match *self {
            Expr::Literal(ref l) => l.interpret(environment),
            Expr::Unary(ref u) => u.interpret(environment),
            Expr::Binary(ref b) => b.interpret(environment),
            Expr::Grouping(ref g) => g.interpret(environment),
            Expr::Identifier(ref i) => i.interpret(environment),
            Expr::Assignment(ref a) => a.interpret(environment),
        }
    }
}

impl Interpret for Literal {
    fn interpret(&self, _: &mut Environment) -> Result<Value, RuntimeError> {
        match *self {
            Literal::NilLiteral => Ok(Value::Nil),
            Literal::BoolLiteral(b) => Ok(Value::Boolean(b)),
            Literal::StringLiteral(ref s) => Ok(Value::String(s.clone())),
            Literal::NumberLiteral(n) => Ok(Value::Number(n)),
        }
    }
}

impl Interpret for Identifier {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        match environment.get(self) {
            Some(value) => Ok(value.clone()),
            None => Err(RuntimeError::UndefinedIdentifier(self.clone())),
        }
    }
}

impl Interpret for Assignment {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let target = match self.lvalue {
            Target::Identifier(ref i) => Identifier { name: i.name.clone() },
        };
        match self.rvalue.interpret(environment) {
            Ok(value) => {
                if environment.try_set(target.clone(), value.clone()) {
                    Ok(value.clone())
                } else {
                    Err(RuntimeError::UndefinedIdentifier(target))
                }
            }
            Err(error) => Err(error),
        }
    }
}

impl Interpret for Grouping {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        self.expr.interpret(environment)
    }
}

impl Interpret for UnaryExpr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let value = try!(self.right.interpret(environment));
        match self.operator {
            UnaryOperator::Bang => Ok(Value::Boolean(!value.is_true())),
            UnaryOperator::Minus => {
                match value {
                    Value::Number(n) => Ok(Value::Number(-n)),
                    _ => Err(RuntimeError::UnaryMinusTypeMismatch(value)),
                }
            }
        }
    }
}

impl Interpret for BinaryExpr {
    fn interpret(&self, environment: &mut Environment) -> Result<Value, RuntimeError> {
        let left = try!(self.left.interpret(environment));
        let right = try!(self.right.interpret(environment));
        match (&self.operator, &left, &right) {
            (&BinaryOperator::Minus, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l - r))
            }
            (&BinaryOperator::Slash, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l / r))
            }
            (&BinaryOperator::Star, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l * r))
            }
            (&BinaryOperator::Plus, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Number(l + r))
            }
            (&BinaryOperator::Plus, &Value::String(ref l), &Value::String(ref r)) => {
                let mut result = String::new();
                result.push_str(l);
                result.push_str(r);
                Ok(Value::String(result))
            }
            (&BinaryOperator::Greater, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l > r))
            }
            (&BinaryOperator::Less, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l < r))
            }
            (&BinaryOperator::GreaterEqual, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l >= r))
            }
            (&BinaryOperator::LessEqual, &Value::Number(l), &Value::Number(r)) => {
                Ok(Value::Boolean(l <= r))
            }
            (&BinaryOperator::NotEqual, l, r) => Ok(Value::Boolean(l != r)),
            (&BinaryOperator::Equal, l, r) => Ok(Value::Boolean(l == r)),
            _ => {
                Err(RuntimeError::BinaryOperatorTypeMismatch(self.operator,
                                                             left.clone(),
                                                             right.clone()))
            }
        }
    }
}

impl Execute for Statement {
    fn execute(&self, mut environment: &mut Environment) -> Result<Option<Value>, RuntimeError> {
        match *self {
            Statement::Expression(ref e) => e.interpret(environment).map(Some),
            Statement::Print(ref e) => {
                e.interpret(environment)
                    .map(|value| {
                             println!("{}", value.to_string());
                             let _ = io::stdout().flush(); //TODO: is this okay?
                             None
                         })
            }
            Statement::VariableDefinition(ref identifier) => {
                let identifier = Identifier { name: identifier.name.clone() };
                environment.define(identifier, Value::Nil);
                Ok(None)
            }
            Statement::VariableDefinitionWithInitalizer(ref identifier, ref expression) => {
                let identifier = Identifier { name: identifier.name.clone() };
                expression
                    .interpret(environment)
                    .map(|initializer| {
                             environment.define(identifier, initializer);
                             None
                         })
            }
            Statement::Block(ref b) => {
                environment.push();
                let mut result = Ok(None);
                for statement in &b.statements {
                    result = statement.execute(environment);
                }
                environment.pop();
                result
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use ast::*;
    use interpreter::{Interpret, Execute, Environment, Value};

    #[test]
    fn literal() {
        let mut environment = Environment::new();
        let string = String::from("abc");
        let expr = Expr::Literal(Literal::StringLiteral(string.clone()));
        assert_eq!(Value::String(string),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn grouping() {
        let mut environment = Environment::new();
        let expr = Grouping { expr: Expr::Literal(Literal::NumberLiteral(45.67f64)) };
        assert_eq!(Value::Number(45.67f64),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn unary() {
        let mut environment = Environment::new();
        let expr = UnaryExpr {
            operator: UnaryOperator::Bang,
            right: Expr::Literal(Literal::BoolLiteral(false)),
        };
        assert_eq!(Value::Boolean(true),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn binary() {
        let mut environment = Environment::new();
        let expr = BinaryExpr {
            operator: BinaryOperator::Plus,
            left: Expr::Literal(Literal::NumberLiteral(1.0f64)),
            right: Expr::Literal(Literal::NumberLiteral(1.0f64)),
        };
        assert_eq!(Value::Number(2.0f64),
                   expr.interpret(&mut environment).unwrap());
    }

    #[test]
    fn expression_statement() {
        let mut environment = Environment::new();
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statement = Statement::Expression(expr);
        assert_eq!(Some(Value::Number(1.0f64)),
                   statement.execute(&mut environment).unwrap());
    }

    #[test]
    fn variable_definition() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let statement = Statement::VariableDefinition(identifier.clone());
        assert_eq!(None, statement.execute(&mut environment).unwrap());
        assert_eq!(&Value::Nil, environment.get(&identifier).unwrap());
    }

    #[test]
    fn error_accessing_undefined_variable() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let statement = Statement::Expression(Expr::Identifier(identifier));
        assert!(statement.execute(&mut environment).is_err());
    }

    #[test]
    fn error_assigning_undefined_variable() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let statement = Statement::Expression(
            Expr::Assignment(Box::new(Assignment{
                lvalue: Target::Identifier(identifier),
                rvalue: Expr::Literal(Literal::BoolLiteral(true))}))
            );
        assert!(statement.execute(&mut environment).is_err());
    }

    #[test]
    fn variable_definition_with_initializer() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statement = Statement::VariableDefinitionWithInitalizer(identifier.clone(), expr);
        assert_eq!(None, statement.execute(&mut environment).unwrap());
        assert_eq!(&Value::Number(1.0f64),
                   environment.get(&identifier).unwrap());
    }

    #[test]
    fn block_affects_outer_scope() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let outer_statement = Statement::VariableDefinitionWithInitalizer(identifier.clone(), expr);
        assert_eq!(None, outer_statement.execute(&mut environment).unwrap());

        let statements = vec![
            Statement::Expression(
                Expr::Assignment(Box::new(Assignment{
                    lvalue: Target::Identifier(Identifier{name: "x".into()}),
                    rvalue: Expr::Literal(Literal::BoolLiteral(false))})))];
        let block = Statement::Block(Box::new(Block { statements: statements }));
        assert!(block.execute(&mut environment).is_ok());
        assert_eq!(&Value::Boolean(false),
                   environment.get(&identifier).unwrap());
    }
    #[test]
    fn block_variable_dont_escape_scope() {
        let mut environment = Environment::new();
        let identifier = Identifier { name: "x".into() };
        let expr = Expr::Literal(Literal::NumberLiteral(1.0f64));
        let statements = vec![Statement::VariableDefinitionWithInitalizer(identifier.clone(),
                                                                          expr)];
        let block = Statement::Block(Box::new(Block { statements: statements }));
        assert_eq!(None, block.execute(&mut environment).unwrap());
        // The variable declaration gets lost when we exit the scope
        assert_eq!(None, environment.get(&identifier));
    }

}
<|endoftext|>