Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- name: Run unit tests
uses: actions-rs/cargo@v1
with:
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ tera = "1.12.1"
convert_case = "0.4.0"
pluralize-rs = "0.1.0"
include_dir = { version = "0.6.0", features = ["search"] }
serde = {version="1.0.126", features = ["derive"]}
serde = { version = "1.0.126", features = ["derive"] }
serde_yaml = "0.8.17"
serde_json = "1.0.64"
toml = "0.5.8"
anyhow = "1.0.41"
structopt = "0.3.21"

[profile.release]
lto = "fat"
99 changes: 99 additions & 0 deletions docs/config.tosd
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
[toml-schema]
version = "1.0"

[types]
[types.field]
[types.field.name]
type = "string"
[types.field.type]
type = "string"
allowedvalues = [
"int",
"serial",
"integer",
"small_int",
"tiny_int",
"medium_int",
"big_int",
"big_serial",
"unsigned_serial",
"uint",
"unsigned_tiny_int",
"unsigned_tiny_integer",
"unsigned",
"unsigned_medium_int",
"unsigned_medium_integer",
"unsigned_big_int",
"unsigned_big_integer",
"float",
"float32",
"double",
"float64",
"string",
"char(\\d+)",
"varchar(\\d+)",
"tiny_text",
"text",
"medium_text",
"long_text",
"tiny_blob",
"blob",
"medium_blob",
"long_blob",
"bool",
"date",
"date_time",
"time",
"timestamp",
]

[elements]
[elements.api]
type = "table"
optional = true
[elements.api.name]
type = "string"
[elements.api.fields]
type = "array"
arraytype = "string"
[elements.api.fields.elements]
type = "array"
arraytypeof = "field"
[elements.implementation]
type = "table"
[elements.implementation.framework]
type = "string"
allowedvalues = ["golang"]
optional = true
default = "golang"
[elements.implementation.database]
type = "table"
[elements.implementation.database.engine]
type = "string"
allowedvalues = ["postgres", "pgsql", "postgresql"]
optional = true
default = "pgsql"
[elements.implementation.database.url]
type = "string"
optional = true
[elements.cicd]
[elements.cicd.docker]
type = "table"
optional = true
[elements.cicd.docker.generate_file]
type = "bool"
default = true
[elements.cicd.docker.username]
type = string
optional = true
[elements.cicd.docker.tag]
type = string
optional = true
[elements.cicd.k8s]
type = "bool"
optional = true
default = true
[elements.cicd.github_actions]
type = "bool"
optional = true
default = true
18 changes: 18 additions & 0 deletions example-configs/course.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[api]
name = "course"
[[api.fields]]
name = "name"
type = "string"
[[api.fields]]
name = "start"
type = "tinyint"
[[api.fields]]
name = "end"
type = "tinyint"

[implementation]
framework = "golang"

[implemention.database]
engine = "pgsql"
url = "postgresql://localhost:5432/postgres"
41 changes: 41 additions & 0 deletions src/config/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::path::PathBuf;
use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(
name = "autoapi",
about = "A tool for generating CRUD API program automatically.",
rename_all = "kebab"
)]
pub struct Config {
/// Config file path.
#[structopt(short = "i", long, alias = "input", parse(from_os_str), env = "CONFIG")]
pub config: PathBuf,
/// Output project path.
#[structopt(short, long, parse(from_os_str), env = "OUTPUT")]
pub output: PathBuf,
/// Set this flag to overwrite the `output` directory before generating instead of report an error.
#[structopt(short, long, env = "FORCE")]
pub force: bool,

/// Output project path.
#[structopt(alias = "dbms", long, env = "DATABASE")]
pub database_engine: Option<String>,
#[structopt(short, long, env = "API_NAME")]
pub name: Option<String>,

#[structopt(alias = "ddl", long, env = "DDL")]
pub load_from_ddl: Option<String>,
#[structopt(short = "load-db", long, env = "LOAD_DB")]
pub load_from_db: Option<String>,

#[structopt(short = "d", long, env = "DOCKER")]
pub generate_docker: Option<bool>,
#[structopt(alias = "du", long, env = "DOCKER_USERNAME")]
pub docker_username: Option<String>,
#[structopt(alias = "dt", long, env = "DOCKER_TAG")]
pub docker_tag: Option<String>,

#[structopt(short = "k8s", long, env = "KUBERNETES")]
pub kubernetes: Option<bool>,
}
84 changes: 84 additions & 0 deletions src/config/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Field {
pub name: String,
#[serde(rename = "type")]
pub data_type: String,
}

