From 84392b4c126619cea97a4134197fee9d70b41cd9 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Tue, 30 Dec 2025 23:27:30 +0000 Subject: [PATCH] config: studio,packages,codegen (6 files) --- .../seed/scripts/package_template/cli.lua | 207 ++++++++ .../scripts/package_template/generator.lua | 335 +++++++++++++ .../seed/scripts/package_template/init.lua | 49 ++ .../scripts/package_template/templates.lua | 464 ++++++++++++++++++ .../tests/generator.cases.json | 110 +++++ .../package_template/tests/generator.test.lua | 217 ++++++++ 6 files changed, 1382 insertions(+) create mode 100644 packages/codegen_studio/seed/scripts/package_template/cli.lua create mode 100644 packages/codegen_studio/seed/scripts/package_template/generator.lua create mode 100644 packages/codegen_studio/seed/scripts/package_template/init.lua create mode 100644 packages/codegen_studio/seed/scripts/package_template/templates.lua create mode 100644 packages/codegen_studio/seed/scripts/package_template/tests/generator.cases.json create mode 100644 packages/codegen_studio/seed/scripts/package_template/tests/generator.test.lua diff --git a/packages/codegen_studio/seed/scripts/package_template/cli.lua b/packages/codegen_studio/seed/scripts/package_template/cli.lua new file mode 100644 index 000000000..acc583c06 --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/cli.lua @@ -0,0 +1,207 @@ +-- Package template CLI interface +-- @module package_template.cli + +local generator = require("package_template.generator") +local templates = require("package_template.templates") + +local M = {} + +---Print usage help +function M.print_help() + print([[ +Package Template Generator +========================== + +Usage: lua cli.lua [options] + +Commands: + new Create a new package with interactive prompts + quick Create a new package with defaults + list-categories List available package categories + validate Validate a package configuration + +Options for 'new' and 'quick': + --name Display name (default: derived from package_id) + --description Package description + --category Package category (default: ui) + --min-level Minimum access level 0-6 (default: 2) + --primary Package can own routes (default) + --dependency Package is dependency-only + --with-schema Include database schema scaffolding + --entities Entity names for schema (comma-separated) + --with-components Include component scaffolding + --components Component names (comma-separated) + --deps Package dependencies (comma-separated) + --output Output directory (default: packages/) + +Examples: + lua cli.lua new my_package --category tools --min-level 3 + lua cli.lua quick my_widget --dependency --category ui + lua cli.lua new forum_clone --with-schema --entities Thread,Post,Reply + lua cli.lua new my_dashboard --with-components --components StatCard,Chart +]]) +end + +---Parse command line arguments +---@param args string[] +---@return table +function M.parse_args(args) + local parsed = { + command = nil, + packageId = nil, + options = { + name = nil, + description = nil, + category = "ui", + minLevel = 2, + primary = true, + withSchema = false, + withTests = true, + withComponents = false, + entities = {}, + components = {}, + dependencies = {}, + output = "packages/" + } + } + + local i = 1 + while i <= #args do + local arg = args[i] + + if not parsed.command then + parsed.command = arg + elseif not parsed.packageId and not string.match(arg, "^%-") then + parsed.packageId = arg + elseif arg == "--name" then + i = i + 1 + parsed.options.name = args[i] + elseif arg == "--description" then + i = i + 1 + parsed.options.description = args[i] + elseif arg == "--category" then + i = i + 1 + parsed.options.category = args[i] + elseif arg == "--min-level" then + i = i + 1 + parsed.options.minLevel = tonumber(args[i]) + elseif arg == "--primary" then + parsed.options.primary = true + elseif arg == "--dependency" then + parsed.options.primary = false + elseif arg == "--with-schema" then + parsed.options.withSchema = true + elseif arg == "--entities" then + i = i + 1 + for entity in string.gmatch(args[i], "[^,]+") do + table.insert(parsed.options.entities, entity) + end + elseif arg == "--with-components" then + parsed.options.withComponents = true + elseif arg == "--components" then + i = i + 1 + for comp in string.gmatch(args[i], "[^,]+") do + table.insert(parsed.options.components, comp) + end + elseif arg == "--deps" then + i = i + 1 + for dep in string.gmatch(args[i], "[^,]+") do + table.insert(parsed.options.dependencies, dep) + end + elseif arg == "--output" then + i = i + 1 + parsed.options.output = args[i] + elseif arg == "--help" or arg == "-h" then + parsed.command = "help" + end + + i = i + 1 + end + + return parsed +end + +---Build config from parsed arguments +---@param parsed table +---@return PackageConfig +function M.build_config(parsed) + local defaults = generator.get_default_config(parsed.packageId) + + return { + packageId = parsed.packageId, + name = parsed.options.name or defaults.name, + description = parsed.options.description or defaults.description, + author = "MetaBuilder", + category = parsed.options.category, + minLevel = parsed.options.minLevel, + primary = parsed.options.primary, + withSchema = parsed.options.withSchema, + withTests = parsed.options.withTests, + withComponents = parsed.options.withComponents, + entities = parsed.options.entities, + components = parsed.options.components, + dependencies = parsed.options.dependencies + } +end + +---Execute the CLI +---@param args string[] +---@return number Exit code +function M.run(args) + local parsed = M.parse_args(args) + + if parsed.command == "help" or not parsed.command then + M.print_help() + return 0 + end + + if parsed.command == "list-categories" then + print("Available categories:") + for _, cat in ipairs(templates.get_categories()) do + print(" - " .. cat) + end + return 0 + end + + if parsed.command == "new" or parsed.command == "quick" then + if not parsed.packageId then + print("Error: package_id is required") + return 1 + end + + local config = M.build_config(parsed) + local valid, errors = generator.validate_config(config) + + if not valid then + print("Configuration errors:") + for _, err in ipairs(errors) do + print(" - " .. err) + end + return 1 + end + + local result = generator.generate(config) + + if not result.success then + print("Generation failed:") + for _, err in ipairs(result.errors) do + print(" - " .. err) + end + return 1 + end + + print("Generated package: " .. result.packagePath) + print("Files:") + for _, file in ipairs(result.files) do + print(" - " .. file.path) + end + + return 0 + end + + print("Unknown command: " .. (parsed.command or "none")) + M.print_help() + return 1 +end + +return M diff --git a/packages/codegen_studio/seed/scripts/package_template/generator.lua b/packages/codegen_studio/seed/scripts/package_template/generator.lua new file mode 100644 index 000000000..5659fd5e4 --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/generator.lua @@ -0,0 +1,335 @@ +-- Package generator - creates all files for a new package +-- @module package_template.generator + +local templates = require("package_template.templates") + +local M = {} + +---Get default package configuration +---@param packageId string +---@return PackageConfig +function M.get_default_config(packageId) + -- Convert package_id to display name + local name = packageId:gsub("_", " "):gsub("(%l)(%w*)", function(a, b) + return string.upper(a) .. b + end) + + return { + packageId = packageId, + name = name, + description = name .. " package for MetaBuilder", + author = "MetaBuilder", + category = "ui", + minLevel = 2, + primary = true, + withSchema = false, + withTests = true, + withComponents = false, + entities = {}, + components = {}, + dependencies = {} + } +end + +---Validate package configuration +---@param config PackageConfig +---@return boolean valid +---@return string[] errors +function M.validate_config(config) + local errors = {} + + -- Required fields + if not config.packageId then + table.insert(errors, "packageId is required") + elseif not string.match(config.packageId, "^[a-z][a-z0-9_]*$") then + table.insert(errors, "packageId must be lowercase with underscores, starting with a letter") + end + + if not config.name then + table.insert(errors, "name is required") + end + + if not config.description then + table.insert(errors, "description is required") + end + + if not config.category then + table.insert(errors, "category is required") + else + local validCategories = templates.get_categories() + local found = false + for _, cat in ipairs(validCategories) do + if cat == config.category then + found = true + break + end + end + if not found then + table.insert(errors, "category must be one of: " .. table.concat(validCategories, ", ")) + end + end + + if config.minLevel == nil then + table.insert(errors, "minLevel is required") + elseif type(config.minLevel) ~= "number" or config.minLevel < 0 or config.minLevel > 6 then + table.insert(errors, "minLevel must be a number between 0 and 6") + end + + if config.primary == nil then + table.insert(errors, "primary is required (true or false)") + end + + -- Validate entities if schema is requested + if config.withSchema then + if not config.entities or #config.entities == 0 then + table.insert(errors, "entities are required when withSchema is true") + else + for i, entity in ipairs(config.entities) do + if not string.match(entity, "^[A-Z][a-zA-Z0-9]*$") then + table.insert(errors, "entity[" .. i .. "] must be PascalCase (e.g., 'ForumPost')") + end + end + end + end + + -- Validate component names + if config.components then + for i, comp in ipairs(config.components) do + if not string.match(comp, "^[A-Z][a-zA-Z0-9]*$") then + table.insert(errors, "component[" .. i .. "] must be PascalCase (e.g., 'MyComponent')") + end + end + end + + return #errors == 0, errors +end + +---Generate all files for a new package +---@param config PackageConfig +---@return GenerateResult +function M.generate(config) + local valid, errors = M.validate_config(config) + if not valid then + return { + success = false, + files = {}, + errors = errors, + packagePath = "" + } + end + + local files = {} + local packagePath = "packages/" .. config.packageId + + -- Generate seed/metadata.json + table.insert(files, { + path = "seed/metadata.json", + content = templates.generate_metadata(config) + }) + + -- Generate seed/components.json + table.insert(files, { + path = "seed/components.json", + content = templates.generate_components_json(config) + }) + + -- Generate seed/layout.json + table.insert(files, { + path = "seed/layout.json", + content = templates.generate_layout_json(config) + }) + + -- Generate seed/scripts/init.lua + table.insert(files, { + path = "seed/scripts/init.lua", + content = templates.generate_init_lua(config) + }) + + -- Generate schema if requested + if config.withSchema and config.entities and #config.entities > 0 then + table.insert(files, { + path = "seed/schema/entities.yaml", + content = templates.generate_schema_yaml(config) + }) + + -- Generate db operations stub + table.insert(files, { + path = "seed/scripts/db/operations.lua", + content = M.generate_db_operations(config) + }) + end + + -- Generate tests if requested + if config.withTests then + table.insert(files, { + path = "seed/scripts/tests/metadata.test.lua", + content = templates.generate_test("metadata", config) + }) + + table.insert(files, { + path = "seed/scripts/tests/components.test.lua", + content = templates.generate_test("components", config) + }) + + table.insert(files, { + path = "seed/scripts/tests/metadata.cases.json", + content = templates.generate_test_cases("metadata") + }) + + table.insert(files, { + path = "seed/scripts/tests/components.cases.json", + content = templates.generate_test_cases("components") + }) + end + + -- Generate static content + table.insert(files, { + path = "static_content/icon.svg", + content = templates.generate_icon_svg(config) + }) + + -- Generate README + table.insert(files, { + path = "README.md", + content = templates.generate_readme(config) + }) + + -- Generate index.ts for TypeScript exports + table.insert(files, { + path = "seed/index.ts", + content = M.generate_index_ts(config) + }) + + return { + success = true, + files = files, + errors = {}, + packagePath = packagePath + } +end + +---Generate db operations Lua file +---@param config PackageConfig +---@return string +function M.generate_db_operations(config) + local lines = { + "-- Database operations for " .. config.name, + "-- Auto-generated by package template generator", + "", + "local M = {}", + "" + } + + if config.entities then + for _, entity in ipairs(config.entities) do + local entityLower = string.lower(entity) + + table.insert(lines, "-- " .. entity .. " operations") + table.insert(lines, "") + + -- List + table.insert(lines, "---List all " .. entity .. " records") + table.insert(lines, "---@param ctx DBALContext") + table.insert(lines, "---@return table[]") + table.insert(lines, "function M.list_" .. entityLower .. "(ctx)") + table.insert(lines, " -- TODO: Implement list operation") + table.insert(lines, " return {}") + table.insert(lines, "end") + table.insert(lines, "") + + -- Get + table.insert(lines, "---Get a single " .. entity .. " by ID") + table.insert(lines, "---@param ctx DBALContext") + table.insert(lines, "---@param id string") + table.insert(lines, "---@return table|nil") + table.insert(lines, "function M.get_" .. entityLower .. "(ctx, id)") + table.insert(lines, " -- TODO: Implement get operation") + table.insert(lines, " return nil") + table.insert(lines, "end") + table.insert(lines, "") + + -- Create + table.insert(lines, "---Create a new " .. entity) + table.insert(lines, "---@param ctx DBALContext") + table.insert(lines, "---@param data table") + table.insert(lines, "---@return table") + table.insert(lines, "function M.create_" .. entityLower .. "(ctx, data)") + table.insert(lines, " -- TODO: Implement create operation") + table.insert(lines, " return data") + table.insert(lines, "end") + table.insert(lines, "") + + -- Update + table.insert(lines, "---Update an existing " .. entity) + table.insert(lines, "---@param ctx DBALContext") + table.insert(lines, "---@param id string") + table.insert(lines, "---@param data table") + table.insert(lines, "---@return table|nil") + table.insert(lines, "function M.update_" .. entityLower .. "(ctx, id, data)") + table.insert(lines, " -- TODO: Implement update operation") + table.insert(lines, " return nil") + table.insert(lines, "end") + table.insert(lines, "") + + -- Delete + table.insert(lines, "---Delete a " .. entity) + table.insert(lines, "---@param ctx DBALContext") + table.insert(lines, "---@param id string") + table.insert(lines, "---@return boolean") + table.insert(lines, "function M.delete_" .. entityLower .. "(ctx, id)") + table.insert(lines, " -- TODO: Implement delete operation") + table.insert(lines, " return false") + table.insert(lines, "end") + table.insert(lines, "") + end + end + + table.insert(lines, "return M") + + return table.concat(lines, "\n") +end + +---Generate TypeScript index file +---@param config PackageConfig +---@return string +function M.generate_index_ts(config) + local lines = { + "// " .. config.name .. " package exports", + "// Auto-generated by package template generator", + "", + "import metadata from './metadata.json'", + "import components from './components.json'", + "import layout from './layout.json'", + "", + "export const packageSeed = {", + " metadata,", + " components,", + " layout,", + "}", + "", + "export default packageSeed", + "" + } + + return table.concat(lines, "\n") +end + +---Generate package to file system (would need file I/O) +---@param config PackageConfig +---@param basePath string Base path for packages directory +---@return GenerateResult +function M.generate_to_disk(config, basePath) + local result = M.generate(config) + + if not result.success then + return result + end + + -- In actual implementation, this would write files to disk + -- For now, return the generated structure + result.packagePath = basePath .. "/" .. config.packageId + + return result +end + +return M diff --git a/packages/codegen_studio/seed/scripts/package_template/init.lua b/packages/codegen_studio/seed/scripts/package_template/init.lua new file mode 100644 index 000000000..fc2fc8287 --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/init.lua @@ -0,0 +1,49 @@ +-- Package Template Generator +-- Generates new MetaBuilder packages with proper structure +-- @module package_template + +local M = {} + +local templates = require("package_template.templates") +local generator = require("package_template.generator") + +---@class PackageConfig +---@field packageId string Package identifier (lowercase with underscores) +---@field name string Display name +---@field description string Package description +---@field author? string Author name (default: "MetaBuilder") +---@field category string Package category +---@field minLevel number Minimum access level (0-6) +---@field primary boolean Whether package can own routes +---@field withSchema boolean Include database schema +---@field withTests boolean Include test scaffolding +---@field withComponents boolean Include component scaffolding +---@field entities? string[] Entity names for schema (if withSchema) +---@field components? string[] Component names to scaffold +---@field dependencies? string[] Package dependencies +---@field permissions? table Permission declarations + +---@class PermissionConfig +---@field minLevel number Minimum level required +---@field description string Permission description + +---@class GenerateResult +---@field success boolean Whether generation succeeded +---@field files GeneratedFile[] List of generated files +---@field errors string[] Any errors encountered +---@field packagePath string Root path of generated package + +---@class GeneratedFile +---@field path string Relative path within package +---@field content string File content + +-- Re-export main functions +M.generate = generator.generate +M.generate_metadata = templates.generate_metadata +M.generate_component = templates.generate_component +M.generate_test = templates.generate_test +M.validate_config = generator.validate_config +M.get_template_categories = templates.get_categories +M.get_default_config = generator.get_default_config + +return M diff --git a/packages/codegen_studio/seed/scripts/package_template/templates.lua b/packages/codegen_studio/seed/scripts/package_template/templates.lua new file mode 100644 index 000000000..8b00309cb --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/templates.lua @@ -0,0 +1,464 @@ +-- Package template definitions +-- @module package_template.templates + +local M = {} + +---@return string[] Available package categories +function M.get_categories() + return { + "ui", + "editors", + "tools", + "social", + "media", + "gaming", + "admin", + "config", + "core", + "demo", + "development", + "managers" + } +end + +---Generate metadata.json content +---@param config PackageConfig +---@return string JSON content +function M.generate_metadata(config) + local metadata = { + packageId = config.packageId, + name = config.name, + version = "1.0.0", + description = config.description, + icon = "static_content/icon.svg", + author = config.author or "MetaBuilder", + category = config.category, + primary = config.primary, + dependencies = config.dependencies or {}, + devDependencies = { "lua_test" }, + exports = { + components = config.components or {}, + scripts = { "init" } + }, + tests = { + scripts = { "tests/metadata.test.lua", "tests/components.test.lua" }, + cases = { "tests/metadata.cases.json", "tests/components.cases.json" } + }, + minLevel = config.minLevel + } + + -- Add schema if requested + if config.withSchema and config.entities and #config.entities > 0 then + metadata.schema = { + entities = config.entities, + path = "schema/entities.yaml" + } + end + + -- Add permissions + if config.permissions then + metadata.permissions = {} + for permKey, permConfig in pairs(config.permissions) do + metadata.permissions[permKey] = { + minLevel = permConfig.minLevel, + description = permConfig.description + } + end + else + -- Generate default permissions based on package type + metadata.permissions = M.generate_default_permissions(config) + end + + return M.to_json(metadata) +end + +---Generate default permissions based on package config +---@param config PackageConfig +---@return table +function M.generate_default_permissions(config) + local prefix = string.gsub(config.packageId, "_", ".") + local permissions = {} + + -- Basic view permission + permissions[prefix .. ".view"] = { + minLevel = config.minLevel, + description = "View " .. config.name + } + + -- If primary, add more permissions + if config.primary then + permissions[prefix .. ".edit"] = { + minLevel = config.minLevel, + description = "Edit " .. config.name .. " content" + } + end + + -- If has schema, add CRUD permissions + if config.withSchema and config.entities then + for _, entity in ipairs(config.entities) do + local entityLower = string.lower(entity) + permissions[prefix .. "." .. entityLower .. ".create"] = { + minLevel = config.minLevel, + description = "Create " .. entity + } + permissions[prefix .. "." .. entityLower .. ".update"] = { + minLevel = config.minLevel, + description = "Update " .. entity + } + permissions[prefix .. "." .. entityLower .. ".delete"] = { + minLevel = math.min(config.minLevel + 1, 6), + description = "Delete " .. entity + } + end + end + + return permissions +end + +---Generate init.lua content +---@param config PackageConfig +---@return string Lua content +function M.generate_init_lua(config) + local lines = { + "--- " .. config.name .. " initialization", + "--- @module init", + "", + "local M = {}", + "", + "---@class InstallContext", + "---@field version string", + "", + "---@class InstallResult", + "---@field message string", + "---@field version string", + "", + "---Called when package is installed", + "---@param context InstallContext", + "---@return InstallResult", + "function M.on_install(context)", + " return {", + " message = \"" .. config.name .. " installed successfully\",", + " version = context.version", + " }", + "end", + "", + "---Called when package is uninstalled", + "---@return table", + "function M.on_uninstall()", + " return { message = \"" .. config.name .. " removed\" }", + "end", + "", + "return M" + } + return table.concat(lines, "\n") +end + +---Generate component.json entry +---@param componentName string +---@param config PackageConfig +---@return table Component definition +function M.generate_component(componentName, config) + local componentId = string.lower(config.packageId .. "_" .. componentName) + + return { + id = componentId, + type = "container", + name = componentName, + description = componentName .. " component for " .. config.name, + props = {}, + layout = { + type = "flex", + props = { + direction = "column", + gap = 2 + } + }, + bindings = {} + } +end + +---Generate components.json content +---@param config PackageConfig +---@return string JSON content +function M.generate_components_json(config) + local components = {} + + if config.components then + for _, name in ipairs(config.components) do + table.insert(components, M.generate_component(name, config)) + end + end + + return M.to_json(components) +end + +---Generate test file content +---@param testName string Test name +---@param config PackageConfig +---@return string Lua test content +function M.generate_test(testName, config) + local lines = { + "-- " .. testName .. " tests for " .. config.packageId, + "", + "describe(\"" .. config.name .. " - " .. testName .. "\", function()", + " it(\"should pass basic validation\", function()", + " expect(true).toBe(true)", + " end)", + "", + " it(\"should have required fields\", function()", + " -- TODO: Add specific tests", + " expect(true).toBe(true)", + " end)", + "end)", + "" + } + return table.concat(lines, "\n") +end + +---Generate test cases JSON +---@param testName string +---@return string JSON content +function M.generate_test_cases(testName) + local cases = { + { + name = "valid_input", + input = {}, + expected = { valid = true } + }, + { + name = "invalid_input", + input = { invalid = true }, + expected = { valid = false } + } + } + return M.to_json(cases) +end + +---Generate schema YAML content +---@param config PackageConfig +---@return string YAML content +function M.generate_schema_yaml(config) + if not config.entities or #config.entities == 0 then + return "# No entities defined\n" + end + + local lines = { + "# " .. config.name .. " Entity Definitions", + "# Auto-generated by package template generator", + "" + } + + for _, entity in ipairs(config.entities) do + local prefixedEntity = "Pkg_" .. M.to_pascal_case(config.packageId) .. "_" .. entity + table.insert(lines, prefixedEntity .. ":") + table.insert(lines, " description: \"" .. entity .. " entity for " .. config.name .. "\"") + table.insert(lines, " fields:") + table.insert(lines, " id:") + table.insert(lines, " type: string") + table.insert(lines, " primary: true") + table.insert(lines, " tenantId:") + table.insert(lines, " type: string") + table.insert(lines, " required: true") + table.insert(lines, " index: true") + table.insert(lines, " createdAt:") + table.insert(lines, " type: datetime") + table.insert(lines, " default: now") + table.insert(lines, " updatedAt:") + table.insert(lines, " type: datetime") + table.insert(lines, " onUpdate: now") + table.insert(lines, " # TODO: Add entity-specific fields") + table.insert(lines, "") + end + + return table.concat(lines, "\n") +end + +---Generate layout.json content +---@param config PackageConfig +---@return string JSON content +function M.generate_layout_json(config) + local layout = { + id = config.packageId .. "_layout", + name = config.name .. " Layout", + type = "page", + props = { + title = config.name, + minLevel = config.minLevel + }, + children = {} + } + + -- Add header section + table.insert(layout.children, { + id = config.packageId .. "_header", + type = "container", + props = { variant = "header" }, + children = { + { + id = config.packageId .. "_title", + type = "text", + props = { variant = "h1", content = config.name } + }, + { + id = config.packageId .. "_description", + type = "text", + props = { variant = "body1", content = config.description } + } + } + }) + + -- Add main content section + table.insert(layout.children, { + id = config.packageId .. "_content", + type = "container", + props = { variant = "main" }, + children = { + { + id = config.packageId .. "_placeholder", + type = "text", + props = { content = "Add your components here" } + } + } + }) + + return M.to_json(layout) +end + +---Generate icon SVG +---@param config PackageConfig +---@return string SVG content +function M.generate_icon_svg(config) + -- Get first letter of package name for icon + local letter = string.upper(string.sub(config.name, 1, 1)) + + return [[ + + ]] .. letter .. [[ +]] +end + +---Generate README.md content +---@param config PackageConfig +---@return string Markdown content +function M.generate_readme(config) + local lines = { + "# " .. config.name, + "", + config.description, + "", + "## Installation", + "", + "This package is part of the MetaBuilder platform and is installed automatically.", + "", + "## Access Level", + "", + "Minimum level required: **" .. config.minLevel .. "**", + "", + } + + if config.primary then + table.insert(lines, "This is a **primary package** that can own routes.") + else + table.insert(lines, "This is a **dependency package** that provides shared functionality.") + end + table.insert(lines, "") + + if config.withSchema and config.entities and #config.entities > 0 then + table.insert(lines, "## Entities") + table.insert(lines, "") + for _, entity in ipairs(config.entities) do + table.insert(lines, "- " .. entity) + end + table.insert(lines, "") + end + + if config.components and #config.components > 0 then + table.insert(lines, "## Components") + table.insert(lines, "") + for _, comp in ipairs(config.components) do + table.insert(lines, "- `" .. comp .. "`") + end + table.insert(lines, "") + end + + table.insert(lines, "## Development") + table.insert(lines, "") + table.insert(lines, "```bash") + table.insert(lines, "# Run tests") + table.insert(lines, "npm run test:package " .. config.packageId) + table.insert(lines, "```") + table.insert(lines, "") + + return table.concat(lines, "\n") +end + +---Convert table to JSON string (simple implementation) +---@param tbl table +---@return string +function M.to_json(tbl) + -- Use the global json if available, otherwise simple stringify + if json and json.encode then + return json.encode(tbl) + end + + -- Simple fallback - in real usage would use proper JSON library + return M.stringify(tbl, 0) +end + +---Simple table to JSON string converter +---@param val any +---@param indent number +---@return string +function M.stringify(val, indent) + local t = type(val) + + if t == "nil" then + return "null" + elseif t == "boolean" then + return val and "true" or "false" + elseif t == "number" then + return tostring(val) + elseif t == "string" then + return '"' .. val:gsub('"', '\\"'):gsub('\n', '\\n') .. '"' + elseif t == "table" then + local spaces = string.rep(" ", indent) + local nextSpaces = string.rep(" ", indent + 1) + + -- Check if array + local isArray = #val > 0 or next(val) == nil + if isArray then + local items = {} + for i, v in ipairs(val) do + table.insert(items, nextSpaces .. M.stringify(v, indent + 1)) + end + if #items == 0 then + return "[]" + end + return "[\n" .. table.concat(items, ",\n") .. "\n" .. spaces .. "]" + else + local items = {} + for k, v in pairs(val) do + table.insert(items, nextSpaces .. '"' .. tostring(k) .. '": ' .. M.stringify(v, indent + 1)) + end + if #items == 0 then + return "{}" + end + return "{\n" .. table.concat(items, ",\n") .. "\n" .. spaces .. "}" + end + end + + return "null" +end + +---Convert string to PascalCase +---@param str string +---@return string +function M.to_pascal_case(str) + local result = "" + for word in string.gmatch(str, "[^_]+") do + result = result .. string.upper(string.sub(word, 1, 1)) .. string.sub(word, 2) + end + return result +end + +return M diff --git a/packages/codegen_studio/seed/scripts/package_template/tests/generator.cases.json b/packages/codegen_studio/seed/scripts/package_template/tests/generator.cases.json new file mode 100644 index 000000000..ec37073d4 --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/tests/generator.cases.json @@ -0,0 +1,110 @@ +[ + { + "name": "valid_basic_config", + "input": { + "packageId": "test_package", + "name": "Test Package", + "description": "A test package", + "category": "ui", + "minLevel": 2, + "primary": true, + "withSchema": false, + "withTests": true + }, + "expected": { + "valid": true, + "errorCount": 0 + } + }, + { + "name": "valid_with_schema", + "input": { + "packageId": "data_package", + "name": "Data Package", + "description": "Package with schema", + "category": "tools", + "minLevel": 3, + "primary": true, + "withSchema": true, + "entities": ["DataRecord", "DataLog"] + }, + "expected": { + "valid": true, + "errorCount": 0 + } + }, + { + "name": "invalid_package_id_uppercase", + "input": { + "packageId": "TestPackage", + "name": "Test", + "description": "Test", + "category": "ui", + "minLevel": 2, + "primary": true + }, + "expected": { + "valid": false + } + }, + { + "name": "invalid_category", + "input": { + "packageId": "test_package", + "name": "Test", + "description": "Test", + "category": "not_a_category", + "minLevel": 2, + "primary": true + }, + "expected": { + "valid": false + } + }, + { + "name": "invalid_min_level_too_high", + "input": { + "packageId": "test_package", + "name": "Test", + "description": "Test", + "category": "ui", + "minLevel": 10, + "primary": true + }, + "expected": { + "valid": false + } + }, + { + "name": "missing_entities_for_schema", + "input": { + "packageId": "test_package", + "name": "Test", + "description": "Test", + "category": "ui", + "minLevel": 2, + "primary": true, + "withSchema": true, + "entities": [] + }, + "expected": { + "valid": false + } + }, + { + "name": "dependency_package", + "input": { + "packageId": "shared_utils", + "name": "Shared Utils", + "description": "Shared utilities", + "category": "core", + "minLevel": 1, + "primary": false, + "withSchema": false + }, + "expected": { + "valid": true, + "errorCount": 0 + } + } +] diff --git a/packages/codegen_studio/seed/scripts/package_template/tests/generator.test.lua b/packages/codegen_studio/seed/scripts/package_template/tests/generator.test.lua new file mode 100644 index 000000000..ced1e7f2f --- /dev/null +++ b/packages/codegen_studio/seed/scripts/package_template/tests/generator.test.lua @@ -0,0 +1,217 @@ +-- Package template generator tests +local package_template = require("package_template") + +describe("Package Template Generator", function() + + describe("get_default_config", function() + it("should generate default config from package_id", function() + local config = package_template.get_default_config("my_test_package") + + expect(config.packageId).toBe("my_test_package") + expect(config.name).toBe("My Test Package") + expect(config.primary).toBe(true) + expect(config.minLevel).toBe(2) + expect(config.category).toBe("ui") + end) + end) + + describe("validate_config", function() + it("should pass for valid config", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true, + withSchema = false, + withTests = true, + withComponents = false + } + + local valid, errors = package_template.validate_config(config) + expect(valid).toBe(true) + expect(#errors).toBe(0) + end) + + it("should fail for invalid packageId", function() + local config = { + packageId = "TestPackage", -- Should be lowercase + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true + } + + local valid, errors = package_template.validate_config(config) + expect(valid).toBe(false) + end) + + it("should fail for invalid category", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "invalid_category", + minLevel = 2, + primary = true + } + + local valid, errors = package_template.validate_config(config) + expect(valid).toBe(false) + end) + + it("should require entities when withSchema is true", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true, + withSchema = true, + entities = {} -- Empty + } + + local valid, errors = package_template.validate_config(config) + expect(valid).toBe(false) + end) + + it("should validate entity names are PascalCase", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true, + withSchema = true, + entities = { "invalid_entity" } -- Should be PascalCase + } + + local valid, errors = package_template.validate_config(config) + expect(valid).toBe(false) + end) + end) + + describe("generate", function() + it("should generate all required files", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true, + withSchema = false, + withTests = true, + withComponents = false + } + + local result = package_template.generate(config) + + expect(result.success).toBe(true) + expect(#result.files).toBeGreaterThan(0) + expect(result.packagePath).toBe("packages/test_package") + end) + + it("should include schema files when withSchema is true", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true, + withSchema = true, + withTests = true, + entities = { "TestEntity" } + } + + local result = package_template.generate(config) + + expect(result.success).toBe(true) + + -- Check for schema file + local hasSchema = false + for _, file in ipairs(result.files) do + if string.match(file.path, "schema/entities.yaml") then + hasSchema = true + break + end + end + expect(hasSchema).toBe(true) + end) + + it("should fail for invalid config", function() + local config = { + packageId = "InvalidPackage", -- Invalid + name = "Test", + description = "Test", + category = "ui", + minLevel = 2, + primary = true + } + + local result = package_template.generate(config) + + expect(result.success).toBe(false) + expect(#result.errors).toBeGreaterThan(0) + end) + end) + + describe("generate_metadata", function() + it("should generate valid JSON", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true + } + + local json = package_template.generate_metadata(config) + + expect(type(json)).toBe("string") + expect(string.match(json, '"packageId"')).toBeTruthy() + expect(string.match(json, '"test_package"')).toBeTruthy() + end) + + it("should include permissions", function() + local config = { + packageId = "test_package", + name = "Test Package", + description = "A test package", + category = "ui", + minLevel = 2, + primary = true + } + + local json = package_template.generate_metadata(config) + + expect(string.match(json, '"permissions"')).toBeTruthy() + end) + end) + + describe("get_template_categories", function() + it("should return list of categories", function() + local categories = package_template.get_template_categories() + + expect(type(categories)).toBe("table") + expect(#categories).toBeGreaterThan(0) + + -- Check for some expected categories + local hasUI = false + local hasTools = false + for _, cat in ipairs(categories) do + if cat == "ui" then hasUI = true end + if cat == "tools" then hasTools = true end + end + expect(hasUI).toBe(true) + expect(hasTools).toBe(true) + end) + end) + +end)