<commit_before>//! Exported verifier functions

use std::env;
use std::str;
use std::str::FromStr;
use std::sync::Arc;

use clap::{AppSettings, ArgMatches, ErrorKind};
use log::{debug, LevelFilter};
use simplelog::{Config, TerminalMode, TermLogger};

use pact_matching::models::http_utils::HttpAuth;
use pact_matching::s;
use pact_models::PactSpecification;
use pact_verifier::*;
use pact_verifier::callback_executors::HttpRequestProviderStateExecutor;

use super::args;

fn pact_source(matches: &ArgMatches) -> Vec<PactSource> {
  let mut sources = vec![];
  if let Some(values) = matches.values_of("file") {
    sources.extend(values.map(|v| PactSource::File(s!(v))).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("dir") {
    sources.extend(values.map(|v| PactSource::Dir(s!(v))).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("url") {
    sources.extend(values.map(|v| {
      if matches.is_present("user") {
        PactSource::URL(s!(v), matches.value_of("user").map(|user| {
          HttpAuth::User(user.to_string(), matches.value_of("password").map(|p| p.to_string()))
        }))
      } else if matches.is_present("token") {
        PactSource::URL(s!(v), matches.value_of("token").map(|token| HttpAuth::Token(token.to_string())))
      } else {
        PactSource::URL(s!(v), None)
      }
    }).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("broker-url") {
    sources.extend(values.map(|v| {
      if matches.is_present("user") || matches.is_present("token") {
        let name = matches.value_of("provider-name").unwrap().to_string();
        let pending = matches.is_present("enable-pending");
        let wip = matches.value_of("include-wip-pacts-since").map(|wip| wip.to_string());
        let provider_tags = matches.values_of("provider-tags")
        .map_or_else(Vec::new, |tags| tags.map(|tag| tag.to_string()).collect());

        let mut selectors: Vec<ConsumerVersionSelector> = vec![];

        if matches.is_present("consumer-version-selectors") {
          selectors = matches.values_of("consumer-version-selectors")
            .map_or_else(Vec::new, |s| json_to_selectors(s.collect::<Vec<_>>()));
        } else if matches.is_present("consumer-version-tags") {
          selectors = matches.values_of("consumer-version-tags")
            .map_or_else(Vec::new, |tags| consumer_tags_to_selectors(tags.collect::<Vec<_>>()));
        }

        if matches.is_present("token") {
          PactSource::BrokerWithDynamicConfiguration {
            provider_name: name,
            broker_url: v.into(),
            enable_pending: pending,
            include_wip_pacts_since: wip,
            provider_tags,
            selectors: selectors,
            auth: matches.value_of("token").map(|token| HttpAuth::Token(token.to_string())),
            links: vec![]
          }
        } else {
        let auth = matches.value_of("user").map(|user| {
          HttpAuth::User(user.to_string(), matches.value_of("password").map(|p| p.to_string()))
        });
          PactSource::BrokerWithDynamicConfiguration {
            provider_name: name,
            broker_url: v.into(),
            enable_pending: pending,
            include_wip_pacts_since: wip,
            provider_tags,
            selectors: selectors,
            auth,
            links: vec![]
          }
        }
      } else {
        PactSource::BrokerUrl(s!(matches.value_of("provider-name").unwrap()), s!(v), None, vec![])
      }
    }).collect::<Vec<PactSource>>());
  };
  sources
}

fn consumer_tags_to_selectors(tags: Vec<&str>) -> Vec<pact_verifier::ConsumerVersionSelector> {
  tags.iter().map(|t| {
    pact_verifier::ConsumerVersionSelector {
      consumer: None,
      fallback_tag: None,
      tag: t.to_string(),
      latest: Some(true),
    }
  }).collect()
}

fn json_to_selectors(tags: Vec<&str>) -> Vec<pact_verifier::ConsumerVersionSelector> {
  tags.iter().map(|t| serde_json::from_str(t))
  .flatten()
  .collect()
}

fn interaction_filter(matches: &ArgMatches) -> FilterInfo {
  if matches.is_present("filter-description") &&
      (matches.is_present("filter-state") || matches.is_present("filter-no-state")) {
      if matches.is_present("filter-state") {
          FilterInfo::DescriptionAndState(s!(matches.value_of("filter-description").unwrap()),
              s!(matches.value_of("filter-state").unwrap()))
      } else {
          FilterInfo::DescriptionAndState(s!(matches.value_of("filter-description").unwrap()),
              s!(""))
      }
  } else if matches.is_present("filter-description") {
      FilterInfo::Description(s!(matches.value_of("filter-description").unwrap()))
  } else if matches.is_present("filter-state") {
      FilterInfo::State(s!(matches.value_of("filter-state").unwrap()))
  } else if matches.is_present("filter-no-state") {
      FilterInfo::State(s!(""))
  } else {
      FilterInfo::None
  }
}

/// Handles the command line arguments from the running process
pub async fn handle_cli() -> Result<(), i32> {
  let args: Vec<String> = env::args().collect();
  let program = args[0].clone();
  let version = format!("v{}", clap::crate_version!()).as_str().to_owned();
  let app = args::setup_app(program, &version);
  let matches = app
                  .setting(AppSettings::ArgRequiredElseHelp)
                  .setting(AppSettings::ColoredHelp)
                  .get_matches_safe();

  match matches {
    Ok(results) => handle_matches(&results).await,
    Err(ref err) => {
      match err.kind {
          ErrorKind::HelpDisplayed => {
              println!("{}", err.message);
              Ok(())
          },
          ErrorKind::VersionDisplayed => {
              print_version();
              println!();
              Ok(())
          },
          _ => {
              err.exit()
          }
      }
    }
  }
}

// TODO: it's possible to introspect the clap::Error and return it or wrapped error type
// so that the caller could have more control over the error output.
//
// Currently, clap prints things out as if it were a CLI call
#[allow(dead_code, missing_docs)]
pub async fn handle_args(args: Vec<String>) -> Result<(), i32> {
  let program = "pact_verifier_cli".to_string();
  let version = format!("v{}", clap::crate_version!()).as_str().to_owned();
  let app = args::setup_app(program, &version);
  let matches = app
                  .setting(AppSettings::NoBinaryName)
                  .setting(AppSettings::ColorNever)
                  .get_matches_from_safe(args);

  match matches {
    Ok(results) => handle_matches(&results).await,
    Err(ref err) => {
      log::error!("error verifying Pact: {:?} {:?}", err.message, err);
      Err(1)
    }
  }
}

async fn handle_matches(matches: &clap::ArgMatches<'_>) -> Result<(), i32> {
    let level = matches.value_of("loglevel").unwrap_or("warn");
    let log_level = match level {
        "none" => LevelFilter::Off,
        _ => LevelFilter::from_str(level).unwrap()
    };
    TermLogger::init(log_level, Config::default(), TerminalMode::Mixed).unwrap_or_default();
    let provider = ProviderInfo {
      host: s!(matches.value_of("hostname").unwrap_or("localhost")),
      port: matches.value_of("port").map(|port| port.parse::<u16>().unwrap()),
      path: matches.value_of("base-path").unwrap_or("/").into(),
      protocol: s!(matches.value_of("scheme").unwrap_or("http")),
      .. ProviderInfo::default()
    };
    let source = pact_source(matches);
    let filter = interaction_filter(matches);
    let provider_state_executor = Arc::new(HttpRequestProviderStateExecutor {
      state_change_url: matches.value_of("state-change-url").map(|s| s.to_string()),
      state_change_body: !matches.is_present("state-change-as-query"),
      state_change_teardown: matches.is_present("state-change-teardown")
    });

    let options = VerificationOptions {
      publish: matches.is_present("publish"),
      provider_version: matches.value_of("provider-version").map(|v| v.to_string()),
      build_url: matches.value_of("build-url").map(|v| v.to_string()),
      request_filter: None::<Arc<NullRequestFilterExecutor>>,
      provider_tags: matches.values_of("provider-tags").map_or_else(Vec::new, |tags| tags.map(|tag| tag.to_string()).collect()),
      disable_ssl_verification: matches.is_present("disable-ssl-verification"),
      callback_timeout: matches.value_of("callback-timeout").map(|t| t.parse::<u64>().unwrap_or(5000)).unwrap_or(5000),
      .. VerificationOptions::default()
    };

    for s in &source {
      debug!("Pact source to verify = {}", s);
    };

    if verify_provider_async(
        provider,
        source,
        filter,
        matches.values_of_lossy("filter-consumer").unwrap_or_default(),
        options,
        &provider_state_executor
    ).await {
        Ok(())
    } else {
        Err(1)
    }
}

fn print_version() {
  println!("\npact verifier version     : v{}", clap::crate_version!());
  println!("pact specification version: v{}", PactSpecification::V3.version_str());
}
<commit_msg>wip: tidy up selectors code<commit_after>//! Exported verifier functions

use std::env;
use std::str;
use std::str::FromStr;
use std::sync::Arc;

use clap::{AppSettings, ArgMatches, ErrorKind};
use log::{debug, LevelFilter};
use simplelog::{Config, TerminalMode, TermLogger};

use pact_matching::models::http_utils::HttpAuth;
use pact_matching::s;
use pact_models::PactSpecification;
use pact_verifier::*;
use pact_verifier::callback_executors::HttpRequestProviderStateExecutor;

use super::args;

fn pact_source(matches: &ArgMatches) -> Vec<PactSource> {
  let mut sources = vec![];
  if let Some(values) = matches.values_of("file") {
    sources.extend(values.map(|v| PactSource::File(s!(v))).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("dir") {
    sources.extend(values.map(|v| PactSource::Dir(s!(v))).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("url") {
    sources.extend(values.map(|v| {
      if matches.is_present("user") {
        PactSource::URL(s!(v), matches.value_of("user").map(|user| {
          HttpAuth::User(user.to_string(), matches.value_of("password").map(|p| p.to_string()))
        }))
      } else if matches.is_present("token") {
        PactSource::URL(s!(v), matches.value_of("token").map(|token| HttpAuth::Token(token.to_string())))
      } else {
        PactSource::URL(s!(v), None)
      }
    }).collect::<Vec<PactSource>>());
  };
  if let Some(values) = matches.values_of("broker-url") {
    sources.extend(values.map(|v| {
      if matches.is_present("user") || matches.is_present("token") {
        let name = matches.value_of("provider-name").unwrap().to_string();
        let pending = matches.is_present("enable-pending");
        let wip = matches.value_of("include-wip-pacts-since").map(|wip| wip.to_string());
        let provider_tags = matches.values_of("provider-tags")
        .map_or_else(Vec::new, |tags| tags.map(|tag| tag.to_string()).collect());

        let selectors = if matches.is_present("consumer-version-selectors") {
          matches.values_of("consumer-version-selectors")
            .map_or_else(Vec::new, |s| json_to_selectors(s.collect::<Vec<_>>()));
        } else if matches.is_present("consumer-version-tags") {
          matches.values_of("consumer-version-tags")
            .map_or_else(Vec::new, |tags| consumer_tags_to_selectors(tags.collect::<Vec<_>>()));
        } else {
          vec![]
        };

        if matches.is_present("token") {
          PactSource::BrokerWithDynamicConfiguration {
            provider_name: name,
            broker_url: v.into(),
            enable_pending: pending,
            include_wip_pacts_since: wip,
            provider_tags,
            selectors: selectors,
            auth: matches.value_of("token").map(|token| HttpAuth::Token(token.to_string())),
            links: vec![]
          }
        } else {
        let auth = matches.value_of("user").map(|user| {
          HttpAuth::User(user.to_string(), matches.value_of("password").map(|p| p.to_string()))
        });
          PactSource::BrokerWithDynamicConfiguration {
            provider_name: name,
            broker_url: v.into(),
            enable_pending: pending,
            include_wip_pacts_since: wip,
            provider_tags,
            selectors: selectors,
            auth,
            links: vec![]
          }
        }
      } else {
        PactSource::BrokerUrl(s!(matches.value_of("provider-name").unwrap()), s!(v), None, vec![])
      }
    }).collect::<Vec<PactSource>>());
  };
  sources
}

fn consumer_tags_to_selectors(tags: Vec<&str>) -> Vec<pact_verifier::ConsumerVersionSelector> {
  tags.iter().map(|t| {
    pact_verifier::ConsumerVersionSelector {
      consumer: None,
      fallback_tag: None,
      tag: t.to_string(),
      latest: Some(true),
    }
  }).collect()
}

fn json_to_selectors(tags: Vec<&str>) -> Vec<pact_verifier::ConsumerVersionSelector> {
  tags.iter().map(|t| serde_json::from_str(t))
  .flatten()
  .collect()
}

fn interaction_filter(matches: &ArgMatches) -> FilterInfo {
  if matches.is_present("filter-description") &&
      (matches.is_present("filter-state") || matches.is_present("filter-no-state")) {
      if matches.is_present("filter-state") {
          FilterInfo::DescriptionAndState(s!(matches.value_of("filter-description").unwrap()),
              s!(matches.value_of("filter-state").unwrap()))
      } else {
          FilterInfo::DescriptionAndState(s!(matches.value_of("filter-description").unwrap()),
              s!(""))
      }
  } else if matches.is_present("filter-description") {
      FilterInfo::Description(s!(matches.value_of("filter-description").unwrap()))
  } else if matches.is_present("filter-state") {
      FilterInfo::State(s!(matches.value_of("filter-state").unwrap()))
  } else if matches.is_present("filter-no-state") {
      FilterInfo::State(s!(""))
  } else {
      FilterInfo::None
  }
}

/// Handles the command line arguments from the running process
pub async fn handle_cli() -> Result<(), i32> {
  let args: Vec<String> = env::args().collect();
  let program = args[0].clone();
  let version = format!("v{}", clap::crate_version!()).as_str().to_owned();
  let app = args::setup_app(program, &version);
  let matches = app
                  .setting(AppSettings::ArgRequiredElseHelp)
                  .setting(AppSettings::ColoredHelp)
                  .get_matches_safe();

  match matches {
    Ok(results) => handle_matches(&results).await,
    Err(ref err) => {
      match err.kind {
          ErrorKind::HelpDisplayed => {
              println!("{}", err.message);
              Ok(())
          },
          ErrorKind::VersionDisplayed => {
              print_version();
              println!();
              Ok(())
          },
          _ => {
              err.exit()
          }
      }
    }
  }
}

// TODO: it's possible to introspect the clap::Error and return it or wrapped error type
// so that the caller could have more control over the error output.
//
// Currently, clap prints things out as if it were a CLI call
#[allow(dead_code, missing_docs)]
pub async fn handle_args(args: Vec<String>) -> Result<(), i32> {
  let program = "pact_verifier_cli".to_string();
  let version = format!("v{}", clap::crate_version!()).as_str().to_owned();
  let app = args::setup_app(program, &version);
  let matches = app
                  .setting(AppSettings::NoBinaryName)
                  .setting(AppSettings::ColorNever)
                  .get_matches_from_safe(args);

  match matches {
    Ok(results) => handle_matches(&results).await,
    Err(ref err) => {
      log::error!("error verifying Pact: {:?} {:?}", err.message, err);
      Err(1)
    }
  }
}

async fn handle_matches(matches: &clap::ArgMatches<'_>) -> Result<(), i32> {
    let level = matches.value_of("loglevel").unwrap_or("warn");
    let log_level = match level {
        "none" => LevelFilter::Off,
        _ => LevelFilter::from_str(level).unwrap()
    };
    TermLogger::init(log_level, Config::default(), TerminalMode::Mixed).unwrap_or_default();
    let provider = ProviderInfo {
      host: s!(matches.value_of("hostname").unwrap_or("localhost")),
      port: matches.value_of("port").map(|port| port.parse::<u16>().unwrap()),
      path: matches.value_of("base-path").unwrap_or("/").into(),
      protocol: s!(matches.value_of("scheme").unwrap_or("http")),
      .. ProviderInfo::default()
    };
    let source = pact_source(matches);
    let filter = interaction_filter(matches);
    let provider_state_executor = Arc::new(HttpRequestProviderStateExecutor {
      state_change_url: matches.value_of("state-change-url").map(|s| s.to_string()),
      state_change_body: !matches.is_present("state-change-as-query"),
      state_change_teardown: matches.is_present("state-change-teardown")
    });

    let options = VerificationOptions {
      publish: matches.is_present("publish"),
      provider_version: matches.value_of("provider-version").map(|v| v.to_string()),
      build_url: matches.value_of("build-url").map(|v| v.to_string()),
      request_filter: None::<Arc<NullRequestFilterExecutor>>,
      provider_tags: matches.values_of("provider-tags").map_or_else(Vec::new, |tags| tags.map(|tag| tag.to_string()).collect()),
      disable_ssl_verification: matches.is_present("disable-ssl-verification"),
      callback_timeout: matches.value_of("callback-timeout").map(|t| t.parse::<u64>().unwrap_or(5000)).unwrap_or(5000),
      .. VerificationOptions::default()
    };

    for s in &source {
      debug!("Pact source to verify = {}", s);
    };

    if verify_provider_async(
        provider,
        source,
        filter,
        matches.values_of_lossy("filter-consumer").unwrap_or_default(),
        options,
        &provider_state_executor
    ).await {
        Ok(())
    } else {
        Err(1)
    }
}

fn print_version() {
  println!("\npact verifier version     : v{}", clap::crate_version!());
  println!("pact specification version: v{}", PactSpecification::V3.version_str());
}
<|endoftext|>


<commit_before>extern crate inotify;
extern crate libc;

use inotify::INotify;
use inotify::wrapper::Event;
use inotify::wrapper::Watch;
use inotify::ffi::*;

use std::collections::HashMap;
use std::io;
use std::path::PathBuf;

pub struct FilesWatcher {
    inotify: INotify,
    watches: HashMap<Watch, PathBuf>
}

impl FilesWatcher {
    pub fn new() -> FilesWatcher {
        let inotify = INotify::init().ok().expect(
            "Fatal Error: Could not initialize inotify."
        );

        FilesWatcher {
            inotify: inotify,
            watches: HashMap::new()
        }
    }

    pub fn add_file(&mut self, path: PathBuf) {
        let watch_id = self.inotify.add_watch(&path, IN_MODIFY | IN_DELETE);

        if watch_id.is_ok() {
           self.watches.insert(watch_id.unwrap(), path);
        } else {
            println!("Error adding watch for file: {:?}", watch_id.err());
        }
    }

    pub fn wait_for_events(&mut self) -> io::Result<Vec<EventPath>> {
        let events = try!(self.inotify.wait_for_events());

        let mut event_paths: Vec<EventPath> = vec![];
        for event in events.iter() {
            let path = self.watches.get(&event.wd);
            if path.is_some() {
                event_paths.push(EventPath::new(
                    path.unwrap().clone(),
                    event.clone()
                ));
            } else {
                println!("Error: no path for watch: {:?}", event.wd);
            }
        }

        Ok(event_paths)
    }

    pub fn close(&mut self) {
        self.inotify.close().ok().expect(
            "Fatal Error: Could not close inotify."
        );
    }
}

pub struct EventPath {
    event: Event,
    path: PathBuf
}

impl EventPath {
    fn new(path: PathBuf, event: Event) -> EventPath {
        EventPath {
            event: event,
            path: path
        }
    }
}

#[cfg(test)]
mod test {
    extern crate rand;

    use super::*;

    use self::rand::{thread_rng, Rng};
    use std::env::temp_dir;
    use std::fs::File;
    use std::fs::remove_file;
    use std::io::Write;
    use std::path::Path;
    use std::path::PathBuf;

    #[test]
    fn watch_a_single_file() {
        let (path, mut file) = create_temp_file();
        let filepath = path.clone();

        let mut fw = FilesWatcher::new();
        fw.add_file(path);

        write_to(&mut file);

        {
            let event_paths = fw.wait_for_events().unwrap();
            assert_eq!(1, event_paths.len());
            for event_path in event_paths.iter() {
                assert_eq!(filepath, event_path.path);
            }
        }

        for path in fw.watches.values() {
            remove_temp_file(path);
        }
        fw.close();
    }

    #[test]
    fn watch_two_files() {
        let (path1, mut file1) = create_temp_file();
        let (path2, mut file2) = create_temp_file();

        let filepath1 = path1.clone();
        let filepath2 = path2.clone();

        let mut fw = FilesWatcher::new();
        fw.add_file(path1);
        fw.add_file(path2);

        write_to(&mut file1);
        write_to(&mut file2);

        {
            let event_paths = fw.wait_for_events().unwrap();
            assert_eq!(2, event_paths.len());
            for event_path in event_paths.iter() {
                assert!(filepath1 == event_path.path || filepath2 == event_path.path);
            }
        }

        for path in fw.watches.values() {
            remove_temp_file(path);
        }
        fw.close();
    }

    fn create_temp_file() -> (PathBuf, File) {
        let rand_part: String = thread_rng()
            .gen_ascii_chars()
            .take(8)
            .collect();

        let filename = "eagleeye-test-".to_string() + &rand_part;
        let path = temp_dir().join(filename);
        let file = File::create(&path).unwrap_or_else(|error|
            panic!("Failed to create temporary file: {}", error)
        );
        let path_buf = PathBuf::from(path);

        (path_buf, file)
    }

    fn remove_temp_file(path: &Path) {
        remove_file(path).unwrap_or_else(|error|
            panic!("Failed to create temporary file: {}", error)
        );
    }

    fn write_to(file: &mut File) {
        file
            .write(b"This should trigger an inotify event.")
            .unwrap_or_else(|error|
                panic!("Failed to write to file: {}", error)
            );
    }

}

<commit_msg>Give out borrows in EventPaths<commit_after>extern crate inotify;
extern crate libc;

use inotify::INotify;
use inotify::wrapper::Event;
use inotify::wrapper::Watch;
use inotify::ffi::*;

use std::collections::HashMap;
use std::io;
use std::path::PathBuf;

pub struct FilesWatcher {
    inotify: INotify,
    watches: HashMap<Watch, PathBuf>
}

impl FilesWatcher {
    pub fn new() -> FilesWatcher {
        let inotify = INotify::init().ok().expect(
            "Fatal Error: Could not initialize inotify."
        );

        FilesWatcher {
            inotify: inotify,
            watches: HashMap::new()
        }
    }

    pub fn add_file(&mut self, path: PathBuf) {
        let watch_id = self.inotify.add_watch(&path, IN_MODIFY | IN_DELETE);

        if watch_id.is_ok() {
           self.watches.insert(watch_id.unwrap(), path);
        } else {
            println!("Error adding watch for file: {:?}", watch_id.err());
        }
    }

    pub fn wait_for_events(&mut self) -> io::Result<Vec<EventPath>> {
        let events = try!(self.inotify.wait_for_events());

        let mut event_paths: Vec<EventPath> = vec![];
        for event in events.iter() {
            let path = self.watches.get(&event.wd);
            if path.is_some() {
                event_paths.push(EventPath::new(
                    path.unwrap(),
                    event
                ));
            } else {
                println!("Error: no path for watch: {:?}", event.wd);
            }
        }

        Ok(event_paths)
    }

    pub fn close(&mut self) {
        self.inotify.close().ok().expect(
            "Fatal Error: Could not close inotify."
        );
    }
}

pub struct EventPath<'a> {
    event: &'a Event,
    path: &'a PathBuf
}

impl <'a>EventPath<'a> {
    fn new(path: &'a PathBuf, event: &'a Event) -> EventPath<'a> {
        EventPath {
            event: event,
            path: path
        }
    }
}

#[cfg(test)]
mod test {
    extern crate rand;

    use super::*;

    use self::rand::{thread_rng, Rng};
    use std::env::temp_dir;
    use std::fs::File;
    use std::fs::remove_file;
    use std::io::Write;
    use std::path::Path;
    use std::path::PathBuf;

    #[test]
    fn watch_a_single_file() {
        let (path, mut file) = create_temp_file();
        let filepath = path.clone();

        let mut fw = FilesWatcher::new();
        fw.add_file(path);

        write_to(&mut file);

        {
            let event_paths = fw.wait_for_events().unwrap();
            assert_eq!(1, event_paths.len());
            for event_path in event_paths.iter() {
                assert_eq!(&filepath, event_path.path);
            }
        }

        for path in fw.watches.values() {
            remove_temp_file(path);
        }
        fw.close();
    }

    #[test]
    fn watch_two_files() {
        let (path1, mut file1) = create_temp_file();
        let (path2, mut file2) = create_temp_file();

        let filepath1 = path1.clone();
        let filepath2 = path2.clone();

        let mut fw = FilesWatcher::new();
        fw.add_file(path1);
        fw.add_file(path2);

        write_to(&mut file1);
        write_to(&mut file2);

        {
            let event_paths = fw.wait_for_events().unwrap();
            assert_eq!(2, event_paths.len());
            for event_path in event_paths.iter() {
                assert!(&filepath1 == event_path.path || &filepath2 == event_path.path);
            }
        }

        for path in fw.watches.values() {
            remove_temp_file(path);
        }
        fw.close();
    }

    fn create_temp_file() -> (PathBuf, File) {
        let rand_part: String = thread_rng()
            .gen_ascii_chars()
            .take(8)
            .collect();

        let filename = "eagleeye-test-".to_string() + &rand_part;
        let path = temp_dir().join(filename);
        let file = File::create(&path).unwrap_or_else(|error|
            panic!("Failed to create temporary file: {}", error)
        );
        let path_buf = PathBuf::from(path);

        (path_buf, file)
    }

    fn remove_temp_file(path: &Path) {
        remove_file(path).unwrap_or_else(|error|
            panic!("Failed to create temporary file: {}", error)
        );
    }

    fn write_to(file: &mut File) {
        file
            .write(b"This should trigger an inotify event.")
            .unwrap_or_else(|error|
                panic!("Failed to write to file: {}", error)
            );
    }

}

<|endoftext|>


<commit_before>// http://rosettacode.org/wiki/Function_composition

// TODO: decide what to do for 1.0
// the manual implementation of Fn traits for a struct
// as done here for Composed is not going to be un-feature-gated
// for 1.0 (so it's going to work only with the Rust nightlies)
#![feature(unboxed_closures)]

#[cfg(not(test))]
fn main() {
    use std::f32::consts;

    fn f(x: usize) -> String { x.to_string() }
    fn g(x: f32) -> usize { x as usize }

    let comp = Composed::new(f, g);

    println!("{}", comp(consts::PI));
}

struct Composed<A, B, C, F1, F2>
    where F1: Fn(A) -> B, F2: Fn(C) -> A {
    f: F1,
    g: F2
}

impl <A, B, C, F1, F2> Composed<A, B, C, F1, F2>
    where F1: Fn(A) -> B, F2: Fn(C) -> A {
    fn new(f: F1, g: F2) -> Composed<A, B, C, F1, F2> { Composed{f: f, g: g} }
}

impl<A, B, C, F1, F2> Fn<(C,), B> for Composed<A, B, C, F1, F2>
    where F1: Fn(A) -> B, F2: Fn(C) -> A {
    extern "rust-call" fn call(&self, (x,): (C,)) -> B { self.f.call((self.g.call((x,)),)) }
}

#[test]
fn test_compose() {
    fn inc(x: usize) -> usize { x + 1 }
    fn mul(x: usize) -> usize { x * 3 }

    let comp = Composed::new(inc, mul);
    assert_eq!(comp(3), 10);
}
<commit_msg>function_composition stabilization<commit_after>// http://rosettacode.org/wiki/Function_composition
#![allow(unstable)]
#[cfg(not(test))]
fn main() {
    use std::f32::consts;

    // the two functions we will compose:
    let f = |&: x: u32| x.to_string();
    let g = |&: x: f32| x as u32;

    // their omposition
    let comp = compose(f, g);

    println!("{:?}", (*comp)(consts::PI));
}

fn compose<'a, F, G, A, B, C>(f: F, g: G) -> Box<Fn(A) -> C + 'a>
    where G: Fn(A) -> B + 'a, F: Fn(B) -> C + 'a
{
    Box::new( move |&: a: A| f(g(a)) )
}

#[test]
fn test_compose() {
    fn inc(x: usize) -> usize { x + 1 }
    fn mul(x: usize) -> usize { x * 3 }

    let comp = compose(inc, mul);
    assert_eq!((*comp)(3), 10);
}<|endoftext|>


<commit_before>use gettextrs::gettext;
use glib;
use gtk;
use gtk::prelude::*;

use std::{
    collections::HashSet,
    path::{Path, PathBuf},
    rc::Rc,
    sync::{Arc, RwLock},
};

use crate::{
    media::{MediaEvent, PlaybackPipeline},
    metadata,
    metadata::{Format, MediaInfo},
};

use super::{UIController, UIEventSender};

const PROGRESS_TIMER_PERIOD: u32 = 250; // 250 ms

pub enum ProcessingType {
    Async(glib::Receiver<MediaEvent>),
    Sync,
}

#[derive(Debug, PartialEq)]
pub enum ProcessingState {
    AllComplete(String),
    Cancelled,
    ConfirmedOutputTo(Rc<Path>),
    SkipCurrent,
    DoneWithCurrent,
    PendingAsyncMediaEvent,
    Start,
    WouldOutputTo(Rc<Path>),
}

pub trait MediaProcessor {
    fn init(&mut self) -> ProcessingType;
    fn next(&mut self) -> Result<ProcessingState, String>;
    fn process(&mut self, path: &Path) -> Result<ProcessingState, String>;
    fn handle_media_event(&mut self, event: MediaEvent) -> Result<ProcessingState, String>;
    fn report_progress(&self) -> Option<f64>;
}

pub trait OutputControllerImpl {
    const BTN_NAME: &'static str;
    const LIST_NAME: &'static str;
    const PROGRESS_BAR_NAME: &'static str;
}

pub struct OutputMediaFileInfo {
    pub format: Format,
    pub path: PathBuf,
    pub extension: String,
    pub stream_ids: Arc<RwLock<HashSet<String>>>,
}

impl OutputMediaFileInfo {
    pub fn new(format: Format, src_info: &MediaInfo) -> Self {
        let (stream_ids, content) = src_info.get_stream_ids_to_export(format);
        let extension = metadata::Factory::get_extension(format, content).to_owned();

        OutputMediaFileInfo {
            path: src_info.path.with_extension(&extension),
            extension,
            format,
            stream_ids: Arc::new(RwLock::new(stream_ids)),
        }
    }
}

type OverwriteResponseCb = Fn(gtk::ResponseType, &Rc<Path>);

pub struct OutputBaseController<Impl> {
    impl_: Impl,
    ui_event: UIEventSender,

    progress_bar: gtk::ProgressBar,
    list: gtk::ListBox,
    pub(super) btn: gtk::Button,

    perspective_selector: gtk::MenuButton,
    open_btn: gtk::Button,
    chapter_grid: gtk::Grid,

    playback_pipeline: Option<PlaybackPipeline>,

    pub(super) media_event_handler: Option<Rc<Fn(MediaEvent)>>,
    media_event_handler_src: Option<glib::SourceId>,

    pub(super) progress_updater: Option<Rc<Fn()>>,
    progress_timer_src: Option<glib::SourceId>,

    pub(super) overwrite_response_cb: Option<Rc<OverwriteResponseCb>>,
    overwrite_all: bool,
}

impl<Impl> OutputBaseController<Impl>
where
    Impl: OutputControllerImpl + MediaProcessor + UIController + 'static,
{
    pub fn new_base(impl_: Impl, builder: &gtk::Builder, ui_event_sender: UIEventSender) -> Self {
        OutputBaseController {
            impl_,
            ui_event: ui_event_sender,

            btn: builder.get_object(Impl::BTN_NAME).unwrap(),
            list: builder.get_object(Impl::LIST_NAME).unwrap(),
            progress_bar: builder.get_object(Impl::PROGRESS_BAR_NAME).unwrap(),

            perspective_selector: builder.get_object("perspective-menu-btn").unwrap(),
            open_btn: builder.get_object("open-btn").unwrap(),
            chapter_grid: builder.get_object("info-chapter_list-grid").unwrap(),

            playback_pipeline: None,

            media_event_handler: None,
            media_event_handler_src: None,

            progress_updater: None,
            progress_timer_src: None,

            overwrite_response_cb: None,
            overwrite_all: false,
        }
    }

    #[allow(clippy::redundant_closure)]
    fn attach_media_event_handler(&mut self, receiver: glib::Receiver<MediaEvent>) {
        debug_assert!(self.media_event_handler_src.is_none());

        let media_event_handler = Rc::clone(self.media_event_handler.as_ref().unwrap());
        self.media_event_handler_src = Some(receiver.attach(None, move |event| {
            media_event_handler(event);
            // will be removed in `OutputBaseController::switch_to_available`
            glib::Continue(true)
        }));
    }

    fn remove_media_event_handler(&mut self) {
        if let Some(src) = self.media_event_handler_src.take() {
            let _res = glib::Source::remove(src);
        }
    }

    pub fn handle_media_event(&mut self, event: MediaEvent) {
        let res = self.impl_.handle_media_event(event);
        self.handle_processing_states(res);
    }

    fn register_progress_timer(&mut self) {
        debug_assert!(self.progress_timer_src.is_none());

        let progress_updater = Rc::clone(self.progress_updater.as_ref().unwrap());
        self.progress_timer_src = Some(glib::timeout_add_local(PROGRESS_TIMER_PERIOD, move || {
            progress_updater();
            glib::Continue(true)
        }));
    }

    fn remove_progress_timer(&mut self) {
        if let Some(src) = self.progress_timer_src.take() {
            let _res = glib::Source::remove(src);
        }
    }

    pub fn update_progress(&self) {
        if let Some(progress) = self.impl_.report_progress() {
            self.progress_bar.set_fraction(progress);
        }
    }

    pub fn have_pipeline(&mut self, playback_pipeline: PlaybackPipeline) {
        self.playback_pipeline = Some(playback_pipeline);
    }

    fn ask_overwrite_question(&self, path: &Rc<Path>) {
        self.ui_event.reset_cursor();

        let filename = path.file_name().expect("no `filename` in `path`");
        let filename = filename
            .to_str()
            .expect("can't get printable `str` from `filename`");
        let question = gettext("{output_file}\nalready exists. Overwrite?").replacen(
            "{output_file}",
            filename,
            1,
        );

        let path_cb = Rc::clone(path);
        let overwrite_response_cb = Rc::clone(self.overwrite_response_cb.as_ref().unwrap());
        self.ui_event.ask_question(
            question,
            Rc::new(move |response_type| overwrite_response_cb(response_type, &path_cb)),
        );
    }

    pub fn handle_overwrite_response(&mut self, response_type: gtk::ResponseType, path: &Rc<Path>) {
        let next_state = match response_type {
            gtk::ResponseType::Apply => {
                // This one is used for "Yes to all"
                self.overwrite_all = true;
                ProcessingState::ConfirmedOutputTo(Rc::clone(path))
            }
            gtk::ResponseType::Cancel => ProcessingState::Cancelled,
            gtk::ResponseType::No => ProcessingState::SkipCurrent,
            gtk::ResponseType::Yes => ProcessingState::ConfirmedOutputTo(Rc::clone(path)),
            other => unimplemented!(
                concat!(
                    "Response type {:?} in ",
                    "OutputBaseController::handle_processing_states (`GotUserResponse`)",
                ),
                other,
            ),
        };
        self.handle_processing_states(Ok(next_state));
    }

    fn switch_to_busy(&self) {
        self.list.set_sensitive(false);
        self.btn.set_sensitive(false);

        self.perspective_selector.set_sensitive(false);
        self.open_btn.set_sensitive(false);
        self.chapter_grid.set_sensitive(false);

        self.ui_event.set_cursor_waiting();
    }

    fn switch_to_available(&mut self) {
        self.remove_media_event_handler();
        self.remove_progress_timer();

        self.progress_bar.set_fraction(0f64);
        self.list.set_sensitive(true);
        self.btn.set_sensitive(true);

        self.perspective_selector.set_sensitive(true);
        self.open_btn.set_sensitive(true);
        self.chapter_grid.set_sensitive(true);

        let playback_pipeline = self.playback_pipeline.take().expect(concat!(
            "OutputBaseController: `playback_pipeline` is already taken in ",
            "`switch_to_available`",
        ));
        self.ui_event.hand_back_pipeline(playback_pipeline);
        self.ui_event.reset_cursor();
    }

    pub fn handle_processing_states(&mut self, mut res: Result<ProcessingState, String>) {
        loop {
            match res {
                Ok(ProcessingState::AllComplete(msg)) => {
                    self.switch_to_available();
                    self.ui_event.show_info(msg);
                    break;
                }
                Ok(ProcessingState::Cancelled) => {
                    self.switch_to_available();
                    self.ui_event.show_info(gettext("Operation cancelled"));
                    break;
                }
                Ok(ProcessingState::ConfirmedOutputTo(path)) => {
                    self.ui_event.set_cursor_waiting();
                    res = self.impl_.process(path.as_ref());
                    if res == Ok(ProcessingState::PendingAsyncMediaEvent) {
                        // Next state handled asynchronously in media event handler
                        break;
                    }
                }
                Ok(ProcessingState::DoneWithCurrent) => {
                    res = self.impl_.next();
                }
                Ok(ProcessingState::PendingAsyncMediaEvent) => {
                    // Next state handled asynchronously in media event handler
                    break;
                }
                Ok(ProcessingState::Start) => {
                    self.switch_to_busy();
                    self.overwrite_all = false;

                    match self.impl_.init() {
                        ProcessingType::Sync => (),
                        ProcessingType::Async(receiver) => {
                            self.attach_media_event_handler(receiver);
                            self.register_progress_timer();
                        }
                    }

                    res = self.impl_.next();
                }
                Ok(ProcessingState::SkipCurrent) => {
                    res = match self.impl_.next() {
                        Ok(state) => match state {
                            ProcessingState::AllComplete(_) => {
                                // Don't display the success message when the user decided
                                // to skip (not overwrite) last part as it seems missleading
                                self.switch_to_available();
                                break;
                            }
                            other => Ok(other),
                        },
                        Err(err) => Err(err),
                    };
                }
                Ok(ProcessingState::WouldOutputTo(path)) => {
                    if !self.overwrite_all && path.exists() {
                        self.ask_overwrite_question(&path);
                        // Pending user response
                        // Next steps handled asynchronously (see closure above)
                        break;
                    } else {
                        // handle processing in next iteration
                        res = Ok(ProcessingState::ConfirmedOutputTo(path));
                    }
                }
                Err(err) => {
                    self.switch_to_available();
                    self.ui_event.show_error(err);
                    break;
                }
            }
        }
    }
}

impl<Impl> UIController for OutputBaseController<Impl>
where
    Impl: OutputControllerImpl + MediaProcessor + UIController + 'static,
{
    fn setup(&mut self) {
        self.btn.set_sensitive(false);
        self.impl_.setup();
    }

    fn new_media(&mut self, pipeline: &PlaybackPipeline) {
        self.btn.set_sensitive(true);
        self.impl_.new_media(pipeline);
    }

    fn cleanup(&mut self) {
        self.progress_bar.set_fraction(0f64);
        self.btn.set_sensitive(false);
        self.overwrite_all = false;
        self.impl_.cleanup();
    }

    fn streams_changed(&mut self, info: &MediaInfo) {
        self.impl_.streams_changed(info);
    }
}
<commit_msg>OutputBaseController: fix unimplemented message (#118)<commit_after>use gettextrs::gettext;
use glib;
use gtk;
use gtk::prelude::*;

use std::{
    collections::HashSet,
    path::{Path, PathBuf},
    rc::Rc,
    sync::{Arc, RwLock},
};

use crate::{
    media::{MediaEvent, PlaybackPipeline},
    metadata,
    metadata::{Format, MediaInfo},
};

use super::{UIController, UIEventSender};

const PROGRESS_TIMER_PERIOD: u32 = 250; // 250 ms

pub enum ProcessingType {
    Async(glib::Receiver<MediaEvent>),
    Sync,
}

#[derive(Debug, PartialEq)]
pub enum ProcessingState {
    AllComplete(String),
    Cancelled,
    ConfirmedOutputTo(Rc<Path>),
    SkipCurrent,
    DoneWithCurrent,
    PendingAsyncMediaEvent,
    Start,
    WouldOutputTo(Rc<Path>),
}

pub trait MediaProcessor {
    fn init(&mut self) -> ProcessingType;
    fn next(&mut self) -> Result<ProcessingState, String>;
    fn process(&mut self, path: &Path) -> Result<ProcessingState, String>;
    fn handle_media_event(&mut self, event: MediaEvent) -> Result<ProcessingState, String>;
    fn report_progress(&self) -> Option<f64>;
}

pub trait OutputControllerImpl {
    const BTN_NAME: &'static str;
    const LIST_NAME: &'static str;
    const PROGRESS_BAR_NAME: &'static str;
}

pub struct OutputMediaFileInfo {
    pub format: Format,
    pub path: PathBuf,
    pub extension: String,
    pub stream_ids: Arc<RwLock<HashSet<String>>>,
}

impl OutputMediaFileInfo {
    pub fn new(format: Format, src_info: &MediaInfo) -> Self {
        let (stream_ids, content) = src_info.get_stream_ids_to_export(format);
        let extension = metadata::Factory::get_extension(format, content).to_owned();

        OutputMediaFileInfo {
            path: src_info.path.with_extension(&extension),
            extension,
            format,
            stream_ids: Arc::new(RwLock::new(stream_ids)),
        }
    }
}

type OverwriteResponseCb = Fn(gtk::ResponseType, &Rc<Path>);

pub struct OutputBaseController<Impl> {
    impl_: Impl,
    ui_event: UIEventSender,

    progress_bar: gtk::ProgressBar,
    list: gtk::ListBox,
    pub(super) btn: gtk::Button,

    perspective_selector: gtk::MenuButton,
    open_btn: gtk::Button,
    chapter_grid: gtk::Grid,

    playback_pipeline: Option<PlaybackPipeline>,

    pub(super) media_event_handler: Option<Rc<Fn(MediaEvent)>>,
    media_event_handler_src: Option<glib::SourceId>,

    pub(super) progress_updater: Option<Rc<Fn()>>,
    progress_timer_src: Option<glib::SourceId>,

    pub(super) overwrite_response_cb: Option<Rc<OverwriteResponseCb>>,
    overwrite_all: bool,
}

impl<Impl> OutputBaseController<Impl>
where
    Impl: OutputControllerImpl + MediaProcessor + UIController + 'static,
{
    pub fn new_base(impl_: Impl, builder: &gtk::Builder, ui_event_sender: UIEventSender) -> Self {
        OutputBaseController {
            impl_,
            ui_event: ui_event_sender,

            btn: builder.get_object(Impl::BTN_NAME).unwrap(),
            list: builder.get_object(Impl::LIST_NAME).unwrap(),
            progress_bar: builder.get_object(Impl::PROGRESS_BAR_NAME).unwrap(),

            perspective_selector: builder.get_object("perspective-menu-btn").unwrap(),
            open_btn: builder.get_object("open-btn").unwrap(),
            chapter_grid: builder.get_object("info-chapter_list-grid").unwrap(),

            playback_pipeline: None,

            media_event_handler: None,
            media_event_handler_src: None,

            progress_updater: None,
            progress_timer_src: None,

            overwrite_response_cb: None,
            overwrite_all: false,
        }
    }

    #[allow(clippy::redundant_closure)]
    fn attach_media_event_handler(&mut self, receiver: glib::Receiver<MediaEvent>) {
        debug_assert!(self.media_event_handler_src.is_none());

        let media_event_handler = Rc::clone(self.media_event_handler.as_ref().unwrap());
        self.media_event_handler_src = Some(receiver.attach(None, move |event| {
            media_event_handler(event);
            // will be removed in `OutputBaseController::switch_to_available`
            glib::Continue(true)
        }));
    }

    fn remove_media_event_handler(&mut self) {
        if let Some(src) = self.media_event_handler_src.take() {
            let _res = glib::Source::remove(src);
        }
    }

    pub fn handle_media_event(&mut self, event: MediaEvent) {
        let res = self.impl_.handle_media_event(event);
        self.handle_processing_states(res);
    }

    fn register_progress_timer(&mut self) {
        debug_assert!(self.progress_timer_src.is_none());

        let progress_updater = Rc::clone(self.progress_updater.as_ref().unwrap());
        self.progress_timer_src = Some(glib::timeout_add_local(PROGRESS_TIMER_PERIOD, move || {
            progress_updater();
            glib::Continue(true)
        }));
    }

    fn remove_progress_timer(&mut self) {
        if let Some(src) = self.progress_timer_src.take() {
            let _res = glib::Source::remove(src);
        }
    }

    pub fn update_progress(&self) {
        if let Some(progress) = self.impl_.report_progress() {
            self.progress_bar.set_fraction(progress);
        }
    }

    pub fn have_pipeline(&mut self, playback_pipeline: PlaybackPipeline) {
        self.playback_pipeline = Some(playback_pipeline);
    }

    fn ask_overwrite_question(&self, path: &Rc<Path>) {
        self.ui_event.reset_cursor();

        let filename = path.file_name().expect("no `filename` in `path`");
        let filename = filename
            .to_str()
            .expect("can't get printable `str` from `filename`");
        let question = gettext("{output_file}\nalready exists. Overwrite?").replacen(
            "{output_file}",
            filename,
            1,
        );

        let path_cb = Rc::clone(path);
        let overwrite_response_cb = Rc::clone(self.overwrite_response_cb.as_ref().unwrap());
        self.ui_event.ask_question(
            question,
            Rc::new(move |response_type| overwrite_response_cb(response_type, &path_cb)),
        );
    }

    pub fn handle_overwrite_response(&mut self, response_type: gtk::ResponseType, path: &Rc<Path>) {
        let next_state = match response_type {
            gtk::ResponseType::Apply => {
                // This one is used for "Yes to all"
                self.overwrite_all = true;
                ProcessingState::ConfirmedOutputTo(Rc::clone(path))
            }
            gtk::ResponseType::Cancel => ProcessingState::Cancelled,
            gtk::ResponseType::No => ProcessingState::SkipCurrent,
            gtk::ResponseType::Yes => ProcessingState::ConfirmedOutputTo(Rc::clone(path)),
            other => unimplemented!(
                "Response type {:?} in OutputBaseController::handle_overwrite_response",
                other,
            ),
        };
        self.handle_processing_states(Ok(next_state));
    }

    fn switch_to_busy(&self) {
        self.list.set_sensitive(false);
        self.btn.set_sensitive(false);

        self.perspective_selector.set_sensitive(false);
        self.open_btn.set_sensitive(false);
        self.chapter_grid.set_sensitive(false);

        self.ui_event.set_cursor_waiting();
    }

    fn switch_to_available(&mut self) {
        self.remove_media_event_handler();
        self.remove_progress_timer();

        self.progress_bar.set_fraction(0f64);
        self.list.set_sensitive(true);
        self.btn.set_sensitive(true);

        self.perspective_selector.set_sensitive(true);
        self.open_btn.set_sensitive(true);
        self.chapter_grid.set_sensitive(true);

        let playback_pipeline = self.playback_pipeline.take().expect(concat!(
            "OutputBaseController: `playback_pipeline` is already taken in ",
            "`switch_to_available`",
        ));
        self.ui_event.hand_back_pipeline(playback_pipeline);
        self.ui_event.reset_cursor();
    }

    pub fn handle_processing_states(&mut self, mut res: Result<ProcessingState, String>) {
        loop {
            match res {
                Ok(ProcessingState::AllComplete(msg)) => {
                    self.switch_to_available();
                    self.ui_event.show_info(msg);
                    break;
                }
                Ok(ProcessingState::Cancelled) => {
                    self.switch_to_available();
                    self.ui_event.show_info(gettext("Operation cancelled"));
                    break;
                }
                Ok(ProcessingState::ConfirmedOutputTo(path)) => {
                    self.ui_event.set_cursor_waiting();
                    res = self.impl_.process(path.as_ref());
                    if res == Ok(ProcessingState::PendingAsyncMediaEvent) {
                        // Next state handled asynchronously in media event handler
                        break;
                    }
                }
                Ok(ProcessingState::DoneWithCurrent) => {
                    res = self.impl_.next();
                }
                Ok(ProcessingState::PendingAsyncMediaEvent) => {
                    // Next state handled asynchronously in media event handler
                    break;
                }
                Ok(ProcessingState::Start) => {
                    self.switch_to_busy();
                    self.overwrite_all = false;

                    match self.impl_.init() {
                        ProcessingType::Sync => (),
                        ProcessingType::Async(receiver) => {
                            self.attach_media_event_handler(receiver);
                            self.register_progress_timer();
                        }
                    }

                    res = self.impl_.next();
                }
                Ok(ProcessingState::SkipCurrent) => {
                    res = match self.impl_.next() {
                        Ok(state) => match state {
                            ProcessingState::AllComplete(_) => {
                                // Don't display the success message when the user decided
                                // to skip (not overwrite) last part as it seems missleading
                                self.switch_to_available();
                                break;
                            }
                            other => Ok(other),
                        },
                        Err(err) => Err(err),
                    };
                }
                Ok(ProcessingState::WouldOutputTo(path)) => {
                    if !self.overwrite_all && path.exists() {
                        self.ask_overwrite_question(&path);
                        // Pending user response
                        // Next steps handled asynchronously (see closure above)
                        break;
                    } else {
                        // handle processing in next iteration
                        res = Ok(ProcessingState::ConfirmedOutputTo(path));
                    }
                }
                Err(err) => {
                    self.switch_to_available();
                    self.ui_event.show_error(err);
                    break;
                }
            }
        }
    }
}

impl<Impl> UIController for OutputBaseController<Impl>
where
    Impl: OutputControllerImpl + MediaProcessor + UIController + 'static,
{
    fn setup(&mut self) {
        self.btn.set_sensitive(false);
        self.impl_.setup();
    }

    fn new_media(&mut self, pipeline: &PlaybackPipeline) {
        self.btn.set_sensitive(true);
        self.impl_.new_media(pipeline);
    }

    fn cleanup(&mut self) {
        self.progress_bar.set_fraction(0f64);
        self.btn.set_sensitive(false);
        self.overwrite_all = false;
        self.impl_.cleanup();
    }

    fn streams_changed(&mut self, info: &MediaInfo) {
        self.impl_.streams_changed(info);
    }
}
<|endoftext|>


<commit_before>use super::catcodes::Catcodes;
use super::*;

#[test]
fn test_basic_lexer() {
    let catcodes = Catcodes::default();

    let input = "foo bar \\{
\\bar \\foo{bar}";

    let mut lexer = Lexer::new(input);

    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::Text("foo bar ".into()), Position::new(0, 0))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::EscapedChar('{'), Position::new(0, 8))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::LineBreak, Position::new(0, 10))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::Control("bar".into()), Position::new(1, 0))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::Space, Position::new(1, 4))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::Control("foo".into()), Position::new(1, 5))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::BeginGroup, Position::new(1, 9))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::Text("bar".into()), Position::new(1, 10))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::EndGroup, Position::new(1, 13))
    );
    assert_eq!(
        lexer.next(&catcodes),
        LexerElem::new(Elem::EndOfFile, Position::new(1, 14))
    );
}
<commit_msg>improve lexer test<commit_after>use super::catcodes::Catcodes;
use super::*;

