use std::cmp::max;
use std::collections::HashSet;
use std::fs;
use crate::basetype::BaseType;
use crate::language::Language;
use crate::{aperror, basetype, language, lint};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::SystemTime;
pub enum TypeImpl {
Label,
Join,
JoinOptional,
Scalar,
Variant,
}
#[derive(Default)]
pub struct ColumnConfig {
pub name: String,
pub iterable: bool,
pub optional: bool,
pub help: String,
}
pub struct ColumnInfo {
pub config: ColumnConfig,
pub len: usize,
pub interface_type: basetype::BaseType, pub table_type: basetype::BaseType, }
impl ColumnInfo {
pub fn type_impl(&self) -> TypeImpl {
match self.interface_type {
BaseType::Label { .. } => TypeImpl::Label,
BaseType::Bool
| BaseType::F32
| BaseType::F64
| BaseType::I8
| BaseType::I16
| BaseType::I32
| BaseType::I64
| BaseType::U8
| BaseType::U16
| BaseType::U32
| BaseType::U64
| BaseType::Str
| BaseType::Object { .. } => TypeImpl::Scalar,
BaseType::Join { .. } => {
if self.config.optional {
TypeImpl::JoinOptional
} else {
TypeImpl::Join
}
}
BaseType::Variant => TypeImpl::Variant,
}
}
pub fn join_table(&self) -> String {
match &self.interface_type {
basetype::BaseType::Join { strname: name, .. } => name.to_string(),
_ => "".to_string(),
}
}
pub fn has_iter_range(&self) -> bool {
match self.interface_type {
basetype::BaseType::Join { .. } | basetype::BaseType::Variant => false, _ => self.config.iterable,
}
}
}
pub struct Variant {
pub name: String, pub index: usize,
pub count: usize,
pub reverse: String, pub is_none: bool, }
pub trait Column {
fn info(&self) -> &ColumnInfo;
fn name(&self) -> &String {
&self.info().config.name
}
fn iterable(&self) -> bool {
self.info().config.iterable
}
fn optional(&self) -> bool {
self.info().config.optional
}
fn help(&self) -> &String {
&self.info().config.help
}
fn emit_table_cell(&self, row: usize, lang: &dyn Language) -> String;
fn emit_label(&self, _row: usize) -> String {
"EMIT_LABEL_UNSUPORTED".to_string()
}
fn emit_label_help(&self, _row: usize) -> String {
"".to_string()
}
fn indexes(&self) -> Vec<usize>;
fn lint(&self, linter: &lint::Linter);
fn reverse_name(&self) -> String {
"".to_string()
}
fn fill_import(&self, _out: &mut HashSet<String>) {}
fn variants(&self) -> Option<&Vec<Variant>> {
None
}
}
pub struct Table {
pub name: String,
pub len: usize,
pub columns: Vec<Box<dyn Column>>,
pub get_array: bool,
pub outcol_indexes: Vec<usize>, pub labcol_indexes: Vec<usize>, }
impl Table {
pub fn new(name: &str, columns: Vec<Box<dyn Column>>, get_array: bool) -> Table {
let mut outcol_indexes: Vec<usize> = Vec::new();
let mut labcol_indexes: Vec<usize> = Vec::new();
let len = match columns.first() {
None => 0,
Some(c) => c.info().len,
};
for (index, col) in columns.iter().enumerate() {
let info = col.info();
match info.table_type {
basetype::BaseType::Label { .. } => {
if !col.name().is_empty() {
outcol_indexes.push(index)
}
labcol_indexes.push(index)
}
_ => outcol_indexes.push(index),
}
}
Table {
name: name.to_owned(),
len,
columns,
get_array,
outcol_indexes,
labcol_indexes,
}
}
fn lint(&self, linter: &lint::Linter) {
linter.context(&self.name, |lt_table| {
lt_table.err(lint::label(&self.name), "invalid table name");
lt_table.err(
self.has_data() || !self.get_array,
"unable to use array on table without values",
);
let mut colnames = HashSet::<&String>::new();
for col in &self.data_columns() {
let info = col.info();
lt_table.context(col.name(), |lt_col| {
lt_col.err(lint::label(col.name()), "invalid column name");
lt_col.err(!colnames.contains(col.name()), "duplicated column name");
lt_col.err(self.len == info.len, "mismatched number of rows");
col.lint(lt_col)
});
colnames.insert(col.name());
}
})
}
pub fn data_columns(&self) -> Vec<&dyn Column> {
self.outcol_indexes
.iter()
.map(|i| self.columns[*i].as_ref())
.collect()
}
pub fn has_data(&self) -> bool {
!self.outcol_indexes.is_empty()
}
pub fn label_columns(&self) -> Vec<&dyn Column> {
self.labcol_indexes
.iter()
.map(|i| self.columns[*i].as_ref())
.collect()
}
pub fn index_type(&self) -> basetype::BaseType {
basetype::int_type_for_range(0..=self.len as i64)
}
pub fn imports(&self) -> HashSet<String> {
let mut imports: HashSet<String> = HashSet::new();
for col in &self.columns {
col.fill_import(&mut imports);
}
imports
}
}
pub struct JoinTo<'a> {
pub table: &'a Table,
pub col: &'a dyn Column,
pub offset: usize,
pub reverse_name: String,
}
impl<'a> JoinTo<'a> {
fn from_join(join: &'a Table, col: &'a dyn Column) -> JoinTo<'a> {
JoinTo {
table: join,
col,
offset: col.optional() as usize,
reverse_name: col.reverse_name().to_string(),
}
}
fn from_variant(join: &'a Table, col: &'a dyn Column, vrt: &Variant) -> JoinTo<'a> {
JoinTo {
table: join,
col,
offset: vrt.index,
reverse_name: vrt.reverse.to_string(),
}
}
}
pub struct Project {
pub dst_path: PathBuf,
pub lang: &'static dyn language::Language,
pub tables: Vec<Table>,
pub src_paths: Vec<PathBuf>,
}
impl Project {
pub fn lint(&self, linter: &lint::Linter) {
let projectname = self.name();
linter.context(&projectname, |lt| {
lt.err(lint::label(&projectname), "invalid project name");
let mut tblnames = HashSet::<&String>::new();
for table in &self.tables {
lt.err(!tblnames.contains(&table.name), "duplicated table name");
table.lint(lt);
lt.err(
table.has_data() || self.join_to_columns(table).is_empty(),
&format!(
"unable to create reverse join to table without values {}",
table.name
),
);
tblnames.insert(&table.name);
}
})
}
pub fn src_modified(&self) -> SystemTime {
let mut last_modified = std::time::UNIX_EPOCH;
for pathbuf in &self.src_paths {
let modified = match fs::metadata(pathbuf.as_path()) {
Ok(v) => v.modified().unwrap_or(SystemTime::now()),
Err(_) => SystemTime::now(), };
last_modified = max(last_modified, modified);
}
last_modified
}
pub fn modified(&self) -> bool {
let dst_modified = self
.lang
.dst_modified(self)
.unwrap_or(std::time::UNIX_EPOCH);
dst_modified < self.src_modified()
}
pub fn emit(&self) -> aperror::Result<()> {
self.lang.emit(self)
}
pub fn name(&self) -> String {
match self.dst_path.file_stem() {
Some(name) => name.to_os_string().into_string().unwrap(),
None => "unnamed".to_string(),
}
}
pub fn table_need_iter(&self, table: &Table) -> bool {
for col in &table.columns {
if col.iterable() {
return true;
}
}
!self.join_to_columns(table).is_empty()
}
pub fn join_to_columns(&self, table: &Table) -> Vec<JoinTo> {
let mut columns = Vec::<JoinTo>::new();
for join in &self.tables {
if join.name == table.name {
continue;
}
for col in &join.columns {
let info = col.info();
match &info.interface_type {
basetype::BaseType::Join { strname, .. } => {
if strname == &table.name && col.iterable() {
columns.push(JoinTo::from_join(join, col.as_ref()));
}
}
basetype::BaseType::Variant => {
for vrt in col.variants().expect("variant expected") {
if vrt.name == table.name && !vrt.reverse.is_empty() {
columns.push(JoinTo::from_variant(join, col.as_ref(), vrt));
}
}
}
_ => {}
};
}
}
columns
}
pub fn imports(&self) -> HashSet<String> {
let mut imports: HashSet<String> = HashSet::new();
for table in &self.tables {
for col in &table.columns {
col.fill_import(&mut imports);
}
}
imports
}
}
pub fn parse_vec<T: FromStr>(strvals: &[String]) -> aperror::Result<Vec<T>> {
let mut vals: Vec<T> = vec![];
for (i, s) in strvals.iter().enumerate() {
let Ok(v) = s.parse::<T>() else {
return Err(aperror::Error::new(&format!(
"{} not a number at row {}",
s, i
)));
};
vals.push(v);
}
Ok(vals)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::colstr::ColStr;
use crate::langrust;
use crate::lint::test_linter;
#[test]
fn duplicate_table_name() {
let t1 = Table::new("mytable", vec![], false);
let t2 = Table::new("mytable", vec![], false);
let project = Project {
dst_path: PathBuf::new(),
lang: langrust::RUST,
tables: vec![t1, t2],
src_paths: vec![],
};
let linter = test_linter();
project.lint(&linter);
assert!(linter.errors() == 1);
}
#[test]
fn duplicate_col_name() {
let a1 = ColStr::parse(
ColumnConfig {
name: "mycol".to_string(),
..Default::default()
},
&vec![],
)
.unwrap();
let a2 = ColStr::parse(
ColumnConfig {
name: "mycol".to_string(),
..Default::default()
},
&vec![],
)
.unwrap();
let t = Table::new("table", vec![a1, a2], false);
let linter = test_linter();
t.lint(&linter);
assert!(linter.errors() == 1);
}
}