Refactor package validation and quick guide modules

- Simplified structure validation by re-exporting functions from structure_config and validate_structure modules for better maintainability.
- Consolidated validation logic in validate.lua, delegating to validate_package module for clearer orchestration.
- Introduced new quick guide functionalities including step management (add, remove, update, reset ordering) and media handling (thumbnail and video URL validation).
- Added utility functions for URL validation and step creation, enhancing the quick guide's usability.
- Established type definitions for steps and media states to improve code clarity and type safety.
- Enhanced schema editor with new field and relation definitions, providing a more robust structure for database schema management.
This commit is contained in:
2025-12-30 11:17:40 +00:00
parent 73fb98d053
commit 9d27207fbc
30 changed files with 630 additions and 1092 deletions

View File

@@ -1,130 +1,30 @@
-- CLI tool for validating packages
-- Usage: lua cli.lua <package_name> [options]
--- CLI tool facade for validating packages
--- Usage: lua cli.lua <package_name> [options]
---@module cli
local validate = require("validate")
local parse_args = require("parse_args")
local print_help = require("print_help")
local output_json = require("output_json")
local output_human = require("output_human")
---@class CLI
local M = {}
-- Parse command line arguments
function M.parse_args(args)
local options = {
package_name = nil,
skipStructure = false,
skipLua = false,
verbose = false,
json_output = false
}
M.parse_args = parse_args
M.print_help = print_help
M.output_json = output_json
M.output_human = output_human
local i = 1
while i <= #args do
local arg = args[i]
if arg == "--skip-structure" then
options.skipStructure = true
elseif arg == "--skip-lua" then
options.skipLua = true
elseif arg == "--verbose" or arg == "-v" then
options.verbose = true
elseif arg == "--json" then
options.json_output = true
elseif arg == "--help" or arg == "-h" then
M.print_help()
os.exit(0)
elseif not options.package_name then
options.package_name = arg
end
i = i + 1
end
return options
end
-- Print help message
function M.print_help()
print([[
Package Validator CLI
Usage:
lua cli.lua <package_name> [options]
Arguments:
package_name Name of the package to validate
Options:
--skip-structure Skip folder structure validation
--skip-lua Skip Lua file validation
--verbose, -v Show detailed validation information
--json Output results as JSON
--help, -h Show this help message
Examples:
# Validate audit_log package
lua cli.lua audit_log
# Validate with verbose output
lua cli.lua audit_log --verbose
# Skip structure validation
lua cli.lua audit_log --skip-structure
# Output as JSON (for CI/CD integration)
lua cli.lua audit_log --json
Exit Codes:
0 - Validation passed
1 - Validation failed
2 - Invalid arguments or package not found
]])
end
-- Output results as JSON
function M.output_json(results)
local json_output = {
valid = results.valid,
errors = results.errors,
warnings = results.warnings
}
-- Simple JSON serialization
local function serialize_array(arr)
local items = {}
for _, item in ipairs(arr) do
table.insert(items, '"' .. item:gsub('"', '\\"') .. '"')
end
return "[" .. table.concat(items, ",") .. "]"
end
print("{")
print(' "valid": ' .. tostring(results.valid) .. ',')
print(' "errors": ' .. serialize_array(results.errors) .. ',')
print(' "warnings": ' .. serialize_array(results.warnings))
print("}")
end
-- Output results in human-readable format
function M.output_human(results, verbose)
local output = validate.format_results(results)
print(output)
if verbose and #results.errors > 0 then
print("\n--- Detailed Error Information ---")
for i, err in ipairs(results.errors) do
print(i .. ". " .. err)
end
end
if verbose and #results.warnings > 0 then
print("\n--- Detailed Warning Information ---")
for i, warn in ipairs(results.warnings) do
print(i .. ". " .. warn)
end
end
end
-- Main entry point
--- Main entry point
---@param args string[] Command line arguments
function M.run(args)
local options = M.parse_args(args)
local options = parse_args(args)
if options.show_help then
print_help()
os.exit(0)
end
if not options.package_name then
print("Error: Package name is required")
@@ -151,9 +51,9 @@ function M.run(args)
-- Output results
if options.json_output then
M.output_json(results)
output_json(results)
else
M.output_human(results, options.verbose)
output_human(results, options.verbose)
end
-- Exit with appropriate code

View File

