using ScadaLink.Commons.Entities.Scripts;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types;
namespace ScadaLink.TemplateEngine;
///
/// WP-5: Shared Script CRUD.
/// System-wide scripts not associated with templates.
/// Same parameter/return definition structure as template scripts.
/// Includes syntax/structural validation (basic C# compilation check).
///
public class SharedScriptService
{
private readonly ITemplateEngineRepository _repository;
private readonly IAuditService _auditService;
public SharedScriptService(ITemplateEngineRepository repository, IAuditService auditService)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_auditService = auditService ?? throw new ArgumentNullException(nameof(auditService));
}
public async Task> CreateSharedScriptAsync(
string name,
string code,
string? parameterDefinitions,
string? returnDefinition,
string user,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
return Result.Failure("Shared script name is required.");
if (string.IsNullOrWhiteSpace(code))
return Result.Failure("Shared script code is required.");
// Check unique name
var existing = await _repository.GetSharedScriptByNameAsync(name, cancellationToken);
if (existing != null)
return Result.Failure($"A shared script named '{name}' already exists.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result.Failure(syntaxError);
var script = new SharedScript(name, code)
{
ParameterDefinitions = parameterDefinitions,
ReturnDefinition = returnDefinition
};
await _repository.AddSharedScriptAsync(script, cancellationToken);
await _auditService.LogAsync(user, "Create", "SharedScript", "0", name, script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result.Success(script);
}
public async Task> UpdateSharedScriptAsync(
int scriptId,
string code,
string? parameterDefinitions,
string? returnDefinition,
string user,
CancellationToken cancellationToken = default)
{
var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
if (script == null)
return Result.Failure($"Shared script with ID {scriptId} not found.");
if (string.IsNullOrWhiteSpace(code))
return Result.Failure("Shared script code is required.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result.Failure(syntaxError);
script.Code = code;
script.ParameterDefinitions = parameterDefinitions;
script.ReturnDefinition = returnDefinition;
await _repository.UpdateSharedScriptAsync(script, cancellationToken);
await _auditService.LogAsync(user, "Update", "SharedScript", scriptId.ToString(), script.Name, script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result.Success(script);
}
public async Task> DeleteSharedScriptAsync(
int scriptId,
string user,
CancellationToken cancellationToken = default)
{
var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
if (script == null)
return Result.Failure($"Shared script with ID {scriptId} not found.");
await _repository.DeleteSharedScriptAsync(scriptId, cancellationToken);
await _auditService.LogAsync(user, "Delete", "SharedScript", scriptId.ToString(), script.Name, null, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result.Success(true);
}
public async Task GetSharedScriptByIdAsync(int scriptId, CancellationToken cancellationToken = default)
{
return await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
}
public async Task> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
{
return await _repository.GetAllSharedScriptsAsync(cancellationToken);
}
///
/// Basic structural validation of C# script code.
/// Checks for balanced braces and basic syntax structure.
/// Full Roslyn compilation would be added in a later phase when the scripting sandbox is available.
///
internal static string? ValidateSyntax(string code)
{
if (string.IsNullOrWhiteSpace(code))
return "Script code cannot be empty.";
// Check for balanced braces
int braceCount = 0;
int bracketCount = 0;
int parenCount = 0;
foreach (var ch in code)
{
switch (ch)
{
case '{': braceCount++; break;
case '}': braceCount--; break;
case '[': bracketCount++; break;
case ']': bracketCount--; break;
case '(': parenCount++; break;
case ')': parenCount--; break;
}
if (braceCount < 0)
return "Syntax error: unmatched closing brace '}'.";
if (bracketCount < 0)
return "Syntax error: unmatched closing bracket ']'.";
if (parenCount < 0)
return "Syntax error: unmatched closing parenthesis ')'.";
}
if (braceCount != 0)
return "Syntax error: unmatched opening brace '{'.";
if (bracketCount != 0)
return "Syntax error: unmatched opening bracket '['.";
if (parenCount != 0)
return "Syntax error: unmatched opening parenthesis '('.";
return null;
}
}