#[test]
fn test_basic_lexer() {
    let catcodes = Catcodes::default();

    let input = "foo bar \\{
\\bar \\foo{bar}
\\def \\foo #1v#2{bar}";

    let mut lexer = Lexer::new(input);

    let data = vec![
        LexerElem::new(Elem::Text("foo bar ".into()), Position::new(0, 0)),
        LexerElem::new(Elem::EscapedChar('{'), Position::new(0, 8)),
        LexerElem::new(Elem::LineBreak, Position::new(0, 10)),
        LexerElem::new(Elem::Control("bar".into()), Position::new(1, 0)),
        LexerElem::new(Elem::Space, Position::new(1, 4)),
        LexerElem::new(Elem::Control("foo".into()), Position::new(1, 5)),
        LexerElem::new(Elem::BeginGroup, Position::new(1, 9)),
        LexerElem::new(Elem::Text("bar".into()), Position::new(1, 10)),
        LexerElem::new(Elem::EndGroup, Position::new(1, 13)),
        LexerElem::new(Elem::LineBreak, Position::new(1, 14)),
        LexerElem::new(Elem::Control("def".into()), Position::new(2, 0)),
        LexerElem::new(Elem::Space, Position::new(2, 4)),
        LexerElem::new(Elem::Control("foo".into()), Position::new(2, 5)),
        LexerElem::new(Elem::Space, Position::new(2, 9)),
        LexerElem::new(Elem::MacroParam, Position::new(2, 10)),
        LexerElem::new(Elem::Number(1), Position::new(2, 11)),
        LexerElem::new(Elem::Text("v".into()), Position::new(2, 12)),
        LexerElem::new(Elem::MacroParam, Position::new(2, 13)),
        LexerElem::new(Elem::Number(2), Position::new(2, 14)),
        LexerElem::new(Elem::BeginGroup, Position::new(2, 15)),
        LexerElem::new(Elem::Text("bar".into()), Position::new(2, 16)),
        LexerElem::new(Elem::EndGroup, Position::new(2, 19)),
        LexerElem::new(Elem::EndOfFile, Position::new(2, 20)),
    ];

    for d in data {
        assert_eq!(lexer.next(&catcodes), d);
    }
}
<|endoftext|>