@@ -1,126 +1,30 @@
-- Component JSON schema definitions
--- Component JSON schema validation facade
--- Re-exports component validation functions for backward compatibility
---@module component_schema
local validate_component = require("validate_component")
local validate_layout = require("validate_layout")
local validate_components = require("validate_components")
---@class ComponentSchema
local M = {}
-- Validate a single component structure
function M.validate_component(component, index)
local errors = {}
local prefix = index and ("components[" .. index .. "]") or "component"
--- Validate a single component structure
---@param component Component The component to validate
---@param index? number Optional component index for error messages
---@return string[] errors List of validation errors
M.validate_component = validate_component
-- Required fields
if not component.id then
table.insert(errors, prefix .. ": Missing required field 'id'")
elseif type(component.id) ~= "string" then
table.insert(errors, prefix .. ": 'id' must be a string")
end
--- Validate layout structure recursively
---@param layout ComponentLayout The layout to validate
---@param path string The current path for error messages
---@return string[] errors List of validation errors
M.validate_layout = validate_layout
if not component.type then
table.insert(errors, prefix .. ": Missing required field 'type'")
elseif type(component.type) ~= "string" then
table.insert(errors, prefix .. ": 'type' must be a string")
end
-- Optional but recommended fields
if component.name and type(component.name) ~= "string" then
table.insert(errors, prefix .. ": 'name' must be a string")
end
if component.description and type(component.description) ~= "string" then
table.insert(errors, prefix .. ": 'description' must be a string")
end
-- Validate props if present
if component.props then
if type(component.props) ~= "table" then
table.insert(errors, prefix .. ": 'props' must be an object")
end
end
-- Validate layout if present
if component.layout then
if type(component.layout) ~= "table" then
table.insert(errors, prefix .. ": 'layout' must be an object")
else
local layout_errors = M.validate_layout(component.layout, prefix .. ".layout")
for _, err in ipairs(layout_errors) do
table.insert(errors, err)
end
end
end
-- Validate scripts if present
if component.scripts then
if type(component.scripts) ~= "table" then
table.insert(errors, prefix .. ": 'scripts' must be an object")
end
end
-- Validate bindings if present
if component.bindings then
if type(component.bindings) ~= "table" then
table.insert(errors, prefix .. ": 'bindings' must be an object")
else
if component.bindings.dbal ~= nil and type(component.bindings.dbal) ~= "boolean" then
table.insert(errors, prefix .. ": 'bindings.dbal' must be a boolean")
end
if component.bindings.browser ~= nil and type(component.bindings.browser) ~= "boolean" then
table.insert(errors, prefix .. ": 'bindings.browser' must be a boolean")
end
end
end
return errors
end
-- Validate layout structure recursively
function M.validate_layout(layout, path)
local errors = {}
if not layout.type then
table.insert(errors, path .. ": Missing required field 'type'")
elseif type(layout.type) ~= "string" then
table.insert(errors, path .. ": 'type' must be a string")
end
if layout.props and type(layout.props) ~= "table" then
table.insert(errors, path .. ": 'props' must be an object")
end
if layout.children then
if type(layout.children) ~= "table" then
table.insert(errors, path .. ": 'children' must be an array")
else
for i, child in ipairs(layout.children) do
if type(child) == "table" then
local child_errors = M.validate_layout(child, path .. ".children[" .. i .. "]")
for _, err in ipairs(child_errors) do
table.insert(errors, err)
end
end
end
end
end
return errors
end
-- Validate components.json (array of components)
function M.validate_components(components)
local errors = {}
if type(components) ~= "table" then
table.insert(errors, "components.json must be an array")
return false, errors
end
-- Validate each component
for i, component in ipairs(components) do
local comp_errors = M.validate_component(component, i)
for _, err in ipairs(comp_errors) do
table.insert(errors, err)
end
end
return #errors == 0, errors
end
--- Validate components.json (array of components)
---@param components Component[] Array of component definitions
---@return boolean valid Whether all components are valid
---@return string[] errors List of validation errors
M.validate_components = validate_components
return M

View File

@@ -1,9 +1,14 @@
-- Schema Validator initialization
--- Schema Validator initialization
--- Package validator entry point providing validation utilities
---@module init
local validate = require("validate")
---@class PackageValidator
local M = {}
-- Initialize the validator
--- Initialize the validator
---@return { name: string, version: string, description: string } info Validator info
function M.init()
return {
name = "Schema Validator",
@@ -12,18 +17,26 @@ function M.init()
}
end
-- Main validation entry point
--- Main validation entry point
---@param package_id string Package identifier to validate
---@return ValidationResult results Validation results
function M.validate_package(package_id)
local package_path = "packages/" .. package_id .. "/seed"
return validate.validate_package(package_path)
end
-- Quick validation for metadata
--- Quick validation for metadata
---@param metadata Metadata Metadata to validate
---@return boolean valid Whether valid
---@return string[] errors List of errors
function M.validate_metadata(metadata)
return validate.validate_metadata_only(metadata)
end
-- Quick validation for components
--- Quick validation for components
---@param components Component[] Components to validate
---@return boolean valid Whether valid
---@return string[] errors List of errors
function M.validate_components(components)
return validate.validate_components_only(components)
end

View File

@@ -1,146 +1,24 @@
-- Lua file validation
--- Lua file validation facade
--- Re-exports Lua validation functions for backward compatibility
---@module lua_validator
local validate_lua_syntax = require("validate_lua_syntax")
local validate_lua_structure = require("validate_lua_structure")
local validate_test_file = require("validate_test_file")
local validate_script_exports = require("validate_script_exports")
local validate_package_lua_files = require("validate_package_lua_files")
local check_lua_quality = require("check_lua_quality")
local validate_lua_requires = require("validate_lua_requires")
---@class LuaValidator
local M = {}
-- Check if Lua file has valid syntax
function M.validate_lua_syntax(filepath, content)
local errors = {}
-- Try to load the Lua content
local func, err = loadstring(content)
if not func then
table.insert(errors, filepath .. ": Syntax error - " .. (err or "unknown error"))
return false, errors
end
return true, errors
end
-- Check if Lua file follows common patterns
function M.validate_lua_structure(filepath, content)
local warnings = {}
-- Check for module pattern
if not string.match(content, "local%s+M%s*=%s*{}") and
not string.match(content, "local%s+[%w_]+%s*=%s*{}") then
table.insert(warnings, filepath .. ": Missing module pattern (local M = {})")
end
-- Check for return statement
if not string.match(content, "return%s+[%w_]+") then
table.insert(warnings, filepath .. ": Missing return statement")
end
return warnings
end
-- Validate test file structure
function M.validate_test_file(filepath, content)
local errors = {}
local warnings = {}
-- Check for describe blocks
if not string.match(content, "describe%(") then
table.insert(warnings, filepath .. ": Missing describe() blocks")
end
-- Check for it/test blocks
if not string.match(content, "it%(") and not string.match(content, "test%(") then
table.insert(warnings, filepath .. ": Missing it() or test() blocks")
end
-- Check for expect assertions
if not string.match(content, "expect%(") then
table.insert(warnings, filepath .. ": Missing expect() assertions")
end
return errors, warnings
end
-- Validate script exports match metadata
function M.validate_script_exports(package_path, metadata)
local errors = {}
local warnings = {}
if not metadata.exports or not metadata.exports.scripts then
return errors, warnings
end
local scripts_path = package_path .. "/scripts"
-- Check each exported script exists
for _, script_name in ipairs(metadata.exports.scripts) do
local script_file = scripts_path .. "/" .. script_name .. ".lua"
-- Check if file exists
local file = io.open(script_file, "r")
if not file then
table.insert(errors, "Exported script not found: " .. script_name .. ".lua")
else
file:close()
end
end
return errors, warnings
end
-- Validate all Lua files in a package
function M.validate_package_lua_files(package_path)
local results = {
valid = true,
errors = {},
warnings = {}
}
local scripts_path = package_path .. "/scripts"
-- Find all Lua files
local lua_files = {}
-- Note: In real implementation, this would recursively find all .lua files
-- For now, we'll validate the pattern
if not file_exists(scripts_path) then
table.insert(results.warnings, "No scripts directory found")
return results
end
return results
end
-- Check for common Lua anti-patterns
function M.check_lua_quality(filepath, content)
local warnings = {}
-- Check for global variables (potential issue)
if string.match(content, "[^%w_]function%s+[%w_]+%(") then
table.insert(warnings, filepath .. ": Global function definition found (consider local)")
end
-- Check for TODO comments
if string.match(content, "TODO") or string.match(content, "FIXME") then
table.insert(warnings, filepath .. ": Contains TODO/FIXME comments")
end
-- Check for print statements (should use proper logging)
local print_count = select(2, string.gsub(content, "print%(", ""))
if print_count > 0 then
table.insert(warnings, filepath .. ": Contains " .. print_count .. " print() statements")
end
return warnings
end
-- Validate Lua file dependencies
function M.validate_lua_requires(filepath, content)
local errors = {}
local requires = {}
-- Extract all require statements
for req in string.gmatch(content, 'require%s*%(%s*["\']([^"\']+)["\']%s*%)') do
table.insert(requires, req)
end
return requires, errors
end
M.validate_lua_syntax = validate_lua_syntax
M.validate_lua_structure = validate_lua_structure
M.validate_test_file = validate_test_file
M.validate_script_exports = validate_script_exports
M.validate_package_lua_files = validate_package_lua_files
M.check_lua_quality = check_lua_quality
M.validate_lua_requires = validate_lua_requires
return M

