1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#![allow(non_snake_case)]

mod attr;
mod metadata;
mod error;
mod ext;
mod syndecode;

use std::collections::HashMap;
use std::fs;
use std::path::Path;
use syn::{DeriveInput, Item};
use ignore::Walk;

use syndecode::{Attributes};
pub use metadata::*;
pub use attr::*;
pub use error::*;
pub use ext::*;

#[derive(Default, Debug)]
pub struct LoadOptions {
    pub verbose: bool,
}

pub struct OrmliteSchema {
    pub tables: Vec<TableMetadata>,
    // map of rust structs (e.g. enums) to database encodings.
    // note that these are not bona fide postgres types.
    pub type_reprs: HashMap<String, String>,
}

type WithAttr<T> = (T, Attributes);

struct Intermediate {
    model_structs: Vec<WithAttr<syn::ItemStruct>>,
    type_structs: Vec<WithAttr<syn::ItemStruct>>,
    type_enums: Vec<WithAttr<syn::ItemEnum>>,
}

impl Intermediate {
    fn into_models_and_types(self) -> (impl Iterator<Item=WithAttr<syn::ItemStruct>>, impl Iterator<Item=WithAttr<String>>) {
        let models = self.model_structs.into_iter();
        let types = self.type_structs.into_iter().map(|(s, a)| (s.ident.to_string(), a))
            .chain(self.type_enums.into_iter().map(|(e, a)| (e.ident.to_string(), a)));
        (models, types)
    }

    fn from_with_opts(value: syn::File, opts: &LoadOptions) -> Self {
        let mut model_structs = Vec::new();
        let mut type_structs = Vec::new();
        let mut type_enums = Vec::new();
        for item in value.items {
            match item {
                Item::Struct(s) => {
                    let mut attrs = Attributes::from(s.attrs.as_slice());
                    if opts.verbose {
                        eprintln!("Found struct: {}. Attributes: {}", s.ident, attrs);
                    }
                    if attrs.has_derive("Model") {
                        attrs.retain("ormlite");
                        model_structs.push((s, attrs));
                    } else if attrs.has_derive("Type") {
                        type_structs.push((s, attrs));
                    } else if attrs.has_derive("ManualType") {
                        type_structs.push((s, attrs));
                    }
                }
                Item::Enum(e) => {
                    let attrs = Attributes::from(e.attrs.as_slice());
                    if opts.verbose {
                        eprintln!("Found struct: {}. Attributes: {}", e.ident, attrs);
                    }
                    if attrs.has_derive("Type") || attrs.has_derive("ManualType") {
                        type_enums.push((e, attrs));
                    }
                }
                _ => {}
            }
        }
        Self {
            model_structs,
            type_structs,
            type_enums,
        }
    }
}

pub fn schema_from_filepaths(paths: &[&Path], opts: &LoadOptions) -> anyhow::Result<OrmliteSchema> {
    let walk = paths.iter().flat_map(Walk::new);

    let walk = walk.filter_map(|e| e.ok())
        .filter(|e| e.path().extension().map(|e| e == "rs")
            .unwrap_or(false))
        .map(|e| e.into_path())
        .chain(paths.iter()
            .filter(|p| p.ends_with(".rs"))
            .map(|p| p.to_path_buf())
        );

    let mut tables = vec![];
    let mut type_aliases = HashMap::new();
    for entry in walk {
        let contents = fs::read_to_string(&entry)?;
        if !contents.contains("Model") {
            continue;
        }
        if opts.verbose {
            eprintln!("{}: Checking for #[derive(Model)]", entry.display());
        }
        let ast = syn::parse_file(&contents)?;
        let intermediate = Intermediate::from_with_opts(ast, opts);
        let (models, types) = intermediate.into_models_and_types();

        for (item, _attrs) in models {
            let derive: DeriveInput = item.into();
            let table = TableMetadata::try_from(&derive)
                .map_err(|e| SyndecodeError(format!(
                    "{}: Encountered an error while scanning for #[derive(Model)] structs: {}",
                    entry.display(), e))
                )?;
            tables.push(table);
        }

        for (name, attrs) in types {
            let mut ty = "String".to_string();
            for attr in attrs.iter_args() {
                if attr.name != "repr" {
                    continue;
                }
                ty = attr.args.first().expect("repr attribute must have at least one argument").clone();
            }
            type_aliases.insert(name, ty);
        }
    }
    Ok(OrmliteSchema {
        tables,
        type_reprs: type_aliases,
    })
}