Compare commits

..

8 Commits

Author SHA1 Message Date
Thomas Eppers
0a844c42ee remove warnings of dead code 2021-09-24 16:36:16 +02:00
Thomas Eppers
a068e3f192 Merge branch 'master' of ssh://gitea.eppixx.freeddns.org:10022/eppixx/query-docker-tags into dev/repo-opject 2021-09-24 16:17:53 +02:00
Thomas Eppers
e66b75c021 added a list of common docker-compose file names to open 2021-09-15 10:00:28 +02:00
Thomas Eppers
7203a7309d removed unused warning 2021-09-14 16:46:07 +02:00
Thomas Eppers
d097c41192 removed unused warning 2021-09-14 16:45:55 +02:00
Thomas Eppers
4f0cd0423a use more functions from repo 2021-09-13 17:05:06 +02:00
Thomas Eppers
48c45a1372 switched out regex in ServiceSwitcher to repo functions 2021-09-13 15:54:37 +02:00
Thomas Eppers
fadbe48b05 WIP 2021-09-11 11:07:56 +02:00
7 changed files with 232 additions and 79 deletions

1
Cargo.lock generated
View File

@ -541,6 +541,7 @@ name = "query-docker-tags"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"lazy_static",
"regex", "regex",
"reqwest", "reqwest",
"serde", "serde",

View File

@ -14,6 +14,7 @@ chrono = "0.4.19"
tui = "0.16" tui = "0.16"
termion = "1.5" termion = "1.5"
regex = "1.5.4" regex = "1.5.4"
lazy_static = "1.4.0"
[profile.release] [profile.release]
lto = "yes" lto = "yes"

View File

@ -1,3 +1,4 @@
mod repo;
mod tags; mod tags;
mod ui; mod ui;
mod widget; mod widget;

164
src/repo.rs Normal file
View File

