<commit_before>use crate::{logging, plugins, projects, schemas, serve, upgrade};
use eyre::{bail, Result};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use validator::Validate;

#[derive(Debug, Default, PartialEq, Clone, JsonSchema, Deserialize, Serialize, Validate)]
#[serde(default)]
#[schemars(deny_unknown_fields)]
pub struct Config {
    #[validate]
    pub projects: projects::config::ProjectsConfig,

    #[validate]
    pub logging: logging::config::LoggingConfig,

    #[validate]
    pub serve: serve::config::ServeConfig,

    #[validate]
    pub plugins: plugins::config::PluginsConfig,

    #[validate]
    pub upgrade: upgrade::config::UpgradeConfig,
}

/// Get the JSON Schema for the configuration
pub fn schema() -> Result<serde_json::Value> {
    schemas::generate::<Config>()
}

/// Get the directory where configuration data is stored
pub fn dir(ensure: bool) -> Result<PathBuf> {
    let config_base = dirs_next::config_dir().unwrap_or_else(|| env::current_dir().unwrap());
    let dir = match env::consts::OS {
        "macos" => config_base.join("Stencila"),
        "windows" => config_base.join("Stencila").join("Config"),
        _ => config_base.join("stencila"),
    };
    if ensure {
        fs::create_dir_all(&dir)?;
    }
    Ok(dir)
}

const CONFIG_FILE: &str = "config.toml";

/// Get the path of the configuration file
#[tracing::instrument]
fn path() -> Result<PathBuf> {
    #[cfg(not(test))]
    return Ok(dir(true)?.join(CONFIG_FILE));

    // When running tests, avoid messing with users existing config
    #[cfg(test)]
    {
        let path = std::env::temp_dir()
            .join("stencila")
            .join("test")
            .join(CONFIG_FILE);
        fs::create_dir_all(path.parent().unwrap())?;
        Ok(path)
    }
}

/// Read a config from the configuration file
#[tracing::instrument]
pub fn read() -> Result<Config> {
    let config_file = path()?;

    let content = if config_file.exists() {
        match fs::read_to_string(config_file.clone()) {
            Ok(content) => content,
            // If there was a problem reading the config file warn the user
            // but return an empty string.
            Err(error) => {
                tracing::warn!("Error while reading config file: {}", error.to_string());
                String::new()
            }
        }
    } else {
        // If there is no config file just return an empty string
        String::new()
    };

    let config = match toml::from_str(content.as_str()) {
        Ok(config) => config,
        // If there was a problem reading the config file. e.g syntax error, a change
        // in the enum variants for an option, then warn the user and return the
        // defaults instead. This avoids a "corrupt" config file from preventing the
        // user from doing anything.
        Err(error) => {
            tracing::warn!(
                "Error while parsing config file; use `stencila config reset all` or edit {} to fix; defaults will be used: {}",
                config_file.display(),
                error.to_string()
            );
            Config {
                ..Default::default()
            }
        }
    };

    if let Err(errors) = config.validate() {
        tracing::error!(
            "Invalid config file; use `stencila config set`, `stencila config reset` or edit {} to fix.\n\n{}",
            config_file.display(),
            serde_json::to_string_pretty(&errors)?
        )
    }

    Ok(config)
}

/// Write a config to the configuration file
#[tracing::instrument]
pub fn write(config: &Config) -> Result<()> {
    let config_file = path()?;

    let mut file = fs::File::create(config_file)?;
    file.write_all(toml::to_string(&config)?.as_bytes())?;
    Ok(())
}

/// Ensure that a string is a valid JSON pointer
///
/// Replaces dots (`.`) with slashes (`/`) and ensures a
/// leading slash.
#[tracing::instrument]
pub fn json_pointer(pointer: &str) -> String {
    let pointer = pointer.replace(".", "/");
    if pointer.starts_with('/') {
        pointer
    } else {
        format!("/{}", pointer)
    }
}

/// Get a config property
#[tracing::instrument]
pub fn get(config: &Config, pointer: Option<String>) -> Result<serde_json::Value> {
    match pointer {
        None => Ok(serde_json::to_value(config)?),
        Some(pointer) => {
            let config = serde_json::to_value(config)?;
            if let Some(part) = config.pointer(json_pointer(&pointer).as_str()) {
                let json = serde_json::to_string(part)?;
                let part: toml::Value = serde_json::from_str(&json)?;
                Ok(serde_json::to_value(part)?)
            } else {
                bail!("No configuration value at pointer: {}", pointer)
            }
        }
    }
}

/// Validate a configuration
#[tracing::instrument]
pub fn validate(config: &Config) -> Result<()> {
    if let Err(errors) = config.validate() {
        bail!(
            "Invalid configuration value/s:\n\n{}",
            serde_json::to_string_pretty(&errors)?
        )
    } else {
        Ok(())
    }
}

/// Set a config property
#[tracing::instrument]
pub fn set(config: &Config, pointer: &str, value: &str) -> Result<Config> {
    let mut config = serde_json::to_value(config)?;
    if let Some(property) = config.pointer_mut(json_pointer(&pointer).as_str()) {
        let value = match serde_json::from_str(&value) {
            Ok(value) => value,
            Err(_) => serde_json::Value::String(value.into()),
        };
        *property = value;
    } else {
        bail!("No configuration value at pointer: {}", pointer)
    };

    let config: Config = serde_json::from_value(config)?;
    validate(&config)?;
    Ok(config)
}

/// Reset a config property
#[tracing::instrument]
pub fn reset(config: &Config, property: &str) -> Result<Config> {
    Ok(match property {
        "all" => Default::default(),
        "logging" => Config {
            logging: Default::default(),
            ..config.clone()
        },
        "plugins" => Config {
            plugins: Default::default(),
            ..config.clone()
        },
        "serve" => Config {
            serve: Default::default(),
            ..config.clone()
        },
        "upgrade" => Config {
            upgrade: Default::default(),
            ..config.clone()
        },
        _ => bail!("No top level configuration property named: {}", property),
    })
}