<commit_before>// Copyright (C) 2016 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Caribon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received ba copy of the GNU Lesser General Public License
// along with Crowbook.  If not, see <http://www.gnu.org/licenses/>.

/// Custom function because we don't really want to touch \t or \n
fn is_whitespace(c: char) -> bool {
    c == ' ' || c == ' ' || c == ' '
}

/// Trait for cleaning a string.
/// This trait should be called for text that is e.g. in a paragraph, a title,
/// NOT for code blocks, hyperlinks and so on!
pub trait Cleaner {
    /// Cleans a string. The default implementation is to remove multiple consecutive whitespaces
    fn clean(&self, s: &mut String) {
        if s.contains(is_whitespace) { // if not, no need to do anything
            let mut new_s = String::with_capacity(s.len());
            let mut previous_space = false;
            for c in s.chars() {
                if is_whitespace(c) {
                    if previous_space {
                        // previous char already a space, don't copy it
                    } else {
                        new_s.push(c);
                        previous_space = true;
                    }
                } else {
                    previous_space = false;
                    new_s.push(c);
                }
            }

            *s = new_s
        }
    }
}

impl Cleaner for () {}

/// Implementation for french 'cleaning'
pub struct French {
    nb_char: char,
}

impl French {
    /// Creates a new french cleaner, which will replace spaces with the defined
    /// non-breaking space when appropriate.
    pub fn new(nb_char: char) -> French {
        French { nb_char: nb_char }
    }
}

