mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 12:54:09 +02:00
Move template rendering and themes onto PluginHost (#559)
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
//! is anything only a desktop can do — open a native window, run the updater,
|
||||
//! show a native dialog — those handlers stay with the desktop.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use yaak_core::WorkspaceContext;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
@@ -21,8 +22,12 @@ use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::models::Plugin;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::PluginContext;
|
||||
use yaak_plugins::events::{
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
JsonPrimitive, PluginContext, RenderPurpose,
|
||||
};
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
/// Only `Clone` is required here. `Send`/`Sync`/`'static` are deliberately
|
||||
/// *not*: a browser host is single-threaded and its connection pool is an
|
||||
@@ -106,6 +111,30 @@ pub trait PluginHost: Host {
|
||||
/// loaded. A host without a runtime can return them untouched.
|
||||
fn resolve_plugins(&self, plugins: Vec<Plugin>) -> impl Future<Output = Vec<Plugin>>;
|
||||
|
||||
/// The template functions this host can run, as a callback the renderer
|
||||
/// drives. This is the *only* thing the plugin runtime uniquely provides to
|
||||
/// a render — the variables come from the environment chain, which is an
|
||||
/// ordinary database read — so handing back the callback keeps the rest of
|
||||
/// rendering shared instead of pushing whole commands behind this trait.
|
||||
fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback;
|
||||
|
||||
/// Every template function the installed plugins expose, for the
|
||||
/// autocomplete menu.
|
||||
fn template_function_summaries(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetTemplateFunctionSummaryResponse>>>;
|
||||
|
||||
/// The form a template function wants to show for the given values.
|
||||
fn template_function_config(
|
||||
&self,
|
||||
function_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> impl Future<Output = crate::Result<GetTemplateFunctionConfigResponse>>;
|
||||
|
||||
/// Themes contributed by plugins.
|
||||
fn themes(&self) -> impl Future<Output = crate::Result<Vec<GetThemesResponse>>>;
|
||||
|
||||
/// Re-encrypt the `secure(...)` values in a template.
|
||||
///
|
||||
/// Whole operation rather than its pieces because the encryption is only
|
||||
|
||||
@@ -17,7 +17,9 @@ pub mod error;
|
||||
pub mod host;
|
||||
pub mod models;
|
||||
pub mod plugins;
|
||||
pub mod render;
|
||||
pub mod responses;
|
||||
pub mod templates;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use host::{Host, PluginHost};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Rendering a template against an environment chain.
|
||||
//!
|
||||
//! The variables come from the chain, the functions come from the host's
|
||||
//! template callback. Neither of these knows which host it is running under —
|
||||
//! that is the whole point of taking the callback as a parameter.
|
||||
|
||||
use serde_json::Value;
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||
|
||||
pub async fn render_template<T: TemplateCallback>(
|
||||
template: &str,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
parse_and_render(template, vars, cb, opt).await
|
||||
}
|
||||
|
||||
pub async fn render_json_value<T: TemplateCallback>(
|
||||
value: Value,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<Value> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
render_json_value_raw(value, vars, cb, opt).await
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Templates, the functions plugins put in them, and themes.
|
||||
//!
|
||||
//! Everything here needs the plugin runtime, but only for the one thing it
|
||||
//! uniquely provides: running a template function. Resolving the environment
|
||||
//! chain and deciding what a render should do about errors are ordinary work
|
||||
//! and stay here, where every host gets them the same.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::host::PluginHost;
|
||||
use crate::render::render_template;
|
||||
use yaak_plugins::events::{
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
RenderPurpose,
|
||||
};
|
||||
use yaak_rpc_schema::*;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions, transform_args};
|
||||
|
||||
pub async fn cmd_render_template<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdRenderTemplateReq,
|
||||
) -> Result<String> {
|
||||
let environment_chain =
|
||||
host.db().resolve_environments(&req.workspace_id, None, req.environment_id.as_deref())?;
|
||||
let cb = host.template_callback(req.purpose.unwrap_or(RenderPurpose::Preview));
|
||||
let options = RenderOptions {
|
||||
// A preview that throws would show the user an error where they expect
|
||||
// to see the value so far, so callers rendering *into the UI* ask for
|
||||
// empties instead.
|
||||
error_behavior: match req.ignore_error {
|
||||
Some(true) => RenderErrorBehavior::ReturnEmpty,
|
||||
_ => RenderErrorBehavior::Throw,
|
||||
},
|
||||
};
|
||||
Ok(render_template(&req.template, environment_chain, &cb, &options).await?)
|
||||
}
|
||||
|
||||
/// Render only the *arguments* of a template's function calls, leaving the
|
||||
/// calls themselves intact. This is what turns a parsed template back into
|
||||
/// something displayable without evaluating it.
|
||||
pub async fn cmd_template_tokens_to_string<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdTemplateTokensToStringReq,
|
||||
) -> Result<String> {
|
||||
let cb = host.template_callback(RenderPurpose::Preview);
|
||||
Ok(transform_args(req.tokens, &cb)?.to_string())
|
||||
}
|
||||
|
||||
pub async fn cmd_template_function_summaries<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdTemplateFunctionSummariesReq,
|
||||
) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
host.template_function_summaries().await
|
||||
}
|
||||
|
||||
pub async fn cmd_template_function_config<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdTemplateFunctionConfigReq,
|
||||
) -> Result<GetTemplateFunctionConfigResponse> {
|
||||
host.template_function_config(&req.function_name, req.values, &req.model.id()).await
|
||||
}
|
||||
|
||||
pub async fn cmd_get_themes<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdGetThemesReq,
|
||||
) -> Result<Vec<GetThemesResponse>> {
|
||||
host.themes().await
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
//! `PluginHost` too, without one, which is only possible because that trait
|
||||
//! names operations rather than handing back a manager.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
@@ -15,18 +16,24 @@ use yaak_commands::models::{
|
||||
cmd_default_headers, cmd_get_workspace_meta, models_delete, models_upsert,
|
||||
models_workspace_models,
|
||||
};
|
||||
use yaak_commands::templates::cmd_render_template;
|
||||
use yaak_commands::{Host, PluginHost};
|
||||
use yaak_core::WorkspaceContext;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::models::{AnyModel, Plugin, Workspace};
|
||||
use yaak_models::models::{AnyModel, Environment, EnvironmentVariable, Plugin, Workspace};
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::{ModelPayload, UpdateSource};
|
||||
use yaak_plugins::events::{
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
JsonPrimitive, RenderPurpose,
|
||||
};
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_rpc_schema::{
|
||||
CmdDefaultHeadersReq, CmdGetWorkspaceMetaReq, ModelsDeleteReq, ModelsUpsertReq,
|
||||
ModelsWorkspaceModelsReq,
|
||||
CmdDefaultHeadersReq, CmdGetWorkspaceMetaReq, CmdRenderTemplateReq, ModelsDeleteReq,
|
||||
ModelsUpsertReq, ModelsWorkspaceModelsReq,
|
||||
};
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHost {
|
||||
@@ -183,6 +190,32 @@ impl Host for SingleThreadedHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// A template callback with no plugins behind it: variables still resolve,
|
||||
/// function calls have nothing to run them. A browser host would put a Worker
|
||||
/// round-trip where this returns an error.
|
||||
struct NoTemplateFunctions;
|
||||
|
||||
impl TemplateCallback for NoTemplateFunctions {
|
||||
async fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
_args: HashMap<String, serde_json::Value>,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
Err(yaak_templates::error::Error::RenderError(format!(
|
||||
"no plugin runtime to run {fn_name}()"
|
||||
)))
|
||||
}
|
||||
|
||||
fn transform_arg(
|
||||
&self,
|
||||
_fn_name: &str,
|
||||
_arg_name: &str,
|
||||
arg_value: &str,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
Ok(arg_value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Answering plugin questions with no plugin runtime behind them. A browser
|
||||
/// host would put a `postMessage` round-trip to its Worker where these return
|
||||
/// constants; the shape of the trait is what makes either possible.
|
||||
@@ -204,6 +237,29 @@ impl PluginHost for SingleThreadedHost {
|
||||
async fn encrypt_secure_template(&self, _template: &str) -> yaak_commands::Result<String> {
|
||||
Err(yaak_commands::Error::Generic("no plugin runtime on this host".into()))
|
||||
}
|
||||
|
||||
fn template_callback(&self, _purpose: RenderPurpose) -> impl TemplateCallback {
|
||||
NoTemplateFunctions
|
||||
}
|
||||
|
||||
async fn template_function_summaries(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn template_function_config(
|
||||
&self,
|
||||
function_name: &str,
|
||||
_values: HashMap<String, JsonPrimitive>,
|
||||
_model_id: &str,
|
||||
) -> yaak_commands::Result<GetTemplateFunctionConfigResponse> {
|
||||
Err(yaak_commands::Error::Generic(format!("no plugin provides {function_name}()")))
|
||||
}
|
||||
|
||||
async fn themes(&self) -> yaak_commands::Result<Vec<GetThemesResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -227,6 +283,43 @@ async fn a_single_threaded_host_can_implement_the_trait() {
|
||||
.expect("workspace models");
|
||||
assert!(json.contains(&id), "the workspace should be in its own bootstrap payload");
|
||||
|
||||
// Rendering, on a host whose template callback has no plugins behind it.
|
||||
// Resolving the environment chain is a database read and the render is
|
||||
// shared code; only the callback came from the host. Rendering a real
|
||||
// variable is what proves the chain was resolved rather than skipped.
|
||||
let environment = host
|
||||
.db()
|
||||
.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: id.clone(),
|
||||
name: "Test env".to_string(),
|
||||
base: true,
|
||||
variables: vec![EnvironmentVariable {
|
||||
enabled: true,
|
||||
name: "greeting".to_string(),
|
||||
value: "hello".to_string(),
|
||||
id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
.expect("seed environment");
|
||||
|
||||
let rendered = cmd_render_template(
|
||||
host.clone(),
|
||||
CmdRenderTemplateReq {
|
||||
template: "${[ greeting ]} world".to_string(),
|
||||
workspace_id: id.clone(),
|
||||
environment_id: Some(environment.id.clone()),
|
||||
purpose: None,
|
||||
ignore_error: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("render");
|
||||
assert_eq!(rendered, "hello world", "the environment chain should have been resolved");
|
||||
|
||||
// The delete path too, since it is the one that used to reach for a
|
||||
// blocking thread this host does not have.
|
||||
let workspace = host.db().get_workspace(&id).expect("get workspace");
|
||||
|
||||
Reference in New Issue
Block a user