Skip to content

Commit 430a6e1

Browse files
committed
initial commit
1 parent ac833c7 commit 430a6e1

File tree

14 files changed

+442
-32
lines changed

14 files changed

+442
-32
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ serde = {version="1.0.126", features = ["derive"]}
1414
serde_yaml = "0.8.17"
1515
serde_json = "1.0.64"
1616
toml = "0.5.8"
17+
anyhow = "1.0.41"
18+
structopt = "0.3.21"
1719

1820
[profile.release]
1921
lto = "fat"

docs/config.tosd

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
[toml-schema]
2+
version = "1.0"
3+
4+
[types]
5+
[types.field]
6+
[types.field.name]
7+
type = "string"
8+
[types.field.type]
9+
type = "string"
10+
allowedvalues = [
11+
"int",
12+
"serial",
13+
"integer",
14+
"small_int",
15+
"tiny_int",
16+
"medium_int",
17+
"big_int",
18+
"big_serial",
19+
"unsigned_serial",
20+
"uint",
21+
"unsigned_tiny_int",
22+
"unsigned_tiny_integer",
23+
"unsigned",
24+
"unsigned_medium_int",
25+
"unsigned_medium_integer",
26+
"unsigned_big_int",
27+
"unsigned_big_integer",
28+
"float",
29+
"float32",
30+
"double",
31+
"float64",
32+
"string",
33+
"char(\\d+)",
34+
"varchar(\\d+)",
35+
"tiny_text",
36+
"text",
37+
"medium_text",
38+
"long_text",
39+
"tiny_blob",
40+
"blob",
41+
"medium_blob",
42+
"long_blob",
43+
"bool",
44+
"date",
45+
"date_time",
46+
"time",
47+
"timestamp",
48+
]
49+
50+
[elements]
51+
[elements.api]
52+
type = "table"
53+
optional = true
54+
[elements.api.name]
55+
type = "string"
56+
[elements.api.fields]
57+
type = "array"
58+
arraytype = "string"
59+
[elements.api.fields.elements]
60+
type = "array"
61+
arraytypeof = "field"
62+
[elements.implementation]
63+
type = "table"
64+
[elements.implementation.framework]
65+
type = "string"
66+
allowedvalues = ["golang"]
67+
optional = true
68+
default = "golang"
69+
[elements.implementation.database]
70+
type = "table"
71+
[elements.implementation.database.engine]
72+
type = "string"
73+
allowedvalues = ["postgres", "pgsql", "postgresql"]
74+
optional = true
75+
default = "pgsql"
76+
[elements.implementation.database.url]
77+
type = "string"
78+
optional = true
79+
[elements.cicd]
80+
[elements.cicd.docker]
81+
type = "table"
82+
optional = true
83+
[elements.cicd.docker.generate_file]
84+
type = "bool"
85+
default = true
86+
[elements.cicd.docker.username]
87+
type = string
88+
optional = true
89+
[elements.cicd.docker.tag]
90+
type = string
91+
optional = true
92+
[elements.cicd.k8s]
93+
type = "bool"
94+
optional = true
95+
default = true
96+
[elements.cicd.github_actions]
97+
type = "bool"
98+
optional = true
99+
default = true

example-configs/course.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[api]
2+
name = "course"
3+
[[api.fields]]
4+
name = "name"
5+
type = "string"
6+
[[api.fields]]
7+
name = "start"
8+
type = "tinyint"
9+
[[api.fields]]
10+
name = "end"
11+
type = "tinyint"
12+
13+
[implementation]
14+
framework = "golang"
15+
16+
[implemention.database]
17+
engine = "pgsql"
18+
url = "postgresql://localhost:5432/postgres"

