├── .cargo └── config.toml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── release.yml │ └── run-tests.yml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── rustfmt.toml ├── src ├── main.rs ├── model │ ├── data_type.rs │ └── mod.rs ├── render │ ├── filter.rs │ ├── mod.rs │ └── template.rs └── technology │ ├── database │ ├── mod.rs │ └── postgres.rs │ ├── language │ ├── golang.rs │ └── mod.rs │ └── mod.rs └── templates └── go ├── go.mod.template ├── handler └── handler.go.template ├── infrastructure └── db.go.template ├── main.go.template └── model └── model.go.template /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | 4 | [target.x86_64-pc-windows-gnu] 5 | rustflags = ["-C", "target-feature=+crt-static"] 6 | 7 | [target.x86_64-apple-darwin] 8 | rustflags = ["-C", "target-feature=+crt-static"] 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | /target 4 | .DS_Store 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: []# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://qr.alipay.com/fkx12853mikmdlvmpatnr9d'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Desktop (please complete the following information):** 35 | - OS: [e.g. iOS] 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | **Describe the solution you'd like** 27 | A clear and concise description of what you want to happen. 28 | 29 | **Additional context** 30 | Add any other context or screenshots about the feature request here. 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | ### What problem does this PR solve? 19 | 20 | Issue Number: close #xxx 21 | 22 | Problem Summary: 23 | 24 | ### What is changed and how it works? 25 | 26 | What's Changed: -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | jobs: 8 | build-binary: 9 | strategy: 10 | matrix: 11 | target: 12 | - ubuntu-latest 13 | - macos-latest 14 | - windows-latest 15 | runs-on: ${{ matrix.target }} 16 | name: Build 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: nightly 23 | - name: Build binary 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: build 27 | args: --release 28 | - name: Compress 29 | if: matrix.target != 'windows-latest' 30 | run: | 31 | chmod +x target/release/auto-api 32 | zip auto-api-${{ matrix.target }}.zip target/release/auto-api 33 | - name: Compress 34 | if: matrix.target == 'windows-latest' 35 | run: | 36 | tar.exe -a -c -f auto-api-${{ matrix.target }}.zip target/release/auto-api.exe 37 | - uses: actions/upload-artifact@master 38 | with: 39 | name: auto-api-${{ matrix.target }}.zip 40 | path: auto-api-${{ matrix.target }}.zip 41 | release-binary: 42 | name: Release 43 | needs: build-binary 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: download productions 47 | uses: actions/download-artifact@master 48 | with: 49 | name: auto-api-ubuntu-latest.zip 50 | path: . 51 | - uses: actions/download-artifact@master 52 | with: 53 | name: auto-api-macos-latest.zip 54 | path: . 55 | - uses: actions/download-artifact@master 56 | with: 57 | name: auto-api-windows-latest.zip 58 | path: . 59 | - name: Release 60 | uses: softprops/action-gh-release@v1 61 | with: 62 | body: ${{ github.event.head_commit.message }} 63 | files: | 64 | auto-api-ubuntu-latest.zip 65 | auto-api-macos-latest.zip 66 | auto-api-windows-latest.zip 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | release-docker: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | - name: Build the Docker image 74 | run: | 75 | docker build . -t ${{ secrets.DOCKER_USERNAME }}/auto-api:${GITHUB_REF##*/} 76 | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin 77 | docker push ${{ secrets.DOCKER_USERNAME }}/auto-api:${GITHUB_REF##*/} 78 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | jobs: 11 | unit-test: 12 | runs-on: ubuntu-latest 13 | name: Run unit tests 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: nightly 20 | - name: Run unit tests 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: test 24 | # todo: integration tests 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auto-api" 3 | version = "2.0.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tera = "1.12.1" 10 | convert_case = "0.4.0" 11 | pluralize-rs = "0.1.0" 12 | include_dir = { version = "0.6.0", features = ["search"] } 13 | serde = {version="1.0.126", features = ["derive"]} 14 | serde_yaml = "0.8.17" 15 | serde_json = "1.0.64" 16 | toml = "0.5.8" 17 | 18 | [profile.release] 19 | lto = "fat" 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN apk add --no-cache -U musl-dev 5 | RUN cargo build --release 6 | 7 | FROM alpine 8 | COPY --from=builder /app/target/release/auto-api /auto-api 9 | WORKDIR / 10 | ENTRYPOINT ["/auto-api"] 11 | CMD [""] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autoAPI 2 | 3 | ![Tests](https://github.com/SHUReeducation/autoAPI/workflows/Run%20tests/badge.svg) 4 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSHUReeducation%2FautoAPI.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSHUReeducation%2FautoAPI?ref=badge_shield) 5 | 6 | Create an CRUD API server directly from a table in database or a yaml file. 7 | 8 | 9 | 10 | ## Things we'll never ever ever ever ever support 11 | 12 | ### DBMS 13 | 14 | - Oracle 15 | 16 | ### Language 17 | 18 | - Java (**CAUSE IT IS THE WORST LANGUAGE IN THIS UNIVERSE!!!**) 19 | 20 | 25 | 26 | 27 | 28 | ## License 29 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSHUReeducation%2FautoAPI.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FSHUReeducation%2FautoAPI?ref=badge_large) -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use technology::Technology; 2 | 3 | use crate::{ 4 | model::{data_type::DataType, Field, Model}, 5 | technology::language::golang, 6 | }; 7 | 8 | mod model; 9 | mod render; 10 | mod technology; 11 | 12 | fn main() { 13 | let mut tera = render::load_templates(); 14 | render::filter::register(&mut tera); 15 | let config = Technology { 16 | database: technology::DataBase::PgSQL, 17 | }; 18 | let model = Model { 19 | name: "shuSB".to_string(), 20 | primary_key: Field { 21 | name: "id".to_string(), 22 | data_type: DataType::UInt(64), 23 | }, 24 | fields: vec![ 25 | Field { 26 | name: "name".to_string(), 27 | data_type: DataType::String(None), 28 | }, 29 | Field { 30 | name: "IQ".to_string(), 31 | data_type: DataType::Int(32), 32 | }, 33 | ], 34 | }; 35 | let mut context = tera::Context::new(); 36 | context.insert("model", &model); 37 | golang::register(&mut tera, &config, &model, &mut context); 38 | golang::render(&tera, "./shuSB", &mut context); 39 | } 40 | -------------------------------------------------------------------------------- /src/model/data_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Meta information about a string. 4 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 5 | pub struct StringMeta { 6 | pub length: usize, 7 | /// If a string's length is fixed, we can use `CHAR` type in database. 8 | pub fixed_length: bool, 9 | } 10 | 11 | /// A type of data. 12 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 13 | #[serde(rename_all = "lowercase")] 14 | pub enum DataType { 15 | Int(usize), 16 | UInt(usize), 17 | Float(usize), 18 | String(Option), 19 | // TODO: separate date, time and datetime. 20 | DateTime, 21 | } 22 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the "model" related code. 2 | //! "model" here means how the user wants the API to look like, 3 | //! but has nothing to do with the any specific technology the user selects. 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use self::data_type::DataType; 8 | 9 | pub mod data_type; 10 | 11 | /// Field means a field of a model. 12 | /// Can usually be mapped to a database column. 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct Field { 15 | pub name: String, 16 | pub data_type: DataType, 17 | } 18 | 19 | /// Model is the object the user wants to generate API for. 20 | /// Can usually be mapped to a database table. 21 | #[derive(Debug, Serialize, Deserialize)] 22 | pub struct Model { 23 | pub name: String, 24 | pub primary_key: Field, 25 | pub fields: Vec, 26 | } 27 | -------------------------------------------------------------------------------- /src/render/filter.rs: -------------------------------------------------------------------------------- 1 | //! Useful filters to be registered into [tera](https://tera.netlify.app/docs/)'s template. 2 | 3 | use std::collections::HashMap; 4 | 5 | use convert_case::{Case, Casing}; 6 | use pluralize_rs::{to_plural, to_singular}; 7 | use tera::{Error, Result, Tera, Value}; 8 | 9 | /// Change the case of a string into camelCase. 10 | fn camel_case(value: &Value, _args: &HashMap) -> Result { 11 | Ok(Value::String( 12 | value 13 | .as_str() 14 | .ok_or_else(|| Error::msg("camel_case filter can only applied on strings"))? 15 | .to_case(Case::Camel), 16 | )) 17 | } 18 | 19 | /// Change the case of a string into kebab-case. 20 | fn kebab_case(value: &Value, _args: &HashMap) -> Result { 21 | Ok(Value::String( 22 | value 23 | .as_str() 24 | .ok_or_else(|| Error::msg("kebab_case filter can only applied on strings"))? 25 | .to_case(Case::Kebab), 26 | )) 27 | } 28 | 29 | /// Change the case of a string into PascalCase. 30 | fn pascal_case(value: &Value, _args: &HashMap) -> Result { 31 | Ok(Value::String( 32 | value 33 | .as_str() 34 | .ok_or_else(|| Error::msg("pascal_case filter can only applied on strings"))? 35 | .to_case(Case::Pascal), 36 | )) 37 | } 38 | 39 | /// Change the case of a string into snake_case. 40 | fn snake_case(value: &Value, _args: &HashMap) -> Result { 41 | Ok(Value::String( 42 | value 43 | .as_str() 44 | .ok_or_else(|| Error::msg("snake_case filter can only applied on strings"))? 45 | .to_case(Case::Snake), 46 | )) 47 | } 48 | 49 | /// Pluralize form of a string. 50 | fn pluralize(value: &Value, _args: &HashMap) -> Result { 51 | Ok(Value::String(to_plural(value.as_str().ok_or_else( 52 | || Error::msg("pluralize filter can only applied on strings"), 53 | )?))) 54 | } 55 | 56 | /// Singular form of a string. 57 | fn singular(value: &Value, _args: &HashMap) -> Result { 58 | Ok(Value::String(to_singular(value.as_str().ok_or_else( 59 | || Error::msg("pluralize filter can only applied on strings"), 60 | )?))) 61 | } 62 | 63 | /// Change the case of an array of string into the case given by param `case`. 64 | fn map_case(value: &Value, args: &HashMap) -> Result { 65 | let case = args 66 | .get("case") 67 | .ok_or_else(|| Error::msg("map_case filter requires a case argument"))? 68 | .as_str() 69 | .ok_or_else(|| Error::msg("map_case filter requires a case argument"))?; 70 | let value = value 71 | .as_array() 72 | .ok_or_else(|| Error::msg("map_case filter can only applied on array of strings"))? 73 | .iter() 74 | .map(|it| { 75 | it.as_str() 76 | .ok_or_else(|| Error::msg("map_case filter can only applied on array of strings")) 77 | }) 78 | .collect::>>()?; 79 | let case = match case { 80 | "camel" => Case::Camel, 81 | "kebab" => Case::Kebab, 82 | "pascal" => Case::Pascal, 83 | "snake" => Case::Snake, 84 | _ => return Err(Error::msg("map_case filter requires a case argument")), 85 | }; 86 | Ok(Value::Array( 87 | value 88 | .into_iter() 89 | .map(|it| Value::String(it.to_case(case))) 90 | .collect(), 91 | )) 92 | } 93 | 94 | /// Add a suffix for an array of string. 95 | fn map_suffix(value: &Value, args: &HashMap) -> Result { 96 | let suffix = args 97 | .get("suffix") 98 | .ok_or_else(|| Error::msg("map_suffix filter requires a suffix argument"))? 99 | .as_str() 100 | .ok_or_else(|| Error::msg("map_suffix filter requires a string suffix argument"))?; 101 | let value = value 102 | .as_array() 103 | .ok_or_else(|| Error::msg("map_suffix filter can only applied on array of strings"))? 104 | .iter() 105 | .map(|v| { 106 | v.as_str() 107 | .ok_or_else(|| Error::msg("map_suffix filter can only applied on array of strings")) 108 | }) 109 | .collect::>>()?; 110 | let value: Vec = value 111 | .into_iter() 112 | .map(|it| Value::String(format!("{}{}", it, suffix))) 113 | .collect(); 114 | Ok(Value::Array(value)) 115 | } 116 | 117 | /// Add a prefix for an array of string. 118 | fn map_prefix(value: &Value, args: &HashMap) -> Result { 119 | let map_prefix = args 120 | .get("prefix") 121 | .ok_or_else(|| Error::msg("map_prefix filter requires a map_prefix argument"))? 122 | .as_str() 123 | .ok_or_else(|| Error::msg("map_prefix filter requires a string prefix argument"))?; 124 | let value = value 125 | .as_array() 126 | .ok_or_else(|| Error::msg("map_prefix filter can only applied on array of strings"))? 127 | .iter() 128 | .map(|v| { 129 | v.as_str() 130 | .ok_or_else(|| Error::msg("map_prefix filter can only applied on array of strings")) 131 | }) 132 | .collect::>>()?; 133 | let value: Vec = value 134 | .into_iter() 135 | .map(|it| Value::String(format!("{}{}", map_prefix, it))) 136 | .collect(); 137 | Ok(Value::Array(value)) 138 | } 139 | 140 | /// Register all filters above into the given tera template. 141 | pub fn register(tera: &mut Tera) { 142 | tera.register_filter("camel_case", camel_case); 143 | tera.register_filter("kebab_case", kebab_case); 144 | tera.register_filter("pascal_case", pascal_case); 145 | tera.register_filter("snake_case", snake_case); 146 | tera.register_filter("pluralize", pluralize); 147 | tera.register_filter("singular", singular); 148 | tera.register_filter("map_prefix", map_prefix); 149 | tera.register_filter("map_suffix", map_suffix); 150 | tera.register_filter("map_case", map_case); 151 | } 152 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | //! Template rendering utils shared between technologies. 2 | //! Note: Currently, for each language, call ::render for the actual rendering. 3 | 4 | pub mod filter; 5 | mod template; 6 | use std::{ 7 | fs::{self, File}, 8 | io::Write, 9 | path::Path, 10 | }; 11 | pub use template::load_templates; 12 | 13 | use tera::Tera; 14 | 15 | /// Render home/filename with context. 16 | pub fn render_simple( 17 | tera: &Tera, 18 | home: impl AsRef, 19 | language: &str, 20 | filename: &str, 21 | context: &tera::Context, 22 | ) { 23 | let path_str = format!("{}/{}", home.as_ref().display(), filename); 24 | let path = Path::new(&path_str); 25 | let parent_path = path.parent().unwrap(); 26 | fs::create_dir_all(parent_path).unwrap(); 27 | let mut f = File::create(path).unwrap(); 28 | let template_name = format!("{}/{}.template", language, filename); 29 | let content = tera.render(&template_name, context).unwrap(); 30 | f.write_all(content.as_bytes()).unwrap(); 31 | } 32 | -------------------------------------------------------------------------------- /src/render/template.rs: -------------------------------------------------------------------------------- 1 | use include_dir::{include_dir, Dir, DirEntry}; 2 | use tera::Tera; 3 | 4 | /// All templates are included into the binary, so the binary can be distributed alone without any folders. 5 | static TEMPLATES_DIR: Dir = include_dir!("./templates"); 6 | 7 | /// Loads all templates from the templates "directory". 8 | pub fn load_templates() -> Tera { 9 | let mut tera = Tera::default(); 10 | for entry in TEMPLATES_DIR.find("**/*.template").unwrap() { 11 | if let DirEntry::File(f) = entry { 12 | tera.add_raw_template(f.path().to_str().unwrap(), f.contents_utf8().unwrap()) 13 | .unwrap(); 14 | } 15 | } 16 | tera 17 | } 18 | -------------------------------------------------------------------------------- /src/technology/database/mod.rs: -------------------------------------------------------------------------------- 1 | //! Database the user wants to use and related tools. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub mod postgres; 6 | 7 | /// The database the user wants to use. 8 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 9 | #[serde(rename_all = "lowercase")] 10 | pub enum DataBase { 11 | PgSQL, 12 | MySQL, 13 | } 14 | -------------------------------------------------------------------------------- /src/technology/database/postgres.rs: -------------------------------------------------------------------------------- 1 | //! PostgreSQL database tool functions 2 | use std::collections::HashMap; 3 | 4 | use tera::{Result, Value}; 5 | 6 | use crate::model::data_type::DataType; 7 | 8 | fn db_type(data_type: DataType) -> String { 9 | match data_type { 10 | DataType::Int(x) if x <= 8 => "int8".to_string(), 11 | DataType::Int(x) if x <= 16 => "int16".to_string(), 12 | DataType::Int(x) if x <= 32 => "int32".to_string(), 13 | DataType::Int(_) => "int64".to_string(), 14 | DataType::UInt(x) if x <= 8 => "uint8".to_string(), 15 | DataType::UInt(x) if x <= 16 => "uint16".to_string(), 16 | DataType::UInt(x) if x <= 32 => "uint32".to_string(), 17 | DataType::UInt(_) => "uint64".to_string(), 18 | DataType::Float(x) if x <= 32 => "float32".to_string(), 19 | DataType::Float(_) => "float64".to_string(), 20 | DataType::String(None) => "text".to_string(), 21 | DataType::String(Some(x)) if x.fixed_length => format!("char({})", x.length), 22 | DataType::String(Some(x)) if !x.fixed_length => format!("varchar({})", x.length), 23 | DataType::String(Some(_)) => unreachable!(), 24 | DataType::DateTime => "time".to_string(), 25 | } 26 | } 27 | 28 | fn db_type_in_template(args: &HashMap) -> Result { 29 | let data_type_value: DataType = serde_json::from_value(args.get("data_type").unwrap().clone())?; 30 | Ok(Value::String(db_type(data_type_value))) 31 | } 32 | 33 | pub fn register(tera: &mut tera::Tera) { 34 | tera.register_function("db_type", db_type_in_template); 35 | } 36 | -------------------------------------------------------------------------------- /src/technology/language/golang.rs: -------------------------------------------------------------------------------- 1 | //! Golang specific tools. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{collections::HashMap, path::Path}; 5 | use tera::{Result, Tera, Value}; 6 | 7 | use crate::{ 8 | model::{data_type::DataType, Model}, 9 | render::render_simple, 10 | technology::Technology, 11 | }; 12 | 13 | /// "imports" for different golang files 14 | #[derive(Serialize, Deserialize, Debug)] 15 | pub struct Imports { 16 | /// some data_types may require specific imports in "model.go" 17 | pub model: Vec<&'static str>, 18 | /// each database has its own library 19 | pub database_library: &'static str, 20 | } 21 | 22 | /// Root of the golang struct 23 | #[derive(Serialize, Deserialize, Debug)] 24 | pub struct Golang { 25 | pub imports: Imports, 26 | /// db_driver is used as the param of `sql.Open` 27 | pub db_driver: &'static str, 28 | } 29 | 30 | // TODO: It is highly possible that each language needs `data_type`, `register` and `render`, maybe we can add a trait. 31 | /// Get the string representing of a data_type in Golang. 32 | fn data_type(data_type: DataType) -> String { 33 | match data_type { 34 | DataType::Int(x) if x <= 8 => "int8".to_string(), 35 | DataType::Int(x) if x <= 16 => "int16".to_string(), 36 | DataType::Int(x) if x <= 32 => "int32".to_string(), 37 | DataType::Int(_) => "int64".to_string(), 38 | DataType::UInt(x) if x <= 8 => "uint8".to_string(), 39 | DataType::UInt(x) if x <= 16 => "uint16".to_string(), 40 | DataType::UInt(x) if x <= 32 => "uint32".to_string(), 41 | DataType::UInt(_) => "uint64".to_string(), 42 | DataType::Float(x) if x <= 32 => "float32".to_string(), 43 | DataType::Float(_) => "float64".to_string(), 44 | DataType::String(_) => "string".to_string(), 45 | DataType::DateTime => "time".to_string(), 46 | } 47 | } 48 | 49 | fn data_type_in_template(args: &HashMap) -> Result { 50 | let data_type_value: DataType = serde_json::from_value(args.get("data_type").unwrap().clone())?; 51 | Ok(Value::String(data_type(data_type_value))) 52 | } 53 | 54 | /// A function which can be used in the template for judging whether the datatype is a string 55 | /// Useful when accepting user input. See `templates/go/handler.go.template` for detail. 56 | fn data_type_is_string(args: &HashMap) -> Result { 57 | let data_type_value: DataType = 58 | serde_json::from_value(args.get("data_type").unwrap_or(&Value::Null).clone())?; 59 | Ok(Value::Bool(matches!(data_type_value, DataType::String(_)))) 60 | } 61 | 62 | impl Golang { 63 | pub fn new(technology: &Technology, model: &Model) -> Self { 64 | let imports_model = if model 65 | .fields 66 | .iter() 67 | .any(|field| field.data_type == DataType::DateTime) 68 | { 69 | vec!["time"] 70 | } else { 71 | vec![] 72 | }; 73 | let (db_driver, imports_database) = match technology.database { 74 | crate::technology::DataBase::PgSQL => ("pgsql", "github.com/lib/pq"), 75 | crate::technology::DataBase::MySQL => ("mysql", "github.com/go-sql-driver/mysql"), 76 | }; 77 | Self { 78 | imports: Imports { 79 | model: imports_model, 80 | database_library: imports_database, 81 | }, 82 | db_driver, 83 | } 84 | } 85 | } 86 | 87 | pub fn register( 88 | tera: &mut Tera, 89 | technology: &Technology, 90 | model: &Model, 91 | context: &mut tera::Context, 92 | ) { 93 | tera.register_function("data_type", data_type_in_template); 94 | tera.register_function("is_string", data_type_is_string); 95 | let golang = Golang::new(technology, model); 96 | context.insert("golang", &golang); 97 | } 98 | 99 | pub fn render(tera: &Tera, home: impl AsRef, context: &mut tera::Context) { 100 | render_simple(tera, home.as_ref(), "go", "go.mod", context); 101 | render_simple(tera, home.as_ref(), "go", "model/model.go", context); 102 | render_simple(tera, home.as_ref(), "go", "infrastructure/db.go", context); 103 | render_simple(tera, home.as_ref(), "go", "handler/handler.go", context); 104 | render_simple(tera, home, "go", "main.go", context); 105 | } 106 | -------------------------------------------------------------------------------- /src/technology/language/mod.rs: -------------------------------------------------------------------------------- 1 | //! Language the user want to use. 2 | // TODO: maybe we want to support multiple framework for a single language in the future, then "framework" would be a better name 3 | 4 | pub mod golang; 5 | -------------------------------------------------------------------------------- /src/technology/mod.rs: -------------------------------------------------------------------------------- 1 | mod database; 2 | pub mod language; 3 | pub use database::DataBase; 4 | 5 | // TODO: language enum 6 | /// Technology is about the specific technology the user want to implement the API. 7 | pub struct Technology { 8 | pub database: DataBase, 9 | } 10 | -------------------------------------------------------------------------------- /templates/go/go.mod.template: -------------------------------------------------------------------------------- 1 | module {{ model.name | singular | camel_case }} 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /templates/go/handler/handler.go.template: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "{{ model.name | singular | camel_case }}/model" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | "fmt" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | {%- set type_name = model.name | singular | pascal_case -%} 15 | {%- set variable_name = model.name | singular | camel_case %} 16 | 17 | func PostHandler(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Add("Content-Type", "application/json") 19 | body, _ := ioutil.ReadAll(r.Body) 20 | var toCreate model.{{ type_name }} 21 | _ = json.Unmarshal(body, &toCreate) 22 | result, err := model.Create(toCreate) 23 | if err != nil { 24 | log.Println("Create {{ type_name }} failed:", err) 25 | w.WriteHeader(http.StatusInternalServerError) 26 | _, _ = w.Write([]byte(err.Error())) 27 | return 28 | } else { 29 | log.Println("{{ type_name }}", result, "created") 30 | } 31 | response, err := json.Marshal(result) 32 | w.WriteHeader(http.StatusCreated) 33 | _, _ = w.Write(response) 34 | } 35 | 36 | func GetHandler(w http.ResponseWriter, r *http.Request) { 37 | w.Header().Add("Content-Type", "application/json") 38 | {#- TODO: These id relate code is same with those in DeleteHandler, maybe introduce a macro? #} 39 | {% if is_string(data_type=model.primary_key.data_type) %} 40 | pk := mux.Vars(r)["{{ model.primary_key.name }}"] 41 | {% else %} 42 | pkStr := mux.Vars(r)["{{ model.primary_key.name }}"] 43 | var pk {{ data_type(data_type=model.primary_key.data_type) }} 44 | {# TODO: Sscan might be slow, maybe we should add a fast path for int #} 45 | _, err := fmt.Sscan(pkStr, &pk) 46 | {% endif %} 47 | object, err := model.Get(pk) 48 | if err != nil { 49 | w.WriteHeader(http.StatusNotFound) 50 | return 51 | } 52 | resp, _ := json.Marshal(object) 53 | _, _ = w.Write(resp) 54 | } 55 | 56 | func ScanHandler(w http.ResponseWriter, r *http.Request) { 57 | w.Header().Add("Content-Type", "application/json") 58 | limitStr := r.URL.Query().Get("limit") 59 | limit, err := strconv.ParseUint(limitStr, 10, 64) 60 | if err != nil { 61 | log.Println("Scan {{ type_name }} failed:", err) 62 | w.WriteHeader(http.StatusBadRequest) 63 | _, _ = w.Write([]byte(err.Error())) 64 | return 65 | } 66 | offsetStr := r.URL.Query().Get("offset") 67 | offset, err := strconv.ParseUint(offsetStr, 10, 64) 68 | if err != nil { 69 | log.Println("Scan {{ type_name }} failed:", err) 70 | w.WriteHeader(http.StatusBadRequest) 71 | _, _ = w.Write([]byte(err.Error())) 72 | return 73 | } 74 | result, err := model.Scan(offset, limit) 75 | if err != nil { 76 | log.Println("Scan {{ type_name }} failed:", err) 77 | w.WriteHeader(http.StatusInternalServerError) 78 | _, _ = w.Write([]byte(err.Error())) 79 | return 80 | } 81 | var body []byte 82 | if len(result) != 0 { 83 | body, _ = json.Marshal(result) 84 | } else { 85 | body = []byte("[]") 86 | } 87 | _, _ = w.Write(body) 88 | } 89 | 90 | func PutHandler(w http.ResponseWriter, r *http.Request) { 91 | w.Header().Add("Content-Type", "application/json") 92 | body, _ := ioutil.ReadAll(r.Body) 93 | var toUpdate model.{{ type_name }} 94 | _ = json.Unmarshal(body, &toUpdate) 95 | result, err := model.Put(toUpdate) 96 | if err != nil { 97 | log.Println("Update {{ type_name }} failed", err) 98 | w.WriteHeader(http.StatusInternalServerError) 99 | _, _ = w.Write([]byte(err.Error())) 100 | return 101 | } else { 102 | log.Println("{{ type_name }}", toUpdate, "updated") 103 | resp, _ := json.Marshal(result) 104 | w.WriteHeader(http.StatusCreated) 105 | _, _ = w.Write(resp) 106 | } 107 | } 108 | 109 | func DeleteHandler(w http.ResponseWriter, r *http.Request) { 110 | w.Header().Add("Content-Type", "application/json") 111 | {% if is_string(data_type=model.primary_key.data_type) %} 112 | pk := mux.Vars(r)["{{ model.primary_key.name }}"] 113 | {% else %} 114 | pkStr := mux.Vars(r)["{{ model.primary_key.name }}"] 115 | var pk {{ data_type(data_type=model.primary_key.data_type) }} 116 | _, err := fmt.Sscan(pkStr, &pk) 117 | {% endif %} 118 | if err != nil { 119 | log.Println("Delete {{ model.name }} failed", err) 120 | w.WriteHeader(http.StatusInternalServerError) 121 | _, _ = w.Write([]byte(err.Error())) 122 | return 123 | } else { 124 | log.Println("{{ model.name }} {{ model.primary_key.name }}=", pk, "deleted") 125 | w.WriteHeader(http.StatusNoContent) 126 | } 127 | } 128 | 129 | func PingPongHandler(w http.ResponseWriter, r *http.Request) { 130 | _, _ = w.Write([]byte("pong")) 131 | } -------------------------------------------------------------------------------- /templates/go/infrastructure/db.go.template: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "database/sql" 5 | _ "{{ golang.imports.database_library }}" 6 | "os" 7 | ) 8 | 9 | var DB *sql.DB 10 | 11 | func init() { 12 | var err error 13 | DB, err = sql.Open("{{ golang.db_driver }}", os.Getenv("DB_ADDRESS")) 14 | if err != nil { 15 | panic(err) 16 | } 17 | } -------------------------------------------------------------------------------- /templates/go/main.go.template: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "{{ model.name | singular | camel_case }}/handler" 6 | "net/http" 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | func main() { 11 | r := mux.NewRouter() 12 | r.HandleFunc("/ping", handler.PingPongHandler) 13 | r.HandleFunc("/{{ model.name | pluralize | kebab_case }}", handler.PostHandler).Methods("POST") 14 | r.HandleFunc("/{{ model.name | pluralize | kebab_case }}/{id:[0-9]+}", handler.GetHandler).Methods("GET") 15 | r.HandleFunc("/{{ model.name | pluralize | kebab_case }}", handler.ScanHandler).Queries("limit", "{limit}").Queries("offset", "{offset}").Methods("GET") 16 | r.HandleFunc("/{{ model.name | pluralize | kebab_case }}/{id:[0-9]+}", handler.PutHandler).Methods("PUT") 17 | r.HandleFunc("/{{ model.name | pluralize | kebab_case }}/{id:[0-9]+}", handler.DeleteHandler).Methods("DELETE") 18 | 19 | err := http.ListenAndServe(":8000", r) 20 | if err != nil { 21 | log.Fatal("ListenAndServe: ", err) 22 | } 23 | } -------------------------------------------------------------------------------- /templates/go/model/model.go.template: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "{{ model.name | singular | camel_case }}/infrastructure" 5 | {% for extra_import in golang.imports.model %}"{{ extra_import }}" 6 | {% endfor %} 7 | ) 8 | 9 | {%- set type_name = model.name | singular | pascal_case -%} 10 | {%- set table_name = model.name | pluralize | pascal_case -%} 11 | {%- set all_field_names = model.fields | map(attribute="name") | map_case(case="snake") | join(sep=", ") %} 12 | 13 | type {{ type_name }} struct { 14 | {{ model.primary_key.name | pascal_case }} {{ data_type(data_type=model.primary_key.data_type) }} `json:"{{ model.primary_key.name | snake_case }}"` 15 | {% for field in model.fields %}{{ field.name | pascal_case }} {{ data_type(data_type=field.data_type) }} `json:"{{ field.name | snake_case }}"` 16 | {% endfor %} 17 | } 18 | 19 | 20 | func Create(object {{ type_name }}) ({{ type_name }}, error) { 21 | row := infrastructure.DB.QueryRow(` 22 | INSERT INTO {{ table_name }}({{ all_field_names }}) 23 | VALUES ({% for field in model.fields %}${{loop.index}}{% if not loop.last %}, {% endif %}{% endfor %}) RETURNING {{ model.primary_key.name | snake_case }};`, {{ model.fields | map(attribute="name") | map_case(case="pascal") | map_prefix(prefix="object.") | join(sep=", ") }}) 24 | err := row.Scan(&object.{{ model.primary_key.name | pascal_case }}) 25 | return object, err 26 | } 27 | 28 | func Get({{ model.primary_key.name | camel_case }} {{ data_type(data_type=model.primary_key.data_type) }}) ({{ type_name }}, error) { 29 | row := infrastructure.DB.QueryRow(` 30 | SELECT {{ all_field_names }} 31 | FROM {{ table_name }} WHERE {{ model.primary_key.name | snake_case }}=$1;`, {{ model.primary_key.name}}); 32 | object := {{ type_name }} { {{ model.primary_key.name | pascal_case }}: {{ model.primary_key.name | camel_case }} } 33 | err := row.Scan({{ model.fields | map(attribute="name") | map_case(case="pascal") | map_prefix(prefix="&object.") | join(sep=", ") }}) 34 | return object, err 35 | } 36 | 37 | func Scan(offset uint64, limit uint64) ([]{{ type_name }}, error) { 38 | rows, err := infrastructure.DB.Query(` 39 | SELECT {{ model.primary_key.name | snake_case }}{% if all_field_names | length != 0 %}, {% endif %}{{ all_field_names }} 40 | FROM {{ table_name }} 41 | LIMIT $1 OFFSET $2;`, limit, offset) 42 | if err != nil { 43 | return nil, err 44 | } 45 | var result []{{ type_name }} 46 | for rows.Next() { 47 | var scanned {{ type_name }} 48 | err := rows.Scan(&scanned.{{ model.primary_key.name | pascal_case }}{% if all_field_names | length != 0 %}, {% endif %}{{ model.fields | map(attribute="name") | map_case(case="pascal") | map_prefix(prefix="&scanned.") | join(sep=", ") }}) 49 | if err != nil { 50 | return result, err 51 | } 52 | result = append(result, scanned) 53 | } 54 | return result, nil 55 | } 56 | 57 | func Put(object {{ type_name }}) ({{ type_name }}, error) { 58 | row := infrastructure.DB.QueryRow(` 59 | UPDATE {{ table_name }} 60 | SET {{ all_field_names }} 61 | WHERE id=${{ model.fields | length + 1 }} 62 | RETURNING {{ all_field_names }}; 63 | `, {{ model.fields | map(attribute="name") | map_case(case="pascal") | map_prefix(prefix="object.") | join(sep=", ") }}, object.Id) 64 | err := row.Scan({{ model.fields | map(attribute="name") | map_case(case="pascal") | map_prefix(prefix="&object.") | join(sep=", ") }}) 65 | return object, err 66 | } 67 | 68 | func Delete({{ model.primary_key.name | pascal_case }} {{ data_type(data_type=model.primary_key.data_type) }}) error { 69 | _, err := infrastructure.DB.Exec(`DELETE FROM {{ table_name }} WHERE {{ model.primary_key.name | snake_case }}=$1;`, {{ model.primary_key.name | pascal_case }}) 70 | return err 71 | } 72 | --------------------------------------------------------------------------------