impl Cleaner for French {
    // puts non breaking spaces between :, ;, ?, !, «, »
    fn clean(&self, s: &mut String) {
        fn is_trouble(c: char) -> bool {
            match c {
                '?'|'!'|';'|':'|'»'|'«'|'—' => true,
                _ => false
            }
        }

        if !s.contains(is_trouble) { // if not, no need to do anything
            return;
        }
        ().clean(s); // first pass with default impl
        let mut new_s = String::with_capacity(s.len());
        {
            let mut chars = s.chars();
            if let Some(mut current) = chars.next() {
                while let  Some(next) = chars.next() {
                    if is_whitespace(current) {
                        match next {
                            // handle nb space before char
                            '?' | '»' | '!' | ';' | ':' => new_s.push(self.nb_char),
                            _ => new_s.push(current)
                        }
                    } else {
                        new_s.push(current);
                        match current {
                            // handle nb space after char
                            '—' => {
                                if is_whitespace(next) {
                                    // use demi "em space"
                                    new_s.push('\u{2002}');
                                    if let Some(next) = chars.next() {
                                        current = next;
                                        continue;
                                    }
                                }
                            },
                            '«' => {
                                if is_whitespace(next) {
                                    new_s.push(self.nb_char);
                                    if let Some(next) = chars.next() {
                                        current = next;
                                        continue;
                                    }
                                }

                            },
                            _ => (),
                        }
                    }
                    current = next;
                }
                new_s.push(current);
            }
        }

        *s = new_s
    }
}