src/config/cli.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::path::PathBuf;
2+
use structopt::StructOpt;
3+
4+
#[derive(Debug, StructOpt)]
5+
#[structopt(
6+
name = "autoapi",
7+
about = "A tool for generating CRUD API program automatically.",
8+
rename_all = "kebab"
9+
)]
10+
pub struct Config {
11+
/// Config file path.
12+
#[structopt(short="i", long, alias="input", parse(from_os_str), env = "CONFIG")]
13+
pub config: PathBuf,
14+
/// Output project path.
15+
#[structopt(short, long, parse(from_os_str), env = "OUTPUT")]
16+
pub output: PathBuf,
17+
/// Set this flag to overwrite the `output` directory before generating instead of report an error.
18+
#[structopt(short, long, env = "FORCE")]
19+
pub force: bool,
20+
21+
/// Output project path.
22+
#[structopt(alias="dbms", long, env = "DATABASE")]
23+
pub database_engine: Option<String>,
24+
#[structopt(short, long, env = "API_NAME")]
25+
pub name: Option<String>,
26+
27+
#[structopt(alias="ddl", long, env = "DDL")]
28+
pub load_from_ddl: Option<String>,
29+
#[structopt(short="load-db", long, env = "LOAD_DB")]
30+
pub load_from_db: Option<String>,
31+
32+
#[structopt(short="d", long, env = "DOCKER")]
33+
pub generate_docker: Option<bool>,
34+
#[structopt(alias="du", long, env = "DOCKER_USERNAME")]
35+
pub docker_username: Option<String>,
36+
#[structopt(alias="dt", long, env = "DOCKER_TAG")]
37+
pub docker_tag: Option<String>,
38+
39+
#[structopt(short="k8s", long, env = "KUBERNETES")]
40+
pub kubernetes: Option<bool>,
41+
}

src/config/file.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use std::fs::OpenOptions;
2+
3+
use anyhow::{anyhow, Result};
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Serialize, Deserialize, Debug, Clone)]
7+
pub struct Field {
8+
pub name: String,
9+
#[serde(rename = "type")]
10+
pub data_type: String,
11+
}
12+
13+
#[derive(Serialize, Deserialize, Debug, Clone)]
14+
pub struct API {
15+
pub name: String,
16+
pub fields: Vec<Field>,
17+
}
18+
19+
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
20+
pub struct DataBase {
21+
pub engine: Option<String>,
22+
#[serde(alias = "address")]
23+
#[serde(alias = "connection_string")]
24+
pub url: Option<String>,
25+
}
26+
27+
#[derive(Serialize, Deserialize, Debug, Clone)]
28+
pub struct Implementation {
29+
pub framework: String,
30+
#[serde(default)]
31+
#[serde(alias = "db")]
32+
pub database: DataBase,
33+
}
34+
35+
fn fn_true() -> bool {
36+
true
37+
}
38+
39+
#[derive(Serialize, Deserialize, Debug, Clone)]
40+
pub struct Docker {
41+
#[serde(default = "fn_true")]
42+
pub generate_file: bool,
43+
pub username: Option<String>,
44+
pub tag: Option<String>,
45+
}
46+
47+
impl Default for Docker {
48+
fn default() -> Self {
49+
Self {
50+
generate_file: true,
51+
username: None,
52+
tag: None,
53+
}
54+
}
55+
}
56+
57+
#[derive(Serialize, Deserialize, Debug, Clone)]
58+
pub struct CICD {
59+
#[serde(default)]
60+
pub docker: Docker,
61+
#[serde(alias = "k8s")]
62+
pub kubernetes: Option<bool>,
63+
#[serde(default = "fn_true")]
64+
#[serde(alias = "ghaction")]
65+
#[serde(alias = "gh_action")]
66+
pub github_action: bool,
67+
}
68+
69+
impl Default for CICD {
70+
fn default() -> Self {
71+
Self {
72+
docker: Default::default(),
73+
kubernetes: None,
74+
github_action: true,
75+
}
76+
}
77+
}
78+
79+
#[derive(Serialize, Deserialize, Debug)]
80+
pub struct Config {
81+
pub api: Option<API>,
82+
pub implementation: Implementation,
83+
#[serde(default)]
84+
pub cicd: CICD,
85+
}
86+