View File

@@ -1,126 +1,16 @@
-- Metadata JSON schema definitions
--- Metadata JSON schema validation facade
--- Re-exports validate_metadata for backward compatibility
---@module metadata_schema
local validate_metadata = require("validate_metadata")
---@class MetadataSchema
local M = {}
-- Validate metadata.json structure
function M.validate_metadata(metadata)
local errors = {}
-- Required fields
if not metadata.packageId then
table.insert(errors, "Missing required field: packageId")
elseif type(metadata.packageId) ~= "string" then
table.insert(errors, "packageId must be a string")
elseif not string.match(metadata.packageId, "^[a-z_]+$") then
table.insert(errors, "packageId must contain only lowercase letters and underscores")
end
if not metadata.name then
table.insert(errors, "Missing required field: name")
elseif type(metadata.name) ~= "string" then
table.insert(errors, "name must be a string")
end
if not metadata.version then
table.insert(errors, "Missing required field: version")
elseif type(metadata.version) ~= "string" then
table.insert(errors, "version must be a string")
elseif not string.match(metadata.version, "^%d+%.%d+%.%d+$") then
table.insert(errors, "version must follow semantic versioning (e.g., 1.0.0)")
end
if not metadata.description then
table.insert(errors, "Missing required field: description")
elseif type(metadata.description) ~= "string" then
table.insert(errors, "description must be a string")
end
if not metadata.author then
table.insert(errors, "Missing required field: author")
elseif type(metadata.author) ~= "string" then
table.insert(errors, "author must be a string")
end
if not metadata.category then
table.insert(errors, "Missing required field: category")
elseif type(metadata.category) ~= "string" then
table.insert(errors, "category must be a string")
end
-- Optional but must be correct type if present
if metadata.dependencies then
if type(metadata.dependencies) ~= "table" then
table.insert(errors, "dependencies must be an array")
else
for i, dep in ipairs(metadata.dependencies) do
if type(dep) ~= "string" then
table.insert(errors, "dependencies[" .. i .. "] must be a string")
end
end
end
end
-- Validate devDependencies if present
if metadata.devDependencies then
if type(metadata.devDependencies) ~= "table" then
table.insert(errors, "devDependencies must be an array")
else
for i, dep in ipairs(metadata.devDependencies) do
if type(dep) ~= "string" then
table.insert(errors, "devDependencies[" .. i .. "] must be a string")
end
end
end
end
if metadata.exports then
if type(metadata.exports) ~= "table" then
table.insert(errors, "exports must be an object")
else
-- Validate exports.components
if metadata.exports.components then
if type(metadata.exports.components) ~= "table" then
table.insert(errors, "exports.components must be an array")
end
end
-- Validate exports.scripts
if metadata.exports.scripts then
if type(metadata.exports.scripts) ~= "table" then
table.insert(errors, "exports.scripts must be an array")
end
end
-- Validate exports.pages
if metadata.exports.pages then
if type(metadata.exports.pages) ~= "table" then
table.insert(errors, "exports.pages must be an array")
end
end
end
end
if metadata.minLevel then
if type(metadata.minLevel) ~= "number" then
table.insert(errors, "minLevel must be a number")
elseif metadata.minLevel < 1 or metadata.minLevel > 6 then
table.insert(errors, "minLevel must be between 1 and 6")
end
end
if metadata.bindings then
if type(metadata.bindings) ~= "table" then
table.insert(errors, "bindings must be an object")
else
if metadata.bindings.dbal ~= nil and type(metadata.bindings.dbal) ~= "boolean" then
table.insert(errors, "bindings.dbal must be a boolean")
end
if metadata.bindings.browser ~= nil and type(metadata.bindings.browser) ~= "boolean" then
table.insert(errors, "bindings.browser must be a boolean")
end
end
end
return #errors == 0, errors
end
--- Validate metadata.json structure
---@param metadata Metadata The metadata object to validate
---@return boolean valid Whether the metadata is valid
---@return string[] errors List of validation errors
M.validate_metadata = validate_metadata
return M

View File