<commit_msg>Use 'word' non-bk-space instead of narrow ones for ':' and '«/»'<commit_after>// Copyright (C) 2016 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Caribon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received ba copy of the GNU Lesser General Public License
// along with Crowbook.  If not, see <http://www.gnu.org/licenses/>.

/// Custom function because we don't really want to touch \t or \n
fn is_whitespace(c: char) -> bool {
    c == ' ' || c == ' ' || c == ' '
}

/// Trait for cleaning a string.
/// This trait should be called for text that is e.g. in a paragraph, a title,
/// NOT for code blocks, hyperlinks and so on!
pub trait Cleaner {
    /// Cleans a string. The default implementation is to remove multiple consecutive whitespaces
    fn clean(&self, s: &mut String) {
        if s.contains(is_whitespace) { // if not, no need to do anything
            let mut new_s = String::with_capacity(s.len());
            let mut previous_space = false;
            for c in s.chars() {
                if is_whitespace(c) {
                    if previous_space {
                        // previous char already a space, don't copy it
                    } else {
                        new_s.push(c);
                        previous_space = true;
                    }
                } else {
                    previous_space = false;
                    new_s.push(c);
                }
            }

            *s = new_s
        }
    }
}

impl Cleaner for () {}

/// Implementation for french 'cleaning'
pub struct French {
    nb_char: char,
}