src/config/mod.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
mod cli;
2+
mod file;
3+
use anyhow::anyhow;
4+
use std::{fs::File, io::Read, path::PathBuf};
5+
6+
use structopt::StructOpt;
7+
8+
pub struct Config {
9+
pub output: PathBuf,
10+
pub force: bool,
11+
// For now, `file::Config` contains all the configuration options we need for generating the API.
12+
pub generate_config: file::Config,
13+
}
14+
15+
pub fn from_cli_config() -> anyhow::Result<Config> {
16+
let cli_config = cli::Config::from_args_safe()?;
17+
let mut config_file = File::open(&cli_config.config)?;
18+
let mut file_config: file::Config = match cli_config
19+
.config
20+
.extension()
21+
.and_then(|it| it.to_str())
22+
.ok_or(anyhow!("Cannot open config file"))?
23+
{
24+
"toml" => {
25+
let mut content = String::new();
26+
config_file.read_to_string(&mut content)?;
27+
toml::from_str(&content)?
28+
}
29+
"json" => serde_json::from_reader(config_file)?,
30+
"yaml" => serde_yaml::from_reader(config_file)?,
31+
_ => return Err(anyhow!("Unsupported config file type")),
32+
};
33+
// Merging config from file and cli
34+
if file_config.implementation.database.engine.is_none() {
35+
file_config.implementation.database.engine = Some("pgsql".to_string());
36+
}
37+
if let Some(database_engine) = cli_config.database_engine {
38+
file_config.implementation.database.engine = Some(database_engine);
39+
}
40+
if file_config.implementation.database.engine.is_none()
41+
&& file_config.implementation.database.url.is_some()
42+
{
43+
let mut url = file_config.implementation.database.url.take().unwrap();
44+
if url.starts_with("postgres://") {
45+
file_config.implementation.database.engine = Some("postgres".to_string());
46+
} else if url.starts_with("pgsql://") {
47+
println!("warning: pgsql:// won't work, I'll use postgres:// instead");
48+
url = url.replace("pgsql://", "postgres://");
49+
file_config.implementation.database.engine = Some("postgres".to_string());
50+
} else if url.starts_with("mysql://") {
51+
file_config.implementation.database.engine = Some("mysql".to_string());
52+
} else if url.starts_with("sqlite://") {
53+
file_config.implementation.database.engine = Some("sqlite".to_string());
54+
} else {
55+
return Err(anyhow!(
56+
"database engine not set and cannot auto refereed from connection string"
57+
));
58+
}
59+
file_config.implementation.database.url = Some(url);
60+
}
61+
if file_config.api.is_none() {
62+
if let Some(_load_from_ddl) = cli_config.load_from_ddl {
63+
todo!("load api config from ddl_file");
64+
}
65+
let try_load_from_db = if let Some(addr) = cli_config.load_from_db {
66+
Some(addr)
67+
} else if let Some(addr) = file_config.implementation.database.url {
68+
Some(addr)
69+
} else {
70+
None
71+
};
72+
if try_load_from_db.is_some() {
73+
if cli_config.name.is_none() {
74+
return Err(anyhow!("I need to know API name before load it from db!"));
75+
}
76+
}
77+
todo!("load api config from database");
78+
}
79+
80+
if let Some(generate_docker) = cli_config.generate_docker {
81+
file_config.cicd.docker.generate_file = generate_docker;
82+
}
83+
if let Some(docker_tag) = cli_config.docker_tag {
84+
file_config.cicd.docker.tag = Some(docker_tag);
85+
}
86+
if let Some(docker_username) = cli_config.docker_username {
87+
file_config.cicd.docker.username = Some(docker_username);
88+
}
89+
if let Some(k8s) = cli_config.kubernetes {
90+
file_config.cicd.kubernetes = Some(k8s);
91+
}
92+
file_config.cicd.kubernetes = match file_config.cicd.kubernetes {
93+
Some(false) => Some(false),
94+
Some(true) if file_config.implementation.database.url.is_none() => {
95+
return Err(anyhow!("generating kubernetes yaml file requires database connection string"));
96+
}
97+
Some(true) if file_config.cicd.docker.username.is_none() => {
98+
// todo: support other docker image registry than dockerhub
99+
return Err(anyhow!("generating kubernetes yaml file requires docker username"));
100+
}
101+
Some(true) => Some(true),
102+
None if file_config.implementation.database.url.is_none() => Some(false),
103+
None => Some(true)
104+
};
105+
Ok(Config {
106+
output: cli_config.output,
107+
force: cli_config.force,
108+
generate_config: file_config,
109+
})
110+
}

0 commit comments

Comments
 (0)