WIP
ci/woodpecker/push/woodpecker Pipeline failed Details

feat/plugins-backend
Tomáš Mládek 2024-04-21 12:13:26 +02:00
parent e06d2bccfe
commit 7e368df812
15 changed files with 675 additions and 31 deletions

View File

@ -184,16 +184,17 @@ test:
END
test-backend:
FROM +base-backend
CACHE --id=rust-target target
FROM +base-rust
RUN cargo nextest run --workspace
test-jslib:
FROM +base-node
WORKDIR sdks/js
RUN pnpm build && pnpm test
# Deployment targets
appimage-signed:
FROM alpine
RUN apk add gpg gpg-agent
RUN --secret GPG_SIGN_KEY echo "$GPG_SIGN_KEY" | gpg --import
COPY +appimage/*.AppImage .
RUN gpg --clear-sign *.AppImage
SAVE ARTIFACT *.AppImage
SAVE ARTIFACT *.asc
deploy-appimage-nightly:
FROM alpine
@ -239,6 +240,40 @@ NPM_PUBLISH:
ELSE
RUN echo "Nothing to do for $pkg_name."
END
# Extensions (WIP)
extensions:
WAIT
BUILD +extensions-dummy
END
extensions-dummy:
FROM +base-extensions
WORKDIR dummy
RUN cargo build --release --target wasm32-unknown-unknown
SAVE ARTIFACT ../target/wasm32-unknown-unknown/release/upend_plugin_dummy.wasm
extensions-dummy-signed:
FROM +base-sign
COPY +extensions-dummy/upend_plugin_dummy.wasm .
RUN gpg --detach-sign --sign --armor upend_plugin_dummy.wasm
SAVE ARTIFACT upend_plugin_dummy.wasm
SAVE ARTIFACT upend_plugin_dummy.wasm.asc
base-extensions:
FROM rust:bookworm
RUN rustup component add clippy
RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
RUN cargo install wasm-pack wasm-bindgen-cli && rustup target add wasm32-unknown-unknown
WORKDIR /upend/extensions
COPY extensions/Cargo.toml Cargo.toml
COPY extensions/Cargo.lock Cargo.lock
COPY extensions/base/Cargo.toml base/Cargo.toml
COPY extensions/dummy/Cargo.toml dummy/Cargo.toml
RUN cargo fetch --locked
COPY --dir extensions/base ./
COPY --dir extensions/dummy ./
# Utility targets
@ -253,11 +288,21 @@ git-version:
RUN ./build/get_version.sh > /tmp/upend_version.txt && cat /tmp/upend_version.txt
SAVE ARTIFACT /tmp/upend_version.txt version.txt
changelog:
FROM orhunp/git-cliff
COPY .git .git
RUN git-cliff --bump -o CHANGELOG.md
SAVE ARTIFACT CHANGELOG.md
base-rust:
FROM rust:bookworm
RUN rustup component add clippy
RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
RUN cargo install wasm-pack wasm-bindgen-cli && rustup target add wasm32-unknown-unknown
RUN cargo install cargo-audit
WORKDIR /upend
COPY Cargo.toml Cargo.lock .
COPY base/Cargo.toml base/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
COPY db/Cargo.toml db/Cargo.toml
COPY tools/upend_wasm/Cargo.toml tools/upend_wasm/Cargo.toml
RUN cargo fetch --locked
COPY --dir base cli db Cargo.toml Cargo.lock .
COPY --dir tools/upend_wasm tools/
current-changelog:
FROM orhunp/git-cliff

View File

@ -29,10 +29,10 @@ once_cell = "1.7.2"
lru = "0.7.0"
diesel = { version = "1.4", features = [
"sqlite",
"r2d2",
"chrono",
"serde_json",
"sqlite",
"r2d2",
"chrono",
"serde_json",
] }
diesel_migrations = "1.4"
libsqlite3-sys = { version = "^0", features = ["bundled"] }
@ -54,10 +54,10 @@ regex = "1"
multibase = "0.9"
multihash = { version = "*", default-features = false, features = [
"alloc",
"multihash-impl",
"sha2",
"identity",
"alloc",
"multihash-impl",
"sha2",
"identity",
] }
uuid = { version = "1.4", features = ["v4"] }
@ -91,18 +91,21 @@ bytes = "1.4.0"
signal-hook = "0.3.15"
actix-web-lab = { version = "0.20.2", features = ["spa"] }
extism = "1.2.0"
upend-extension-base = { path = "../extensions/base" }
[build-dependencies]
shadow-rs = { version = "0.23", default-features = false }
[features]
default = [
"desktop",
"previews",
"previews-image",
"extractors-web",
"extractors-audio",
"extractors-exif",
"extractors-media",
"desktop",
"previews",
"previews-image",
"extractors-web",
"extractors-audio",
"extractors-exif",
"extractors-media",
]
desktop = ["webbrowser", "opener", "is_executable"]
previews = []

View File

@ -36,8 +36,9 @@ mod routes;
mod serve;
mod util;
mod extractors;
mod previews;
mod extractors; // TODO REMOVE
mod plugins;
mod previews; // TODO REMOVE
#[derive(Debug, Parser)]
#[command(name = "upend", author)]
@ -394,12 +395,15 @@ async fn main() -> Result<()> {
.collect()
});
let plugins = crate::plugins::Plugins::init(&get_resource_path("plugins")?)?;
let state = routes::State {
upend: upend.clone(),
store,
job_container: job_container.clone(),
preview_store,
preview_thread_pool,
plugins: plugins.into(),
config: UpEndConfig {
vault_name: Some(args.vault_name.unwrap_or_else(|| {
vault_path

13
cli/src/plugins/host.rs Normal file
View File

@ -0,0 +1,13 @@
use extism::{CurrentPlugin, UserData, Val};
use upend_base::error::UpEndError;
fn hello_world(
_plugin: &mut CurrentPlugin,
inputs: &[Val],
outputs: &mut [Val],
_user_data: UserData,
) -> Result<(), UpEndError> {
println!("Hello from Rust!");
outputs[0] = inputs[0].clone();
Ok(())
}

175
cli/src/plugins/mod.rs Normal file
View File

@ -0,0 +1,175 @@
use anyhow::Result;
use extism::{manifest::Wasm, *};
use std::{
path::{Path, PathBuf},
process::Command,
};
use upend_extension_base::PluginInfo;
mod host;
pub struct Plugins<'a> {
pub plugins: Vec<UpEndPlugin<'a>>,
}
#[derive(Debug)]
pub enum UpEndPlugin<'a> {
Initialized(UpEndPluginInitialized<'a>),
Failed(UpEndPluginFailed),
}
pub struct UpEndPluginInitialized<'a> {
pub path: PathBuf,
pub info: PluginInfo,
pub plugin: Box<extism::Plugin<'a>>,
pub verified: Result<(), String>,
}
impl std::fmt::Debug for UpEndPluginInitialized<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UpEndPluginInitialized")
.field("path", &self.path)
.field("info", &self.info)
.field("verified", &self.verified)
.finish()
}
}
#[derive(Debug)]
pub struct UpEndPluginFailed {
pub path: PathBuf,
pub error: String,
}
impl Plugins<'_> {
pub fn init(plugin_path: &Path) -> Result<Self> {
let plugin_files = plugin_path
.read_dir()?
.filter_map(|p| p.ok().and_then(|p| Some(p.path())))
.filter(|p| p.is_file() && p.extension().unwrap_or_default() == "wasm");
let mut plugins = vec![];
for plugin_path in plugin_files {
debug!("Attempting to load plugin: {:?}", plugin_path);
let file = Wasm::file(plugin_path.clone());
let manifest = Manifest::new([file]);
let plugin = Plugin::create_with_manifest(&manifest, [], true);
match plugin {
Ok(mut plugin) => {
debug!("Plugin loaded: {:?}", plugin_path);
let info = plugin.call("info", []).and_then(|v| {
serde_json::from_slice::<PluginInfo>(&v).map_err(|e| anyhow::anyhow!(e))
});
match info {
Ok(info) => {
debug!("Plugin info: {:?}", info);
let mut gpg_cmd = Command::new("gpg");
let verify_cmd = gpg_cmd
.arg("--verify")
.arg(&plugin_path.with_extension("wasm.asc"));
let verify_result = verify_cmd
.output()
.map_err(|e| format!("Failed to run gpg: {:?}", e))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
Err(format!(
"Failed to verify plugin: {:?}",
String::from_utf8_lossy(&output.stderr)
))
}
});
let verified = verify_result.and_then(|_| Ok(()));
plugins.push(UpEndPlugin::Initialized(UpEndPluginInitialized {
path: plugin_path.clone(),
info,
plugin: Box::new(plugin),
verified,
}));
}
Err(e) => {
error!("Failed to get plugin info: {:?}", e);
plugins.push(UpEndPlugin::Failed(UpEndPluginFailed {
path: plugin_path.clone(),
error: format!("Failed to get plugin info: {:?}", e),
}));
}
}
}
Err(e) => {
error!("Failed to create plugin: {:?}", e);
plugins.push(UpEndPlugin::Failed(UpEndPluginFailed {
path: plugin_path.clone(),
error: format!("Failed to create plugin: {:?}", e),
}));
}
}
}
Ok(Self { plugins })
}
}
#[cfg(test)]
mod tests {
use core::panic;
use super::*;
use crate::common::get_resource_path;
#[test]
fn test_plugins_init() {
let plugins = Plugins::init(&get_resource_path("plugins").unwrap()).unwrap();
assert!(plugins.plugins.len() > 0);
for plugin in plugins.plugins {
assert!(
match plugin {
UpEndPlugin::Initialized(_) => true,
_ => false,
},
"{:?}",
plugin
);
}
}
#[test]
fn test_plugins_verify() {
let plugins = Plugins::init(&get_resource_path("plugins").unwrap()).unwrap();
let verified_plugin = plugins
.plugins
.iter()
.find(|p| match p {
UpEndPlugin::Initialized(p) => p.path.to_string_lossy().contains("verified"),
_ => false,
})
.unwrap();
assert!(
match verified_plugin {
UpEndPlugin::Initialized(p) => p.verified.is_ok(),
_ => false,
},
"{:?}",
verified_plugin
);
let unverified_plugin = plugins
.plugins
.iter()
.find(|p| match p {
UpEndPlugin::Initialized(p) => !p.path.to_string_lossy().contains("verified"),
_ => false,
})
.unwrap();
assert!(
match unverified_plugin {
UpEndPlugin::Initialized(p) => p.verified.is_err(),
_ => false,
},
"{:?}",
unverified_plugin
);
}
}

View File

@ -2,6 +2,7 @@ use crate::common::build;
use crate::common::REQWEST_CLIENT;
use crate::config::UpEndConfig;
use crate::extractors;
use crate::plugins::Plugins;
use crate::previews::PreviewStore;
use crate::util::exec::block_background;
use actix_files::NamedFile;
@ -43,6 +44,7 @@ use upend_db::stores::{Blob, UpStore};
use upend_db::BlobMode;
use upend_db::OperationContext;
use upend_db::UpEndDatabase;
use upend_extension_base::PluginInfo;
use upend_db::VaultOptions;
use url::Url;
@ -55,6 +57,7 @@ pub struct State {
pub upend: Arc<UpEndDatabase>,
pub store: Arc<Box<dyn UpStore + Sync + Send>>,
pub config: UpEndConfig,
pub plugins: Arc<Plugins<'static>>,
pub job_container: jobs::JobContainer,
pub preview_store: Option<Arc<PreviewStore>>,
pub preview_thread_pool: Option<Arc<rayon::ThreadPool>>,
@ -1114,6 +1117,42 @@ pub async fn get_user_entries(
Ok(HttpResponse::Ok().json(result.as_hash().map_err(ErrorInternalServerError)?))
}
#[derive(Serialize)]
pub struct Plugin {
name: String,
info: Option<PluginInfo>,
error: Option<String>,
}
#[get("/api/plugins")]
pub async fn get_plugins(state: web::Data<State>) -> Result<HttpResponse, Error> {
let plugins: Vec<Plugin> = state
.plugins
.clone()
.plugins
.iter()
.map(|p| match p {
crate::plugins::UpEndPlugin::Initialized(p) => Plugin {
name: p.info.name.clone(),
info: Some(p.info.clone()),
error: None,
},
crate::plugins::UpEndPlugin::Failed(p) => Plugin {
name: p
.path
.components()
.last()
.map(|p| p.as_os_str().to_string_lossy().into_owned())
.unwrap_or_else(|| "???".into()),
info: None,
error: Some(p.error.clone()),
},
})
.collect();
Ok(HttpResponse::Ok().json(plugins))
}
#[derive(Debug)]
enum ExternalFetchError {
Status(anyhow::Error),
@ -1383,6 +1422,7 @@ mod tests {
)
.unwrap();
let plugins = Plugins { plugins: vec![] };
State {
upend,
store,
@ -1392,6 +1432,7 @@ mod tests {
trust_executables: false,
secret: "secret".to_string(),
},
plugins: plugins.into(),
job_container,
preview_store: None,
preview_thread_pool: None,

View File

@ -69,7 +69,8 @@ where
.service(routes::get_info)
.service(routes::get_options)
.service(routes::put_options)
.service(routes::get_user_entries);
.service(routes::get_user_entries)
.service(routes::get_plugins);
if ui_enabled {
return app.service(

1
extensions/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

299
extensions/Cargo.lock generated Normal file
View File

@ -0,0 +1,299 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "const_format"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
]
[[package]]
name = "extism-manifest"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22b0e600ec289630715ffdc11aca36a26297c3ab7908f14d5bbf3770d102bce7"
dependencies = [
"base64",
"serde",
]
[[package]]
name = "extism-pdk"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c20fe9cafa572607e22192bf2040849e7456664895bdc589c89387876e2067"
dependencies = [
"anyhow",
"base64",
"extism-manifest",
"extism-pdk-derive",
"rmp-serde",
"serde",
"serde_json",
]
[[package]]
name = "extism-pdk-derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2be216330f7304de051e0faf1578880e9e0dc1ecbd2c0fea5765c63a079d0ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "is_debug"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89"
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "libc"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rmp"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "shadow-rs"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970538704756fd0bb4ec8cb89f80674afb661e7c0fe716f9ba5be57717742300"
dependencies = [
"const_format",
"is_debug",
"time",
]
[[package]]
name = "syn"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa",
"libc",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "upend-extension-base"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "upend-plugin-dummy"
version = "0.0.0"
dependencies = [
"extism-pdk",
"serde",
"shadow-rs",
"upend-extension-base",
]

3
extensions/Cargo.toml Normal file
View File

@ -0,0 +1,3 @@
[workspace]
members = ["base", "dummy"]
resolver = "2"

View File

@ -0,0 +1,10 @@
[package]
name = "upend-extension-base"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }

View File

@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub r#type: PluginType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PluginType {
Extractor,
Transformer,
}

View File

@ -0,0 +1,18 @@
[package]
edition = "2021"
name = "upend-plugin-dummy"
version = "0.0.0"
build = "build.rs"
[lib]
path = "src/lib.rs"
crate_type = ["cdylib"]
[dependencies]
extism-pdk = "1.1.0"
serde = "1.0.181"
upend-extension-base = { path = "../base", version = "0.1.0" }
shadow-rs = { version = "0.23", default-features = false }
[build-dependencies]
shadow-rs = { version = "0.23", default-features = false }

View File

@ -0,0 +1,3 @@
fn main() -> shadow_rs::SdResult<()> {
shadow_rs::new()
}

View File

@ -0,0 +1,14 @@
use extism_pdk::*;
use shadow_rs::shadow;
use upend_extension_base::{PluginInfo, PluginType};
shadow!(build);
#[plugin_fn]
pub fn info(_arg: ()) -> FnResult<Json<PluginInfo>> {
Ok(Json(PluginInfo {
name: "Text".to_string(),
version: build::PKG_VERSION.to_string(),
r#type: PluginType::Transformer,
}))
}