config: studio,packages,codegen (6 files)

This commit is contained in:
Richard Ward
2025-12-30 23:27:30 +00:00
parent 6b8cbf23d7
commit 84392b4c12
6 changed files with 1382 additions and 0 deletions

View File

@@ -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 <command> [options]
Commands:
new <package_id> Create a new package with interactive prompts
quick <package_id> Create a new package with defaults
list-categories List available package categories
validate <config> Validate a package configuration
Options for 'new' and 'quick':
--name <name> Display name (default: derived from package_id)
--description <desc> Package description
--category <cat> Package category (default: ui)
--min-level <n> 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 <e1,e2> Entity names for schema (comma-separated)
--with-components Include component scaffolding
--components <c1,c2> Component names (comma-separated)
--deps <d1,d2> Package dependencies (comma-separated)
--output <path> 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

View File

@@ -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

View File

@@ -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<string, PermissionConfig> 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

View File

@@ -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<string, 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 [[<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<text x="12" y="16" text-anchor="middle" font-size="12" fill="currentColor" stroke="none">]] .. letter .. [[</text>
</svg>]]
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

View File

@@ -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
}
}
]

View File

@@ -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)