@ -0,0 +1,164 @@
use std::fmt;
use regex::Regex;
// use crate::common;
#[derive(Debug, PartialEq)]
pub enum Error {
// Conversion,
// Empty,
NoTagFound,
// InvalidChar,
MisformedInput,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error::Conversion => write!(f, "Conversion error"),
// Error::Empty => write!(f, "Input is empty"),
Error::NoTagFound => write!(f, "Expected a tag"),
// Error::InvalidChar => write!(f, "Invalid character found"),
Error::MisformedInput => write!(f, "Unexpected input"),
}
}
}
#[derive(Debug, PartialEq)]
pub enum Repo {
WithServer(String, String, String),
WithOrga(String, String),
Project(String),
}
/// check if yaml line matches and returns the split of repo string and rest
pub fn match_yaml_image(input: &str) -> Result<(&str, &str), Error> {
lazy_static::lazy_static! {
static ref REGEX: Regex = Regex::new(r"^( +image *: *)([a-z0-9\./:]+)").unwrap();
}
let caps = match REGEX.captures(input) {
Some(caps) => caps,
None => return Err(Error::NoTagFound),
};
Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
}
pub fn split_tag_from_repo(input: &str) -> Result<(&str, &str), Error> {
lazy_static::lazy_static! {
static ref REGEX: Regex = Regex::new(r"^([a-z0-9\./[^:]]*):?([a-z0-9._\-]*)").unwrap();
}
let (front, back) = match REGEX.captures(input) {
None => return Err(Error::MisformedInput),
Some(caps) => {
let front = match caps.get(1) {
None => return Err(Error::MisformedInput),
Some(cap) => cap.as_str(),
};
let back = match caps.get(2) {
None => "",
Some(cap) => cap.as_str(),
};
(front, back)
}
};
Ok((front, back))
}
pub fn split_repo_without_tag(repo: &str) -> Result<Repo, Error> {
let repo = repo.trim();
let split_repo: Vec<&str> = repo.split("/").collect();
match split_repo.len() {
1 => {
let regex = regex::Regex::new(r"[a-z0-9]+").unwrap();
match regex.is_match(repo) {
false => Err(Error::MisformedInput),
true => Ok(Repo::Project(split_repo[0].into())),
}
}
2 => {
let regex = regex::Regex::new(r"[a-z0-9]+/[a-z0-9]+").unwrap();
match regex.is_match(repo) {
false => Err(Error::MisformedInput),
true => Ok(Repo::WithOrga(split_repo[0].into(), split_repo[1].into())),
}
}
3 => {
let regex = regex::Regex::new(r"[a-z0-9\.]+/[a-z0-9]+/[a-z0-9]+").unwrap();
match regex.is_match(repo) {
false => Err(Error::MisformedInput),
true => Ok(Repo::WithServer(
split_repo[0].into(),
split_repo[1].into(),
split_repo[2].into(),
)),
}
}
_ => Err(Error::MisformedInput),
}
}
#[cfg(test)]
mod tests {
use crate::repo;
use crate::repo::{Error, Repo};
#[test]
fn test_split_repo_without_tag() {
use crate::repo::split_repo_without_tag as test_fn;
assert_eq!(test_fn(""), Err(Error::MisformedInput));
assert_eq!(test_fn("NGINX"), Err(Error::MisformedInput));
assert_eq!(test_fn("nginx"), Ok(Repo::Project("nginx".into())));
assert_eq!(
test_fn("library/nginx"),
Ok(Repo::WithOrga("library".into(), "nginx".into()))
);
assert_eq!(
test_fn("ghcr.io/library/nginx"),
Ok(Repo::WithServer(
"ghcr.io".into(),
"library".into(),
"nginx".into(),
))
);
}
#[test]
fn test_match_yaml_image() {
use crate::repo::match_yaml_image as test_fn;
assert_eq!(test_fn(""), None);
assert_eq!(test_fn("version: '2'"), None);
assert_eq!(test_fn("image: "), None);
assert_eq!(test_fn(" image: "), None);
assert_eq!(test_fn(" image: nginx"), Some((" image: ", "nginx")));
assert_eq!(
test_fn(" image: library/nginx"),
Some((" image: ", "library/nginx"))
);
assert_eq!(
test_fn(" image: ghcr.io/library/nginx"),
Some((" image: ", "ghcr.io/library/nginx"))
);
assert_eq!(test_fn("# image: nginx"), None);
assert_eq!(
test_fn(" image: nginx #comment"),
Some((" image: ", "nginx"))
);
}
#[test]
fn test_split_tag_from_repo() {
use crate::repo::split_tag_from_repo as test_fn;
assert_eq!(test_fn("nginx"), Ok(("nginx", "")));
assert_eq!(test_fn("library/nginx"), Ok(("library/nginx", "")));
assert_eq!(
test_fn("ghcr.io/library/nginx"),
Ok(("ghcr.io/library/nginx", ""))
);
assert_eq!(test_fn("nginx:"), Ok(("nginx", "")));
assert_eq!(test_fn("nginx:1"), Ok(("nginx", "1")));
assert_eq!(test_fn("nginx:latest"), Ok(("nginx", "latest")));
}
}

View File