@@ -1,284 +1,32 @@
-- Package folder structure validation
--- Structure validation facade
--- Re-exports structure validation functions for backward compatibility
---@module structure_validator
local structure_config = require("structure_config")
local validate_structure = require("validate_structure")
local validate_scripts_structure = require("validate_scripts_structure")
local validate_static_content = require("validate_static_content")
local validate_naming_conventions = require("validate_naming_conventions")
local validate_test_structure = require("validate_test_structure")
local validate_package_structure = require("validate_package_structure")
---@class StructureValidator
local M = {}
-- Expected package structure
M.REQUIRED_STRUCTURE = {
["seed/metadata.json"] = true,
["seed/components.json"] = true
}
-- Structure configuration
M.REQUIRED_STRUCTURE = structure_config.REQUIRED
M.OPTIONAL_STRUCTURE = structure_config.OPTIONAL
M.OPTIONAL_STRUCTURE = {
["seed/scripts/"] = "directory",
["seed/scripts/init.lua"] = "file",
["seed/scripts/tests/"] = "directory",
["static_content/"] = "directory",
["static_content/icon.svg"] = "file",
["README.md"] = "file",
["examples/"] = "directory"
}
-- Validate basic folder structure
function M.validate_structure(package_path)
local errors = {}
local warnings = {}
-- Check required files
for path, _ in pairs(M.REQUIRED_STRUCTURE) do
local full_path = package_path .. "/" .. path
local file = io.open(full_path, "r")
if not file then
table.insert(errors, "Required file missing: " .. path)
else
file:close()
end
end
-- Check optional but recommended files
for path, type in pairs(M.OPTIONAL_STRUCTURE) do
local full_path = package_path .. "/" .. path
if type == "file" then
local file = io.open(full_path, "r")
if not file then
table.insert(warnings, "Recommended file missing: " .. path)
else
file:close()
end
elseif type == "directory" then
-- Note: Directory checking would be done with OS-specific commands
-- This is a placeholder for the pattern
end
end
return errors, warnings
end
-- Validate scripts directory structure
function M.validate_scripts_structure(package_path, metadata)
local errors = {}
local warnings = {}
local scripts_path = package_path .. "/scripts"
-- Check if scripts directory exists when exports.scripts is defined
if metadata.exports and metadata.exports.scripts and #metadata.exports.scripts > 0 then
local dir_exists = false
local test_file = io.open(scripts_path .. "/init.lua", "r")
if test_file then
test_file:close()
dir_exists = true
end
if not dir_exists then
table.insert(errors, "scripts/ directory required when exports.scripts is defined")
end
-- Check for init.lua
local init_file = io.open(scripts_path .. "/init.lua", "r")
if not init_file then
table.insert(warnings, "scripts/init.lua is recommended as entry point")
else
init_file:close()
end
-- Check for tests directory if lua_test is a devDependency
local has_lua_test = false
if metadata.devDependencies then
for _, dep in ipairs(metadata.devDependencies) do
if dep == "lua_test" then
has_lua_test = true
break
end
end
end
if has_lua_test then
-- lua_test is a devDependency, so tests are expected
local test_init = io.open(scripts_path .. "/tests/metadata.test.lua", "r")
if not test_init then
table.insert(warnings, "scripts/tests/ directory with test files recommended when lua_test is a devDependency")
else
test_init:close()
end
end
end
return errors, warnings
end
-- Validate static content structure
function M.validate_static_content(package_path, metadata)
local errors = {}
local warnings = {}
if metadata.icon then
local icon_path = package_path .. "/" .. metadata.icon
local icon_file = io.open(icon_path, "r")
if not icon_file then
table.insert(errors, "Icon file not found: " .. metadata.icon)
else
icon_file:close()
end
else
table.insert(warnings, "No icon defined in metadata")
end
return errors, warnings
end
-- Check for orphaned files (files not referenced in metadata)
function M.check_orphaned_files(package_path, metadata)
local warnings = {}
-- This would scan the package directory and check if files are referenced
-- Placeholder for the pattern
return warnings
end
-- Validate naming conventions
function M.validate_naming_conventions(package_path, metadata)
local errors = {}
local warnings = {}
-- Package directory should match packageId
local dir_name = package_path:match("([^/]+)$")
if dir_name ~= metadata.packageId then
table.insert(errors, "Package directory name '" .. dir_name .. "' does not match packageId '" .. metadata.packageId .. "'")
end
-- Check script file naming
if metadata.exports and metadata.exports.scripts then
for _, script_name in ipairs(metadata.exports.scripts) do
-- Script names should be lowercase with underscores
if not string.match(script_name, "^[a-z_]+$") then
table.insert(warnings, "Script name '" .. script_name .. "' should use lowercase and underscores only")
end
end
end
-- Check component naming
if metadata.exports and metadata.exports.components then
for _, component_name in ipairs(metadata.exports.components) do
-- Component names can be PascalCase or snake_case
local is_valid = string.match(component_name, "^[A-Z][a-zA-Z]+$") or
string.match(component_name, "^[a-z_]+$")
if not is_valid then
table.insert(warnings, "Component name '" .. component_name .. "' should use PascalCase or snake_case")
end
end
end
return errors, warnings
end
-- Validate test files structure
function M.validate_test_structure(package_path, metadata)
local errors = {}
local warnings = {}
-- Check if lua_test is a devDependency
local has_lua_test = false
if metadata.devDependencies then
for _, dep in ipairs(metadata.devDependencies) do
if dep == "lua_test" then
has_lua_test = true
break
end
end
end
if not has_lua_test then
-- No lua_test, tests are optional
return errors, warnings
end
local tests_path = package_path .. "/scripts/tests"
-- Check for common test files
local common_tests = {
"metadata.test.lua",
"components.test.lua"
}
local found_tests = false
for _, test_file in ipairs(common_tests) do
local test_path = tests_path .. "/" .. test_file
local file = io.open(test_path, "r")
if file then
file:close()
found_tests = true
end
end
if not found_tests then
table.insert(warnings, "No test files found (metadata.test.lua, components.test.lua) despite lua_test devDependency")
end
return errors, warnings
end
-- Validate complete package structure
function M.validate_package_structure(package_path, metadata)
local results = {
valid = true,
errors = {},
warnings = {}
}
-- Basic structure
local struct_errors, struct_warnings = M.validate_structure(package_path)
for _, err in ipairs(struct_errors) do
table.insert(results.errors, err)
results.valid = false
end
for _, warn in ipairs(struct_warnings) do
table.insert(results.warnings, warn)
end
-- Scripts structure
local script_errors, script_warnings = M.validate_scripts_structure(package_path, metadata)
for _, err in ipairs(script_errors) do
table.insert(results.errors, err)
results.valid = false
end
for _, warn in ipairs(script_warnings) do
table.insert(results.warnings, warn)
end
-- Static content
local static_errors, static_warnings = M.validate_static_content(package_path, metadata)
for _, err in ipairs(static_errors) do
table.insert(results.errors, err)
results.valid = false
end
for _, warn in ipairs(static_warnings) do
table.insert(results.warnings, warn)
end
-- Naming conventions
local naming_errors, naming_warnings = M.validate_naming_conventions(package_path, metadata)
for _, err in ipairs(naming_errors) do
table.insert(results.errors, err)
results.valid = false
end
for _, warn in ipairs(naming_warnings) do
table.insert(results.warnings, warn)
end
-- Test structure (only if lua_test is a devDependency)
local test_errors, test_warnings = M.validate_test_structure(package_path, metadata)
for _, err in ipairs(test_errors) do
table.insert(results.errors, err)
results.valid = false
end
for _, warn in ipairs(test_warnings) do
table.insert(results.warnings, warn)
end
return results
-- Validation functions
M.validate_structure = validate_structure
M.validate_scripts_structure = validate_scripts_structure
M.validate_static_content = validate_static_content
M.check_orphaned_files = function(package_path, metadata)
-- Placeholder for orphaned file checking
return {}
end
M.validate_naming_conventions = validate_naming_conventions
M.validate_test_structure = validate_test_structure
M.validate_package_structure = validate_package_structure
return M