impl French {
    /// Creates a new french cleaner, which will replace spaces with the defined
    /// non-breaking space when appropriate.
    pub fn new(nb_char: char) -> French {
        French { nb_char: nb_char }
    }
}

impl Cleaner for French {
    // puts non breaking spaces between :, ;, ?, !, «, »
    fn clean(&self, s: &mut String) {
        fn is_trouble(c: char) -> bool {
            match c {
                '?'|'!'|';'|':'|'»'|'«'|'—' => true,
                _ => false
            }
        }

        if !s.contains(is_trouble) { // if not, no need to do anything
            return;
        }
        ().clean(s); // first pass with default impl
        let mut new_s = String::with_capacity(s.len());
        {
            let mut chars = s.chars();
            if let Some(mut current) = chars.next() {
                while let  Some(next) = chars.next() {
                    if is_whitespace(current) {
                        match next {
                            // handle narrow nb space before char
                            '?' | '!' | ';' => new_s.push(self.nb_char),
                            ':' | '»' => new_s.push(' '),
                            _ => new_s.push(current)
                        }
                    } else {
                        new_s.push(current);
                        match current {
                            // handle nb space after char
                            '—' => {
                                if is_whitespace(next) {
                                    // use demi "em space"
                                    new_s.push('\u{2002}');
                                    if let Some(next) = chars.next() {
                                        current = next;
                                        continue;
                                    }
                                }
                            },
                            '«' => {
                                if is_whitespace(next) {
                                    new_s.push(' ');
                                    if let Some(next) = chars.next() {
                                        current = next;
                                        continue;
                                    }
                                }

                            },
                            _ => (),
                        }
                    }
                    current = next;
                }
                new_s.push(current);
            }
        }

        *s = new_s
    }
}

<|endoftext|>


<commit_before>//! byt - io
//!
//! Implements a piece table to abstract over accessing and
//! modifying a file on disk.

// TODO: convert u32 to usize

// EXTERNS

// LIBRARY INCLUDES
use std::fmt;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Empty;
use std::io::BufReader;
use std::io::Read;
use std::io;

// SUBMODULES

// LOCAL INCLUDES

#[derive(Debug, Copy, Clone, PartialEq)]
enum SourceFile {
    /// The original file on disk.
    Original,
    /// The append file, held in memory.
    Append
}

/// A Piece stores some information about a part of the
/// modified file.
#[derive(Clone)]
struct Piece {
    /// The file this piece refers to.
    file : SourceFile,
    /// The offset (in bytes) of the text in this Piece in its file.
    file_offset : u32,
    /// The length of the text in this Piece.
    length : u32,
    /// The logical offset of the Piece.
    logical_offset : u32,
}