@ -1,5 +1,6 @@
use std::fmt; use std::fmt;
use crate::repo;
use chrono::DateTime; use chrono::DateTime;
use serde::Deserialize; use serde::Deserialize;
@ -34,10 +35,8 @@ pub struct Tags {
pub results: Vec<Images>, pub results: Vec<Images>,
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
/// repo string contains an illegal character
InvalidCharacter(char),
/// couldn't fetch json with reqwest /// couldn't fetch json with reqwest
Fetching(String), Fetching(String),
/// a serde error /// a serde error
@ -51,7 +50,6 @@ pub enum Error {
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Error::InvalidCharacter(c) => write!(f, "Invalid Character: {}", c),
Error::Fetching(s) => write!(f, "Fetching error: {}", s), Error::Fetching(s) => write!(f, "Fetching error: {}", s),
Error::Converting(s) => write!(f, "Converting error: {}", s), Error::Converting(s) => write!(f, "Converting error: {}", s),
Error::NoNextPage => write!(f, "No next page available"), Error::NoNextPage => write!(f, "No next page available"),
@ -90,18 +88,17 @@ impl Tags {
} }
/// checks the repo name and may add a prefix for official images /// checks the repo name and may add a prefix for official images
pub fn check_repo(mut name: String) -> Result<String, Error> { pub fn check_repo(name: &str) -> Result<String, Error> {
//check for right set of characters let repo = match repo::split_tag_from_repo(name) {
if name.bytes().any(|c| !c.is_ascii()) { Err(e) => return Err(Error::Converting(format!("{}", e))),
return Err(Error::InvalidCharacter('a')); Ok((name, _)) => name,
} };
//check if need to inject "library" of given repo match repo::split_repo_without_tag(name) {
let regex = regex::Regex::new(r".*/.*").unwrap(); Ok(repo::Repo::Project(s)) => Ok(format!("library/{}", s)),
if !regex.is_match(&name) { Ok(_) => Ok(repo.to_string()),
name.insert_str(0, "library/"); Err(e) => Err(Error::Converting(format!("{}", e))),
} }
Ok(name)
} }
/// returns tags of next page /// returns tags of next page
@ -167,26 +164,14 @@ fn format_time_nice(time: chrono::Duration) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tags; use crate::tags::{Error, Tags};
#[test] #[test]
fn test_check_repo() { fn test_check_repo() {
let check_eq = |s, s2| { assert_eq!(Tags::check_repo("nginx").unwrap(), "library/nginx");
assert_eq!(&tags::Tags::check_repo(String::from(s)).unwrap(), s2); assert_eq!(Tags::check_repo("library/nginx").unwrap(), "library/nginx");
}; assert_eq!(
let check_neq = |s, s2| { Tags::check_repo("rocketchat/rocket.chat").unwrap(),
assert_ne!(&tags::Tags::check_repo(String::from(s)).unwrap(), s2); "rocketchat/rocket.chat"
}; );
let check_err = |s: &str| {
assert_eq!(tags::Tags::check_repo(String::from(s)).is_err(), true);
};
check_eq("nginx", "library/nginx");
check_neq("nginx", "nginx");
check_eq("rocketchat/rocket.chat", "rocketchat/rocket.chat");
check_eq("mysql", "library/mysql");
check_neq("mysql", "mysql");
check_err("nginxä");
check_err("nginx²");
check_eq("selim13/automysqlbackup", "selim13/automysqlbackup");
} }
} }

View File