View File

@@ -1,132 +1,15 @@
-- Main validation orchestrator
local metadata_schema = require("metadata_schema")
local component_schema = require("component_schema")
local structure_validator = require("structure_validator")
local lua_validator = require("lua_validator")
--- Main validation orchestrator facade
--- Re-exports validation functions for backward compatibility
---@module validate
local validate_package_module = require("validate_package")
---@class Validate
local M = {}
-- Validate a complete package
function M.validate_package(package_path, options)
options = options or {}
local results = {
valid = true,
errors = {},
warnings = {}
}
-- Load and validate metadata.json
local metadata_path = package_path .. "/metadata.json"
local metadata_content = load_file(metadata_path)
if not metadata_content then
table.insert(results.errors, "Failed to load metadata.json")
results.valid = false
return results
end
local metadata = parse_json(metadata_content)
if not metadata then
table.insert(results.errors, "Failed to parse metadata.json")
results.valid = false
return results
end
-- 1. Validate metadata schema
local metadata_valid, metadata_errors = metadata_schema.validate_metadata(metadata)
if not metadata_valid then
for _, err in ipairs(metadata_errors) do
table.insert(results.errors, "metadata.json: " .. err)
end
results.valid = false
end
-- 2. Validate components.json
local components_path = package_path .. "/components.json"
local components_content = load_file(components_path)
if components_content then
local components = parse_json(components_content)
if components then
local components_valid, component_errors = component_schema.validate_components(components)
if not components_valid then
for _, err in ipairs(component_errors) do
table.insert(results.errors, "components.json: " .. err)
end
results.valid = false
end
else
table.insert(results.errors, "Failed to parse components.json")
results.valid = false
end
else
table.insert(results.warnings, "components.json not found (optional)")
end
-- 3. Validate folder structure
if not options.skipStructure then
local structure_results = structure_validator.validate_package_structure(package_path, metadata)
if not structure_results.valid then
results.valid = false
end
for _, err in ipairs(structure_results.errors) do
table.insert(results.errors, "Structure: " .. err)
end
for _, warn in ipairs(structure_results.warnings) do
table.insert(results.warnings, "Structure: " .. warn)
end
end
-- 4. Validate Lua script exports
if not options.skipLua and metadata.exports and metadata.exports.scripts then
local script_errors, script_warnings = lua_validator.validate_script_exports(package_path, metadata)
for _, err in ipairs(script_errors) do
table.insert(results.errors, "Lua: " .. err)
results.valid = false
end
for _, warn in ipairs(script_warnings) do
table.insert(results.warnings, "Lua: " .. warn)
end
end
return results
end
-- Validate just metadata
function M.validate_metadata_only(metadata)
return metadata_schema.validate_metadata(metadata)
end
-- Validate just components
function M.validate_components_only(components)
return component_schema.validate_components(components)
end
-- Format validation results for display
function M.format_results(results)
local output = {}
if results.valid then
table.insert(output, "✓ Validation passed")
else
table.insert(output, "✗ Validation failed")
end
if #results.errors > 0 then
table.insert(output, "\nErrors:")
for _, err in ipairs(results.errors) do
table.insert(output, "" .. err)
end
end
if #results.warnings > 0 then
table.insert(output, "\nWarnings:")
for _, warn in ipairs(results.warnings) do
table.insert(output, "" .. warn)
end
end
return table.concat(output, "\n")
end
M.validate_package = validate_package_module.validate_package
M.validate_metadata_only = validate_package_module.validate_metadata_only
M.validate_components_only = validate_package_module.validate_components_only
M.format_results = validate_package_module.format_results
return M

View File

