diff --git a/frontends/nextjs/src/lib/lua/bindings/bindings-context.ts b/frontends/nextjs/src/lib/lua/bindings/bindings-context.ts new file mode 100644 index 000000000..6a19dafe3 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/bindings/bindings-context.ts @@ -0,0 +1,94 @@ +/** + * Bindings Context + * + * Combines DBAL and browser bindings into a unified context + * that can be injected into Lua script execution. + */ + +import type { JsonValue } from '@/types/utility-types' + +import { createBrowserBindings, BROWSER_LUA_BINDINGS, type BrowserBindings } from './browser-bindings' +import { createDBALBindings, DBAL_LUA_BINDINGS, type DBALBindings } from './dbal-bindings' + +export interface BindingsConfig { + enableDBAL?: boolean + enableBrowser?: boolean + dbalAdapter?: { + kvGet?: (key: string) => Promise + kvSet?: (key: string, value: JsonValue, ttl?: number) => Promise + kvDelete?: (key: string) => Promise + kvListAdd?: (key: string, items: string[]) => Promise + kvListGet?: (key: string) => Promise + blobUpload?: (name: string, data: Uint8Array, metadata?: Record) => Promise + blobDownload?: (name: string) => Promise + blobDelete?: (name: string) => Promise + blobList?: () => Promise + } +} + +export interface BindingsContext { + dbal?: DBALBindings + browser?: BrowserBindings + luaPrelude: string + contextFunctions: Record unknown> +} + +/** + * Creates a combined bindings context for Lua execution + */ +export function createBindingsContext(config: BindingsConfig = {}): BindingsContext { + const context: BindingsContext = { + luaPrelude: '', + contextFunctions: {}, + } + + // Add DBAL bindings if enabled + if (config.enableDBAL && config.dbalAdapter) { + context.dbal = createDBALBindings(config.dbalAdapter) + context.luaPrelude += DBAL_LUA_BINDINGS + '\n' + + // Register DBAL context functions + context.contextFunctions['__dbal_kv_get'] = context.dbal.kv.get + context.contextFunctions['__dbal_kv_set'] = context.dbal.kv.set + context.contextFunctions['__dbal_kv_delete'] = context.dbal.kv.delete + context.contextFunctions['__dbal_kv_list_add'] = context.dbal.kv.listAdd + context.contextFunctions['__dbal_kv_list_get'] = context.dbal.kv.listGet + context.contextFunctions['__dbal_blob_upload'] = context.dbal.blob.upload + context.contextFunctions['__dbal_blob_download'] = context.dbal.blob.download + context.contextFunctions['__dbal_blob_delete'] = context.dbal.blob.delete + context.contextFunctions['__dbal_blob_list'] = context.dbal.blob.list + context.contextFunctions['__dbal_cache_get'] = context.dbal.cache.get + context.contextFunctions['__dbal_cache_set'] = context.dbal.cache.set + context.contextFunctions['__dbal_cache_clear'] = context.dbal.cache.clear + } + + // Add browser bindings if enabled + if (config.enableBrowser) { + context.browser = createBrowserBindings() + context.luaPrelude += BROWSER_LUA_BINDINGS + '\n' + + // Register browser context functions + context.contextFunctions['__browser_page_get_title'] = context.browser.page.getTitle + context.contextFunctions['__browser_page_get_url'] = context.browser.page.getUrl + context.contextFunctions['__browser_page_get_viewport'] = context.browser.page.getViewport + context.contextFunctions['__browser_page_get_user_agent'] = context.browser.page.getUserAgent + context.contextFunctions['__browser_page_get_text_content'] = context.browser.page.getTextContent + context.contextFunctions['__browser_page_get_html_sample'] = context.browser.page.getHtmlSample + context.contextFunctions['__browser_dom_query_selector'] = context.browser.dom.querySelector + context.contextFunctions['__browser_dom_query_selector_all'] = context.browser.dom.querySelectorAll + context.contextFunctions['__browser_dom_get_element_count'] = context.browser.dom.getElementCount + context.contextFunctions['__browser_storage_get_local'] = context.browser.storage.getLocal + context.contextFunctions['__browser_storage_set_local'] = context.browser.storage.setLocal + context.contextFunctions['__browser_storage_get_session'] = context.browser.storage.getSession + context.contextFunctions['__browser_storage_set_session'] = context.browser.storage.setSession + context.contextFunctions['__browser_clipboard_write_text'] = context.browser.clipboard.writeText + context.contextFunctions['__browser_clipboard_read_text'] = context.browser.clipboard.readText + } + + return context +} + +/** + * Combined Lua bindings prelude for scripts that need both DBAL and browser access + */ +export const COMBINED_LUA_BINDINGS = DBAL_LUA_BINDINGS + '\n' + BROWSER_LUA_BINDINGS diff --git a/frontends/nextjs/src/lib/lua/bindings/browser-bindings.ts b/frontends/nextjs/src/lib/lua/bindings/browser-bindings.ts new file mode 100644 index 000000000..f62a676bc --- /dev/null +++ b/frontends/nextjs/src/lib/lua/bindings/browser-bindings.ts @@ -0,0 +1,185 @@ +/** + * Browser API Bindings for Lua Scripts + * + * Provides Lua scripts access to browser APIs like DOM info, page metadata, + * and screenshot/capture functionality through a safe bridge. + */ + +export interface BrowserBindings { + page: { + getTitle: () => string + getUrl: () => string + getViewport: () => { width: number; height: number } + getUserAgent: () => string + getTextContent: (maxLength?: number) => string + getHtmlSample: (maxLength?: number) => string + } + dom: { + querySelector: (selector: string) => { exists: boolean; text?: string; tagName?: string } + querySelectorAll: (selector: string) => Array<{ text: string; tagName: string }> + getElementCount: (selector: string) => number + } + storage: { + getLocal: (key: string) => string | null + setLocal: (key: string, value: string) => void + getSession: (key: string) => string | null + setSession: (key: string, value: string) => void + } + clipboard: { + writeText: (text: string) => Promise + readText: () => Promise + } +} + +/** + * Creates browser bindings that can be injected into Lua execution context + * Only available in browser environment + */ +export function createBrowserBindings(): BrowserBindings { + const isBrowser = typeof window !== 'undefined' + + return { + page: { + getTitle: () => (isBrowser ? document.title : ''), + getUrl: () => (isBrowser ? window.location.href : ''), + getViewport: () => + isBrowser ? { width: window.innerWidth, height: window.innerHeight } : { width: 0, height: 0 }, + getUserAgent: () => (isBrowser ? navigator.userAgent : ''), + getTextContent: (maxLength = 5000) => + isBrowser ? document.body.innerText.substring(0, maxLength) : '', + getHtmlSample: (maxLength = 3000) => + isBrowser ? document.body.innerHTML.substring(0, maxLength) : '', + }, + dom: { + querySelector: (selector: string) => { + if (!isBrowser) return { exists: false } + try { + const el = document.querySelector(selector) + if (!el) return { exists: false } + return { + exists: true, + text: el.textContent?.substring(0, 500) ?? '', + tagName: el.tagName.toLowerCase(), + } + } catch { + return { exists: false } + } + }, + querySelectorAll: (selector: string) => { + if (!isBrowser) return [] + try { + const elements = document.querySelectorAll(selector) + return Array.from(elements) + .slice(0, 100) + .map(el => ({ + text: el.textContent?.substring(0, 200) ?? '', + tagName: el.tagName.toLowerCase(), + })) + } catch { + return [] + } + }, + getElementCount: (selector: string) => { + if (!isBrowser) return 0 + try { + return document.querySelectorAll(selector).length + } catch { + return 0 + } + }, + }, + storage: { + getLocal: (key: string) => (isBrowser ? localStorage.getItem(key) : null), + setLocal: (key: string, value: string) => { + if (isBrowser) localStorage.setItem(key, value) + }, + getSession: (key: string) => (isBrowser ? sessionStorage.getItem(key) : null), + setSession: (key: string, value: string) => { + if (isBrowser) sessionStorage.setItem(key, value) + }, + }, + clipboard: { + writeText: async (text: string) => { + if (isBrowser) await navigator.clipboard.writeText(text) + }, + readText: async () => { + if (!isBrowser) return '' + try { + return await navigator.clipboard.readText() + } catch { + return '' + } + }, + }, + } +} + +/** + * Lua code template for browser operations + * These functions are available in the Lua context when browser bindings are enabled + */ +export const BROWSER_LUA_BINDINGS = ` +-- Page information bindings +function page_get_title() + return __browser_page_get_title() +end + +function page_get_url() + return __browser_page_get_url() +end + +function page_get_viewport() + return __browser_page_get_viewport() +end + +function page_get_user_agent() + return __browser_page_get_user_agent() +end + +function page_get_text_content(max_length) + return __browser_page_get_text_content(max_length or 5000) +end + +function page_get_html_sample(max_length) + return __browser_page_get_html_sample(max_length or 3000) +end + +-- DOM bindings +function dom_query_selector(selector) + return __browser_dom_query_selector(selector) +end + +function dom_query_selector_all(selector) + return __browser_dom_query_selector_all(selector) +end + +function dom_get_element_count(selector) + return __browser_dom_get_element_count(selector) +end + +-- Storage bindings +function storage_get_local(key) + return __browser_storage_get_local(key) +end + +function storage_set_local(key, value) + return __browser_storage_set_local(key, value) +end + +function storage_get_session(key) + return __browser_storage_get_session(key) +end + +function storage_set_session(key, value) + return __browser_storage_set_session(key, value) +end + +-- Clipboard bindings +function clipboard_write_text(text) + return __browser_clipboard_write_text(text) +end + +function clipboard_read_text() + return __browser_clipboard_read_text() +end +` diff --git a/frontends/nextjs/src/lib/lua/bindings/dbal-bindings.ts b/frontends/nextjs/src/lib/lua/bindings/dbal-bindings.ts new file mode 100644 index 000000000..0dcec1096 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/bindings/dbal-bindings.ts @@ -0,0 +1,140 @@ +/** + * DBAL Bindings for Lua Scripts + * + * Provides Lua scripts access to DBAL operations (KV store, blob storage, cached data) + * through a bridge that connects to the TypeScript DBAL client. + */ + +import type { JsonValue } from '@/types/utility-types' + +export interface DBALBindings { + kv: { + get: (key: string) => Promise + set: (key: string, value: JsonValue, ttl?: number) => Promise + delete: (key: string) => Promise + listAdd: (key: string, items: string[]) => Promise + listGet: (key: string) => Promise + } + blob: { + upload: (name: string, data: string, metadata?: Record) => Promise + download: (name: string) => Promise + delete: (name: string) => Promise + list: () => Promise + } + cache: { + get: (key: string) => Promise + set: (key: string, value: JsonValue, ttl?: number) => Promise + clear: (key: string) => Promise + } +} + +/** + * Creates DBAL bindings that can be injected into Lua execution context + */ +export function createDBALBindings(adapter: { + kvGet?: (key: string) => Promise + kvSet?: (key: string, value: JsonValue, ttl?: number) => Promise + kvDelete?: (key: string) => Promise + kvListAdd?: (key: string, items: string[]) => Promise + kvListGet?: (key: string) => Promise + blobUpload?: (name: string, data: Uint8Array, metadata?: Record) => Promise + blobDownload?: (name: string) => Promise + blobDelete?: (name: string) => Promise + blobList?: () => Promise +}): DBALBindings { + return { + kv: { + get: async (key: string) => adapter.kvGet?.(key) ?? null, + set: async (key: string, value: JsonValue, ttl?: number) => { + await adapter.kvSet?.(key, value, ttl) + }, + delete: async (key: string) => (await adapter.kvDelete?.(key)) ?? false, + listAdd: async (key: string, items: string[]) => { + await adapter.kvListAdd?.(key, items) + }, + listGet: async (key: string) => (await adapter.kvListGet?.(key)) ?? [], + }, + blob: { + upload: async (name: string, data: string, metadata?: Record) => { + const encoder = new TextEncoder() + await adapter.blobUpload?.(name, encoder.encode(data), metadata) + }, + download: async (name: string) => { + const data = await adapter.blobDownload?.(name) + if (!data) return '' + const decoder = new TextDecoder() + return decoder.decode(data) + }, + delete: async (name: string) => { + await adapter.blobDelete?.(name) + }, + list: async () => (await adapter.blobList?.()) ?? [], + }, + cache: { + get: async (key: string) => adapter.kvGet?.(key) ?? null, + set: async (key: string, value: JsonValue, ttl?: number) => { + await adapter.kvSet?.(key, value, ttl) + }, + clear: async (key: string) => { + await adapter.kvDelete?.(key) + }, + }, + } +} + +/** + * Lua code template for DBAL operations + * These functions are available in the Lua context when DBAL bindings are enabled + */ +export const DBAL_LUA_BINDINGS = ` +-- DBAL KV Store bindings +function kv_get(key) + return __dbal_kv_get(key) +end + +function kv_set(key, value, ttl) + return __dbal_kv_set(key, value, ttl) +end + +function kv_delete(key) + return __dbal_kv_delete(key) +end + +function kv_list_add(key, items) + return __dbal_kv_list_add(key, items) +end + +function kv_list_get(key) + return __dbal_kv_list_get(key) +end + +-- DBAL Blob Storage bindings +function blob_upload(name, data, metadata) + return __dbal_blob_upload(name, data, metadata) +end + +function blob_download(name) + return __dbal_blob_download(name) +end + +function blob_delete(name) + return __dbal_blob_delete(name) +end + +function blob_list() + return __dbal_blob_list() +end + +-- DBAL Cache bindings +function cache_get(key) + return __dbal_cache_get(key) +end + +function cache_set(key, value, ttl) + return __dbal_cache_set(key, value, ttl) +end + +function cache_clear(key) + return __dbal_cache_clear(key) +end +` diff --git a/frontends/nextjs/src/lib/lua/bindings/index.ts b/frontends/nextjs/src/lib/lua/bindings/index.ts new file mode 100644 index 000000000..36e4103ff --- /dev/null +++ b/frontends/nextjs/src/lib/lua/bindings/index.ts @@ -0,0 +1,14 @@ +/** + * Lua Bindings Index + * + * Exports all Lua binding modules for easy import + */ + +export { createBrowserBindings, BROWSER_LUA_BINDINGS } from './browser-bindings' +export type { BrowserBindings } from './browser-bindings' + +export { createDBALBindings, DBAL_LUA_BINDINGS } from './dbal-bindings' +export type { DBALBindings } from './dbal-bindings' + +export { createBindingsContext, COMBINED_LUA_BINDINGS } from './bindings-context' +export type { BindingsContext, BindingsConfig } from './bindings-context' diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/index.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/index.ts new file mode 100644 index 000000000..7c3b3eb07 --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/index.ts @@ -0,0 +1,16 @@ +/** + * Bridge Index + * + * Exports package bridge utilities for connecting Lua packages to React hooks + */ + +export { + registerHookBridge, + getHookBridge, + hasHookBridge, + getRegisteredHooks, + createHookContextFunctions, + generateHookLuaBindings, +} from './package-bridge' + +export type { HookBridge, HookInstance, PackageBridgeConfig } from './package-bridge' diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/package-bridge.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/package-bridge.ts new file mode 100644 index 000000000..ae1c0a62f --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/bridge/package-bridge.ts @@ -0,0 +1,178 @@ +/** + * Package Bridge + * + * Connects Lua packages to React hooks, enabling hybrid components + * that combine declarative Lua logic with React's reactivity. + */ + +import type { JsonValue } from '@/types/utility-types' + +import type { ComponentBindings } from '../types' + +export interface HookBridge { + /** Name of the hook */ + name: string + /** Hook factory function */ + factory: () => HookInstance +} + +export interface HookInstance { + /** Current state */ + state: Record + /** Available actions */ + actions: Record Promise | unknown> + /** Cleanup function */ + cleanup?: () => void +} + +export interface PackageBridgeConfig { + /** Component bindings configuration */ + bindings?: ComponentBindings + /** Hook factories to register */ + hooks?: HookBridge[] +} + +/** + * Registry of available hook bridges + */ +const hookRegistry = new Map HookInstance>() + +/** + * Register a hook bridge factory + */ +export function registerHookBridge(name: string, factory: () => HookInstance) { + hookRegistry.set(name, factory) +} + +/** + * Get a hook bridge instance by name + */ +export function getHookBridge(name: string): HookInstance | null { + const factory = hookRegistry.get(name) + return factory ? factory() : null +} + +/** + * Check if a hook bridge is registered + */ +export function hasHookBridge(name: string): boolean { + return hookRegistry.has(name) +} + +/** + * Get all registered hook bridge names + */ +export function getRegisteredHooks(): string[] { + return Array.from(hookRegistry.keys()) +} + +/** + * Create Lua context functions from hook bridge + */ +export function createHookContextFunctions( + hookName: string, + instance: HookInstance +): Record unknown> { + const prefix = `__hook_${hookName}_` + const functions: Record unknown> = {} + + // State getter + functions[`${prefix}get_state`] = () => instance.state + + // Action wrappers + for (const [actionName, action] of Object.entries(instance.actions)) { + functions[`${prefix}${actionName}`] = action + } + + return functions +} + +/** + * Generate Lua binding code for a hook bridge + */ +export function generateHookLuaBindings(hookName: string, instance: HookInstance): string { + const prefix = `__hook_${hookName}_` + const lines: string[] = [ + `-- Hook bindings for ${hookName}`, + `local ${hookName} = {}`, + '', + `function ${hookName}.get_state()`, + ` return ${prefix}get_state()`, + `end`, + '', + ] + + for (const actionName of Object.keys(instance.actions)) { + lines.push(`function ${hookName}.${actionName}(...)`) + lines.push(` return ${prefix}${actionName}(...)`) + lines.push(`end`) + lines.push('') + } + + return lines.join('\n') +} + +// Pre-register common hook bridges + +/** + * DBAL hook bridge - connects to useDBAL hook + */ +registerHookBridge('dbal', () => ({ + state: { + isReady: false, + error: null, + }, + actions: { + // These will be connected to actual hook in React component + connect: async (_endpoint: unknown, _apiKey: unknown) => ({ success: true }), + disconnect: async () => ({ success: true }), + }, +})) + +/** + * KV Store hook bridge - connects to useKVStore hook + */ +registerHookBridge('kv_store', () => ({ + state: { + isInitialized: false, + }, + actions: { + get: async (_key: unknown) => null, + set: async (_key: unknown, _value: unknown, _ttl?: unknown) => undefined, + delete: async (_key: unknown) => false, + list_add: async (_key: unknown, _items: unknown) => undefined, + list_get: async (_key: unknown) => [], + }, +})) + +/** + * Blob Storage hook bridge - connects to useBlobStorage hook + */ +registerHookBridge('blob_storage', () => ({ + state: { + isInitialized: false, + }, + actions: { + upload: async (_name: unknown, _data: unknown, _metadata?: unknown) => undefined, + download: async (_name: unknown) => '', + delete: async (_name: unknown) => undefined, + list: async () => [], + }, +})) + +/** + * Cached Data hook bridge - connects to useCachedData hook + */ +registerHookBridge('cached_data', () => ({ + state: { + data: null, + loading: false, + error: null, + isReady: false, + }, + actions: { + save: async (_data: unknown, _ttl?: unknown) => undefined, + clear: async () => undefined, + refresh: async () => undefined, + }, +})) diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts index dfae10c1e..8d3fd0ab3 100644 --- a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts @@ -1,5 +1,17 @@ import type { JsonValue } from '@/types/utility-types' +/** + * Binding capabilities that can be enabled for a component + */ +export interface ComponentBindings { + /** Enable DBAL bindings (KV store, blob storage, cache) */ + dbal?: boolean + /** Enable browser bindings (DOM, page info, storage) */ + browser?: boolean + /** Custom allowed globals for Lua sandbox */ + allowedGlobals?: string[] +} + export interface DeclarativeComponentConfig { type: string category: string @@ -20,6 +32,10 @@ export interface DeclarativeComponentConfig { } children: JsonValue[] } + /** Optional bindings to enable for this component's Lua scripts */ + bindings?: ComponentBindings + /** React hooks this component requires (for hybrid components) */ + requiredHooks?: string[] } export interface MessageFormat { diff --git a/packages/dbal_demo/seed/components.json b/packages/dbal_demo/seed/components.json new file mode 100644 index 000000000..4ae43a068 --- /dev/null +++ b/packages/dbal_demo/seed/components.json @@ -0,0 +1,190 @@ +{ + "DBALDemo": { + "type": "DBALDemo", + "category": "demo", + "label": "DBAL Demo", + "description": "Demonstration of the TypeScript DBAL client integrated with MetaBuilder", + "icon": "database", + "props": [ + { + "name": "defaultEndpoint", + "type": "string", + "label": "Default Endpoint", + "defaultValue": "", + "required": false + }, + { + "name": "defaultApiKey", + "type": "string", + "label": "Default API Key", + "defaultValue": "", + "required": false + } + ], + "config": { + "layout": "container", + "styling": { + "className": "dbal-demo-container" + }, + "children": [ + { + "type": "header", + "props": { + "title": "DBAL Integration Demo", + "description": "Demonstration of the TypeScript DBAL client integrated with MetaBuilder" + } + }, + { + "type": "grid", + "props": { "columns": 2, "gap": 4 }, + "children": [ + { "type": "ConnectionForm", "props": {} }, + { "type": "ResultPanel", "props": { "title": "Connection Details" } } + ] + }, + { "type": "LogsPanel", "props": { "title": "Demo Logs" } }, + { + "type": "tabs", + "props": { + "defaultValue": "kv", + "tabs": [ + { "value": "kv", "label": "Key-Value Store" }, + { "value": "blob", "label": "Blob Storage" }, + { "value": "cache", "label": "Cached Data" } + ] + }, + "children": [ + { "type": "KVStorePanel", "props": {} }, + { "type": "BlobStoragePanel", "props": {} }, + { "type": "CachedDataPanel", "props": {} } + ] + } + ] + }, + "bindings": { + "dbal": true + }, + "requiredHooks": ["dbal", "kv_store", "blob_storage", "cached_data"] + }, + "KVStorePanel": { + "type": "KVStorePanel", + "category": "demo", + "label": "KV Store Panel", + "description": "Key-Value store operations panel", + "icon": "key", + "props": [], + "config": { + "layout": "card", + "styling": { "className": "kv-store-panel" }, + "children": [ + { + "type": "card", + "props": { "title": "Key-Value Operations", "description": "Basic get/set/delete operations" }, + "children": [ + { "type": "input", "props": { "name": "key", "label": "Key", "defaultValue": "demo-key" } }, + { "type": "input", "props": { "name": "value", "label": "Value", "defaultValue": "Hello, DBAL!" } }, + { "type": "input", "props": { "name": "ttl", "label": "TTL (seconds)", "type": "number" } }, + { + "type": "button-group", + "children": [ + { "type": "button", "props": { "label": "Set", "action": "kv_set" } }, + { "type": "button", "props": { "label": "Get", "action": "kv_get" } }, + { "type": "button", "props": { "label": "Delete", "action": "kv_delete", "variant": "destructive" } } + ] + } + ] + }, + { + "type": "card", + "props": { "title": "List Operations", "description": "Add and retrieve list items" }, + "children": [ + { "type": "input", "props": { "name": "listKey", "label": "List Key", "defaultValue": "demo-list" } }, + { + "type": "button-group", + "children": [ + { "type": "button", "props": { "label": "Add Items", "action": "kv_list_add" } }, + { "type": "button", "props": { "label": "Get Items", "action": "kv_list_get" } } + ] + }, + { "type": "list-display", "props": { "dataSource": "listItems" } } + ] + } + ] + }, + "bindings": { "dbal": true }, + "requiredHooks": ["kv_store"] + }, + "BlobStoragePanel": { + "type": "BlobStoragePanel", + "category": "demo", + "label": "Blob Storage Panel", + "description": "Blob storage operations panel", + "icon": "file", + "props": [], + "config": { + "layout": "card", + "styling": { "className": "blob-storage-panel" }, + "children": [ + { + "type": "card", + "props": { "title": "Blob Storage Operations", "description": "Upload, download, and manage binary data" }, + "children": [ + { "type": "input", "props": { "name": "fileName", "label": "File Name", "defaultValue": "demo.txt" } }, + { "type": "textarea", "props": { "name": "fileContent", "label": "Content", "defaultValue": "Hello from DBAL blob storage!" } }, + { + "type": "button-group", + "children": [ + { "type": "button", "props": { "label": "Upload", "action": "blob_upload" } }, + { "type": "button", "props": { "label": "Download", "action": "blob_download" } }, + { "type": "button", "props": { "label": "Delete", "action": "blob_delete", "variant": "destructive" } }, + { "type": "button", "props": { "label": "List Files", "action": "blob_list" } } + ] + }, + { "type": "code-display", "props": { "dataSource": "downloadedContent", "language": "text" } }, + { "type": "list-display", "props": { "dataSource": "files" } } + ] + } + ] + }, + "bindings": { "dbal": true }, + "requiredHooks": ["blob_storage"] + }, + "CachedDataPanel": { + "type": "CachedDataPanel", + "category": "demo", + "label": "Cached Data Panel", + "description": "Cached data hook demonstration", + "icon": "clock", + "props": [], + "config": { + "layout": "card", + "styling": { "className": "cached-data-panel" }, + "children": [ + { + "type": "card", + "props": { "title": "Cached Data Hook", "description": "Automatic caching with React hooks" }, + "children": [ + { "type": "status-badge", "props": { "condition": "loading", "text": "Loading cached data..." } }, + { "type": "status-badge", "props": { "condition": "error", "text": "Error: {{error}}", "variant": "destructive" } }, + { + "type": "data-display", + "props": { "dataSource": "cachedData", "title": "Cached Preferences" } + }, + { "type": "input", "props": { "name": "theme", "label": "Theme", "defaultValue": "dark" } }, + { "type": "input", "props": { "name": "language", "label": "Language", "defaultValue": "en" } }, + { "type": "checkbox", "props": { "name": "notifications", "label": "Notifications", "defaultValue": true } }, + { + "type": "button-group", + "children": [ + { "type": "button", "props": { "label": "Save Preferences", "action": "cache_save" } }, + { "type": "button", "props": { "label": "Clear Cache", "action": "cache_clear", "variant": "outline" } } + ] + } + ] + } + ] + }, + "bindings": { "dbal": true }, + "requiredHooks": ["cached_data"] + } +} diff --git a/packages/dbal_demo/seed/metadata.json b/packages/dbal_demo/seed/metadata.json new file mode 100644 index 000000000..5b928fdf5 --- /dev/null +++ b/packages/dbal_demo/seed/metadata.json @@ -0,0 +1,21 @@ +{ + "name": "dbal_demo", + "version": "1.0.0", + "description": "DBAL Integration Demo - Demonstrates TypeScript DBAL client with MetaBuilder", + "author": "MetaBuilder", + "license": "MIT", + "minLevel": 3, + "category": "demo", + "tags": ["dbal", "demo", "kv-store", "blob-storage", "cache"], + "dependencies": [], + "exports": { + "components": ["DBALDemo", "KVStorePanel", "BlobStoragePanel", "CachedDataPanel"], + "scripts": ["init", "kv_operations", "blob_operations", "cache_operations"] + }, + "bindings": { + "dbal": true, + "browser": false + }, + "requiredHooks": ["dbal", "kv_store", "blob_storage", "cached_data"], + "layout": "layout.json" +} diff --git a/packages/dbal_demo/seed/scripts/blob_operations.lua b/packages/dbal_demo/seed/scripts/blob_operations.lua new file mode 100644 index 000000000..76c5c5dab --- /dev/null +++ b/packages/dbal_demo/seed/scripts/blob_operations.lua @@ -0,0 +1,104 @@ +-- Blob Storage Operations +local M = {} + +-- Upload a file/blob +function M.upload(name, content, metadata) + if not name or name == "" then + return { success = false, error = "File name is required" } + end + + content = content or "" + metadata = metadata or { + ["content-type"] = "text/plain", + ["uploaded-at"] = os.date("%Y-%m-%dT%H:%M:%SZ") + } + + blob_upload(name, content, metadata) + + return { + success = true, + message = "Uploaded: " .. name, + size = #content, + metadata = metadata + } +end + +-- Download a file/blob +function M.download(name) + if not name or name == "" then + return { success = false, error = "File name is required" } + end + + local content = blob_download(name) + if content and content ~= "" then + return { + success = true, + content = content, + size = #content, + message = "Downloaded successfully" + } + else + return { + success = false, + message = "File not found or empty" + } + end +end + +-- Delete a file/blob +function M.delete(name) + if not name or name == "" then + return { success = false, error = "File name is required" } + end + + blob_delete(name) + + return { + success = true, + message = "Deleted: " .. name + } +end + +-- List all files/blobs +function M.list() + local files = blob_list() + return { + success = true, + files = files, + count = #files, + message = "Found " .. #files .. " files" + } +end + +-- Get file info (without downloading content) +function M.info(name) + if not name or name == "" then + return { success = false, error = "File name is required" } + end + + local files = blob_list() + local found = false + for _, file in ipairs(files) do + if file == name then + found = true + break + end + end + + if found then + return { + success = true, + name = name, + exists = true + } + else + return { + success = false, + name = name, + exists = false, + message = "File not found" + } + end +end + +return M diff --git a/packages/dbal_demo/seed/scripts/cache_operations.lua b/packages/dbal_demo/seed/scripts/cache_operations.lua new file mode 100644 index 000000000..7d84016a1 --- /dev/null +++ b/packages/dbal_demo/seed/scripts/cache_operations.lua @@ -0,0 +1,92 @@ +-- Cache Operations +local M = {} + +-- Save data to cache +function M.save(key, data, ttl) + if not key or key == "" then + return { success = false, error = "Cache key is required" } + end + + ttl = ttl or 3600 -- Default 1 hour TTL + + cache_set(key, data, ttl) + + return { + success = true, + message = "Data cached with TTL: " .. ttl .. "s", + key = key, + ttl = ttl + } +end + +-- Get cached data +function M.get(key) + if not key or key == "" then + return { success = false, error = "Cache key is required" } + end + + local data = cache_get(key) + if data then + return { + success = true, + data = data, + message = "Cache hit" + } + else + return { + success = false, + message = "Cache miss" + } + end +end + +-- Clear cached data +function M.clear(key) + if not key or key == "" then + return { success = false, error = "Cache key is required" } + end + + cache_clear(key) + + return { + success = true, + message = "Cache cleared: " .. key + } +end + +-- Save user preferences (common use case) +function M.save_preferences(prefs) + prefs = prefs or {} + + local data = { + theme = prefs.theme or "dark", + language = prefs.language or "en", + notifications = prefs.notifications ~= false + } + + return M.save("user-preferences", data, 3600) +end + +-- Get user preferences +function M.get_preferences() + local result = M.get("user-preferences") + if result.success then + return { + success = true, + preferences = result.data + } + else + -- Return defaults + return { + success = true, + preferences = { + theme = "dark", + language = "en", + notifications = true + }, + is_default = true + } + end +end + +return M diff --git a/packages/dbal_demo/seed/scripts/connection.lua b/packages/dbal_demo/seed/scripts/connection.lua new file mode 100644 index 000000000..762295b6e --- /dev/null +++ b/packages/dbal_demo/seed/scripts/connection.lua @@ -0,0 +1,75 @@ +-- Connection Management +local M = {} + +M.logs = {} +M.status = "disconnected" + +-- Connect to DBAL endpoint +function M.connect(endpoint, api_key) + if not endpoint or endpoint == "" then + return { success = false, error = "Endpoint is required" } + end + + local timestamp = os.date("%H:%M:%S") + local log_entry = timestamp .. ": Connected to " .. endpoint + + table.insert(M.logs, log_entry) + M.status = "connected" + + return { + success = true, + endpoint = endpoint, + api_key = api_key and "***" or "Not provided", + timestamp = timestamp, + message = "Connected successfully" + } +end + +-- Disconnect from DBAL +function M.disconnect() + local timestamp = os.date("%H:%M:%S") + table.insert(M.logs, timestamp .. ": Disconnected") + M.status = "disconnected" + + return { + success = true, + message = "Disconnected" + } +end + +-- Get connection status +function M.get_status() + return { + status = M.status, + is_connected = M.status == "connected" + } +end + +-- Get connection logs +function M.get_logs() + return { + logs = M.logs, + count = #M.logs + } +end + +-- Clear logs +function M.clear_logs() + M.logs = {} + return { + success = true, + message = "Logs cleared" + } +end + +-- Add custom log entry +function M.log(message) + local timestamp = os.date("%H:%M:%S") + table.insert(M.logs, timestamp .. ": " .. message) + return { + success = true, + entry = timestamp .. ": " .. message + } +end + +return M diff --git a/packages/dbal_demo/seed/scripts/init.lua b/packages/dbal_demo/seed/scripts/init.lua new file mode 100644 index 000000000..2b8562b1b --- /dev/null +++ b/packages/dbal_demo/seed/scripts/init.lua @@ -0,0 +1,26 @@ +-- DBAL Demo Package Initialization +local M = {} + +M.name = "dbal_demo" +M.version = "1.0.0" + +function M.init() + return { + success = true, + message = "DBAL Demo package initialized" + } +end + +function M.get_config() + return { + default_endpoint = "", + default_api_key = "", + tabs = { + { value = "kv", label = "Key-Value Store" }, + { value = "blob", label = "Blob Storage" }, + { value = "cache", label = "Cached Data" } + } + } +end + +return M diff --git a/packages/dbal_demo/seed/scripts/kv_operations.lua b/packages/dbal_demo/seed/scripts/kv_operations.lua new file mode 100644 index 000000000..f0174f252 --- /dev/null +++ b/packages/dbal_demo/seed/scripts/kv_operations.lua @@ -0,0 +1,90 @@ +-- KV Store Operations +local M = {} + +-- Set a key-value pair +function M.set(key, value, ttl) + if not key or key == "" then + return { success = false, error = "Key is required" } + end + + local result = kv_set(key, value, ttl) + return { + success = true, + message = "Stored: " .. key .. " = " .. tostring(value), + ttl = ttl + } +end + +-- Get a value by key +function M.get(key) + if not key or key == "" then + return { success = false, error = "Key is required" } + end + + local value = kv_get(key) + if value then + return { + success = true, + value = value, + message = "Retrieved: " .. tostring(value) + } + else + return { + success = false, + message = "Key not found" + } + end +end + +-- Delete a key +function M.delete(key) + if not key or key == "" then + return { success = false, error = "Key is required" } + end + + local deleted = kv_delete(key) + if deleted then + return { + success = true, + message = "Deleted: " .. key + } + else + return { + success = false, + message = "Key not found" + } + end +end + +-- Add items to a list +function M.list_add(key, items) + if not key or key == "" then + return { success = false, error = "List key is required" } + end + + items = items or { "Item 1", "Item 2", "Item 3" } + kv_list_add(key, items) + + return { + success = true, + message = "Added " .. #items .. " items to list", + items = items + } +end + +-- Get list items +function M.list_get(key) + if not key or key == "" then + return { success = false, error = "List key is required" } + end + + local items = kv_list_get(key) + return { + success = true, + items = items, + count = #items, + message = "Retrieved " .. #items .. " items" + } +end + +return M diff --git a/packages/dbal_demo/seed/scripts/manifest.json b/packages/dbal_demo/seed/scripts/manifest.json new file mode 100644 index 000000000..e33b9b4e3 --- /dev/null +++ b/packages/dbal_demo/seed/scripts/manifest.json @@ -0,0 +1,34 @@ +{ + "scripts": [ + { + "name": "init", + "file": "init.lua", + "category": "lifecycle", + "description": "Package initialization" + }, + { + "name": "kv_operations", + "file": "kv_operations.lua", + "category": "dbal", + "description": "Key-Value store operations" + }, + { + "name": "blob_operations", + "file": "blob_operations.lua", + "category": "dbal", + "description": "Blob storage operations" + }, + { + "name": "cache_operations", + "file": "cache_operations.lua", + "category": "dbal", + "description": "Cache operations" + }, + { + "name": "connection", + "file": "connection.lua", + "category": "dbal", + "description": "Connection management" + } + ] +} diff --git a/packages/lua_test/seed/metadata.json b/packages/lua_test/seed/metadata.json new file mode 100644 index 000000000..f8c574d14 --- /dev/null +++ b/packages/lua_test/seed/metadata.json @@ -0,0 +1,15 @@ +{ + "packageId": "lua_test", + "name": "Lua Test", + "version": "1.0.0", + "description": "Unit testing framework for Lua scripts in MetaBuilder packages", + "icon": "static_content/icon.svg", + "author": "MetaBuilder", + "category": "tools", + "dependencies": [], + "exports": { + "components": ["TestRunner", "TestResults"], + "scripts": ["framework", "runner", "assertions", "mocks"] + }, + "minLevel": 3 +} diff --git a/packages/lua_test/seed/scripts/assertions.lua b/packages/lua_test/seed/scripts/assertions.lua new file mode 100644 index 000000000..a22976c2e --- /dev/null +++ b/packages/lua_test/seed/scripts/assertions.lua @@ -0,0 +1,360 @@ +-- Assertion functions for the test framework +-- Provides expect() with chainable matchers + +local M = {} + +-- Helper to get type with better nil handling +local function getType(value) + if value == nil then return "nil" end + return type(value) +end + +-- Helper to stringify values for error messages +local function stringify(value) + local t = type(value) + if t == "string" then + return '"' .. value .. '"' + elseif t == "table" then + local parts = {} + for k, v in pairs(value) do + parts[#parts + 1] = tostring(k) .. "=" .. stringify(v) + end + return "{" .. table.concat(parts, ", ") .. "}" + elseif t == "nil" then + return "nil" + else + return tostring(value) + end +end + +-- Deep equality check +local function deepEqual(a, b) + if type(a) ~= type(b) then return false end + if type(a) ~= "table" then return a == b end + + -- Check all keys in a exist in b with same values + for k, v in pairs(a) do + if not deepEqual(v, b[k]) then return false end + end + + -- Check all keys in b exist in a + for k, _ in pairs(b) do + if a[k] == nil then return false end + end + + return true +end + +-- Create assertion error +local function assertionError(message, expected, actual) + return { + type = "AssertionError", + message = message, + expected = expected, + actual = actual + } +end + +-- Expect wrapper with chainable matchers +function M.expect(actual) + local expectation = { + actual = actual, + negated = false + } + + -- Not modifier + expectation.never = setmetatable({}, { + __index = function(_, key) + expectation.negated = true + return expectation[key] + end + }) + + -- toBe - strict equality + function expectation.toBe(expected) + local pass = actual == expected + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be " .. stringify(expected) + or "Expected " .. stringify(actual) .. " to be " .. stringify(expected) + error(assertionError(msg, expected, actual)) + end + return true + end + + -- toEqual - deep equality + function expectation.toEqual(expected) + local pass = deepEqual(actual, expected) + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected values not to be deeply equal" + or "Expected values to be deeply equal" + error(assertionError(msg, expected, actual)) + end + return true + end + + -- toBeNil + function expectation.toBeNil() + local pass = actual == nil + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be nil" + or "Expected " .. stringify(actual) .. " to be nil" + error(assertionError(msg, nil, actual)) + end + return true + end + + -- toBeTruthy + function expectation.toBeTruthy() + local pass = actual and true or false + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be truthy" + or "Expected " .. stringify(actual) .. " to be truthy" + error(assertionError(msg, "truthy", actual)) + end + return true + end + + -- toBeFalsy + function expectation.toBeFalsy() + local pass = not actual + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be falsy" + or "Expected " .. stringify(actual) .. " to be falsy" + error(assertionError(msg, "falsy", actual)) + end + return true + end + + -- toBeType + function expectation.toBeType(expectedType) + local actualType = getType(actual) + local pass = actualType == expectedType + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected type not to be " .. expectedType .. ", got " .. actualType + or "Expected type to be " .. expectedType .. ", got " .. actualType + error(assertionError(msg, expectedType, actualType)) + end + return true + end + + -- toContain - for strings and tables + function expectation.toContain(expected) + local pass = false + + if type(actual) == "string" and type(expected) == "string" then + pass = string.find(actual, expected, 1, true) ~= nil + elseif type(actual) == "table" then + for _, v in pairs(actual) do + if deepEqual(v, expected) then + pass = true + break + end + end + end + + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to contain " .. stringify(expected) + or "Expected " .. stringify(actual) .. " to contain " .. stringify(expected) + error(assertionError(msg, expected, actual)) + end + return true + end + + -- toHaveLength + function expectation.toHaveLength(expectedLength) + local actualLength + if type(actual) == "string" then + actualLength = #actual + elseif type(actual) == "table" then + actualLength = #actual + else + error("toHaveLength can only be used with strings or tables") + end + + local pass = actualLength == expectedLength + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected length not to be " .. expectedLength .. ", got " .. actualLength + or "Expected length to be " .. expectedLength .. ", got " .. actualLength + error(assertionError(msg, expectedLength, actualLength)) + end + return true + end + + -- toBeGreaterThan + function expectation.toBeGreaterThan(expected) + local pass = actual > expected + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be greater than " .. stringify(expected) + or "Expected " .. stringify(actual) .. " to be greater than " .. stringify(expected) + error(assertionError(msg, "> " .. expected, actual)) + end + return true + end + + -- toBeLessThan + function expectation.toBeLessThan(expected) + local pass = actual < expected + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to be less than " .. stringify(expected) + or "Expected " .. stringify(actual) .. " to be less than " .. stringify(expected) + error(assertionError(msg, "< " .. expected, actual)) + end + return true + end + + -- toBeCloseTo - for floating point comparison + function expectation.toBeCloseTo(expected, precision) + precision = precision or 2 + local diff = math.abs(actual - expected) + local pass = diff < (10 ^ -precision) / 2 + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. actual .. " not to be close to " .. expected + or "Expected " .. actual .. " to be close to " .. expected .. " (diff: " .. diff .. ")" + error(assertionError(msg, expected, actual)) + end + return true + end + + -- toMatch - regex pattern matching + function expectation.toMatch(pattern) + if type(actual) ~= "string" then + error("toMatch can only be used with strings") + end + + local pass = string.match(actual, pattern) ~= nil + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected " .. stringify(actual) .. " not to match pattern " .. pattern + or "Expected " .. stringify(actual) .. " to match pattern " .. pattern + error(assertionError(msg, pattern, actual)) + end + return true + end + + -- toThrow - expects a function to throw + function expectation.toThrow(expectedMessage) + if type(actual) ~= "function" then + error("toThrow can only be used with functions") + end + + local success, err = pcall(actual) + local pass = not success + + if pass and expectedMessage then + local errMsg = type(err) == "table" and err.message or tostring(err) + pass = string.find(errMsg, expectedMessage, 1, true) ~= nil + end + + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected function not to throw" + or "Expected function to throw" .. (expectedMessage and " with message: " .. expectedMessage or "") + error(assertionError(msg, expectedMessage or "error", success and "no error" or err)) + end + return true + end + + -- toHaveProperty + function expectation.toHaveProperty(key, value) + if type(actual) ~= "table" then + error("toHaveProperty can only be used with tables") + end + + local pass = actual[key] ~= nil + if pass and value ~= nil then + pass = deepEqual(actual[key], value) + end + + if expectation.negated then pass = not pass end + + if not pass then + local msg = expectation.negated + and "Expected table not to have property " .. stringify(key) + or "Expected table to have property " .. stringify(key) .. (value and " with value " .. stringify(value) or "") + error(assertionError(msg, value, actual[key])) + end + return true + end + + return expectation +end + +-- Standalone assertion functions +function M.assertTrue(value, message) + if not value then + error(assertionError(message or "Expected true", true, value)) + end +end + +function M.assertFalse(value, message) + if value then + error(assertionError(message or "Expected false", false, value)) + end +end + +function M.assertEqual(actual, expected, message) + if actual ~= expected then + error(assertionError(message or "Values not equal", expected, actual)) + end +end + +function M.assertNotEqual(actual, expected, message) + if actual == expected then + error(assertionError(message or "Values should not be equal", "not " .. stringify(expected), actual)) + end +end + +function M.assertNil(value, message) + if value ~= nil then + error(assertionError(message or "Expected nil", nil, value)) + end +end + +function M.assertNotNil(value, message) + if value == nil then + error(assertionError(message or "Expected not nil", "not nil", nil)) + end +end + +function M.fail(message) + error(assertionError(message or "Test failed", nil, nil)) +end + +return M diff --git a/packages/lua_test/seed/scripts/framework.lua b/packages/lua_test/seed/scripts/framework.lua new file mode 100644 index 000000000..89a8ecfa1 --- /dev/null +++ b/packages/lua_test/seed/scripts/framework.lua @@ -0,0 +1,145 @@ +-- Core test framework with describe/it blocks +-- Provides BDD-style test organization + +local M = {} + +-- Test suite state +M._suites = {} +M._currentSuite = nil +M._config = { + timeout = 5000, + verbose = true, + stopOnFirstFailure = false, + filter = nil +} + +-- Create a new test suite +function M.createSuite(name) + local suite = { + name = name, + tests = {}, + beforeAll = nil, + afterAll = nil, + beforeEach = nil, + afterEach = nil, + nested = {}, + parent = nil + } + return suite +end + +-- Describe block - groups related tests +function M.describe(name, fn) + local parentSuite = M._currentSuite + local suite = M.createSuite(name) + suite.parent = parentSuite + + if parentSuite then + parentSuite.nested[#parentSuite.nested + 1] = suite + else + M._suites[#M._suites + 1] = suite + end + + M._currentSuite = suite + fn() + M._currentSuite = parentSuite + + return suite +end + +-- It block - defines a single test +function M.it(name, fn) + if not M._currentSuite then + error("it() must be called inside a describe() block") + end + + local test = { + name = name, + fn = fn, + status = "pending", + error = nil, + duration = 0, + skipped = false + } + + M._currentSuite.tests[#M._currentSuite.tests + 1] = test + return test +end + +-- Skip a test +function M.xit(name, fn) + if not M._currentSuite then + error("xit() must be called inside a describe() block") + end + + local test = { + name = name, + fn = fn, + status = "skipped", + error = nil, + duration = 0, + skipped = true + } + + M._currentSuite.tests[#M._currentSuite.tests + 1] = test + return test +end + +-- Only run this test (for debugging) +function M.fit(name, fn) + local test = M.it(name, fn) + test.only = true + return test +end + +-- Setup hooks +function M.beforeAll(fn) + if M._currentSuite then + M._currentSuite.beforeAll = fn + end +end + +function M.afterAll(fn) + if M._currentSuite then + M._currentSuite.afterAll = fn + end +end + +function M.beforeEach(fn) + if M._currentSuite then + M._currentSuite.beforeEach = fn + end +end + +function M.afterEach(fn) + if M._currentSuite then + M._currentSuite.afterEach = fn + end +end + +-- Configure the test framework +function M.configure(options) + for k, v in pairs(options) do + if M._config[k] ~= nil then + M._config[k] = v + end + end +end + +-- Get all registered suites +function M.getSuites() + return M._suites +end + +-- Reset all suites (for fresh test runs) +function M.reset() + M._suites = {} + M._currentSuite = nil +end + +-- Get current config +function M.getConfig() + return M._config +end + +return M diff --git a/packages/lua_test/seed/scripts/helpers.lua b/packages/lua_test/seed/scripts/helpers.lua new file mode 100644 index 000000000..c9db457a6 --- /dev/null +++ b/packages/lua_test/seed/scripts/helpers.lua @@ -0,0 +1,267 @@ +-- Test helper utilities +-- Additional utilities for writing tests + +local M = {} + +-- Generate test data +function M.generateTestData(template, count) + local data = {} + count = count or 10 + + for i = 1, count do + local item = {} + for k, v in pairs(template) do + if type(v) == "function" then + item[k] = v(i) + elseif type(v) == "string" and v:match("^%$") then + -- Template variables + local varName = v:sub(2) + if varName == "index" then + item[k] = i + elseif varName == "random" then + item[k] = math.random(1, 1000) + elseif varName == "uuid" then + item[k] = string.format("%08x-%04x-%04x-%04x-%012x", + math.random(0, 0xffffffff), + math.random(0, 0xffff), + math.random(0, 0xffff), + math.random(0, 0xffff), + math.random(0, 0xffffffffffff)) + else + item[k] = v + end + else + item[k] = v + end + end + data[#data + 1] = item + end + + return data +end + +-- Create parameterized test cases +function M.parameterized(cases, testFn) + return function() + for _, testCase in ipairs(cases) do + testFn(testCase) + end + end +end + +-- Wait for condition (for async-like testing) +function M.waitFor(condition, options) + options = options or {} + local timeout = options.timeout or 1000 + local interval = options.interval or 10 + local startTime = os.clock() * 1000 + + while (os.clock() * 1000 - startTime) < timeout do + if condition() then + return true + end + -- Note: In sandbox, we can't actually sleep, but this provides the pattern + end + + if options.throwOnTimeout ~= false then + error("waitFor timed out after " .. timeout .. "ms") + end + + return false +end + +-- Snapshot testing helper +function M.createSnapshot(name) + local snapshots = {} + + return { + -- Record a snapshot + record = function(key, value) + snapshots[key] = M.serialize(value) + end, + + -- Match against recorded snapshot + match = function(key, value) + local serialized = M.serialize(value) + if snapshots[key] then + return snapshots[key] == serialized + end + -- First run - record the snapshot + snapshots[key] = serialized + return true + end, + + -- Get all snapshots + getSnapshots = function() + return snapshots + end, + + -- Update a snapshot + update = function(key, value) + snapshots[key] = M.serialize(value) + end + } +end + +-- Serialize value for comparison +function M.serialize(value, seen) + seen = seen or {} + local t = type(value) + + if t == "nil" then + return "nil" + elseif t == "boolean" then + return value and "true" or "false" + elseif t == "number" then + return tostring(value) + elseif t == "string" then + return string.format("%q", value) + elseif t == "table" then + if seen[value] then + return "" + end + seen[value] = true + + local parts = {} + local keys = {} + + for k in pairs(value) do + keys[#keys + 1] = k + end + + table.sort(keys, function(a, b) + return tostring(a) < tostring(b) + end) + + for _, k in ipairs(keys) do + parts[#parts + 1] = "[" .. M.serialize(k, seen) .. "]=" .. M.serialize(value[k], seen) + end + + return "{" .. table.concat(parts, ",") .. "}" + else + return "<" .. t .. ">" + end +end + +-- Table utilities for testing +M.table = {} + +function M.table.clone(t) + if type(t) ~= "table" then return t end + local copy = {} + for k, v in pairs(t) do + copy[k] = M.table.clone(v) + end + return setmetatable(copy, getmetatable(t)) +end + +function M.table.merge(...) + local result = {} + for _, t in ipairs({...}) do + if type(t) == "table" then + for k, v in pairs(t) do + result[k] = v + end + end + end + return result +end + +function M.table.keys(t) + local keys = {} + for k in pairs(t) do + keys[#keys + 1] = k + end + return keys +end + +function M.table.values(t) + local values = {} + for _, v in pairs(t) do + values[#values + 1] = v + end + return values +end + +function M.table.size(t) + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count +end + +-- String utilities for testing +M.string = {} + +function M.string.trim(s) + return s:match("^%s*(.-)%s*$") +end + +function M.string.split(s, delimiter) + delimiter = delimiter or "%s" + local result = {} + for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do + result[#result + 1] = match + end + return result +end + +function M.string.startsWith(s, prefix) + return s:sub(1, #prefix) == prefix +end + +function M.string.endsWith(s, suffix) + return suffix == "" or s:sub(-#suffix) == suffix +end + +-- Assertion shortcuts for common patterns +function M.assertThrows(fn, expectedMessage) + local success, err = pcall(fn) + if success then + error("Expected function to throw, but it didn't") + end + if expectedMessage then + local errStr = type(err) == "table" and err.message or tostring(err) + if not string.find(errStr, expectedMessage, 1, true) then + error("Expected error to contain: " .. expectedMessage .. ", got: " .. errStr) + end + end + return err +end + +function M.assertDoesNotThrow(fn) + local success, err = pcall(fn) + if not success then + error("Expected function not to throw, but got: " .. tostring(err)) + end +end + +-- Test context builder +function M.createContext(initial) + local ctx = initial or {} + + return { + get = function(key) + return ctx[key] + end, + + set = function(key, value) + ctx[key] = value + end, + + with = function(overrides) + return M.table.merge(ctx, overrides) + end, + + reset = function() + ctx = initial and M.table.clone(initial) or {} + end, + + getAll = function() + return M.table.clone(ctx) + end + } +end + +return M diff --git a/packages/lua_test/seed/scripts/init.lua b/packages/lua_test/seed/scripts/init.lua new file mode 100644 index 000000000..411f5b8dc --- /dev/null +++ b/packages/lua_test/seed/scripts/init.lua @@ -0,0 +1,34 @@ +-- lua_test package initialization +-- Unit testing framework for MetaBuilder Lua scripts + +local M = {} + +M.version = "1.0.0" +M.name = "lua_test" + +function M.init() + return { + success = true, + message = "Lua Test Framework initialized", + version = M.version + } +end + +function M.info() + return { + name = M.name, + version = M.version, + description = "Unit testing framework for Lua scripts", + features = { + "describe/it blocks", + "assertions (expect)", + "beforeEach/afterEach hooks", + "mocks and spies", + "async test support", + "test filtering", + "detailed reporting" + } + } +end + +return M diff --git a/packages/lua_test/seed/scripts/manifest.json b/packages/lua_test/seed/scripts/manifest.json new file mode 100644 index 000000000..82fdeb428 --- /dev/null +++ b/packages/lua_test/seed/scripts/manifest.json @@ -0,0 +1,10 @@ +{ + "scripts": [ + { "file": "init.lua", "name": "init", "category": "lifecycle", "description": "Package lifecycle initialization" }, + { "file": "framework.lua", "name": "framework", "category": "core", "description": "Core test framework with describe/it blocks" }, + { "file": "assertions.lua", "name": "assertions", "category": "core", "description": "Assertion functions (expect, assert_*)" }, + { "file": "mocks.lua", "name": "mocks", "category": "utilities", "description": "Mock and spy utilities" }, + { "file": "runner.lua", "name": "runner", "category": "core", "description": "Test runner and reporter" }, + { "file": "helpers.lua", "name": "helpers", "category": "utilities", "description": "Test helper utilities" } + ] +} diff --git a/packages/lua_test/seed/scripts/mocks.lua b/packages/lua_test/seed/scripts/mocks.lua new file mode 100644 index 000000000..cd76295b8 --- /dev/null +++ b/packages/lua_test/seed/scripts/mocks.lua @@ -0,0 +1,248 @@ +-- Mock and spy utilities for testing +-- Allows tracking function calls and replacing implementations + +local M = {} + +-- Create a mock function +function M.fn(implementation) + local mock = { + calls = {}, + results = {}, + implementation = implementation + } + + -- The callable mock function + local callable = function(...) + local args = {...} + mock.calls[#mock.calls + 1] = args + + local result + if mock.implementation then + result = {mock.implementation(...)} + else + result = {} + end + + mock.results[#mock.results + 1] = result + return table.unpack(result) + end + + -- Attach mock metadata to the function via a metatable + return setmetatable({}, { + __call = function(_, ...) return callable(...) end, + __index = { + -- Get call count + getCallCount = function() + return #mock.calls + end, + + -- Check if called + wasCalled = function() + return #mock.calls > 0 + end, + + -- Check if called with specific args + wasCalledWith = function(...) + local expectedArgs = {...} + for _, callArgs in ipairs(mock.calls) do + local match = true + for i, expected in ipairs(expectedArgs) do + if callArgs[i] ~= expected then + match = false + break + end + end + if match then return true end + end + return false + end, + + -- Get specific call args + getCall = function(index) + return mock.calls[index] + end, + + -- Get last call args + getLastCall = function() + return mock.calls[#mock.calls] + end, + + -- Get all calls + getCalls = function() + return mock.calls + end, + + -- Get all results + getResults = function() + return mock.results + end, + + -- Clear call history + reset = function() + mock.calls = {} + mock.results = {} + end, + + -- Set return value + mockReturnValue = function(value) + mock.implementation = function() return value end + end, + + -- Set return values in sequence + mockReturnValueOnce = function(value) + local originalImpl = mock.implementation + local called = false + mock.implementation = function(...) + if not called then + called = true + return value + elseif originalImpl then + return originalImpl(...) + end + end + end, + + -- Set implementation + mockImplementation = function(fn) + mock.implementation = fn + end, + + -- Restore original (for spies) + mockRestore = function() + mock.implementation = nil + mock.calls = {} + mock.results = {} + end + } + }) +end + +-- Create a spy on an existing object method +function M.spyOn(obj, methodName) + local original = obj[methodName] + if type(original) ~= "function" then + error("Cannot spy on non-function: " .. methodName) + end + + local spy = M.fn(original) + + -- Add restore functionality + local meta = getmetatable(spy).__index + local originalRestore = meta.mockRestore + meta.mockRestore = function() + obj[methodName] = original + originalRestore() + end + + -- Replace the method + obj[methodName] = function(...) + return spy(...) + end + + return spy +end + +-- Create a mock object with multiple mock functions +function M.mockObject(methods) + local obj = {} + local mocks = {} + + for name, impl in pairs(methods or {}) do + mocks[name] = M.fn(impl) + obj[name] = function(...) return mocks[name](...) end + end + + obj._mocks = mocks + + obj._resetAll = function() + for _, mock in pairs(mocks) do + mock.reset() + end + end + + return obj +end + +-- Timer mocks for testing time-dependent code +function M.useFakeTimers() + local timers = { + now = 0, + scheduled = {} + } + + return { + -- Get current fake time + now = function() + return timers.now + end, + + -- Schedule a callback (like setTimeout) + schedule = function(callback, delay) + local id = #timers.scheduled + 1 + timers.scheduled[id] = { + callback = callback, + time = timers.now + delay, + id = id + } + return id + end, + + -- Cancel a scheduled callback + cancel = function(id) + timers.scheduled[id] = nil + end, + + -- Advance time and run scheduled callbacks + advance = function(ms) + local targetTime = timers.now + ms + + -- Sort by scheduled time + local pending = {} + for _, timer in pairs(timers.scheduled) do + if timer.time <= targetTime then + pending[#pending + 1] = timer + end + end + + table.sort(pending, function(a, b) return a.time < b.time end) + + -- Run each callback at its scheduled time + for _, timer in ipairs(pending) do + timers.now = timer.time + timer.callback() + timers.scheduled[timer.id] = nil + end + + timers.now = targetTime + end, + + -- Run all pending timers + runAll = function() + while next(timers.scheduled) do + local nextTimer + local nextTime = math.huge + + for id, timer in pairs(timers.scheduled) do + if timer.time < nextTime then + nextTime = timer.time + nextTimer = timer + end + end + + if nextTimer then + timers.now = nextTimer.time + nextTimer.callback() + timers.scheduled[nextTimer.id] = nil + end + end + end, + + -- Reset timers + reset = function() + timers.now = 0 + timers.scheduled = {} + end + } +end + +return M diff --git a/packages/lua_test/seed/scripts/runner.lua b/packages/lua_test/seed/scripts/runner.lua new file mode 100644 index 000000000..95b8a05c0 --- /dev/null +++ b/packages/lua_test/seed/scripts/runner.lua @@ -0,0 +1,330 @@ +-- Test runner and reporter +-- Executes test suites and generates reports + +local M = {} + +-- Result types +M.STATUS = { + PASSED = "passed", + FAILED = "failed", + SKIPPED = "skipped", + PENDING = "pending" +} + +-- Run a single test +function M.runTest(test, hooks) + local result = { + name = test.name, + status = M.STATUS.PENDING, + error = nil, + duration = 0 + } + + if test.skipped then + result.status = M.STATUS.SKIPPED + return result + end + + local startTime = os.clock() + + -- Run beforeEach hook + if hooks.beforeEach then + local success, err = pcall(hooks.beforeEach) + if not success then + result.status = M.STATUS.FAILED + result.error = "beforeEach failed: " .. tostring(err) + result.duration = (os.clock() - startTime) * 1000 + return result + end + end + + -- Run the test + local success, err = pcall(test.fn) + + -- Run afterEach hook + if hooks.afterEach then + pcall(hooks.afterEach) + end + + result.duration = (os.clock() - startTime) * 1000 + + if success then + result.status = M.STATUS.PASSED + else + result.status = M.STATUS.FAILED + if type(err) == "table" and err.type == "AssertionError" then + result.error = err.message + result.expected = err.expected + result.actual = err.actual + else + result.error = tostring(err) + end + end + + return result +end + +-- Run a test suite +function M.runSuite(suite, config, parentHooks) + local results = { + name = suite.name, + tests = {}, + nested = {}, + stats = { + total = 0, + passed = 0, + failed = 0, + skipped = 0, + duration = 0 + } + } + + config = config or {} + parentHooks = parentHooks or {} + + local startTime = os.clock() + + -- Combine hooks with parent hooks + local hooks = { + beforeEach = suite.beforeEach or parentHooks.beforeEach, + afterEach = suite.afterEach or parentHooks.afterEach + } + + -- Run beforeAll hook + if suite.beforeAll then + local success, err = pcall(suite.beforeAll) + if not success then + -- Mark all tests as failed + for _, test in ipairs(suite.tests) do + results.tests[#results.tests + 1] = { + name = test.name, + status = M.STATUS.FAILED, + error = "beforeAll failed: " .. tostring(err), + duration = 0 + } + results.stats.failed = results.stats.failed + 1 + results.stats.total = results.stats.total + 1 + end + results.stats.duration = (os.clock() - startTime) * 1000 + return results + end + end + + -- Check for .only tests + local hasOnly = false + for _, test in ipairs(suite.tests) do + if test.only then + hasOnly = true + break + end + end + + -- Run tests + for _, test in ipairs(suite.tests) do + -- Skip if there's an "only" test and this isn't it + if hasOnly and not test.only then + local skipResult = { + name = test.name, + status = M.STATUS.SKIPPED, + duration = 0 + } + results.tests[#results.tests + 1] = skipResult + results.stats.skipped = results.stats.skipped + 1 + -- Apply filter if configured + elseif config.filter and not string.find(test.name, config.filter, 1, true) then + local skipResult = { + name = test.name, + status = M.STATUS.SKIPPED, + duration = 0 + } + results.tests[#results.tests + 1] = skipResult + results.stats.skipped = results.stats.skipped + 1 + else + local testResult = M.runTest(test, hooks) + results.tests[#results.tests + 1] = testResult + + if testResult.status == M.STATUS.PASSED then + results.stats.passed = results.stats.passed + 1 + elseif testResult.status == M.STATUS.FAILED then + results.stats.failed = results.stats.failed + 1 + if config.stopOnFirstFailure then + break + end + elseif testResult.status == M.STATUS.SKIPPED then + results.stats.skipped = results.stats.skipped + 1 + end + end + + results.stats.total = results.stats.total + 1 + end + + -- Run nested suites + for _, nestedSuite in ipairs(suite.nested) do + local nestedResults = M.runSuite(nestedSuite, config, hooks) + results.nested[#results.nested + 1] = nestedResults + + -- Aggregate stats + results.stats.total = results.stats.total + nestedResults.stats.total + results.stats.passed = results.stats.passed + nestedResults.stats.passed + results.stats.failed = results.stats.failed + nestedResults.stats.failed + results.stats.skipped = results.stats.skipped + nestedResults.stats.skipped + end + + -- Run afterAll hook + if suite.afterAll then + pcall(suite.afterAll) + end + + results.stats.duration = (os.clock() - startTime) * 1000 + + return results +end + +-- Run all suites +function M.runAll(suites, config) + local allResults = { + suites = {}, + stats = { + total = 0, + passed = 0, + failed = 0, + skipped = 0, + duration = 0, + suiteCount = 0 + }, + timestamp = os.date("%Y-%m-%dT%H:%M:%S") + } + + local startTime = os.clock() + + for _, suite in ipairs(suites) do + local suiteResults = M.runSuite(suite, config) + allResults.suites[#allResults.suites + 1] = suiteResults + allResults.stats.suiteCount = allResults.stats.suiteCount + 1 + + -- Aggregate stats + allResults.stats.total = allResults.stats.total + suiteResults.stats.total + allResults.stats.passed = allResults.stats.passed + suiteResults.stats.passed + allResults.stats.failed = allResults.stats.failed + suiteResults.stats.failed + allResults.stats.skipped = allResults.stats.skipped + suiteResults.stats.skipped + end + + allResults.stats.duration = (os.clock() - startTime) * 1000 + allResults.stats.success = allResults.stats.failed == 0 + + return allResults +end + +-- Format results as text report +function M.formatReport(results, options) + options = options or {} + local indent = options.indent or "" + local verbose = options.verbose ~= false + local lines = {} + + local function add(line) + lines[#lines + 1] = line + end + + local function formatSuite(suite, depth) + local prefix = string.rep(" ", depth) + add(prefix .. "📦 " .. suite.name) + + for _, test in ipairs(suite.tests) do + local icon = "⏳" + if test.status == M.STATUS.PASSED then + icon = "✅" + elseif test.status == M.STATUS.FAILED then + icon = "❌" + elseif test.status == M.STATUS.SKIPPED then + icon = "⏭️" + end + + local duration = string.format("(%.2fms)", test.duration) + add(prefix .. " " .. icon .. " " .. test.name .. " " .. duration) + + if test.status == M.STATUS.FAILED and verbose then + add(prefix .. " Error: " .. (test.error or "Unknown error")) + if test.expected then + add(prefix .. " Expected: " .. tostring(test.expected)) + end + if test.actual then + add(prefix .. " Actual: " .. tostring(test.actual)) + end + end + end + + for _, nested in ipairs(suite.nested) do + formatSuite(nested, depth + 1) + end + end + + add("═══════════════════════════════════════") + add(" TEST RESULTS REPORT ") + add("═══════════════════════════════════════") + add("") + + for _, suite in ipairs(results.suites) do + formatSuite(suite, 0) + add("") + end + + add("───────────────────────────────────────") + add("Summary:") + add(string.format(" Total: %d tests", results.stats.total)) + add(string.format(" Passed: %d ✅", results.stats.passed)) + add(string.format(" Failed: %d ❌", results.stats.failed)) + add(string.format(" Skipped: %d ⏭️", results.stats.skipped)) + add(string.format(" Duration: %.2fms", results.stats.duration)) + add("") + + if results.stats.success then + add("🎉 All tests passed!") + else + add("💥 Some tests failed!") + end + + add("═══════════════════════════════════════") + + return table.concat(lines, "\n") +end + +-- Format results as JSON +function M.formatJSON(results) + -- Simple JSON serialization + local function serialize(value, indent) + indent = indent or 0 + local t = type(value) + + if t == "nil" then + return "null" + elseif t == "boolean" then + return value and "true" or "false" + elseif t == "number" then + return tostring(value) + elseif t == "string" then + return '"' .. value:gsub('"', '\\"'):gsub("\n", "\\n") .. '"' + elseif t == "table" then + local parts = {} + local isArray = #value > 0 or next(value) == nil + + if isArray then + for _, v in ipairs(value) do + parts[#parts + 1] = serialize(v, indent + 1) + end + return "[" .. table.concat(parts, ",") .. "]" + else + for k, v in pairs(value) do + parts[#parts + 1] = '"' .. tostring(k) .. '":' .. serialize(v, indent + 1) + end + return "{" .. table.concat(parts, ",") .. "}" + end + end + + return '"<' .. t .. '>"' + end + + return serialize(results) +end + +return M diff --git a/packages/screenshot_analyzer/seed/metadata.json b/packages/screenshot_analyzer/seed/metadata.json new file mode 100644 index 000000000..09520e5c9 --- /dev/null +++ b/packages/screenshot_analyzer/seed/metadata.json @@ -0,0 +1,21 @@ +{ + "name": "screenshot_analyzer", + "version": "1.0.0", + "description": "Screenshot Analyzer - Capture and analyze the current page", + "author": "MetaBuilder", + "license": "MIT", + "minLevel": 5, + "category": "demo", + "tags": ["screenshot", "analysis", "browser", "dom"], + "dependencies": [], + "exports": { + "components": ["ScreenshotAnalyzer", "UploadSection", "ResultPanel", "PageInfo"], + "scripts": ["init", "capture", "analyze", "page_info"] + }, + "bindings": { + "dbal": false, + "browser": true + }, + "requiredHooks": [], + "layout": "layout.json" +}