#[allow(clippy::upper_case_acronyms)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct API {
pub name: String,
pub fields: Vec<Field>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DataBase {
pub engine: Option<String>,
#[serde(alias = "address")]
#[serde(alias = "connection_string")]
pub url: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Implementation {
pub framework: String,
#[serde(default)]
#[serde(alias = "db")]
pub database: DataBase,
}

fn fn_true() -> bool {
true
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Docker {
#[serde(default = "fn_true")]
pub generate_file: bool,
pub username: Option<String>,
pub tag: Option<String>,
}

impl Default for Docker {
fn default() -> Self {
Self {
generate_file: true,
username: None,
tag: None,
}
}
}

#[allow(clippy::upper_case_acronyms)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CICD {
#[serde(default)]
pub docker: Docker,
#[serde(alias = "k8s")]
pub kubernetes: Option<bool>,
#[serde(default = "fn_true")]
#[serde(alias = "ghaction")]
#[serde(alias = "gh_action")]
pub github_action: bool,
}

impl Default for CICD {
fn default() -> Self {
Self {
docker: Default::default(),
kubernetes: None,
github_action: true,
}
}
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
pub api: Option<API>,
pub implementation: Implementation,
#[serde(default)]
pub cicd: CICD,
}
112 changes: 112 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
mod cli;
mod file;
use anyhow::anyhow;
use std::{ffi::OsStr, fs::File, io::Read, path::PathBuf};

use structopt::StructOpt;

pub struct Config {
pub output: PathBuf,
pub force: bool,
// For now, `file::Config` contains all the configuration options we need for generating the API.
pub generate_config: file::Config,
}

pub fn from_cli_config() -> anyhow::Result<Config> {
let cli_config = cli::Config::from_args_safe()?;
let mut config_file = File::open(&cli_config.config)?;
let mut file_config: file::Config = match cli_config
.config
.extension()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("Cannot open config file"))?
{
"toml" => {
let mut content = String::new();
config_file.read_to_string(&mut content)?;
toml::from_str(&content)?
}
"json" => serde_json::from_reader(config_file)?,
"yaml" => serde_yaml::from_reader(config_file)?,
_ => return Err(anyhow!("Unsupported config file type")),
};
// Merging config from file and cli
if file_config.implementation.database.engine.is_none() {
file_config.implementation.database.engine = Some("pgsql".to_string());
}
if let Some(database_engine) = cli_config.database_engine {
file_config.implementation.database.engine = Some(database_engine);
}
if file_config.implementation.database.engine.is_none()
&& file_config.implementation.database.url.is_some()
{
let mut url = file_config.implementation.database.url.take().unwrap();
if url.starts_with("postgres://") {
file_config.implementation.database.engine = Some("postgres".to_string());
} else if url.starts_with("pgsql://") {
println!("warning: pgsql:// won't work, I'll use postgres:// instead");
url = url.replace("pgsql://", "postgres://");
file_config.implementation.database.engine = Some("postgres".to_string());
} else if url.starts_with("mysql://") {
file_config.implementation.database.engine = Some("mysql".to_string());
} else if url.starts_with("sqlite://") {
file_config.implementation.database.engine = Some("sqlite".to_string());
} else {
return Err(anyhow!(
"database engine not set and cannot auto refereed from connection string"
));
}
file_config.implementation.database.url = Some(url);
}
if file_config.api.is_none() {
if let Some(_load_from_ddl) = cli_config.load_from_ddl {
todo!("load api config from ddl_file");
}
let try_load_from_db = if let Some(addr) = cli_config.load_from_db {
Some(addr)
} else {
file_config.implementation.database.url.clone()
};
if try_load_from_db.is_some() {
if cli_config.name.is_none() {
return Err(anyhow!("I need to know API name before load it from db!"));
}
todo!("load api config from database");
}
}

if let Some(generate_docker) = cli_config.generate_docker {
file_config.cicd.docker.generate_file = generate_docker;
}
if let Some(docker_tag) = cli_config.docker_tag {
file_config.cicd.docker.tag = Some(docker_tag);
}
if let Some(docker_username) = cli_config.docker_username {
file_config.cicd.docker.username = Some(docker_username);
}
if let Some(k8s) = cli_config.kubernetes {
file_config.cicd.kubernetes = Some(k8s);
}
file_config.cicd.kubernetes = match file_config.cicd.kubernetes {
Some(false) => Some(false),
Some(true) if file_config.implementation.database.url.is_none() => {
return Err(anyhow!(
"generating kubernetes yaml file requires database connection string"
));
}
Some(true) if file_config.cicd.docker.username.is_none() => {
// todo: support other docker image registry than dockerhub
return Err(anyhow!(
"generating kubernetes yaml file requires docker username"
));
}
Some(true) => Some(true),
None if file_config.implementation.database.url.is_none() => Some(false),
None => Some(true),
};
Ok(Config {
output: cli_config.output,
force: cli_config.force,
generate_config: file_config,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

use serde::{Deserialize, Serialize};

pub mod postgres;

/// The database the user wants to use.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
Expand Down
Loading