@@ -0,0 +1,17 @@
local create_step = require("create_step")
--- Add a new step to the list
---@param steps Step[] Array of steps
---@return Step[] Updated steps array
---@return Step New step that was added
local function add_step(steps)
local newStep = create_step()
local result = {}
for i, step in ipairs(steps) do
result[i] = step
end
result[#result + 1] = newStep
return result, newStep
end
return add_step

View File

@@ -0,0 +1,15 @@
local generate_step_id = require("generate_step_id")
--- Create a new empty step
---@return Step New step with default values
local function create_step()
return {
id = generate_step_id(),
title = "New step",
description = "Describe what happens in this step.",
duration = "1-2 min",
mediaUrl = nil
}
end
return create_step

View File

@@ -0,0 +1,7 @@
--- Generate a unique step ID
---@return string Unique step identifier
local function generate_step_id()
return "step_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
end
return generate_step_id

View File

@@ -0,0 +1,19 @@
local is_valid_url = require("is_valid_url")
local is_image_url = require("is_image_url")
--- Handle thumbnail URL change
---@param state MediaState Current state
---@param newUrl string New thumbnail URL
---@return MediaState Updated state
local function handle_thumbnail_change(state, newUrl)
return {
thumbnailUrl = newUrl,
videoUrl = state.videoUrl,
thumbnailValid = is_valid_url(newUrl),
videoValid = state.videoValid,
thumbnailIsImage = is_image_url(newUrl),
videoIsVideo = state.videoIsVideo
}
end
return handle_thumbnail_change

View File

@@ -0,0 +1,19 @@
local is_valid_url = require("is_valid_url")
local is_video_url = require("is_video_url")
--- Handle video URL change
---@param state MediaState Current state
---@param newUrl string New video URL
---@return MediaState Updated state
local function handle_video_change(state, newUrl)
return {
thumbnailUrl = state.thumbnailUrl,
videoUrl = newUrl,
thumbnailValid = state.thumbnailValid,
videoValid = is_valid_url(newUrl),
thumbnailIsImage = state.thumbnailIsImage,
videoIsVideo = is_video_url(newUrl)
}
end
return handle_video_change

View File

@@ -1,9 +1,16 @@
-- Quick Guide package initialization
---@class QuickGuideModule
---@field name string Package name
---@field version string Package version
---@field init fun(): boolean Initialize the module
local M = {}
M.name = "quick_guide"
M.version = "1.0.0"
---Initialize the quick guide module
---@return boolean Success status
function M.init()
log("Quick Guide package initialized")
return true

View File

@@ -0,0 +1,19 @@
local is_valid_url = require("is_valid_url")
--- Check if URL is an image
---@param url? string URL to check
---@return boolean Whether URL points to an image
local function is_image_url(url)
if not is_valid_url(url) then
return false
end
local patterns = { "%.png$", "%.jpg$", "%.jpeg$", "%.gif$", "%.webp$", "%.svg$" }
for _, pattern in ipairs(patterns) do
if string.match(url:lower(), pattern) then
return true
end
end
return false
end
return is_image_url

View File

@@ -0,0 +1,11 @@
--- Validate a URL (basic check)
---@param url? string URL to validate
---@return boolean Whether URL is valid
local function is_valid_url(url)
if not url or url == "" then
return false
end
return string.match(url, "^https?://") ~= nil
end
return is_valid_url

View File

@@ -0,0 +1,19 @@
local is_valid_url = require("is_valid_url")
--- Check if URL is a video embed
---@param url? string URL to check
---@return boolean Whether URL points to a video
local function is_video_url(url)
if not is_valid_url(url) then
return false
end
local patterns = { "youtube%.com", "vimeo%.com", "%.mp4$", "%.webm$" }
for _, pattern in ipairs(patterns) do
if string.match(url:lower(), pattern) then
return true
end
end
return false
end
return is_video_url

View File

@@ -1,77 +1,22 @@
-- Media pane logic for quick guides
--- Media pane logic facade for quick guides
--- Re-exports media functions for backward compatibility
---@module media
local is_valid_url = require("is_valid_url")
local is_image_url = require("is_image_url")
local is_video_url = require("is_video_url")
local prepare_media_state = require("prepare_media_state")
local handle_thumbnail_change = require("handle_thumbnail_change")
local handle_video_change = require("handle_video_change")
---@class MediaModule
local M = {}
-- Validate a URL (basic check)
function M.isValidUrl(url)
if not url or url == "" then
return false
end
return string.match(url, "^https?://") ~= nil
end
-- Check if URL is an image
function M.isImageUrl(url)
if not M.isValidUrl(url) then
return false
end
local patterns = { "%.png$", "%.jpg$", "%.jpeg$", "%.gif$", "%.webp$", "%.svg$" }
for _, pattern in ipairs(patterns) do
if string.match(url:lower(), pattern) then
return true
end
end
return false
end
-- Check if URL is a video embed
function M.isVideoUrl(url)
if not M.isValidUrl(url) then
return false
end
local patterns = { "youtube%.com", "vimeo%.com", "%.mp4$", "%.webm$" }
for _, pattern in ipairs(patterns) do
if string.match(url:lower(), pattern) then
return true
end
end
return false
end
-- Prepare media state
function M.prepareMediaState(props)
props = props or {}
return {
thumbnailUrl = props.thumbnailUrl or "",
videoUrl = props.videoUrl or "",
thumbnailValid = M.isValidUrl(props.thumbnailUrl),
videoValid = M.isValidUrl(props.videoUrl),
thumbnailIsImage = M.isImageUrl(props.thumbnailUrl),
videoIsVideo = M.isVideoUrl(props.videoUrl)
}
end
-- Handle thumbnail change
function M.handleThumbnailChange(state, newUrl)
return {
thumbnailUrl = newUrl,
videoUrl = state.videoUrl,
thumbnailValid = M.isValidUrl(newUrl),
videoValid = state.videoValid,
thumbnailIsImage = M.isImageUrl(newUrl),
videoIsVideo = state.videoIsVideo
}
end
-- Handle video change
function M.handleVideoChange(state, newUrl)
return {
thumbnailUrl = state.thumbnailUrl,
videoUrl = newUrl,
thumbnailValid = state.thumbnailValid,
videoValid = M.isValidUrl(newUrl),
thumbnailIsImage = state.thumbnailIsImage,
videoIsVideo = M.isVideoUrl(newUrl)
}
end
M.isValidUrl = is_valid_url
M.isImageUrl = is_image_url
M.isVideoUrl = is_video_url
M.prepareMediaState = prepare_media_state
M.handleThumbnailChange = handle_thumbnail_change
M.handleVideoChange = handle_video_change
return M

View File

@@ -0,0 +1,20 @@
local is_valid_url = require("is_valid_url")
local is_image_url = require("is_image_url")
local is_video_url = require("is_video_url")
--- Prepare media state from props
---@param props? MediaProps Input props
---@return MediaState Initial media state
local function prepare_media_state(props)
props = props or {}
return {
thumbnailUrl = props.thumbnailUrl or "",
videoUrl = props.videoUrl or "",
thumbnailValid = is_valid_url(props.thumbnailUrl),
videoValid = is_valid_url(props.videoUrl),
thumbnailIsImage = is_image_url(props.thumbnailUrl),
videoIsVideo = is_video_url(props.videoUrl)
}
end
return prepare_media_state

View File

@@ -0,0 +1,15 @@
--- Remove a step from the list
---@param steps Step[] Array of steps
---@param stepId string ID of step to remove
---@return Step[] Updated steps array
local function remove_step(steps, stepId)
local result = {}
for _, step in ipairs(steps) do
if step.id ~= stepId then
result[#result + 1] = step
end
end
return result
end
return remove_step

View File

@@ -0,0 +1,17 @@
--- Reset step IDs to sequential order
---@param steps Step[] Array of steps
---@return Step[] Steps with reset IDs
local function reset_ordering(steps)
local result = {}
for i, step in ipairs(steps) do
local resetStep = {}
for k, v in pairs(step) do
resetStep[k] = v
end
resetStep.id = "step_" .. tostring(i)
result[i] = resetStep
end
return result
end
return reset_ordering

View File

@@ -1,107 +1,26 @@
-- Steps editor logic for quick guides
--- Steps editor logic facade for quick guides
--- Re-exports step functions for backward compatibility
---@module steps
local generate_step_id = require("generate_step_id")
local create_step = require("create_step")
local update_step = require("update_step")
local remove_step = require("remove_step")
local add_step = require("add_step")
local reset_ordering = require("reset_ordering")
local validate_step = require("validate_step")
local validate_all_steps = require("validate_all_steps")
---@class StepsModule
local M = {}
-- Generate a unique step ID
function M.generateStepId()
return "step_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
end
-- Create a new empty step
function M.createStep()
return {
id = M.generateStepId(),
title = "New step",
description = "Describe what happens in this step.",
duration = "1-2 min",
mediaUrl = nil
}
end
-- Update a step in the list
function M.updateStep(steps, stepId, updates)
local result = {}
for i, step in ipairs(steps) do
if step.id == stepId then
local updatedStep = {}
for k, v in pairs(step) do
updatedStep[k] = v
end
for k, v in pairs(updates) do
updatedStep[k] = v
end
result[i] = updatedStep
else
result[i] = step
end
end
return result
end
-- Remove a step from the list
function M.removeStep(steps, stepId)
local result = {}
for _, step in ipairs(steps) do
if step.id ~= stepId then
result[#result + 1] = step
end
end
return result
end
-- Add a new step to the list
function M.addStep(steps)
local newStep = M.createStep()
local result = {}
for i, step in ipairs(steps) do
result[i] = step
end
result[#result + 1] = newStep
return result, newStep
end
-- Reset step IDs to sequential order
function M.resetOrdering(steps)
local result = {}
for i, step in ipairs(steps) do
local resetStep = {}
for k, v in pairs(step) do
resetStep[k] = v
end
resetStep.id = "step_" .. tostring(i)
result[i] = resetStep
end
return result
end
-- Validate a step
function M.validateStep(step)
local errors = {}
if not step.title or step.title == "" then
errors.title = "Title is required"
end
if not step.description or step.description == "" then
errors.description = "Description is required"
end
return { valid = next(errors) == nil, errors = errors }
end
-- Validate all steps
function M.validateAllSteps(steps)
local allErrors = {}
local valid = true
for i, step in ipairs(steps) do
local result = M.validateStep(step)
if not result.valid then
valid = false
allErrors[step.id] = result.errors
end
end
return { valid = valid, errors = allErrors }
end
M.generateStepId = generate_step_id
M.createStep = create_step
M.updateStep = update_step
M.removeStep = remove_step
M.addStep = add_step
M.resetOrdering = reset_ordering
M.validateStep = validate_step
M.validateAllSteps = validate_all_steps
return M

View File

@@ -0,0 +1,35 @@
---@meta
-- Type definitions for quick_guide package
---@class Step
---@field id string Unique step identifier
---@field title string Step title
---@field description string Step description
---@field duration string Estimated duration
---@field mediaUrl? string Optional media URL
---@class StepValidationErrors
---@field title? string Title error message
---@field description? string Description error message
---@class StepValidationResult
---@field valid boolean Whether step is valid
---@field errors StepValidationErrors Validation errors
---@class AllStepsValidationResult
---@field valid boolean Whether all steps are valid
---@field errors table<string, StepValidationErrors> Errors by step ID
---@class MediaState
---@field thumbnailUrl string Thumbnail URL
---@field videoUrl string Video URL
---@field thumbnailValid boolean Whether thumbnail URL is valid
---@field videoValid boolean Whether video URL is valid
---@field thumbnailIsImage boolean Whether thumbnail is an image URL
---@field videoIsVideo boolean Whether video is a video URL
---@class MediaProps
---@field thumbnailUrl? string Initial thumbnail URL
---@field videoUrl? string Initial video URL
return {}

View File

@@ -0,0 +1,25 @@
--- Update a step in the list
---@param steps Step[] Array of steps
---@param stepId string ID of step to update
---@param updates table Partial step updates
---@return Step[] Updated steps array
local function update_step(steps, stepId, updates)
local result = {}
for i, step in ipairs(steps) do
if step.id == stepId then
local updatedStep = {}
for k, v in pairs(step) do
updatedStep[k] = v
end
for k, v in pairs(updates) do
updatedStep[k] = v
end
result[i] = updatedStep
else
result[i] = step
end
end
return result
end
return update_step

View File

@@ -0,0 +1,21 @@
local validate_step = require("validate_step")
--- Validate all steps
---@param steps Step[] Array of steps to validate
---@return AllStepsValidationResult Validation result for all steps
local function validate_all_steps(steps)
local allErrors = {}
local valid = true
for _, step in ipairs(steps) do
local result = validate_step(step)
if not result.valid then
valid = false
allErrors[step.id] = result.errors
end
end
return { valid = valid, errors = allErrors }
end
return validate_all_steps

View File

@@ -0,0 +1,18 @@
--- Validate a single step
---@param step Step Step to validate
---@return StepValidationResult Validation result
local function validate_step(step)
local errors = {}
if not step.title or step.title == "" then
errors.title = "Title is required"
end
if not step.description or step.description == "" then
errors.description = "Description is required"
end
return { valid = next(errors) == nil, errors = errors }
end
return validate_step

View File

@@ -1,4 +1,33 @@
-- Schema field types
---@alias FieldType "string" | "integer" | "float" | "boolean" | "date" | "datetime" | "text" | "json"
---@class FieldOptions
---@field nullable? boolean Whether field can be null
---@field default? any Default value for field
---@field unique? boolean Whether field must be unique
---@class FieldReference
---@field table string Referenced table name
---@field field string Referenced field name
---@class FieldDefinition
---@field name string Field name
---@field type FieldType Field type
---@field nullable boolean Whether field can be null
---@field default? any Default value
---@field unique boolean Whether field must be unique
---@field references? FieldReference Foreign key reference
---@class FieldsModule
---@field STRING string
---@field INTEGER string
---@field FLOAT string
---@field BOOLEAN string
---@field DATE string
---@field DATETIME string
---@field TEXT string
---@field JSON string
local M = {}
M.STRING = "string"
@@ -10,6 +39,11 @@ M.DATETIME = "datetime"
M.TEXT = "text"
M.JSON = "json"
---Define a schema field
---@param name string Field name
---@param type FieldType Field type
---@param options? FieldOptions Field options
---@return FieldDefinition
function M.define(name, type, options)
return {
name = name,
@@ -20,10 +54,18 @@ function M.define(name, type, options)
}
end
---Create a primary key field
---@param name string Field name
---@return FieldDefinition
function M.primary_key(name)
return M.define(name, M.INTEGER, { nullable = false, unique = true })
end
---Create a foreign key field
---@param name string Field name
---@param ref_table string Referenced table name
---@param ref_field string Referenced field name
---@return FieldDefinition
function M.foreign_key(name, ref_table, ref_field)
local field = M.define(name, M.INTEGER, { nullable = true })
field.references = { table = ref_table, field = ref_field }

View File

@@ -1,9 +1,21 @@
-- Schema Editor initialization
---@class SchemaEditorModule
---@field name string Package name
---@field version string Package version
---@field init fun(): SchemaEditorInfo Initialize the module
local M = {}
M.name = "schema_editor"
M.version = "1.0.0"
---@class SchemaEditorInfo
---@field name string Package name
---@field version string Package version
---@field loaded boolean Whether module is loaded
---Initialize the schema editor module
---@return SchemaEditorInfo
function M.init()
return {
name = M.name,

View File

@@ -1,10 +1,39 @@
-- Table relationships
---@alias RelationType "one_to_one" | "one_to_many" | "many_to_many"
---@class RelationEndpoint
---@field table string Table name
---@field field string Field name
---@class RelationOptions
---@field cascade? boolean Whether to cascade deletes
---@class Relation
---@field type RelationType Relation type
---@field from_table string Source table name
---@field from_field string Source field name
---@field to_table string Target table name
---@field to_field string Target field name
---@field cascade boolean Whether to cascade deletes
---@field pivot_table? string Pivot table for many-to-many
---@class RelationsModule
---@field ONE_TO_ONE string
---@field ONE_TO_MANY string
---@field MANY_TO_MANY string
local M = {}
M.ONE_TO_ONE = "one_to_one"
M.ONE_TO_MANY = "one_to_many"
M.MANY_TO_MANY = "many_to_many"
---Define a table relationship
---@param type RelationType Relation type
---@param from RelationEndpoint Source endpoint
---@param to RelationEndpoint Target endpoint
---@param options? RelationOptions Relation options
---@return Relation
function M.define(type, from, to, options)
return {
type = type,
@@ -16,14 +45,27 @@ function M.define(type, from, to, options)
}
end
---Create a one-to-one relationship
---@param from RelationEndpoint Source endpoint
---@param to RelationEndpoint Target endpoint
---@return Relation
function M.has_one(from, to)
return M.define(M.ONE_TO_ONE, from, to)
end
---Create a one-to-many relationship
---@param from RelationEndpoint Source endpoint
---@param to RelationEndpoint Target endpoint
---@return Relation
function M.has_many(from, to)
return M.define(M.ONE_TO_MANY, from, to)
end
---Create a many-to-many relationship
---@param from RelationEndpoint Source endpoint
---@param to RelationEndpoint Target endpoint
---@param pivot string Pivot table name
---@return Relation
function M.belongs_to_many(from, to, pivot)
local rel = M.define(M.MANY_TO_MANY, from, to)
rel.pivot_table = pivot

View File

@@ -1,6 +1,39 @@
-- Table management
---@class CreateTableAction
---@field action "create_table"
---@field name string Table name
---@field fields FieldDefinition[] Array of field definitions
---@class AddFieldAction
---@field action "add_field"
---@field table string Table name
---@field field FieldDefinition Field to add
---@class RemoveFieldAction
---@field action "remove_field"
---@field table string Table name
---@field field string Field name to remove
---@class TableEditorProps
---@field name string Table name
---@field fields FieldDefinition[] Array of field definitions
---@class TableEditorComponent
---@field type "table_editor"
---@field props TableEditorProps
---@class TableDefinition
---@field name string Table name
---@field fields FieldDefinition[] Array of field definitions
---@class TablesModule
local M = {}
---Create a new table definition
---@param name string Table name
---@param fields? FieldDefinition[] Array of field definitions
---@return CreateTableAction
function M.create(name, fields)
return {
action = "create_table",
@@ -9,6 +42,9 @@ function M.create(name, fields)
}
end
---Render a table editor component
---@param table_def TableDefinition Table definition
---@return TableEditorComponent
function M.render(table_def)
return {
type = "table_editor",
@@ -19,6 +55,10 @@ function M.render(table_def)
}
end
---Create an add field action
---@param table_name string Table name
---@param field FieldDefinition Field to add
---@return AddFieldAction
function M.add_field(table_name, field)
return {
action = "add_field",
@@ -27,6 +67,10 @@ function M.add_field(table_name, field)
}
end
---Create a remove field action
---@param table_name string Table name
---@param field_name string Field name to remove
---@return RemoveFieldAction
function M.remove_field(table_name, field_name)
return {
action = "remove_field",

View File

@@ -1,6 +1,37 @@
-- Workflow run card utilities
---@class WorkflowRun
---@field workflow_name string Workflow name
---@field run_number integer Run number
---@field started_at string Start timestamp
---@field duration? string Run duration (or nil if running)
---@field status string Run status
---@class CardChild
---@field type string Child component type
---@field content? string Text content
---@field status? string Status for badge
---@class RunCardProps
---@field title string Card title
---@field subtitle string Card subtitle
---@class RunCard
---@field type "card"
---@field props RunCardProps
---@field children CardChild[]
---@class RunListGrid
---@field type "grid"
---@field columns integer Number of columns
---@field children RunCard[]
---@class RunModule
local M = {}
---Render a workflow run card
---@param run WorkflowRun Workflow run data
---@return RunCard
function M.render(run)
return {
type = "card",
@@ -16,6 +47,9 @@ function M.render(run)
}
end
---Render a list of workflow runs as a grid
---@param runs WorkflowRun[] Array of workflow runs
---@return RunListGrid
function M.render_list(runs)
local cards = {}
for _, run in ipairs(runs) do