Skip to content

A highly highly opinionated template engine to help generate endpoints and related pieces for Rust projects

Notifications You must be signed in to change notification settings

pathscale/EndpointGen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EndpointGen

Crates.io dependency status

NB NB TODO: This readme needs to be updated following binary . Please look at the readme in the examples project for more up to date info.

A highly opinionated template engine to help generate endpoints and related pieces for Rust projects

Running

  1. Set up according to the guide below
  2. Run cargo build for the gen crate
  3. Generation will run on every build where something required for generation has changed since the last build
  4. On first run, /project_root/docs/error_codes.json will be created. This is meant to be manually edited and is used to generate the error code logic for the endpoints, as well as to generate a more readable error_codes.md file

Adding an error code

Add the following to /project_root/docs/error_codes.json:

{
  "language": "en",
  "codes": [
    {
      "code": 100400,
      "symbol": "BadRequest",
      "message": "Bad Request",
      "source": "Custom"
    }
}

Upon next generation, inspect the error_codes.md file, as well as the generated model.rs file, to see that the error code you have added can be seen in those files.

Setup and Getting Started

Note: The directory and module structures proposed below are purely for demonstration purposes and can be changed as required according to the project EndpointGen is added to. This is a good default to follow though.

  1. In a terminal navigated to the root of your project, run: cargo new --lib --vcs none gen
  2. Add the following to the gen crate's Cargo.toml:
[dependencies]
endpoint-gen = "*" 

[build-dependencies]
endpoint-gen = "*"
eyre = "*"
  1. Add a build.rs to the gen directory with at least the following:
use std::{env, path::PathBuf};

use endpoint_gen::Data;

fn main() -> eyre::Result<()> {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=../docs/error_codes/error_codes.json");

    // Set these up to match your environment
    let current_dir = env::current_dir()?;
    let root = current_dir.parent().unwrap(); // This should evaluate to the root of your project where the project Cargo.toml can be found
    let output_dir = &current_dir.join("generated"); // This should evaluate to the `<root>/gen/generated/` dir

    let data = Data {
        project_root: PathBuf::from(&root),
        output_dir: PathBuf::from(root),
        services: gen_src::services::get_services(),
        enums: gen_src::enums::get_enums(),
        pg_funcs: gen_src::proc_funcs::get_proc_functions(),
    };

    endpoint_gen::main(data)?;

    Ok(())
}
  1. Add the gen crate to your project root's Cargo.toml, and add it to the workspace dependencies:
[workspace]
members = [
  "gen",
  ...
]

[workspace.dependencies]
## Internal dependencies
gen = { path = "./gen" }

Adding generation sources

Add gen_src as a module to the gen project:

  • Add gen_src.rs at the same level as build.rs
  • Add the gen_src directory at the same level

Add the following as submodules to /gen/gen_src/

  • services.rs
  • enums.rs
  • proc_funcs.rs

Declare the modules in /gen/gen_src.rs:

pub mod enums;
pub mod proc_funcs;
pub mod services;

Your directory structure should now look like the following:

project_root/
├─ gen/
│  ├─ gen_src/
│  │  ├─ enums.rs
│  │  ├─ proc_funcs.rs
│  │  ├─ services.rs
│  ├─ src/
│  │  ├─ lib.rs
│  ├─ build.rs
│  ├─ Cargo.toml
│  ├─ gen_src.rs
Cargo.toml

Adding Services

Add the following to services.rs:

use endpoint_gen::model::Service;

/// Returns a vector of the available `Service`s (e.g. `auth`, `user`, `admin`, `chatbot`).
pub fn get_services() -> Vec<Service> {
    vec![]
}

Now edit build.rs and add the following:

// ...Includes above

fn main() -> eyre::Result<()> {
    ...
    let data = Data {
        ...
        services: services::get_services(),
        ...
    }
    ...

Adding enums

Add the following to enums.rs:

use endpoint_gen::model::Type;

/// Returns a vector of the available `Service`s (e.g. `auth`, `user`, `admin`, `chatbot`).
pub fn get_enums() -> Vec<Type> {
    vec![]
}

Now edit build.rs and add the following:

...
fn main() -> eyre::Result<()> {
    ...
    let data = Data {
        ...
        enums: enums::get_enums(),
        ...
    }
    ...

Adding procedural functions

Add the following to proc_funcs.rs:

use endpoint_gen::model::ProceduralFunction;

/// Returns a vector of the available `ProceduralFunction`s (e.g. `auth`, `user`, `admin`, `chatbot`).
pub fn get_proc_functions() -> Vec<ProceduralFunction> {
    vec![]
}

Now edit build.rs and add the following:

...
fn main() -> eyre::Result<()> {
    ...
    let data = Data {
        ...
        pg_funcs: proc_funcs::get_proc_functions(),
        ...
    }
    ...

Defining generation sources

Services

"Services" correspond to individual binaries that are intended to run as services within a Linux environment. Defining these allows endpoint-gen to create the corresponding service files that can be deployed to the target system and run.

A service has:

  • A name
  • An ID
  • A list of Websocket endpoints that the service exposes

Add the following to services.rs:

use endpoint_gen::model::{EndpointSchema, Field, Service, Type};

pub fn get_services() -> Vec<Service> {
    vec![
        Service::new("service_1", 1, get_service_endpoints()),
    ]
}

pub fn get_service_endpoints() -> Vec<EndpointSchema> {
    vec![example_endpoint()]
}

pub fn example_endpoint() -> EndpointSchema {
    
}

Defining an endpoint

An endpoint, defined by EndpointSchema, is defined by:

  • A name
  • A unique numeric code
  • A list of input parameters, defined by name and Type
  • A list of return values, defined by name and Type

Add the following to the example_endpoint function:

EndpointSchema::new(
        "Authorize", // name
        10030, // code
        vec![  // input params
            Field::new("username", Type::String),
            Field::new("token", Type::UUID),
            Field::new("service", Type::enum_ref("service")),
            Field::new("device_id", Type::String),
            Field::new("device_os", Type::String),
        ],
        vec![Field::new("success", Type::Boolean)], // returns
    )

Enums

An enum in this context refers to an enum used within the database (currently postgres) for enumerating various types of objects that may be required for logic or frontend display purposes

An enum (which is a variant of the actual Rust enum, Type, defined in endpoint-gen/src/model/types.rs) is defined by the following:

  • A name
  • A list of variants, defined by EnumVariant

Defining an enum

Add the following to the get_enums function in enums.rs:

use endpoint_gen::model::{EnumVariant, Type};

pub fn get_enums() -> Vec<Type> {
    vec![Type::enum_(
        "role".to_owned(),
        vec![
            EnumVariant::new("guest", 0),
            EnumVariant::new("user", 1),
            EnumVariant::new("admin", 2),
            EnumVariant::new("developer", 3),
        ],
    )]
}

As can be seen, an EnumVariant just consists of a name and an ordinal

Procedural functions

A procedural function bundles and wraps an actual database query (currently SQL with Postgres) in a strongly typed and identifiable structure that contains:

  • A name
  • A list of parameters that the function accepts
  • A return row type of the function
  • The raw SQL of the function

Add the following to get_proc_functions and proc_funcs.rs:

use endpoint_gen::model::{Field, ProceduralFunction, Type};

pub fn get_proc_functions() -> Vec<ProceduralFunction> {
    vec![
        get_example_func(),
    ]
    .concat()
}

fn get_example_func() -> Vec<ProceduralFunction> {

}

Defining a procedural function

Add the following to get_example_func:

fn get_example_func() -> Vec<ProceduralFunction> {
    vec![ProceduralFunction::new(
        "fun_user_add_object", // Proc func name
        vec![
            // Proc func input params
            Field::new("kind", Type::Int),
            Field::new("id", Type::Int),
            Field::new("timestamp", Type::BigInt),
            Field::new("transaction_hash", Type::BlockchainTransactionHash),
            Field::new("contract_address", Type::BlockchainAddress),
            Field::new("detail", Type::Object), // JSON object
        ],
        vec![Field::new("success", Type::Boolean)], // Proc func returns
        // Raw sql
        r#"
        BEGIN
            -- delete same kind of object for the same address
            DELETE FROM tbl.object WHERE contract_address = a_contract_address;
            INSERT INTO tbl.object (kind, id, timestamp, transaction_hash, contract_address, detail)
            VALUES (a_kind, a_id, a_timestamp, a_transaction_hash, a_contract_address, a_detail);
        END
        "#,
    )]
}

About

A highly highly opinionated template engine to help generate endpoints and related pieces for Rust projects

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 6

Languages