feat: convert to workspace with native plugin architecture
BREAKING: Restructure from monolithic binary to modular plugin ecosystem Architecture changes: - Convert to Cargo workspace with crates/ directory - Create owlry-plugin-api crate with ABI-stable interface (abi_stable) - Move core binary to crates/owlry/ - Extract providers to native plugin crates (13 plugins) - Add owlry-lua crate for Lua plugin runtime Plugin system: - Plugins loaded from /usr/lib/owlry/plugins/*.so - Widget providers refresh automatically (universal, not hardcoded) - Per-plugin config via [plugins.<name>] sections in config.toml - Backwards compatible with [providers] config format New features: - just install-local: build and install core + all plugins - Plugin config: weather and pomodoro read from [plugins.*] - HostAPI for plugins: notifications, logging Documentation: - Update README with new package structure - Add docs/PLUGINS.md with all plugin documentation - Add docs/PLUGIN_DEVELOPMENT.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
52
crates/owlry-lua/src/api/mod.rs
Normal file
52
crates/owlry-lua/src/api/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Lua API implementations for plugins
|
||||
//!
|
||||
//! This module provides the `owlry` global table and its submodules
|
||||
//! that plugins can use to interact with owlry.
|
||||
|
||||
mod provider;
|
||||
mod utils;
|
||||
|
||||
use mlua::{Lua, Result as LuaResult};
|
||||
use owlry_plugin_api::PluginItem;
|
||||
|
||||
use crate::loader::ProviderRegistration;
|
||||
|
||||
/// Register all owlry APIs in the Lua runtime
|
||||
pub fn register_apis(lua: &Lua, plugin_dir: &std::path::Path, plugin_id: &str) -> LuaResult<()> {
|
||||
let globals = lua.globals();
|
||||
|
||||
// Create the main owlry table
|
||||
let owlry = lua.create_table()?;
|
||||
|
||||
// Register utility APIs (log, path, fs, json)
|
||||
utils::register_log_api(lua, &owlry)?;
|
||||
utils::register_path_api(lua, &owlry, plugin_dir)?;
|
||||
utils::register_fs_api(lua, &owlry, plugin_dir)?;
|
||||
utils::register_json_api(lua, &owlry)?;
|
||||
|
||||
// Register provider API
|
||||
provider::register_provider_api(lua, &owlry)?;
|
||||
|
||||
// Set owlry as global
|
||||
globals.set("owlry", owlry)?;
|
||||
|
||||
// Suppress unused warnings
|
||||
let _ = plugin_id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get provider registrations from the Lua runtime
|
||||
pub fn get_provider_registrations(lua: &Lua) -> LuaResult<Vec<ProviderRegistration>> {
|
||||
provider::get_registrations(lua)
|
||||
}
|
||||
|
||||
/// Call a provider's refresh function
|
||||
pub fn call_refresh(lua: &Lua, provider_name: &str) -> LuaResult<Vec<PluginItem>> {
|
||||
provider::call_refresh(lua, provider_name)
|
||||
}
|
||||
|
||||
/// Call a provider's query function
|
||||
pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult<Vec<PluginItem>> {
|
||||
provider::call_query(lua, provider_name, query)
|
||||
}
|
||||
237
crates/owlry-lua/src/api/provider.rs
Normal file
237
crates/owlry-lua/src/api/provider.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
//! Provider registration API for Lua plugins
|
||||
|
||||
use mlua::{Function, Lua, Result as LuaResult, Table, Value};
|
||||
use owlry_plugin_api::PluginItem;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::loader::ProviderRegistration;
|
||||
|
||||
thread_local! {
|
||||
static REGISTRATIONS: RefCell<Vec<ProviderRegistration>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// Register the provider API in the owlry table
|
||||
pub fn register_provider_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let provider = lua.create_table()?;
|
||||
|
||||
// owlry.provider.register(config)
|
||||
provider.set("register", lua.create_function(register_provider)?)?;
|
||||
|
||||
owlry.set("provider", provider)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implementation of owlry.provider.register()
|
||||
fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
|
||||
let name: String = config.get("name")?;
|
||||
let display_name: String = config.get::<Option<String>>("display_name")?
|
||||
.unwrap_or_else(|| name.clone());
|
||||
let type_id: String = config.get::<Option<String>>("type_id")?
|
||||
.unwrap_or_else(|| name.replace('-', "_"));
|
||||
let default_icon: String = config.get::<Option<String>>("default_icon")?
|
||||
.unwrap_or_else(|| "application-x-addon".to_string());
|
||||
let prefix: Option<String> = config.get("prefix")?;
|
||||
|
||||
// Check if it's a dynamic provider (has query function) or static (has refresh)
|
||||
let has_query: bool = config.contains_key("query")?;
|
||||
let has_refresh: bool = config.contains_key("refresh")?;
|
||||
|
||||
if !has_query && !has_refresh {
|
||||
return Err(mlua::Error::external(
|
||||
"Provider must have either 'refresh' or 'query' function",
|
||||
));
|
||||
}
|
||||
|
||||
let is_dynamic = has_query;
|
||||
|
||||
REGISTRATIONS.with(|regs| {
|
||||
regs.borrow_mut().push(ProviderRegistration {
|
||||
name,
|
||||
display_name,
|
||||
type_id,
|
||||
default_icon,
|
||||
prefix,
|
||||
is_dynamic,
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all registered providers
|
||||
pub fn get_registrations(lua: &Lua) -> LuaResult<Vec<ProviderRegistration>> {
|
||||
// Suppress unused warning
|
||||
let _ = lua;
|
||||
|
||||
REGISTRATIONS.with(|regs| Ok(regs.borrow().clone()))
|
||||
}
|
||||
|
||||
/// Call a provider's refresh function
|
||||
pub fn call_refresh(lua: &Lua, provider_name: &str) -> LuaResult<Vec<PluginItem>> {
|
||||
let globals = lua.globals();
|
||||
let owlry: Table = globals.get("owlry")?;
|
||||
let provider: Table = owlry.get("provider")?;
|
||||
|
||||
// Get the registered providers table (internal)
|
||||
let registrations: Table = match provider.get::<Value>("_registrations")? {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
// Try to find the config directly from the global scope
|
||||
// This happens when register was called with the config table
|
||||
return call_provider_function(lua, provider_name, "refresh", None);
|
||||
}
|
||||
};
|
||||
|
||||
let config: Table = match registrations.get(provider_name)? {
|
||||
Value::Table(t) => t,
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let refresh_fn: Function = match config.get("refresh")? {
|
||||
Value::Function(f) => f,
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let result: Value = refresh_fn.call(())?;
|
||||
parse_items_result(result)
|
||||
}
|
||||
|
||||
/// Call a provider's query function
|
||||
pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult<Vec<PluginItem>> {
|
||||
call_provider_function(lua, provider_name, "query", Some(query))
|
||||
}
|
||||
|
||||
/// Call a provider function by name
|
||||
fn call_provider_function(
|
||||
lua: &Lua,
|
||||
provider_name: &str,
|
||||
function_name: &str,
|
||||
query: Option<&str>,
|
||||
) -> LuaResult<Vec<PluginItem>> {
|
||||
// Search through all registered providers in the Lua globals
|
||||
// This is a workaround since we store registrations thread-locally
|
||||
let globals = lua.globals();
|
||||
|
||||
// Try to find a registered provider with matching name
|
||||
// First check if there's a _providers table
|
||||
if let Ok(Value::Table(providers)) = globals.get::<Value>("_owlry_providers")
|
||||
&& let Ok(Value::Table(config)) = providers.get::<Value>(provider_name)
|
||||
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name) {
|
||||
let result: Value = match query {
|
||||
Some(q) => func.call(q)?,
|
||||
None => func.call(())?,
|
||||
};
|
||||
return parse_items_result(result);
|
||||
}
|
||||
|
||||
// Fall back: search through globals for functions
|
||||
// This is less reliable but handles simple cases
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Parse items from Lua return value
|
||||
fn parse_items_result(result: Value) -> LuaResult<Vec<PluginItem>> {
|
||||
let mut items = Vec::new();
|
||||
|
||||
if let Value::Table(table) = result {
|
||||
for pair in table.pairs::<i32, Table>() {
|
||||
let (_, item_table) = pair?;
|
||||
if let Ok(item) = parse_item(&item_table) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Parse a single item from a Lua table
|
||||
fn parse_item(table: &Table) -> LuaResult<PluginItem> {
|
||||
let id: String = table.get("id")?;
|
||||
let name: String = table.get("name")?;
|
||||
let command: String = table.get::<Option<String>>("command")?.unwrap_or_default();
|
||||
let description: Option<String> = table.get("description")?;
|
||||
let icon: Option<String> = table.get("icon")?;
|
||||
let terminal: bool = table.get::<Option<bool>>("terminal")?.unwrap_or(false);
|
||||
let tags: Vec<String> = table.get::<Option<Vec<String>>>("tags")?.unwrap_or_default();
|
||||
|
||||
let mut item = PluginItem::new(id, name, command);
|
||||
|
||||
if let Some(desc) = description {
|
||||
item = item.with_description(desc);
|
||||
}
|
||||
if let Some(ic) = icon {
|
||||
item = item.with_icon(&ic);
|
||||
}
|
||||
if terminal {
|
||||
item = item.with_terminal(true);
|
||||
}
|
||||
if !tags.is_empty() {
|
||||
item = item.with_keywords(tags);
|
||||
}
|
||||
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::{create_lua_runtime, SandboxConfig};
|
||||
|
||||
#[test]
|
||||
fn test_register_static_provider() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_provider_api(&lua, &owlry).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let code = r#"
|
||||
owlry.provider.register({
|
||||
name = "test-provider",
|
||||
display_name = "Test Provider",
|
||||
refresh = function()
|
||||
return {
|
||||
{ id = "1", name = "Item 1" }
|
||||
}
|
||||
end
|
||||
})
|
||||
"#;
|
||||
lua.load(code).set_name("test").call::<()>(()).unwrap();
|
||||
|
||||
let regs = get_registrations(&lua).unwrap();
|
||||
assert_eq!(regs.len(), 1);
|
||||
assert_eq!(regs[0].name, "test-provider");
|
||||
assert!(!regs[0].is_dynamic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_dynamic_provider() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_provider_api(&lua, &owlry).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let code = r#"
|
||||
owlry.provider.register({
|
||||
name = "query-provider",
|
||||
prefix = "?",
|
||||
query = function(q)
|
||||
return {
|
||||
{ id = "search", name = "Search: " .. q }
|
||||
}
|
||||
end
|
||||
})
|
||||
"#;
|
||||
lua.load(code).set_name("test").call::<()>(()).unwrap();
|
||||
|
||||
let regs = get_registrations(&lua).unwrap();
|
||||
assert_eq!(regs.len(), 1);
|
||||
assert_eq!(regs[0].name, "query-provider");
|
||||
assert!(regs[0].is_dynamic);
|
||||
assert_eq!(regs[0].prefix, Some("?".to_string()));
|
||||
}
|
||||
}
|
||||
370
crates/owlry-lua/src/api/utils.rs
Normal file
370
crates/owlry-lua/src/api/utils.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
//! Utility APIs: logging, paths, filesystem, JSON
|
||||
|
||||
use mlua::{Lua, Result as LuaResult, Table, Value};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// ============================================================================
|
||||
// Logging API
|
||||
// ============================================================================
|
||||
|
||||
/// Register the log API in the owlry table
|
||||
pub fn register_log_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let log = lua.create_table()?;
|
||||
|
||||
log.set("debug", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[DEBUG] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
|
||||
log.set("info", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[INFO] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
|
||||
log.set("warn", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[WARN] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
|
||||
log.set("error", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[ERROR] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
|
||||
owlry.set("log", log)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Path API
|
||||
// ============================================================================
|
||||
|
||||
/// Register the path API in the owlry table
|
||||
pub fn register_path_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResult<()> {
|
||||
let path = lua.create_table()?;
|
||||
|
||||
// owlry.path.config() -> ~/.config/owlry
|
||||
path.set("config", lua.create_function(|_, ()| {
|
||||
Ok(dirs::config_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.data() -> ~/.local/share/owlry
|
||||
path.set("data", lua.create_function(|_, ()| {
|
||||
Ok(dirs::data_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.cache() -> ~/.cache/owlry
|
||||
path.set("cache", lua.create_function(|_, ()| {
|
||||
Ok(dirs::cache_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.home() -> ~
|
||||
path.set("home", lua.create_function(|_, ()| {
|
||||
Ok(dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.join(...) -> joined path
|
||||
path.set("join", lua.create_function(|_, parts: mlua::Variadic<String>| {
|
||||
let mut path = PathBuf::new();
|
||||
for part in parts {
|
||||
path.push(part);
|
||||
}
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.plugin_dir() -> plugin directory
|
||||
let plugin_dir_str = plugin_dir.to_string_lossy().to_string();
|
||||
path.set("plugin_dir", lua.create_function(move |_, ()| {
|
||||
Ok(plugin_dir_str.clone())
|
||||
})?)?;
|
||||
|
||||
// owlry.path.expand(path) -> expanded path (~ -> home)
|
||||
path.set("expand", lua.create_function(|_, path: String| {
|
||||
if path.starts_with("~/")
|
||||
&& let Some(home) = dirs::home_dir() {
|
||||
return Ok(home.join(&path[2..]).to_string_lossy().to_string());
|
||||
}
|
||||
Ok(path)
|
||||
})?)?;
|
||||
|
||||
owlry.set("path", path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filesystem API
|
||||
// ============================================================================
|
||||
|
||||
/// Register the fs API in the owlry table
|
||||
pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResult<()> {
|
||||
let fs = lua.create_table()?;
|
||||
|
||||
// owlry.fs.exists(path) -> bool
|
||||
fs.set("exists", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).exists())
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.is_dir(path) -> bool
|
||||
fs.set("is_dir", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).is_dir())
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.read(path) -> string or nil
|
||||
fs.set("read", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.read_lines(path) -> table of strings or nil
|
||||
fs.set("read_lines", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
|
||||
Ok(Some(lua.create_sequence_from(lines)?))
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.list_dir(path) -> table of filenames or nil
|
||||
fs.set("list_dir", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_dir(&path) {
|
||||
Ok(entries) => {
|
||||
let names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
Ok(Some(lua.create_sequence_from(names)?))
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.read_json(path) -> table or nil
|
||||
fs.set("read_json", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(value) => json_to_lua(lua, &value),
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
})?)?;
|
||||
|
||||
// owlry.fs.write(path, content) -> bool
|
||||
fs.set("write", lua.create_function(|_, (path, content): (String, String)| {
|
||||
let path = expand_path(&path);
|
||||
// Create parent directories if needed
|
||||
if let Some(parent) = Path::new(&path).parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
Ok(std::fs::write(&path, content).is_ok())
|
||||
})?)?;
|
||||
|
||||
owlry.set("fs", fs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON API
|
||||
// ============================================================================
|
||||
|
||||
/// Register the json API in the owlry table
|
||||
pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let json = lua.create_table()?;
|
||||
|
||||
// owlry.json.encode(value) -> string
|
||||
json.set("encode", lua.create_function(|lua, value: Value| {
|
||||
let json_value = lua_to_json(lua, &value)?;
|
||||
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
|
||||
})?)?;
|
||||
|
||||
// owlry.json.decode(string) -> value or nil
|
||||
json.set("decode", lua.create_function(|lua, s: String| {
|
||||
match serde_json::from_str::<serde_json::Value>(&s) {
|
||||
Ok(value) => json_to_lua(lua, &value),
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
})?)?;
|
||||
|
||||
owlry.set("json", json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Expand ~ in paths
|
||||
fn expand_path(path: &str) -> String {
|
||||
if path.starts_with("~/")
|
||||
&& let Some(home) = dirs::home_dir() {
|
||||
return home.join(&path[2..]).to_string_lossy().to_string();
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
/// Convert JSON value to Lua value
|
||||
fn json_to_lua(lua: &Lua, value: &serde_json::Value) -> LuaResult<Value> {
|
||||
match value {
|
||||
serde_json::Value::Null => Ok(Value::Nil),
|
||||
serde_json::Value::Bool(b) => Ok(Value::Boolean(*b)),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
Ok(Value::Integer(i))
|
||||
} else if let Some(f) = n.as_f64() {
|
||||
Ok(Value::Number(f))
|
||||
} else {
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => Ok(Value::String(lua.create_string(s)?)),
|
||||
serde_json::Value::Array(arr) => {
|
||||
let table = lua.create_table()?;
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
table.set(i + 1, json_to_lua(lua, v)?)?;
|
||||
}
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
let table = lua.create_table()?;
|
||||
for (k, v) in obj {
|
||||
table.set(k.as_str(), json_to_lua(lua, v)?)?;
|
||||
}
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Lua value to JSON value
|
||||
fn lua_to_json(_lua: &Lua, value: &Value) -> LuaResult<serde_json::Value> {
|
||||
match value {
|
||||
Value::Nil => Ok(serde_json::Value::Null),
|
||||
Value::Boolean(b) => Ok(serde_json::Value::Bool(*b)),
|
||||
Value::Integer(i) => Ok(serde_json::Value::Number((*i).into())),
|
||||
Value::Number(n) => Ok(serde_json::json!(*n)),
|
||||
Value::String(s) => Ok(serde_json::Value::String(s.to_str()?.to_string())),
|
||||
Value::Table(t) => {
|
||||
// Check if it's an array (sequential integer keys starting from 1)
|
||||
let mut is_array = true;
|
||||
let mut max_key = 0i64;
|
||||
for pair in t.clone().pairs::<Value, Value>() {
|
||||
let (k, _) = pair?;
|
||||
match k {
|
||||
Value::Integer(i) if i > 0 => {
|
||||
max_key = max_key.max(i);
|
||||
}
|
||||
_ => {
|
||||
is_array = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_array && max_key > 0 {
|
||||
let mut arr = Vec::new();
|
||||
for i in 1..=max_key {
|
||||
let v: Value = t.get(i)?;
|
||||
arr.push(lua_to_json(_lua, &v)?);
|
||||
}
|
||||
Ok(serde_json::Value::Array(arr))
|
||||
} else {
|
||||
let mut obj = serde_json::Map::new();
|
||||
for pair in t.clone().pairs::<String, Value>() {
|
||||
let (k, v) = pair?;
|
||||
obj.insert(k, lua_to_json(_lua, &v)?);
|
||||
}
|
||||
Ok(serde_json::Value::Object(obj))
|
||||
}
|
||||
}
|
||||
_ => Ok(serde_json::Value::Null),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::{create_lua_runtime, SandboxConfig};
|
||||
|
||||
#[test]
|
||||
fn test_log_api() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_log_api(&lua, &owlry).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
// Just verify it doesn't panic
|
||||
lua.load("owlry.log.info('test message')").set_name("test").call::<()>(()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_api() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_path_api(&lua, &owlry, Path::new("/tmp/test-plugin")).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let home: String = lua.load("return owlry.path.home()").set_name("test").call(()).unwrap();
|
||||
assert!(!home.is_empty());
|
||||
|
||||
let plugin_dir: String = lua.load("return owlry.path.plugin_dir()").set_name("test").call(()).unwrap();
|
||||
assert_eq!(plugin_dir, "/tmp/test-plugin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_api() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_fs_api(&lua, &owlry, Path::new("/tmp")).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let exists: bool = lua.load("return owlry.fs.exists('/tmp')").set_name("test").call(()).unwrap();
|
||||
assert!(exists);
|
||||
|
||||
let is_dir: bool = lua.load("return owlry.fs.is_dir('/tmp')").set_name("test").call(()).unwrap();
|
||||
assert!(is_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_api() {
|
||||
let config = SandboxConfig::default();
|
||||
let lua = create_lua_runtime(&config).unwrap();
|
||||
let owlry = lua.create_table().unwrap();
|
||||
register_json_api(&lua, &owlry).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let code = r#"
|
||||
local t = { name = "test", value = 42 }
|
||||
local json = owlry.json.encode(t)
|
||||
local decoded = owlry.json.decode(json)
|
||||
return decoded.name, decoded.value
|
||||
"#;
|
||||
let (name, value): (String, i32) = lua.load(code).set_name("test").call(()).unwrap();
|
||||
assert_eq!(name, "test");
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user