Skip to content

Feat integrate minijinja as template engine #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions aiscript-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ redis.workspace = true
toml = "0.8"
oas3 = "0.15"
reqwest.workspace = true
minijinja = { version = "1.0", features = ["loader"] }
lazy_static = "1.4"
6 changes: 6 additions & 0 deletions aiscript-runtime/src/endpoint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::template;
use aiscript_directive::{Validator, route::RouteAnnotation};
use aiscript_vm::{ReturnValue, Vm, VmError};
use axum::{
Expand Down Expand Up @@ -607,3 +608,8 @@ pub(crate) fn convert_field(field: ast::Field) -> Field {
validators: Arc::from(field.validators),
}
}

pub fn render(template: &str, context: serde_json::Value) -> Result<String, String> {
let engine = template::get_template_engine();
engine.render(template, &context)
}
54 changes: 54 additions & 0 deletions aiscript-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod endpoint;
mod error;
mod openapi;
mod parser;
mod template;
mod utils;

use aiscript_lexer as lexer;
Expand Down Expand Up @@ -300,3 +301,56 @@ async fn run_server(
}
}
}

// Generate OpenAPI JSON from routes
fn generate_openapi_json(routes: &[ast::Route]) -> serde_json::Value {
let mut openapi = serde_json::json!({
"openapi": "3.0.0",
"info": {
"title": "AIScript API",
"version": "1.0.0",
"description": "API documentation for AIScript"
},
"paths": {},
});

//Add paths from routes
let paths = openapi["paths"].as_object_mut().unwrap();

for route in routes {
for endpoint in &route.endpoints {
for path_spec in &endpoint.path_specs {
let path = if route.prefix == "/" {
path_spec.path.clone()
} else {
format!("{}{}", route.prefix, path_spec.path)
};

let method = match path_spec.method {
ast::HttpMethod::Get => "get",
ast::HttpMethod::Post => "post",
ast::HttpMethod::Put => "put",
ast::HttpMethod::Delete => "delete",
};

//For each method, add the path and method to the paths object
if !paths.contains_key(&path) {
paths.insert(path.clone(), serde_json::json!({}));
}

//Add the method to the path
let path_obj = paths.get_mut(&path).unwrap();
path_obj[method] = serde_json::json!({
"summary": format!("{} {}", method.to_uppercase(), path),
"responses": {
"200": {
"description": "Successful response"
}
}
});
}
}
}

openapi
}
72 changes: 72 additions & 0 deletions aiscript-runtime/src/template.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use minijinja::Environment;
use std::sync::RwLock;

/// Template engine for AIScript
pub struct TemplateEngine {
env: RwLock<Environment<'static>>,
}

impl TemplateEngine {
/// Create a new template engine
pub fn new() -> Self {
let mut env = Environment::new();

//Set the source to the templates directory
env.set_loader(|name| -> Result<Option<String>, minijinja::Error> {
let path = std::path::Path::new("templates").join(name);
match std::fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(_) => Ok(None),
}
});

Self {
env: RwLock::new(env),
}
}

/// Render a template with the given context
pub fn render(
&self,
template_name: &str,
context: &serde_json::Value,
) -> Result<String, String> {
let env = self.env.read().unwrap();

// get the template
let template = env
.get_template(template_name)
.map_err(|e| format!("Failed to load template '{}': {}", template_name, e))?;

// render the template and return the result
template
.render(context)
.map_err(|e| format!("Failed to render template '{}': {}", template_name, e))
}

/// Reload the templates
pub fn reload(&self) -> Result<(), String> {
let mut env = self.env.write().unwrap();

//reload templates
env.set_loader(|name| -> Result<Option<String>, minijinja::Error> {
let path = std::path::Path::new("templates").join(name);
match std::fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(_) => Ok(None),
}
});

Ok(())
}
}

//Create a global instance of the template engine
lazy_static::lazy_static! {
static ref TEMPLATE_ENGINE: TemplateEngine = TemplateEngine::new();
}

//Get the template engine instance
pub fn get_template_engine() -> &'static TemplateEngine {
&TEMPLATE_ENGINE
}
22 changes: 22 additions & 0 deletions aiscript-vm/src/stdlib/web.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::value::Value;
use aiscript_runtime::template::get_template_engine;

/// Render a template with the given context
pub fn render(args: &[Value]) -> Result<Value, String> {
if args.len() != 2 {
return Err("render expects 2 arguments: template name and context".to_string());
}

//get the template name
let template_name = args[0].as_str().ok_or_else(|| "First argument must be a string (template name)".to_string())?;

//get the context
let context = serde_json::to_value(&args[1])
.map_err(|e| format!("Failed to convert context to JSON: {}", e))?;

//render the template
let result = get_template_engine().render(template_name, context)?;

OK(Value::String(result.into()))

}
7 changes: 7 additions & 0 deletions routes/template-test.ai
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
route /template {
get /hello {
name: str,
is_admin: bool = false,
}
return render("hello-page.jinja", query)
}
5 changes: 5 additions & 0 deletions templates/hello-page.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if query.is_admin %}
<div>Hello, {{ query.name }} (Admin)</div>
{% else %}
<div>Hello, {{ query.name }}</div>
{% endif %}
Loading