@ -13,11 +13,11 @@ use crate::widget::repo_entry;
use crate::widget::service_switcher; use crate::widget::service_switcher;
use crate::widget::tag_list; use crate::widget::tag_list;
pub struct Ui { pub struct Ui<'a> {
state: State, state: State,
repo: crate::widget::repo_entry::RepoEntry, repo: crate::widget::repo_entry::RepoEntry,
tags: crate::widget::tag_list::TagList, tags: crate::widget::tag_list::TagList,
services: crate::widget::service_switcher::ServiceSwitcher, services: crate::widget::service_switcher::ServiceSwitcher<'a>,
info: crate::widget::info::Info, info: crate::widget::info::Info,
} }
@ -41,7 +41,7 @@ impl std::iter::Iterator for State {
} }
} }
impl Ui { impl Ui<'_> {
pub fn run(repo_id: &str) { pub fn run(repo_id: &str) {
let mut ui = Ui { let mut ui = Ui {
state: State::SelectService, state: State::SelectService,
@ -151,14 +151,14 @@ impl Ui {
match ui.services.extract_repo() { match ui.services.extract_repo() {
Err(e) => ui.info.set_info(&format!("{}", e)), Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(s) => { Ok(s) => {
let repo = match crate::tags::Tags::check_repo(s) { let repo = match crate::tags::Tags::check_repo(&s) {
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
} }
Ok(s) => s, Ok(s) => s,
}; };
ui.repo.set(repo); ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
} }
@ -171,14 +171,14 @@ impl Ui {
match ui.services.extract_repo() { match ui.services.extract_repo() {
Err(e) => ui.info.set_info(&format!("{}", e)), Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(s) => { Ok(s) => {
let repo = match crate::tags::Tags::check_repo(s) { let repo = match crate::tags::Tags::check_repo(&s) {
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
} }
Ok(s) => s, Ok(s) => s,
}; };
ui.repo.set(repo); ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
} }

View File

@ -4,10 +4,10 @@ use std::io::BufRead;
use std::io::BufReader; use std::io::BufReader;
use std::io::Write; use std::io::Write;
use regex::Regex;
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, ListState}; use tui::widgets::{Block, Borders, List, ListState};
use crate::repo;
use crate::ui::State; use crate::ui::State;
#[derive(Debug)] #[derive(Debug)]
@ -25,30 +25,42 @@ impl fmt::Display for Error {
} }
} }
pub struct ServiceSwitcher { pub struct ServiceSwitcher<'a> {
list: Vec<String>, list: Vec<String>,
state: ListState, state: ListState,
regex: Regex,
changed: bool, changed: bool,
opened_file: &'a str,
} }
impl ServiceSwitcher { impl ServiceSwitcher<'_> {
pub fn new() -> Self { pub fn new() -> Self {
let list = match File::open("docker-compose.yml") { let file_list = vec!["docker-compose.yml", "docker-compose.yaml"];
Err(e) => vec![format!("No docker-compose.yml found: {}", e)],
Ok(file) => {
let buf = BufReader::new(file);
buf.lines()
.map(|l| l.expect("Could not parse line"))
.collect()
}
};
for file in file_list {
let list = match File::open(file) {
Err(_) => continue,
Ok(file) => {
let buf = BufReader::new(file);
buf.lines()
.map(|l| l.expect("Could not parse line"))
.collect()
}
};
return Self {
list,
state: ListState::default(),
changed: false,
opened_file: file,
};
}
//could not find docker-compose file
Self { Self {
list, list: vec![format!("No docker-compose file found")],
state: ListState::default(), state: ListState::default(),
regex: Regex::new(r"( *image *): *([^:]*):?([^:]?) *").unwrap(),
changed: false, changed: false,
opened_file: "No file",
} }
} }
@ -60,8 +72,8 @@ impl ServiceSwitcher {
}; };
let title = match &self.changed { let title = match &self.changed {
true => "File: *docker-compose.yml*", true => format!("File: *{}*", self.opened_file),
false => "File: docker-compose.yml", false => format!("File: {}", self.opened_file),
}; };
let items: Vec<tui::widgets::ListItem> = self let items: Vec<tui::widgets::ListItem> = self
@ -103,7 +115,7 @@ impl ServiceSwitcher {
} }
//check if line matches //check if line matches
if self.regex.is_match(&self.list[i]) { if repo::match_yaml_image(&self.list[i]).is_ok() {
self.state.select(Some(i)); self.state.select(Some(i));
return true; return true;
} }
@ -135,7 +147,7 @@ impl ServiceSwitcher {
} }
//check if line matches //check if line matches
if self.regex.is_match(&self.list[i]) { if repo::match_yaml_image(&self.list[i]).is_ok() {
self.state.select(Some(i)); self.state.select(Some(i));
return true; return true;
} }
@ -152,14 +164,10 @@ impl ServiceSwitcher {
pub fn extract_repo(&self) -> Result<String, Error> { pub fn extract_repo(&self) -> Result<String, Error> {
match self.state.selected() { match self.state.selected() {
None => return Err(Error::NoneSelected), None => return Err(Error::NoneSelected),
Some(i) => { Some(i) => match repo::match_yaml_image(&self.list[i]) {
let caps = match self.regex.captures(&self.list[i]) { Err(_) => return Err(Error::Parsing(String::from("Nothing found"))),
None => return Err(Error::Parsing(String::from("Nothing found"))), Ok((_, repo)) => return Ok(repo.to_string()),
Some(cap) => cap, },
};
let result: String = caps.get(2).unwrap().as_str().to_string();
return Ok(result);
}
} }
} }
@ -167,24 +175,17 @@ impl ServiceSwitcher {
pub fn change_current_line(&mut self, repo_with_tag: String) { pub fn change_current_line(&mut self, repo_with_tag: String) {
match self.state.selected() { match self.state.selected() {
None => (), None => (),
Some(i) => { Some(i) => match repo::match_yaml_image(&self.list[i]) {
let caps = match self.regex.captures(&self.list[i]) { Err(_) => return,
None => return, Ok((front, _)) => self.list[i] = format!("{}{}", front, repo_with_tag),
Some(cap) => cap, },
};
let mut line = caps.get(1).unwrap().as_str().to_string();
line.push_str(": ");
line.push_str(&repo_with_tag);
self.list[i] = line;
}
} }
self.changed = true; self.changed = true;
} }
/// save the currently opened file /// save the currently opened file
pub fn save(&mut self) -> Result<(), std::io::Error> { pub fn save(&mut self) -> Result<(), std::io::Error> {
let name = "docker-compose.yml"; let mut file = File::create(self.opened_file)?;
let mut file = File::create(name)?;
for line in &self.list { for line in &self.list {
file.write_all(line.as_bytes())?; file.write_all(line.as_bytes())?;
file.write_all("\n".as_bytes())?; file.write_all("\n".as_bytes())?;