impl fmt::Display for Piece {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}:{} ({}:{})", self.file, self.file_offset, self.logical_offset, self.length)
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
enum Operation {
    Insert,
    Delete
}

/// Records a particular modification of the text.
struct Action {
    op     : Operation,
    offset : u32,
    pieces : Vec<Piece>,
    length : u32
}

/// Implements logical operations on a file that are not written
/// until asked to.
pub struct PieceFile {
    /// Stores all mutation operations of the file so that we can
    /// undo.
    actions : Vec<Action>,
    /// The in-memory append string. All edits will refer to bytes
    /// stored here.
    append_file : String,
    /// The total size (in bytes) of the PieceFile.
    length : u32,
    /// The path of the file this PieceFile refers to.
    path : String,
    /// Stores all current Pieces.
    piece_table : Vec<Piece>,
    /// The seekable file reader.
    reader : Option<BufReader<File>>,
}

impl PieceFile {
    // #################################
    // P R I V A T E  F U N C T I O N S
    // #################################
    /// Get a Piece at a particular offset.
    fn get_at_offset(&self, offset : u32) -> Option<usize> {
        let mut logical_length = 0;

        // This has to be O(N) for the time being unless we want
        // to update all logical offsets when the table changes.
        // On modern hardware this will hardly be a bottleneck,
        // but we'll see.
        for index in 0..self.piece_table.len() {
            let piece = &self.piece_table[index];
            logical_length += piece.length;

            if offset > logical_length {
                continue;
            }

            return Some(index);
        }

        None
    }

    /// Update the logical offsets starting at a certain index.
    fn update_offsets(&mut self, start_index : usize) {
        // Don't do anything if this is the last index
        if start_index == self.piece_table.len() - 1 {
            return;
        }

        let mut offset = self.piece_table[start_index].logical_offset +
                         self.piece_table[start_index].length;

        for index in start_index + 1 .. self.piece_table.len() {
            let piece = &mut self.piece_table[index];
            piece.logical_offset = offset;
            offset += piece.length;
        }
    }

    // ###############################
    // P U B L I C  F U N C T I O N S
    // ###############################
    /// Open a new PieceFile. If the file doesn't exist, it is created.
    pub fn open(path : String) -> io::Result<PieceFile> {
        let file = OpenOptions::new()
            .read(true)
            .open(path.clone()).unwrap();

        // Get the length of the file to initialize the first Piece
        let size = file.metadata().unwrap().len() as u32;

        let mut piece_file = PieceFile {
            actions     : Vec::new(),
            append_file : String::new(),
            length      : size,
            path,
            piece_table : Vec::new(),
            reader      : Some(BufReader::new(file)),
        };

        piece_file.piece_table.push(Piece {
            file : SourceFile::Original,
            file_offset : 0,
            length : size as u32,
            logical_offset : 0,
        });

        Ok(piece_file)
    }

    /// Create a new empty PieceFile.
    pub fn empty() -> io::Result<PieceFile> {
        let mut piece_file = PieceFile {
            actions     : Vec::new(),
            append_file : String::new(),
            length      : 0,
            path        : String::from(""),
            piece_table : Vec::new(),
            reader      : None,
        };

        Ok(piece_file)
    }

    /// Insert some text.
    pub fn insert(&mut self, text : &str, offset : u32) {
        let length = text.len() as u32;
        let append_offset = self.append_file.len() as u32;

        self.append_file += text;
        self.length      += length;

        let mut piece = Piece {
            file   : SourceFile::Append,
            file_offset : append_offset,
            logical_offset : 0, // Unknown right now
            length,
        };

        let action = Action {
            op     : Operation::Insert,
            pieces : Vec::new(),
            offset,
            length,
        };

        self.actions.push(action);

        // There are edge cases if you do an insert
        // at the beginning or end of the file.
        if offset == 0 {
            self.piece_table.insert(0, piece);
            self.update_offsets(0);
            return;
        }
        else if offset + length == self.length {
            piece.logical_offset = self.length - length;
            self.piece_table.push(piece);
            // No need to update offsets, this is the last piece
            return;
        }

        // The insertion may create as many as three pieces. It can
        // split a piece that already exists into two parts and then
        // goes in between them.
        let split_index = match self.get_at_offset(offset) {
            Some(v) => v,
            None => return // TODO: Handle this better
        };
        let split_piece = self.piece_table.remove(split_index);

        let lower_size  = offset - split_piece.logical_offset;
        let upper_size  = (split_piece.logical_offset + split_piece.length) - offset;

        if upper_size > 0 {
            self.piece_table.insert(split_index, Piece {
                file           : split_piece.file,
                file_offset    : split_piece.file_offset + lower_size,
                length         : upper_size,
                logical_offset : split_piece.logical_offset + lower_size + length,
            });
        }

        self.piece_table.insert(split_index, piece);

        if lower_size > 0 {
            self.piece_table.insert(split_index, Piece {
                file           : split_piece.file,
                file_offset    : split_piece.file_offset,
                length         : lower_size,
                logical_offset : split_piece.logical_offset,
            });
        }
        self.update_offsets(split_index);
    }

    /// Delete some text.
    pub fn delete(&mut self, offset : u32, length : u32) {
        let start_offset = offset;
        let end_offset   = offset + length;
        let start_index  = self.get_at_offset(start_offset).unwrap();
        let end_index    = self.get_at_offset(end_offset).unwrap();
        let delete_size  = (end_index - start_index) as u32;
        let num_pieces   = (end_index - start_index) + 1;

        let mut action = Action {
            length,
            offset,
            op     : Operation::Delete,
            pieces : Vec::new()
        };

        // Deletes can affect multiple pieces if the user wants to delete
        // across piece boundaries. We have to start at the bottom index
        // and move up. The code below doesn't result in a net positive number
        // of pieces in the piece table. We do any deletion necessary in a second
        // pass.
        for index in start_index .. end_index + 1 {
            let piece              = self.piece_table[index].clone();
            let piece_start_offset = piece.logical_offset;
            let piece_end_offset   = piece.logical_offset + piece.length;

            // Edge case : delete is embedded WITHIN a single piece
            if start_offset > piece_start_offset && end_offset < piece_end_offset {
                self.piece_table.remove(index);
                let upper_size = piece_end_offset - end_offset;
                let lower_size = start_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : delete_size,
                    logical_offset : start_offset,
                });

                // We insert the upper part first
                self.piece_table.insert(index, Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size + delete_size,
                    length         : upper_size,
                    logical_offset : start_offset,
                });

                // Then the lower part
                self.piece_table.insert(index, Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset,
                    length         : lower_size,
                    logical_offset : piece.logical_offset,
                });
                break
            }

            // The deletion area starts in a piece and goes on to another
            if start_offset < piece_end_offset && end_offset >= piece_end_offset {
                let piece      = &mut self.piece_table[index];
                let upper_size = piece_end_offset - start_offset;
                let lower_size = start_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : upper_size,
                    logical_offset : piece_start_offset + lower_size,
                });

                piece.length = lower_size;
                continue;
            }

            // The deletion area finishes in this piece
            if start_offset < piece_start_offset && end_offset >= piece_start_offset {
                let piece      = &mut self.piece_table[index];
                let upper_size = piece_end_offset - end_offset;
                let lower_size = end_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : lower_size,
                    logical_offset : piece_start_offset - lower_size,
                });

                piece.length = upper_size;
                continue;
            }

            if start_offset <= piece_start_offset && end_offset >= piece_end_offset {
                let piece = &mut self.piece_table[index];
                action.pieces.push(piece.clone());
                piece.length = 0;
            }
        }

        // It's possible that we left zero-length pieces above. We should delete them.
        self.piece_table.retain(|ref v| v.length > 0);

        self.update_offsets(start_index);
    }
}

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

    #[test]
    fn it_inserts() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("foo", 0);
        file.insert("bar", 0);

        assert_eq!(file.length, 6);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);

        let first_element  = &piece_table[0];
        let second_element = &piece_table[1];

        assert_eq!(first_element.file_offset, 3);
        assert_eq!(first_element.logical_offset, 0);
        assert_eq!(first_element.length, 3);

        assert_eq!(second_element.file_offset, 0);
        assert_eq!(second_element.logical_offset, 3);
        assert_eq!(second_element.length, 3);

        let action = &file.actions[0];
        assert_eq!(action.op, Operation::Insert);

        // TODO: Check the action's steps
    }

    #[test]
    fn it_deletes_inside_piece() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        // Test a delete that occurs inside a piece
        file.insert("foo", 0);
        file.delete(1, 1);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);
        assert_eq!(piece_table[0].length, 1);
        assert_eq!(piece_table[1].length, 1);

        // TODO: check the action's steps
    }

    #[test]
    fn it_deletes_across_two_pieces() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("bar", 0);
        file.insert("foo", 0);
        file.delete(2,2);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);
        assert_eq!(piece_table[0].length, 2);
        assert_eq!(piece_table[1].length, 2);

        // TODO: check the action's steps
    }
}
<commit_msg>feat(PieceFile): add more delete tests<commit_after>//! byt - io
//!
//! Implements a piece table to abstract over accessing and
//! modifying a file on disk.

// TODO: convert u32 to usize

// EXTERNS

// LIBRARY INCLUDES
use std::fmt;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Empty;
use std::io::BufReader;
use std::io::Read;
use std::io;

// SUBMODULES

// LOCAL INCLUDES

#[derive(Debug, Copy, Clone, PartialEq)]
enum SourceFile {
    /// The original file on disk.
    Original,
    /// The append file, held in memory.
    Append
}

/// A Piece stores some information about a part of the
/// modified file.
#[derive(Clone)]
struct Piece {
    /// The file this piece refers to.
    file : SourceFile,
    /// The offset (in bytes) of the text in this Piece in its file.
    file_offset : u32,
    /// The length of the text in this Piece.
    length : u32,
    /// The logical offset of the Piece.
    logical_offset : u32,
}

impl fmt::Display for Piece {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}:{} ({}:{})", self.file, self.file_offset, self.logical_offset, self.length)
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
enum Operation {
    Insert,
    Delete
}