/// CLI options for the `config` command
#[cfg(feature = "cli")]
pub mod cli {
    use super::*;
    use crate::cli::display;
    use structopt::StructOpt;

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Manage configuration settings",
        setting = structopt::clap::AppSettings::ColoredHelp,
        setting = structopt::clap::AppSettings::VersionlessSubcommands
    )]
    pub struct Command {
        #[structopt(subcommand)]
        pub action: Action,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        setting = structopt::clap::AppSettings::DeriveDisplayOrder
    )]
    pub enum Action {
        Get(Get),
        Set(Set),
        Reset(Reset),

        #[structopt(
            about = "Get the directories used for config, cache etc",
            setting = structopt::clap::AppSettings::ColoredHelp
        )]
        Dirs,

        #[structopt(
            about = "Get the JSON Schema for the configuration",
            setting = structopt::clap::AppSettings::ColoredHelp
        )]
        Schema,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Get configuration properties",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Get {
        /// A pointer to a config property e.g. `upgrade.auto`
        pub pointer: Option<String>,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Set configuration properties",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Set {
        /// A pointer to a config property e.g. `upgrade.auto`
        pub pointer: String,

        /// The value to set the property to
        pub value: String,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Reset configuration properties to their defaults",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Reset {
        /// The config property to reset. Use 'all' to reset the entire config.
        pub property: String,
    }

    pub fn run(args: Command, config: &mut Config) -> display::Result {
        let Command { action } = args;
        match action {
            Action::Get(action) => {
                let Get { pointer } = action;
                let value = get(config, pointer)?;
                display::value(value)
            }
            Action::Set(action) => {
                let Set { pointer, value } = action;
                *config = set(config, &pointer, &value)?;
                write(config)?;
                display::nothing()
            }
            Action::Reset(action) => {
                let Reset { property } = action;
                *config = reset(config, &property)?;
                write(config)?;
                display::nothing()
            }
            Action::Dirs => {
                let config_dir = dir(false)?.display().to_string();
                let logs_dir = crate::logging::config::dir(false)?.display().to_string();
                let plugins_dir = crate::plugins::config::dir(false)?.display().to_string();
                let value = serde_json::json!({
                    "config": config_dir,
                    "logs": logs_dir,
                    "plugins": plugins_dir
                });
                display::value(value)
            }
            Action::Schema => {
                let value = schema()?;
                display::value(value)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn test_path() -> Result<()> {
        let path = path()?;
        assert!(path.starts_with(std::env::temp_dir()));
        Ok(())
    }

    #[test]
    fn test_json_pointer() {
        assert_eq!(json_pointer("a"), "/a");
        assert_eq!(json_pointer("a/b"), "/a/b");
        assert_eq!(json_pointer("/a.b"), "/a/b");
        assert_eq!(json_pointer("a.b.c"), "/a/b/c");
    }

    #[test]
    fn test_display() -> Result<()> {
        let config = Config {
            ..Default::default()
        };

        let all = get(&config, None)?;
        //assert!(all.contains("[serve]\n"));
        //assert!(all.contains("[upgrade]\n"));

        let serve = get(&config, Some("serve".to_string()))?;
        //assert!(!serve.contains("[serve]\n"));
        //assert!(serve.contains("url = "));

        let upgrade = get(&config, Some("upgrade".to_string()))?;
        //assert!(!upgrade.contains("[upgrade]\n"));
        //assert!(upgrade.contains("auto = "));

        assert_eq!(
            get(&config, Some("foo.bar".to_string()))
                .unwrap_err()
                .to_string(),
            "No configuration value at pointer: foo.bar"
        );

        Ok(())
    }

    #[test]
    fn test_set() -> Result<()> {
        let config = Config {
            ..Default::default()
        };

        let result = set(&config, "upgrade.auto", "off")?;
        assert_eq!(result.upgrade.auto, "off");

        let result = set(&config, "upgrade.verbose", "true")?;
        assert_eq!(result.upgrade.verbose, true);

        assert_eq!(
            set(&config, "foo.bar", "baz").unwrap_err().to_string(),
            "No configuration value at pointer: foo.bar"
        );

        Ok(())
    }

    #[test]
    fn test_reset() -> Result<()> {
        let config = serde_json::from_value(json!({
            "upgrade": {
                "auto": "off",
                "verbose": true,
                "confirm": true,
            }
        }))?;

        let default = Config {
            ..Default::default()
        };

        let result = reset(&config, "upgrade")?;
        assert_eq!(result.upgrade.auto, default.upgrade.auto);
        assert_eq!(result.upgrade.verbose, default.upgrade.verbose);

        let result = reset(&config, "all")?;
        assert_eq!(result, default);

        assert_eq!(
            reset(&config, "foo").unwrap_err().to_string(),
            "No top level configuration property named: foo"
        );

        Ok(())
    }

    #[test]
    fn test_cli() -> Result<()> {
        use super::cli::*;

        let mut config = Config {
            ..Default::default()
        };

        run(
            Command {
                action: Action::Get(Get { pointer: None }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Set(Set {
                    pointer: "upgrade.confirm".to_string(),
                    value: "true".to_string(),
                }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Reset(Reset {
                    property: "upgrade".to_string(),
                }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Dirs,
            },
            &mut config,
        )?;

        Ok(())
    }
}
<commit_msg>chore(Tests): Linting<commit_after>use crate::{logging, plugins, projects, schemas, serve, upgrade};
use eyre::{bail, Result};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use validator::Validate;

#[derive(Debug, Default, PartialEq, Clone, JsonSchema, Deserialize, Serialize, Validate)]
#[serde(default)]
#[schemars(deny_unknown_fields)]
pub struct Config {
    #[validate]
    pub projects: projects::config::ProjectsConfig,

    #[validate]
    pub logging: logging::config::LoggingConfig,

    #[validate]
    pub serve: serve::config::ServeConfig,

    #[validate]
    pub plugins: plugins::config::PluginsConfig,

    #[validate]
    pub upgrade: upgrade::config::UpgradeConfig,
}

/// Get the JSON Schema for the configuration
pub fn schema() -> Result<serde_json::Value> {
    schemas::generate::<Config>()
}

/// Get the directory where configuration data is stored
pub fn dir(ensure: bool) -> Result<PathBuf> {
    let config_base = dirs_next::config_dir().unwrap_or_else(|| env::current_dir().unwrap());
    let dir = match env::consts::OS {
        "macos" => config_base.join("Stencila"),
        "windows" => config_base.join("Stencila").join("Config"),
        _ => config_base.join("stencila"),
    };
    if ensure {
        fs::create_dir_all(&dir)?;
    }
    Ok(dir)
}

const CONFIG_FILE: &str = "config.toml";

/// Get the path of the configuration file
#[tracing::instrument]
fn path() -> Result<PathBuf> {
    #[cfg(not(test))]
    return Ok(dir(true)?.join(CONFIG_FILE));

    // When running tests, avoid messing with users existing config
    #[cfg(test)]
    {
        let path = std::env::temp_dir()
            .join("stencila")
            .join("test")
            .join(CONFIG_FILE);
        fs::create_dir_all(path.parent().unwrap())?;
        Ok(path)
    }
}

/// Read a config from the configuration file
#[tracing::instrument]
pub fn read() -> Result<Config> {
    let config_file = path()?;

    let content = if config_file.exists() {
        match fs::read_to_string(config_file.clone()) {
            Ok(content) => content,
            // If there was a problem reading the config file warn the user
            // but return an empty string.
            Err(error) => {
                tracing::warn!("Error while reading config file: {}", error.to_string());
                String::new()
            }
        }
    } else {
        // If there is no config file just return an empty string
        String::new()
    };

    let config = match toml::from_str(content.as_str()) {
        Ok(config) => config,
        // If there was a problem reading the config file. e.g syntax error, a change
        // in the enum variants for an option, then warn the user and return the
        // defaults instead. This avoids a "corrupt" config file from preventing the
        // user from doing anything.
        Err(error) => {
            tracing::warn!(
                "Error while parsing config file; use `stencila config reset all` or edit {} to fix; defaults will be used: {}",
                config_file.display(),
                error.to_string()
            );
            Config {
                ..Default::default()
            }
        }
    };

    if let Err(errors) = config.validate() {
        tracing::error!(
            "Invalid config file; use `stencila config set`, `stencila config reset` or edit {} to fix.\n\n{}",
            config_file.display(),
            serde_json::to_string_pretty(&errors)?
        )
    }

    Ok(config)
}

/// Write a config to the configuration file
#[tracing::instrument]
pub fn write(config: &Config) -> Result<()> {
    let config_file = path()?;

    let mut file = fs::File::create(config_file)?;
    file.write_all(toml::to_string(&config)?.as_bytes())?;
    Ok(())
}

/// Ensure that a string is a valid JSON pointer
///
/// Replaces dots (`.`) with slashes (`/`) and ensures a
/// leading slash.
#[tracing::instrument]
pub fn json_pointer(pointer: &str) -> String {
    let pointer = pointer.replace(".", "/");
    if pointer.starts_with('/') {
        pointer
    } else {
        format!("/{}", pointer)
    }
}

/// Get a config property
#[tracing::instrument]
pub fn get(config: &Config, pointer: Option<String>) -> Result<serde_json::Value> {
    match pointer {
        None => Ok(serde_json::to_value(config)?),
        Some(pointer) => {
            let config = serde_json::to_value(config)?;
            if let Some(part) = config.pointer(json_pointer(&pointer).as_str()) {
                let json = serde_json::to_string(part)?;
                let part: toml::Value = serde_json::from_str(&json)?;
                Ok(serde_json::to_value(part)?)
            } else {
                bail!("No configuration value at pointer: {}", pointer)
            }
        }
    }
}

/// Validate a configuration
#[tracing::instrument]
pub fn validate(config: &Config) -> Result<()> {
    if let Err(errors) = config.validate() {
        bail!(
            "Invalid configuration value/s:\n\n{}",
            serde_json::to_string_pretty(&errors)?
        )
    } else {
        Ok(())
    }
}

/// Set a config property
#[tracing::instrument]
pub fn set(config: &Config, pointer: &str, value: &str) -> Result<Config> {
    let mut config = serde_json::to_value(config)?;
    if let Some(property) = config.pointer_mut(json_pointer(&pointer).as_str()) {
        let value = match serde_json::from_str(&value) {
            Ok(value) => value,
            Err(_) => serde_json::Value::String(value.into()),
        };
        *property = value;
    } else {
        bail!("No configuration value at pointer: {}", pointer)
    };

    let config: Config = serde_json::from_value(config)?;
    validate(&config)?;
    Ok(config)
}

/// Reset a config property
#[tracing::instrument]
pub fn reset(config: &Config, property: &str) -> Result<Config> {
    Ok(match property {
        "all" => Default::default(),
        "logging" => Config {
            logging: Default::default(),
            ..config.clone()
        },
        "plugins" => Config {
            plugins: Default::default(),
            ..config.clone()
        },
        "serve" => Config {
            serve: Default::default(),
            ..config.clone()
        },
        "upgrade" => Config {
            upgrade: Default::default(),
            ..config.clone()
        },
        _ => bail!("No top level configuration property named: {}", property),
    })
}

/// CLI options for the `config` command
#[cfg(feature = "cli")]
pub mod cli {
    use super::*;
    use crate::cli::display;
    use structopt::StructOpt;

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Manage configuration settings",
        setting = structopt::clap::AppSettings::ColoredHelp,
        setting = structopt::clap::AppSettings::VersionlessSubcommands
    )]
    pub struct Command {
        #[structopt(subcommand)]
        pub action: Action,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        setting = structopt::clap::AppSettings::DeriveDisplayOrder
    )]
    pub enum Action {
        Get(Get),
        Set(Set),
        Reset(Reset),

        #[structopt(
            about = "Get the directories used for config, cache etc",
            setting = structopt::clap::AppSettings::ColoredHelp
        )]
        Dirs,

        #[structopt(
            about = "Get the JSON Schema for the configuration",
            setting = structopt::clap::AppSettings::ColoredHelp
        )]
        Schema,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Get configuration properties",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Get {
        /// A pointer to a config property e.g. `upgrade.auto`
        pub pointer: Option<String>,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Set configuration properties",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Set {
        /// A pointer to a config property e.g. `upgrade.auto`
        pub pointer: String,

        /// The value to set the property to
        pub value: String,
    }

    #[derive(Debug, StructOpt)]
    #[structopt(
        about = "Reset configuration properties to their defaults",
        setting = structopt::clap::AppSettings::ColoredHelp
    )]
    pub struct Reset {
        /// The config property to reset. Use 'all' to reset the entire config.
        pub property: String,
    }

    pub fn run(args: Command, config: &mut Config) -> display::Result {
        let Command { action } = args;
        match action {
            Action::Get(action) => {
                let Get { pointer } = action;
                let value = get(config, pointer)?;
                display::value(value)
            }
            Action::Set(action) => {
                let Set { pointer, value } = action;
                *config = set(config, &pointer, &value)?;
                write(config)?;
                display::nothing()
            }
            Action::Reset(action) => {
                let Reset { property } = action;
                *config = reset(config, &property)?;
                write(config)?;
                display::nothing()
            }
            Action::Dirs => {
                let config_dir = dir(false)?.display().to_string();
                let logs_dir = crate::logging::config::dir(false)?.display().to_string();
                let plugins_dir = crate::plugins::config::dir(false)?.display().to_string();
                let value = serde_json::json!({
                    "config": config_dir,
                    "logs": logs_dir,
                    "plugins": plugins_dir
                });
                display::value(value)
            }
            Action::Schema => {
                let value = schema()?;
                display::value(value)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn test_path() -> Result<()> {
        let path = path()?;
        assert!(path.starts_with(std::env::temp_dir()));
        Ok(())
    }

    #[test]
    fn test_json_pointer() {
        assert_eq!(json_pointer("a"), "/a");
        assert_eq!(json_pointer("a/b"), "/a/b");
        assert_eq!(json_pointer("/a.b"), "/a/b");
        assert_eq!(json_pointer("a.b.c"), "/a/b/c");
    }

    #[test]
    fn test_display() -> Result<()> {
        let config = Config {
            ..Default::default()
        };

        let _all = get(&config, None)?;
        //assert!(all.contains("[serve]\n"));
        //assert!(all.contains("[upgrade]\n"));

        let _serve = get(&config, Some("serve".to_string()))?;
        //assert!(!serve.contains("[serve]\n"));
        //assert!(serve.contains("url = "));

        let _upgrade = get(&config, Some("upgrade".to_string()))?;
        //assert!(!upgrade.contains("[upgrade]\n"));
        //assert!(upgrade.contains("auto = "));

        assert_eq!(
            get(&config, Some("foo.bar".to_string()))
                .unwrap_err()
                .to_string(),
            "No configuration value at pointer: foo.bar"
        );

        Ok(())
    }

    #[test]
    fn test_set() -> Result<()> {
        let config = Config {
            ..Default::default()
        };

        let result = set(&config, "upgrade.auto", "off")?;
        assert_eq!(result.upgrade.auto, "off");

        let result = set(&config, "upgrade.verbose", "true")?;
        assert_eq!(result.upgrade.verbose, true);

        assert_eq!(
            set(&config, "foo.bar", "baz").unwrap_err().to_string(),
            "No configuration value at pointer: foo.bar"
        );

        Ok(())
    }

    #[test]
    fn test_reset() -> Result<()> {
        let config = serde_json::from_value(json!({
            "upgrade": {
                "auto": "off",
                "verbose": true,
                "confirm": true,
            }
        }))?;

        let default = Config {
            ..Default::default()
        };

        let result = reset(&config, "upgrade")?;
        assert_eq!(result.upgrade.auto, default.upgrade.auto);
        assert_eq!(result.upgrade.verbose, default.upgrade.verbose);

        let result = reset(&config, "all")?;
        assert_eq!(result, default);

        assert_eq!(
            reset(&config, "foo").unwrap_err().to_string(),
            "No top level configuration property named: foo"
        );

        Ok(())
    }

    #[test]
    fn test_cli() -> Result<()> {
        use super::cli::*;

        let mut config = Config {
            ..Default::default()
        };

        run(
            Command {
                action: Action::Get(Get { pointer: None }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Set(Set {
                    pointer: "upgrade.confirm".to_string(),
                    value: "true".to_string(),
                }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Reset(Reset {
                    property: "upgrade".to_string(),
                }),
            },
            &mut config,
        )?;

        run(
            Command {
                action: Action::Dirs,
            },
            &mut config,
        )?;

        Ok(())
    }
}
<|endoftext|>


<commit_before>use ::cssparser::{Parser, ParserInput, Token};
use ::glib::translate::*;
use ::libc;

use std::f64::consts::*;

use drawing_ctx;
use drawing_ctx::RsvgDrawingCtx;
use parsers::ParseError;
use error::*;

/* Keep this in sync with ../../rsvg-private.h:LengthUnit */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LengthUnit {
    Default,
    Percent,
    FontEm,
    FontEx,
    Inch,
    RelativeLarger,
    RelativeSmaller
}

/* Keep this in sync with ../../rsvg-private.h:LengthDir */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LengthDir {
    Horizontal,
    Vertical,
    Both
}

/* This is *not* an opaque struct; it is actually visible to the C code.  It is so
 * that the remaining C code can create RsvgLength values as part of existing
 * structures or objects, without allocations on the heap.
 */
/* Keep this in sync with ../../rsvg-private.h:RsvgLength */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct RsvgLength {
    pub length: f64,
    pub unit: LengthUnit,
    dir: LengthDir
}

impl Default for RsvgLength {
    fn default () -> RsvgLength {
        RsvgLength {
            length: 0.0,
            unit:   LengthUnit::Default,
            dir:    LengthDir::Both
        }
    }
}

const POINTS_PER_INCH: f64 = 72.0;
const CM_PER_INCH:     f64 = 2.54;
const MM_PER_INCH:     f64 = 25.4;
const PICA_PER_INCH:   f64 = 6.0;

fn compute_named_size (name: &str) -> f64 {
    let power: f64;

    match name {
        "xx-small" => { power = -3.0; },
        "x-small"  => { power = -2.0; },
        "small"    => { power = -1.0; },
        "medium"   => { power = 0.0; },
        "large"    => { power = 1.0; },
        "x-large"  => { power = 2.0; },
        "xx-large" => { power = 3.0; },
        _          => { unreachable! (); }
    }

    12.0 * 1.2f64.powf (power) / POINTS_PER_INCH
}

#[no_mangle]
pub extern fn rsvg_length_parse (string: *const libc::c_char, dir: LengthDir) -> RsvgLength {
    let my_string = unsafe { &String::from_glib_none (string) };

    // FIXME: this ignores errors; propagate them upstream
    RsvgLength::parse (my_string, dir).unwrap_or (RsvgLength::default ())
}

/* https://www.w3.org/TR/SVG/types.html#DataTypeLength
 * https://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#length-units
 *
 * Lengths have units.  When they need to be need resolved to
 * units in the user's coordinate system, some unit types
 * need to know if they are horizontal/vertical/both.  For example,
 * a some_object.width="50%" is 50% with respect to the current
 * viewport's width.  In this case, the @dir argument is used
 * inside RsvgLength::normalize(), when it needs to know to what the
 * length refers.
 */

fn make_err () -> AttributeError {
    AttributeError::Parse (ParseError::new ("expected length: number(\"em\" | \"ex\" | \"px\" | \"in\" | \"cm\" | \"mm\" | \"pt\" | \"pc\" | \"%\")?"))
}

impl RsvgLength {
    pub fn parse (string: &str, dir: LengthDir) -> Result <RsvgLength, AttributeError> {
        let mut input = ParserInput::new (string);
        let mut parser = Parser::new (&mut input);

        let token = parser.next ()
            .map_err (|_| AttributeError::Parse (ParseError::new ("expected number and optional symbol, or number and percentage")))?;

        match token {
            &Token::Number { value, .. } => Ok (RsvgLength { length: value as f64,
                                                                           unit:   LengthUnit::Default,
                                                                           dir:    dir }),

            &Token::Percentage { unit_value, .. } => Ok (RsvgLength { length: unit_value as f64,
                                                                                unit:   LengthUnit::Percent,
                                                                                dir:    dir }),

            &Token::Dimension { value, ref unit, .. } => {
                let value = value as f64;

                match unit.as_ref () {
                    "em" => Ok (RsvgLength { length: value,
                                             unit:   LengthUnit::FontEm,
                                             dir:    dir }),

                    "ex" => Ok (RsvgLength { length: value,
                                             unit:   LengthUnit::FontEx,
                                             dir:    dir }),

                    "pt" => Ok (RsvgLength { length: value / POINTS_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir }),

                    "in" => Ok (RsvgLength { length: value,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir }),

                    "cm" => Ok (RsvgLength { length: value / CM_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir }),

                    "mm" => Ok (RsvgLength { length: value / MM_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir }),

                    "pc" => Ok (RsvgLength { length: value / PICA_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir }),

                    "px" => Ok (RsvgLength { length: value,
                                             unit:   LengthUnit::Default,
                                             dir:    dir }),

                    _ => return Err (make_err ());
                }
            },

            // FIXME: why are the following in Length?  They should be in FontSize
            &Token::Ident (ref cow) => match cow.as_ref () {
                "larger" => Ok (RsvgLength { length: 0.0,
                                             unit:   LengthUnit::RelativeLarger,
                                             dir:    dir }),

                "smaller" => Ok (RsvgLength { length: 0.0,
                                              unit:  LengthUnit::RelativeSmaller,
                                              dir:   dir }),

                "xx-small" |
                "x-small" |
                "small" |
                "medium" |
                "large" |
                "x-large" |
                "xx-large" => Ok (RsvgLength { length: compute_named_size (&*string),
                                               unit:   LengthUnit::Inch,
                                               dir:    dir }),

                _ => return Err (make_err ());
            },

            _ => return Err (make_err ());
        };

        .and_then (|r|
                    parser.expect_exhausted ()
                    .map (|_| r)
                    .map_err (|_| make_err ()))
    }

    pub fn normalize (&self, draw_ctx: *const RsvgDrawingCtx) -> f64 {
        match self.unit {
            LengthUnit::Default => {
                self.length
            },

            LengthUnit::Percent => {
                let (width, height) = drawing_ctx::get_view_box_size (draw_ctx);

                match self.dir {
                    LengthDir::Horizontal => { self.length * width },
                    LengthDir::Vertical   => { self.length * height },
                    LengthDir::Both       => { self.length * viewport_percentage (width, height) }
                }
            },

            LengthUnit::FontEm => {
                self.length * drawing_ctx::get_normalized_font_size (draw_ctx)
            },

            LengthUnit::FontEx => {
                self.length * drawing_ctx::get_normalized_font_size (draw_ctx) / 2.0
            },

            LengthUnit::Inch => {
                let (dpi_x, dpi_y) = drawing_ctx::get_dpi (draw_ctx);

                match self.dir {
                    LengthDir::Horizontal => { self.length * dpi_x },
                    LengthDir::Vertical   => { self.length * dpi_y },
                    LengthDir::Both       => { self.length * viewport_percentage (dpi_x, dpi_y) }
                }
            },

            // FIXME: these are pending: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#propdef-font-size
            LengthUnit::RelativeLarger |
            LengthUnit::RelativeSmaller => { 0.0 }
        }
    }

    pub fn hand_normalize (&self,
                           pixels_per_inch: f64,
                           width_or_height: f64,
                           font_size: f64) -> f64 {
        match self.unit {
            LengthUnit::Default => { self.length },

            LengthUnit::Percent => { self.length * width_or_height },

            LengthUnit::FontEm => { self.length * font_size },

            LengthUnit::FontEx => { self.length * font_size / 2.0 },

            LengthUnit::Inch => { self.length * pixels_per_inch },

            _ => { 0.0 }
        }
    }
}

fn viewport_percentage (x: f64, y: f64) -> f64 {
    /* https://www.w3.org/TR/SVG/coords.html#Units
     *
     * "For any other length value expressed as a percentage of the viewport, the
     * percentage is calculated as the specified percentage of
     * sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)."
     */
    return (x * x + y * y).sqrt () / SQRT_2;
}

#[no_mangle]
pub extern fn rsvg_length_normalize (raw_length: *const RsvgLength, draw_ctx: *const RsvgDrawingCtx) -> f64 {
    assert! (!raw_length.is_null ());

    let length: &RsvgLength = unsafe { &*raw_length };

    length.normalize (draw_ctx)
}

#[no_mangle]
pub extern fn rsvg_length_hand_normalize (raw_length: *const RsvgLength,
                                          pixels_per_inch: f64,
                                          width_or_height: f64,
                                          font_size: f64) -> f64 {
    assert! (!raw_length.is_null ());

    let length: &RsvgLength = unsafe { &*raw_length };

    length.hand_normalize (pixels_per_inch, width_or_height, font_size)
}

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

    #[test]
    fn parses_default () {
        assert_eq! (RsvgLength::parse ("42", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: 42.0,
                        unit:   LengthUnit::Default,
                        dir:    LengthDir::Horizontal}));

        assert_eq! (RsvgLength::parse ("-42px", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: -42.0,
                        unit:   LengthUnit::Default,
                        dir:    LengthDir::Horizontal}));
    }

    #[test]
    fn parses_percent () {
        assert_eq! (RsvgLength::parse ("50.0%", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: 0.5,
                        unit:   LengthUnit::Percent,
                        dir:    LengthDir::Horizontal}));
    }

    #[test]
    fn parses_font_em () {
        assert_eq! (RsvgLength::parse ("22.5em", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 22.5,
                        unit:   LengthUnit::FontEm,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_font_ex () {
        assert_eq! (RsvgLength::parse ("22.5ex", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 22.5,
                        unit:   LengthUnit::FontEx,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_physical_units () {
        assert_eq! (RsvgLength::parse ("72pt", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 1.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("-22.5in", LengthDir::Both),
                    Ok (RsvgLength {
                        length: -22.5,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("-254cm", LengthDir::Both),
                    Ok (RsvgLength {
                        length: -100.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("254mm", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 10.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("60pc", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 10.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));
    }

    #[test]
    fn parses_relative_larger () {
        assert_eq! (RsvgLength::parse ("larger", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 0.0,
                        unit:   LengthUnit::RelativeLarger,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_relative_smaller () {
        assert_eq! (RsvgLength::parse ("smaller", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 0.0,
                        unit:   LengthUnit::RelativeSmaller,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_named_sizes () {
        let names = vec![ "xx-small",
                           "x-small",
                           "small",
                           "medium",
                           "large",
                           "x-large",
                           "xx-large" ];

        let mut previous_value: Option<f64> = None;

        // Just ensure that the values are progressively larger; don't
        // enforce a particular sequence.

        for name in names {
            let length = RsvgLength::parse (name, LengthDir::Both).unwrap ();

            assert_eq! (length.unit, LengthUnit::Inch);
            assert_eq! (length.dir, LengthDir::Both);

            if let Some (v) = previous_value {
                assert! (length.length > v);
            } else {
                previous_value = Some (length.length);
            }

        }
    }

    #[test]
    fn empty_length_yields_error () {
        assert! (is_parse_error (&RsvgLength::parse ("", LengthDir::Both)));
    }

    #[test]
    fn invalid_unit_yields_error () {
        assert! (is_parse_error (&RsvgLength::parse ("8furlong", LengthDir::Both)));
    }

    #[test]
    fn invalid_font_size_yields_error () {
        // FIXME: this is intended to test the (absence of) the "larger" et al values.
        // Since they really be in FontSize, not RsvgLength, we should remember
        // to move this test to that type later.
        assert! (is_parse_error (&RsvgLength::parse ("furlong", LengthDir::Both)));
    }
}
<commit_msg>length.rs: Refactor parser to not have Ok() all over the place<commit_after>use ::cssparser::{Parser, ParserInput, Token};
use ::glib::translate::*;
use ::libc;

use std::f64::consts::*;

use drawing_ctx;
use drawing_ctx::RsvgDrawingCtx;
use parsers::ParseError;
use error::*;

/* Keep this in sync with ../../rsvg-private.h:LengthUnit */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LengthUnit {
    Default,
    Percent,
    FontEm,
    FontEx,
    Inch,
    RelativeLarger,
    RelativeSmaller
}

/* Keep this in sync with ../../rsvg-private.h:LengthDir */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LengthDir {
    Horizontal,
    Vertical,
    Both
}

/* This is *not* an opaque struct; it is actually visible to the C code.  It is so
 * that the remaining C code can create RsvgLength values as part of existing
 * structures or objects, without allocations on the heap.
 */
/* Keep this in sync with ../../rsvg-private.h:RsvgLength */
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct RsvgLength {
    pub length: f64,
    pub unit: LengthUnit,
    dir: LengthDir
}

impl Default for RsvgLength {
    fn default () -> RsvgLength {
        RsvgLength {
            length: 0.0,
            unit:   LengthUnit::Default,
            dir:    LengthDir::Both
        }
    }
}

const POINTS_PER_INCH: f64 = 72.0;
const CM_PER_INCH:     f64 = 2.54;
const MM_PER_INCH:     f64 = 25.4;
const PICA_PER_INCH:   f64 = 6.0;

fn compute_named_size (name: &str) -> f64 {
    let power: f64;

    match name {
        "xx-small" => { power = -3.0; },
        "x-small"  => { power = -2.0; },
        "small"    => { power = -1.0; },
        "medium"   => { power = 0.0; },
        "large"    => { power = 1.0; },
        "x-large"  => { power = 2.0; },
        "xx-large" => { power = 3.0; },
        _          => { unreachable! (); }
    }

    12.0 * 1.2f64.powf (power) / POINTS_PER_INCH
}

#[no_mangle]
pub extern fn rsvg_length_parse (string: *const libc::c_char, dir: LengthDir) -> RsvgLength {
    let my_string = unsafe { &String::from_glib_none (string) };

    // FIXME: this ignores errors; propagate them upstream
    RsvgLength::parse (my_string, dir).unwrap_or (RsvgLength::default ())
}

/* https://www.w3.org/TR/SVG/types.html#DataTypeLength
 * https://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#length-units
 *
 * Lengths have units.  When they need to be need resolved to
 * units in the user's coordinate system, some unit types
 * need to know if they are horizontal/vertical/both.  For example,
 * a some_object.width="50%" is 50% with respect to the current
 * viewport's width.  In this case, the @dir argument is used
 * inside RsvgLength::normalize(), when it needs to know to what the
 * length refers.
 */

fn make_err () -> AttributeError {
    AttributeError::Parse (ParseError::new ("expected length: number(\"em\" | \"ex\" | \"px\" | \"in\" | \"cm\" | \"mm\" | \"pt\" | \"pc\" | \"%\")?"))
}

impl RsvgLength {
    pub fn parse (string: &str, dir: LengthDir) -> Result <RsvgLength, AttributeError> {
        let mut input = ParserInput::new (string);
        let mut parser = Parser::new (&mut input);

        let length = {
            let token = parser.next ()
                .map_err (|_| AttributeError::Parse (ParseError::new ("expected number and optional symbol, or number and percentage")))?;

            match token {
                &Token::Number { value, .. } => RsvgLength { length: value as f64,
                                                             unit:   LengthUnit::Default,
                                                             dir:    dir },

                &Token::Percentage { unit_value, .. } => RsvgLength { length: unit_value as f64,
                                                                      unit:   LengthUnit::Percent,
                                                                      dir:    dir },

                &Token::Dimension { value, ref unit, .. } => {
                    let value = value as f64;

                    match unit.as_ref () {
                        "em" => RsvgLength { length: value,
                                             unit:   LengthUnit::FontEm,
                                             dir:    dir },

                        "ex" => RsvgLength { length: value,
                                             unit:   LengthUnit::FontEx,
                                             dir:    dir },

                        "pt" => RsvgLength { length: value / POINTS_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir },

                        "in" => RsvgLength { length: value,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir },

                        "cm" => RsvgLength { length: value / CM_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir },

                        "mm" => RsvgLength { length: value / MM_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir },

                        "pc" => RsvgLength { length: value / PICA_PER_INCH,
                                             unit:   LengthUnit::Inch,
                                             dir:    dir },

                        "px" => RsvgLength { length: value,
                                             unit:   LengthUnit::Default,
                                             dir:    dir },

                        _ => return Err (make_err ())
                    }
                },

                // FIXME: why are the following in Length?  They should be in FontSize
                &Token::Ident (ref cow) => match cow.as_ref () {
                    "larger" => RsvgLength { length: 0.0,
                                             unit:   LengthUnit::RelativeLarger,
                                             dir:    dir },

                    "smaller" => RsvgLength { length: 0.0,
                                              unit:  LengthUnit::RelativeSmaller,
                                              dir:   dir },

                    "xx-small" |
                    "x-small" |
                    "small" |
                    "medium" |
                    "large" |
                    "x-large" |
                    "xx-large" => RsvgLength { length: compute_named_size (&*string),
                                               unit:   LengthUnit::Inch,
                                               dir:    dir },

                    _ => return Err (make_err ())
                },

                _ => return Err (make_err ())
            }
        };

        parser.expect_exhausted ().map_err (|_| make_err ())?;

        Ok (length)
    }

    pub fn normalize (&self, draw_ctx: *const RsvgDrawingCtx) -> f64 {
        match self.unit {
            LengthUnit::Default => {
                self.length
            },

            LengthUnit::Percent => {
                let (width, height) = drawing_ctx::get_view_box_size (draw_ctx);

                match self.dir {
                    LengthDir::Horizontal => { self.length * width },
                    LengthDir::Vertical   => { self.length * height },
                    LengthDir::Both       => { self.length * viewport_percentage (width, height) }
                }
            },

            LengthUnit::FontEm => {
                self.length * drawing_ctx::get_normalized_font_size (draw_ctx)
            },

            LengthUnit::FontEx => {
                self.length * drawing_ctx::get_normalized_font_size (draw_ctx) / 2.0
            },

            LengthUnit::Inch => {
                let (dpi_x, dpi_y) = drawing_ctx::get_dpi (draw_ctx);

                match self.dir {
                    LengthDir::Horizontal => { self.length * dpi_x },
                    LengthDir::Vertical   => { self.length * dpi_y },
                    LengthDir::Both       => { self.length * viewport_percentage (dpi_x, dpi_y) }
                }
            },

            // FIXME: these are pending: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#propdef-font-size
            LengthUnit::RelativeLarger |
            LengthUnit::RelativeSmaller => { 0.0 }
        }
    }

    pub fn hand_normalize (&self,
                           pixels_per_inch: f64,
                           width_or_height: f64,
                           font_size: f64) -> f64 {
        match self.unit {
            LengthUnit::Default => { self.length },

            LengthUnit::Percent => { self.length * width_or_height },

            LengthUnit::FontEm => { self.length * font_size },

            LengthUnit::FontEx => { self.length * font_size / 2.0 },

            LengthUnit::Inch => { self.length * pixels_per_inch },

            _ => { 0.0 }
        }
    }
}

fn viewport_percentage (x: f64, y: f64) -> f64 {
    /* https://www.w3.org/TR/SVG/coords.html#Units
     *
     * "For any other length value expressed as a percentage of the viewport, the
     * percentage is calculated as the specified percentage of
     * sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)."
     */
    return (x * x + y * y).sqrt () / SQRT_2;
}

#[no_mangle]
pub extern fn rsvg_length_normalize (raw_length: *const RsvgLength, draw_ctx: *const RsvgDrawingCtx) -> f64 {
    assert! (!raw_length.is_null ());

    let length: &RsvgLength = unsafe { &*raw_length };

    length.normalize (draw_ctx)
}

#[no_mangle]
pub extern fn rsvg_length_hand_normalize (raw_length: *const RsvgLength,
                                          pixels_per_inch: f64,
                                          width_or_height: f64,
                                          font_size: f64) -> f64 {
    assert! (!raw_length.is_null ());

    let length: &RsvgLength = unsafe { &*raw_length };

    length.hand_normalize (pixels_per_inch, width_or_height, font_size)
}

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

    #[test]
    fn parses_default () {
        assert_eq! (RsvgLength::parse ("42", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: 42.0,
                        unit:   LengthUnit::Default,
                        dir:    LengthDir::Horizontal}));

        assert_eq! (RsvgLength::parse ("-42px", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: -42.0,
                        unit:   LengthUnit::Default,
                        dir:    LengthDir::Horizontal}));
    }

    #[test]
    fn parses_percent () {
        assert_eq! (RsvgLength::parse ("50.0%", LengthDir::Horizontal),
                    Ok (RsvgLength {
                        length: 0.5,
                        unit:   LengthUnit::Percent,
                        dir:    LengthDir::Horizontal}));
    }

    #[test]
    fn parses_font_em () {
        assert_eq! (RsvgLength::parse ("22.5em", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 22.5,
                        unit:   LengthUnit::FontEm,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_font_ex () {
        assert_eq! (RsvgLength::parse ("22.5ex", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 22.5,
                        unit:   LengthUnit::FontEx,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_physical_units () {
        assert_eq! (RsvgLength::parse ("72pt", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 1.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("-22.5in", LengthDir::Both),
                    Ok (RsvgLength {
                        length: -22.5,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("-254cm", LengthDir::Both),
                    Ok (RsvgLength {
                        length: -100.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("254mm", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 10.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));

        assert_eq! (RsvgLength::parse ("60pc", LengthDir::Both),
                    Ok (RsvgLength {
                        length: 10.0,
                        unit:   LengthUnit::Inch,
                        dir:    LengthDir::Both }));
    }

    #[test]
    fn parses_relative_larger () {
        assert_eq! (RsvgLength::parse ("larger", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 0.0,
                        unit:   LengthUnit::RelativeLarger,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_relative_smaller () {
        assert_eq! (RsvgLength::parse ("smaller", LengthDir::Vertical),
                    Ok (RsvgLength {
                        length: 0.0,
                        unit:   LengthUnit::RelativeSmaller,
                        dir:    LengthDir::Vertical }));
    }

    #[test]
    fn parses_named_sizes () {
        let names = vec![ "xx-small",
                           "x-small",
                           "small",
                           "medium",
                           "large",
                           "x-large",
                           "xx-large" ];

        let mut previous_value: Option<f64> = None;

        // Just ensure that the values are progressively larger; don't
        // enforce a particular sequence.

        for name in names {
            let length = RsvgLength::parse (name, LengthDir::Both).unwrap ();

            assert_eq! (length.unit, LengthUnit::Inch);
            assert_eq! (length.dir, LengthDir::Both);

            if let Some (v) = previous_value {
                assert! (length.length > v);
            } else {
                previous_value = Some (length.length);
            }

        }
    }

    #[test]
    fn empty_length_yields_error () {
        assert! (is_parse_error (&RsvgLength::parse ("", LengthDir::Both)));
    }

    #[test]
    fn invalid_unit_yields_error () {
        assert! (is_parse_error (&RsvgLength::parse ("8furlong", LengthDir::Both)));
    }

    #[test]
    fn invalid_font_size_yields_error () {
        // FIXME: this is intended to test the (absence of) the "larger" et al values.
        // Since they really be in FontSize, not RsvgLength, we should remember
        // to move this test to that type later.
        assert! (is_parse_error (&RsvgLength::parse ("furlong", LengthDir::Both)));
    }
}
<|endoftext|>


<commit_before>enum State {
    Whitespace,
    IntegralPart,
    FractionalPart,
    ExponentSign,
    Exponent
}

pub fn strtod (string: &str) -> (f64, &str) {
    let mut state = State::Whitespace;
    let mut value: f64 = 0.0;
    let mut sign: f64 = 1.0;
    let mut fraction: f64 = 1.0;
    let mut exponent_sign: f64 = 1.0;
    let mut exponent: f64 = 0.0;
    let mut last_pos: usize = 0;

    for (pos, c) in string.chars ().enumerate () {
        last_pos = pos;

        match state {
            State::Whitespace => {
                if c.is_whitespace () {
                    last_pos += 1;
                    continue;
                } else if c == '+' || c == '-' {
                    if c == '-' {
                        sign = -1.0;
                    }

                    last_pos += 1;
                    state = State::IntegralPart;
                } else if c.is_digit (10) {
                    state = State::IntegralPart;
                    value = (c as i32 - '0' as i32) as f64;
                    last_pos += 1;
                    println! ("sign: value={}", value);
                } else if c == '.' {
                    last_pos += 1;
                    state = State::FractionalPart;
                } else {
                    break;
                }
            },

            State::IntegralPart => {
                if c.is_digit (10) {
                    value = value * 10.0 + (c as i32 - '0' as i32) as f64;
                    last_pos += 1;
                    println! ("int: value={}", value);
                } else if c == '.' {
                    last_pos += 1;
                    state = State::FractionalPart;
                } else if c == 'e' || c == 'E' {
                    last_pos += 1;
                    state = State::ExponentSign;
                } else {
                    break;
                }
            },

            State::FractionalPart => {
                if c.is_digit (10) {
                    fraction *= 0.1;
                    value += fraction * (c as i32 - '0' as i32) as f64;
                    println! ("frac: value={}", value);
                    last_pos += 1;
                } else if c == 'e' || c == 'E' {
                    last_pos += 1;
                    state = State::ExponentSign;
                } else {
                    break;
                }
            },

            State::ExponentSign => {
                if c == '+' || c == '-' {
                    if c == '-' {
                        exponent_sign = -1.0;
                    }

                    last_pos += 1;
                    state = State::Exponent;
                } else if c.is_digit (10) {
                    last_pos += 1;
                    exponent = (c as i32 - '0' as i32) as f64;
                } else {
                    break;
                }
            },

            State::Exponent => {
                if c.is_digit (10) {
                    last_pos += 1;
                    exponent = exponent * 10.0 + (c as i32 - '0' as i32) as f64;
                } else {
                    break;
                }
            }
        }
    }

    // return tuple with the value and the non-matching slice

    (sign * value * 10.0f64.powf (exponent * exponent_sign),
     &string[last_pos .. ])
}

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

    #[test]
    fn handles_empty_string () {
        let str = "";
        assert_eq! (strtod (str), (0.0f64, &str[0..]));
    }

    #[test]
    fn handles_integer () {
        let str = "-123foo";
        assert_eq! (strtod (str), (-123.0f64, &str[4..]));

        let str = "12345";
        assert_eq! (strtod (str), (12345.0f64, &str[5..]));
    }

    #[test]
    fn handles_float () {
        let str = "-123.25";
        assert_eq! (strtod (str), (-123.25f64, &str[7..]));

        let str = "123.25bar";
        assert_eq! (strtod (str), (123.25f64, &str[6..]));
    }

    #[test]
    fn handles_dot_numbers () {
        let str = "-.25foo";
        assert_eq! (strtod (str), (-0.25f64, &str[4..]));

        let str = ".25";
        assert_eq! (strtod (str), (0.25f64, &str[3..]));
    }

    #[test]
    fn handles_dot () {
        let str = "-.";
        assert_eq! (strtod (str), (-0.0, &str[2..]));

        let str = ".bar";
        assert_eq! (strtod (str), (0.0, &str[1..]));
    }

    #[test]
    fn handles_exponent () {
        let str = "-123.45e2foo";
        assert_eq! (strtod (str), (-12345.0, &str[9..]));

        let str = "123.45E2";
        assert_eq! (strtod (str), (12345.0, &str[8..]));
    }

    #[test]
    fn handles_negative_exponent () {
        let str = "-123.25e-2";
        assert_eq! (strtod (str), (-1.2325, &str[10..]));

        let str = "123.25E-2bar";
        assert_eq! (strtod (str), (1.2325, &str[9..]));
    }
}
<commit_msg>strtod.rs: Move the last_pos housekeeping until the end<commit_after>enum State {
    Whitespace,
    IntegralPart,
    FractionalPart,
    ExponentSign,
    Exponent
}

pub fn strtod (string: &str) -> (f64, &str) {
    let mut state = State::Whitespace;
    let mut value: f64 = 0.0;
    let mut sign: f64 = 1.0;
    let mut fraction: f64 = 1.0;
    let mut exponent_sign: f64 = 1.0;
    let mut exponent: f64 = 0.0;
    let mut last_pos: usize = 0;

    for (pos, c) in string.chars ().enumerate () {
        last_pos = pos;

        match state {
            State::Whitespace => {
                if c.is_whitespace () {
                    continue;
                } else if c == '+' || c == '-' {
                    if c == '-' {
                        sign = -1.0;
                    }

                    state = State::IntegralPart;
                } else if c.is_digit (10) {
                    state = State::IntegralPart;
                    value = (c as i32 - '0' as i32) as f64;
                } else if c == '.' {
                    state = State::FractionalPart;
                } else {
                    break;
                }
            },

            State::IntegralPart => {
                if c.is_digit (10) {
                    value = value * 10.0 + (c as i32 - '0' as i32) as f64;
                } else if c == '.' {
                    state = State::FractionalPart;
                } else if c == 'e' || c == 'E' {
                    state = State::ExponentSign;
                } else {
                    break;
                }
            },

            State::FractionalPart => {
                if c.is_digit (10) {
                    fraction *= 0.1;
                    value += fraction * (c as i32 - '0' as i32) as f64;
                } else if c == 'e' || c == 'E' {
                    state = State::ExponentSign;
                } else {
                    break;
                }
            },

            State::ExponentSign => {
                if c == '+' || c == '-' {
                    if c == '-' {
                        exponent_sign = -1.0;
                    }

                    state = State::Exponent;
                } else if c.is_digit (10) {
                    exponent = (c as i32 - '0' as i32) as f64;
                } else {
                    break;
                }
            },

            State::Exponent => {
                if c.is_digit (10) {
                    exponent = exponent * 10.0 + (c as i32 - '0' as i32) as f64;
                } else {
                    break;
                }
            }
        }

        last_pos += 1;
    }

    // return tuple with the value and the non-matching slice

    (sign * value * 10.0f64.powf (exponent * exponent_sign),
     &string[last_pos .. ])
}

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

    #[test]
    fn handles_empty_string () {
        let str = "";
        assert_eq! (strtod (str), (0.0f64, &str[0..]));
    }

    #[test]
    fn handles_integer () {
        let str = "-123foo";
        assert_eq! (strtod (str), (-123.0f64, &str[4..]));

        let str = "12345";
        assert_eq! (strtod (str), (12345.0f64, &str[5..]));
    }

    #[test]
    fn handles_float () {
        let str = "-123.25";
        assert_eq! (strtod (str), (-123.25f64, &str[7..]));

        let str = "123.25bar";
        assert_eq! (strtod (str), (123.25f64, &str[6..]));
    }

    #[test]
    fn handles_dot_numbers () {
        let str = "-.25foo";
        assert_eq! (strtod (str), (-0.25f64, &str[4..]));

        let str = ".25";
        assert_eq! (strtod (str), (0.25f64, &str[3..]));
    }

    #[test]
    fn handles_dot () {
        let str = "-.";
        assert_eq! (strtod (str), (-0.0, &str[2..]));

        let str = ".bar";
        assert_eq! (strtod (str), (0.0, &str[1..]));
    }

    #[test]
    fn handles_exponent () {
        let str = "-123.45e2foo";
        assert_eq! (strtod (str), (-12345.0, &str[9..]));

        let str = "123.45E2";
        assert_eq! (strtod (str), (12345.0, &str[8..]));
    }

    #[test]
    fn handles_negative_exponent () {
        let str = "-123.25e-2";
        assert_eq! (strtod (str), (-1.2325, &str[10..]));

        let str = "123.25E-2bar";
        assert_eq! (strtod (str), (1.2325, &str[9..]));
    }
}
<|endoftext|>


<commit_before>//
// Copyright:: Copyright (c) 2016 Chef Software, Inc.
// License:: Apache License, Version 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

use errors::{DeliveryError, Kind};
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::prelude::*;
use utils::{self, read_file};
use utils::say::{say, sayln};
use utils::path_ext::is_file;
use git;
use project;
use regex::Regex;

// Return the path to the metadata.rb file
fn metadata_file(path: &PathBuf) -> String {
    let mut metadata = path.to_str().unwrap().to_string();
    metadata.push_str("/metadata.rb");
    return metadata
}

// Verify if the provided path is a cookbook, or not
fn is_cookbook(path: &PathBuf) -> bool {
    let meta_f = metadata_file(path);
    is_file(&Path::new(&meta_f))
}

// Extract the cookbook version from the provided metadata content
//
// This function expects you to read the metadata in advance and
// pass just the content of the file. From there it will read every
// line and search for the version to return it.
fn metadata_version_from(content: String) -> Result<String, DeliveryError> {
    for l in content.lines() {
        let r = Regex::new(r"version\s+'(?P<version>[0-9]*\.[0-9]*\.[0-9]*)'").unwrap();
        if let Some(version) = r.captures(l) {
            return Ok(version.name("version").unwrap().to_string())
        };
    };
    return Err(DeliveryError{ kind: Kind::MissingMetadataVersion, detail: None })
}

// Bump the patchset of the provided version
fn bump_patchset(version: String) -> Result<String, DeliveryError> {
    let s = version.split(".");
    let m_m_p: Vec<&str> = s.collect();
    let patch = try!(m_m_p[2].parse::<u32>());
    let new_patch: String = (patch + 1).to_string();
    let mut new_version: String = m_m_p[0].to_string();
    new_version.push_str(".");
    new_version.push_str(m_m_p[1]);
    new_version.push_str(".");
    new_version.push_str(&new_patch);
    Ok(new_version)
}

// Saves the new version to the metadata and commit the changes
fn save_version(metadata: &PathBuf, version: String) -> Result<(), DeliveryError> {
    let current_meta = try!(read_file(metadata));
    let current_version = try!(metadata_version_from(current_meta.clone()));
    let new_metadata = current_meta.replace(&*current_version, &*version);

    // Recreate the file and dump the processed contents to it
    let mut recreate_meta = try!(File::create(metadata));
    try!(recreate_meta.write_fmt(format_args!("{}", &new_metadata)));

    // Commit the changes made to the metadata
    let mut commit_msg = String::from("Bump version to ");
    commit_msg.push_str(&version);
    try!(git::git_command(&["add", metadata.to_str().unwrap()], &utils::cwd()));
    try!(git::git_command(&["commit", "-m", &commit_msg], &utils::cwd()));
    Ok(())
}

// @Public

// Bump the metadata version, only if:
// * The project is a cookbook
// * The version hasn't been updated
//
// @param p_root [&PathBuf] The project root path
// @param pipeline [&str] Pipeline the change is targeting to
// @return () if success
pub fn bump_version(p_root: &PathBuf, pipeline: &str) -> Result<(), DeliveryError> {
    if is_cookbook(&p_root) {
        let project = try!(project::project_from_cwd());
        say("white", "Project ");
        say("yellow", &project);
        sayln("white", " is a cookbook");
        sayln("white", "Validating version in metadata");

        let meta_f_p = PathBuf::from(metadata_file(&p_root));
        let current_v = try!(metadata_version_from(try!(read_file(&meta_f_p))));
        let mut t_file = pipeline.to_string();
        t_file.push_str(":metadata.rb");
        let pipeline_meta = try!(git::git_command(&["show", &t_file], &p_root));
        let pipeline_v = try!(metadata_version_from(pipeline_meta.stdout));

        if current_v == pipeline_v {
            say("yellow", "The version hasn't been updated (");
            say("red", &pipeline_v);
            sayln("yellow", ")");
            let new_version = try!(bump_patchset(pipeline_v));
            say("white", "Bumping version to: ");
            sayln("green", &new_version);
            try!(save_version(&meta_f_p, new_version));
        } else {
            say("white", "Version already updated (");
            say("magenta", &pipeline_v);
            say("white", "/");
            say("green", &current_v);
            sayln("white", ")");
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    #[test]
    fn verify_version_bumping_using_bump_patchset() {
        let version = String::from("1.2.3");
        assert_eq!(String::from("1.2.4"), super::bump_patchset(version).unwrap());
    }

    #[test]
    fn return_the_metadata_file_path() {
        let project_path = PathBuf::from("/cookbook");
        assert_eq!(String::from("/cookbook/metadata.rb"), super::metadata_file(&project_path));
    }
}
<commit_msg>Add struct MetadataVersion<commit_after>//
// Copyright:: Copyright (c) 2016 Chef Software, Inc.
// License:: Apache License, Version 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

use errors::{DeliveryError, Kind};
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::prelude::*;
use utils::{self, read_file};
use utils::say::{say, sayln};
use utils::path_ext::is_file;
use git;
use project;
use regex::Regex;

#[derive(Debug, Clone)]
pub struct MetadataVersion {
    pub major: usize,
    pub minor: usize,
    pub patch: usize,
}

impl MetadataVersion {
    pub fn to_string(&self) -> String {
        [
            self.major.to_string(), ".".to_string(),
            self.minor.to_string(), ".".to_string(),
            self.patch.to_string()
        ].concat()
    }
}

// Bump the metadata version, only if:
// * The project is a cookbook
// * The version hasn't been updated
//
// @param p_root [&PathBuf] The project root path
// @param pipeline [&str] Pipeline the change is targeting to
// @return () if success
pub fn bump_version(p_root: &PathBuf, pipeline: &str) -> Result<(), DeliveryError> {
    if is_cookbook(&p_root) {
        let project = try!(project::project_from_cwd());
        say("white", "Project ");
        say("yellow", &project);
        sayln("white", " is a cookbook");
        sayln("white", "Validating version in metadata");

        let meta_f_p = PathBuf::from(metadata_file(&p_root));
        let current_meta_v = try!(metadata_version_from(&try!(read_file(&meta_f_p))));
        let current_v = current_meta_v.to_string();
        let mut t_file = pipeline.to_string();
        t_file.push_str(":metadata.rb");
        let pipeline_meta = try!(git::git_command(&["show", &t_file], &p_root));
        let pipeline_meta_v = try!(metadata_version_from(&pipeline_meta.stdout));
        let pipeline_v = pipeline_meta_v.to_string();

        if current_v == pipeline_v {
            say("yellow", "The version hasn't been updated (");
            say("red", &pipeline_v);
            sayln("yellow", ")");
            let new_meta_version = try!(bump_patchset(pipeline_meta_v.clone()));
            let new_version = new_meta_version.to_string();
            say("white", "Bumping version to: ");
            sayln("green", &new_version);
            try!(save_version(&meta_f_p, new_version));
        } else {
            say("white", "Version already updated (");
            say("magenta", &pipeline_v);
            say("white", "/");
            say("green", &current_v);
            sayln("white", ")");
        }
    }
    Ok(())
}

// @Private

// Return the path to the metadata.rb file
fn metadata_file(path: &PathBuf) -> String {
    let mut metadata = path.to_str().unwrap().to_string();
    metadata.push_str("/metadata.rb");
    return metadata
}

// Verify if the provided path is a cookbook, or not
fn is_cookbook(path: &PathBuf) -> bool {
    let meta_f = metadata_file(path);
    is_file(&Path::new(&meta_f))
}

// Extract the cookbook version from the provided metadata content
//
// This function expects you to read the metadata in advance and
// pass just the content of the file. From there it will read every
// line and search for the version to return it.
//
// There are two valid version formats:
// a) 'x.y.z' - The normal semantic version. (mayor.minor.patch)
// b) 'x.y'   - Where the patchset will be 0 by default. (mayor.minor.0)
fn metadata_version_from(content: &String) -> Result<MetadataVersion, DeliveryError> {
    for l in content.lines() {
        let r_m_m_p = Regex::new(r"version\s+'(?P<major>\d*)\.(?P<minor>\d*)\.(?P<patch>\d*)'").unwrap();
        if let Some(version) = r_m_m_p.captures(l) {
            let ma: usize = try!(version.name("major").unwrap().parse());
            let mi: usize = try!(version.name("minor").unwrap().parse());
            let pa: usize = try!(version.name("patch").unwrap().parse());
            return Ok(MetadataVersion { major: ma, minor: mi, patch: pa });
        }
        let r_m_m = Regex::new(r"version\s+'(?P<major>\d+)\.(?P<minor>\d+)'").unwrap();
        if let Some(version) = r_m_m.captures(l) {
            let ma: usize = try!(version.name("major").unwrap().parse());
            let mi: usize = try!(version.name("minor").unwrap().parse());
            let pa: usize = 0;
            return Ok(MetadataVersion { major: ma, minor: mi, patch: pa })
        }
    };
    return Err(DeliveryError{ kind: Kind::MissingMetadataVersion, detail: None })
}

// Bump the patchset of the provided version
fn bump_patchset(mut version: MetadataVersion) -> Result<MetadataVersion, DeliveryError> {
    version = MetadataVersion { patch: version.patch + 1, .. version };
    Ok(version)
}

// Saves the new version to the metadata and commit the changes
fn save_version(metadata: &PathBuf, version: String) -> Result<(), DeliveryError> {
    let current_meta = try!(read_file(metadata));
    let current_meta_version = try!(metadata_version_from(&current_meta));
    let current_version = current_meta_version.to_string();
    let new_metadata = current_meta.replace(&*current_version, &*version);

    // Recreate the file and dump the processed contents to it
    let mut recreate_meta = try!(File::create(metadata));
    try!(recreate_meta.write(new_metadata.as_bytes()));

    // Commit the changes made to the metadata
    let mut commit_msg = String::from("Bump version to ");
    commit_msg.push_str(&version);
    try!(git::git_command(&["add", metadata.to_str().unwrap()], &utils::cwd()));
    try!(git::git_command(&["commit", "-m", &commit_msg], &utils::cwd()));
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;
    use cookbook::*;

    #[test]
    fn verify_version_bumping_using_bump_patchset() {
        let version = MetadataVersion { major: 1, minor: 2, patch: 3 };
        let MetadataVersion { major: ma, minor: mi, patch: pa } = super::bump_patchset(version).unwrap();
        assert_eq!(ma, 1);
        assert_eq!(mi, 2);
        assert_eq!(pa, 4);
    }

    #[test]
    fn return_the_metadata_file_path() {
        let project_path = PathBuf::from("/cookbook");
        assert_eq!(String::from("/cookbook/metadata.rb"), super::metadata_file(&project_path));
    }

    #[test]
    fn verify_happy_metadata_version_from_content() {
        let happy_version = "1.2.3";
        let happy_metadata_content = metadata_from_version(&happy_version);
        let happy_metadata_version = super::metadata_version_from(&happy_metadata_content).unwrap();
        assert_eq!(String::from(happy_version), happy_metadata_version.to_string());

        let valid_version = "1.2";
        let valid_metadata_content = metadata_from_version(&valid_version);
        let valid_metadata_version = super::metadata_version_from(&valid_metadata_content).unwrap();
        assert_eq!(String::from("1.2.0"), valid_metadata_version.to_string());

        let awesome_version = "123.123.123";
        let awesome_metadata_content = metadata_from_version(&awesome_version);
        let awesome_metadata_version = super::metadata_version_from(&awesome_metadata_content).unwrap();
        assert_eq!(String::from(awesome_version), awesome_metadata_version.to_string());
    }

    #[test]
    fn verify_sad_metadata_version_from_content() {
        let sad_version = "1..";
        let sad_metadata =  metadata_from_version(&sad_version);
        match super::metadata_version_from(&sad_metadata) {
            Ok(_) => assert!(false),
            Err(_) => assert!(true)
        }

        let typo_version = "1.2.";
        let typo_metadata =  metadata_from_version(&typo_version);
        match super::metadata_version_from(&typo_metadata) {
            Ok(_) => assert!(false),
            Err(_) => assert!(true)
        }

        let no_version = "";
        let no_metadata =  metadata_from_version(&no_version);
        match super::metadata_version_from(&no_metadata) {
            Ok(_) => assert!(false),
            Err(_) => assert!(true)
        }
    }

    // Quick helper to render a dummy metadata from a provided version
    fn metadata_from_version(version: &str) -> String {
        String::from(["version '", version, "'"].concat())
    }
}
<|endoftext|>