/// Records a particular modification of the text.
struct Action {
    op     : Operation,
    offset : u32,
    pieces : Vec<Piece>,
    length : u32
}

/// Implements logical operations on a file that are not written
/// until asked to.
pub struct PieceFile {
    /// Stores all mutation operations of the file so that we can
    /// undo.
    actions : Vec<Action>,
    /// The in-memory append string. All edits will refer to bytes
    /// stored here.
    append_file : String,
    /// The total size (in bytes) of the PieceFile.
    length : u32,
    /// The path of the file this PieceFile refers to.
    path : String,
    /// Stores all current Pieces.
    piece_table : Vec<Piece>,
    /// The seekable file reader.
    reader : Option<BufReader<File>>,
}

impl PieceFile {
    // #################################
    // P R I V A T E  F U N C T I O N S
    // #################################
    /// Get a Piece at a particular offset.
    fn get_at_offset(&self, offset : u32) -> Option<usize> {
        let mut logical_length = 0;

        // This has to be O(N) for the time being unless we want
        // to update all logical offsets when the table changes.
        // On modern hardware this will hardly be a bottleneck,
        // but we'll see.
        for index in 0..self.piece_table.len() {
            let piece = &self.piece_table[index];
            logical_length += piece.length;

            if offset > logical_length {
                continue;
            }

            return Some(index);
        }

        None
    }

    /// Update the logical offsets starting at a certain index.
    fn update_offsets(&mut self, start_index : usize) {
        // Don't do anything if this is the last index
        if start_index == self.piece_table.len() - 1 {
            return;
        }

        let mut offset = self.piece_table[start_index].logical_offset +
                         self.piece_table[start_index].length;

        for index in start_index + 1 .. self.piece_table.len() {
            let piece = &mut self.piece_table[index];
            piece.logical_offset = offset;
            offset += piece.length;
        }
    }

    // ###############################
    // P U B L I C  F U N C T I O N S
    // ###############################
    /// Open a new PieceFile. If the file doesn't exist, it is created.
    pub fn open(path : String) -> io::Result<PieceFile> {
        let file = OpenOptions::new()
            .read(true)
            .open(path.clone()).unwrap();

        // Get the length of the file to initialize the first Piece
        let size = file.metadata().unwrap().len() as u32;

        let mut piece_file = PieceFile {
            actions     : Vec::new(),
            append_file : String::new(),
            length      : size,
            path,
            piece_table : Vec::new(),
            reader      : Some(BufReader::new(file)),
        };

        piece_file.piece_table.push(Piece {
            file : SourceFile::Original,
            file_offset : 0,
            length : size as u32,
            logical_offset : 0,
        });

        Ok(piece_file)
    }

    /// Create a new empty PieceFile.
    pub fn empty() -> io::Result<PieceFile> {
        let mut piece_file = PieceFile {
            actions     : Vec::new(),
            append_file : String::new(),
            length      : 0,
            path        : String::from(""),
            piece_table : Vec::new(),
            reader      : None,
        };

        Ok(piece_file)
    }

    /// Insert some text.
    pub fn insert(&mut self, text : &str, offset : u32) {
        let length = text.len() as u32;
        let append_offset = self.append_file.len() as u32;

        self.append_file += text;
        self.length      += length;

        let mut piece = Piece {
            file   : SourceFile::Append,
            file_offset : append_offset,
            logical_offset : 0, // Unknown right now
            length,
        };

        let action = Action {
            op     : Operation::Insert,
            pieces : Vec::new(),
            offset,
            length,
        };

        self.actions.push(action);

        // There are edge cases if you do an insert
        // at the beginning or end of the file.
        if offset == 0 {
            self.piece_table.insert(0, piece);
            self.update_offsets(0);
            return;
        }
        else if offset + length == self.length {
            piece.logical_offset = self.length - length;
            self.piece_table.push(piece);
            // No need to update offsets, this is the last piece
            return;
        }

        // The insertion may create as many as three pieces. It can
        // split a piece that already exists into two parts and then
        // goes in between them.
        let split_index = match self.get_at_offset(offset) {
            Some(v) => v,
            None => return // TODO: Handle this better
        };
        let split_piece = self.piece_table.remove(split_index);

        let lower_size  = offset - split_piece.logical_offset;
        let upper_size  = (split_piece.logical_offset + split_piece.length) - offset;

        if upper_size > 0 {
            self.piece_table.insert(split_index, Piece {
                file           : split_piece.file,
                file_offset    : split_piece.file_offset + lower_size,
                length         : upper_size,
                logical_offset : split_piece.logical_offset + lower_size + length,
            });
        }

        self.piece_table.insert(split_index, piece);

        if lower_size > 0 {
            self.piece_table.insert(split_index, Piece {
                file           : split_piece.file,
                file_offset    : split_piece.file_offset,
                length         : lower_size,
                logical_offset : split_piece.logical_offset,
            });
        }
        self.update_offsets(split_index);
    }

    /// Delete some text.
    pub fn delete(&mut self, offset : u32, length : u32) {
        let start_offset = offset;
        let end_offset   = offset + length;
        let start_index  = self.get_at_offset(start_offset).unwrap();
        let end_index    = self.get_at_offset(end_offset).unwrap();
        let delete_size  = (end_index - start_index) as u32;
        let num_pieces   = (end_index - start_index) + 1;

        let mut action = Action {
            length,
            offset,
            op     : Operation::Delete,
            pieces : Vec::new()
        };

        // Deletes can affect multiple pieces if the user wants to delete
        // across piece boundaries. We have to start at the bottom index
        // and move up. The code below doesn't result in a net positive number
        // of pieces in the piece table. We do any deletion necessary in a second
        // pass.
        for index in start_index .. end_index + 1 {
            let piece              = self.piece_table[index].clone();
            let piece_start_offset = piece.logical_offset;
            let piece_end_offset   = piece.logical_offset + piece.length;

            // Edge case : delete is embedded WITHIN a single piece
            if start_offset > piece_start_offset && end_offset < piece_end_offset {
                self.piece_table.remove(index);
                let upper_size = piece_end_offset - end_offset;
                let lower_size = start_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : delete_size,
                    logical_offset : start_offset,
                });

                // We insert the upper part first
                self.piece_table.insert(index, Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size + delete_size,
                    length         : upper_size,
                    logical_offset : start_offset,
                });

                // Then the lower part
                self.piece_table.insert(index, Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset,
                    length         : lower_size,
                    logical_offset : piece.logical_offset,
                });
                break
            }

            // The deletion area starts in this piece and goes on to another
            if start_offset > piece_start_offset && end_offset >= piece_end_offset {
                let piece      = &mut self.piece_table[index];
                let upper_size = piece_end_offset - start_offset;
                let lower_size = start_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : upper_size,
                    logical_offset : piece_start_offset + lower_size,
                });

                piece.length = lower_size;
                continue;
            }

            // The deletion area finishes in this piece
            if start_offset < piece_start_offset && end_offset <= piece_end_offset {
                let piece      = &mut self.piece_table[index];
                let upper_size = piece_end_offset - end_offset;
                let lower_size = end_offset - piece_start_offset;

                action.pieces.push(Piece {
                    file           : piece.file,
                    file_offset    : piece.file_offset + lower_size,
                    length         : lower_size,
                    logical_offset : piece_start_offset - lower_size,
                });

                piece.length = upper_size;
                continue;
            }

            // The deletion area is outside the bounds of this piece
            if start_offset <= piece_start_offset && end_offset >= piece_end_offset {
                let piece = &mut self.piece_table[index];
                action.pieces.push(piece.clone());
                piece.length = 0;
            }
        }

        // It's possible that we left zero-length pieces above. We should delete them.
        self.piece_table.retain(|ref v| v.length > 0);

        self.update_offsets(start_index);
    }
}

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

    #[test]
    fn it_inserts() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("foo", 0);
        file.insert("bar", 0);

        assert_eq!(file.length, 6);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);

        let first_element  = &piece_table[0];
        let second_element = &piece_table[1];

        assert_eq!(first_element.file_offset, 3);
        assert_eq!(first_element.logical_offset, 0);
        assert_eq!(first_element.length, 3);

        assert_eq!(second_element.file_offset, 0);
        assert_eq!(second_element.logical_offset, 3);
        assert_eq!(second_element.length, 3);

        let action = &file.actions[0];
        assert_eq!(action.op, Operation::Insert);

        // TODO: Check the action's steps
    }

    #[test]
    fn it_inserts_inside_piece() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("aa", 0);
        file.insert("b", 1);

        assert_eq!(file.length, 3);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 3);

        let first_element  = &piece_table[0];
        let second_element = &piece_table[1];
        let third_element  = &piece_table[2];

        assert_eq!(first_element.file_offset, 0);
        assert_eq!(first_element.logical_offset, 0);
        assert_eq!(first_element.length, 1);

        assert_eq!(second_element.file_offset, 2);
        assert_eq!(second_element.logical_offset, 1);
        assert_eq!(second_element.length, 1);

        assert_eq!(third_element.file_offset, 1);
        assert_eq!(third_element.logical_offset, 2);
        assert_eq!(third_element.length, 1);

        let action = &file.actions[0];
        assert_eq!(action.op, Operation::Insert);

        // TODO: Check the action's steps
    }

    #[test]
    fn it_deletes_inside_piece() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        // Test a delete that occurs inside a piece
        file.insert("foo", 0);
        file.delete(1, 1);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);
        assert_eq!(piece_table[0].length, 1);
        assert_eq!(piece_table[1].length, 1);

        // TODO: check the action's steps
    }

    #[test]
    fn it_deletes_across_two_pieces() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("bar", 0);
        file.insert("foo", 0);
        file.delete(2,2);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);
        assert_eq!(piece_table[0].length, 2);
        assert_eq!(piece_table[1].length, 2);

        // TODO: check the action's steps
    }

    #[test]
    fn it_deletes_across_three_pieces() {
        let mut file = PieceFile::empty().unwrap();
        assert_eq!(file.piece_table.len(), 0);

        file.insert("cc", 0);
        file.insert("bb", 0);
        file.insert("aa", 0);
        file.delete(1, 4);

        let piece_table = &file.piece_table;
        assert_eq!(piece_table.len(), 2);

        // TODO: check the action's steps
    }
}
<|endoftext|>