Files
scadalink-design/src/ScadaLink.TemplateEngine/SharedScriptService.cs
Joseph Doherty faef2d0de6 Phase 2 WP-1–13+23: Template Engine CRUD, composition, overrides, locking, collision detection, acyclicity
- WP-23: ITemplateEngineRepository full EF Core implementation
- WP-1: Template CRUD with deletion constraints (instances, children, compositions)
- WP-2–4: Attribute, alarm, script definitions with lock flags and override granularity
- WP-5: Shared script CRUD with syntax validation
- WP-6–7: Composition with recursive nesting and canonical naming
- WP-8–11: Override granularity, locking rules, inheritance/composition scope
- WP-12: Naming collision detection on canonical names (recursive)
- WP-13: Graph acyclicity (inheritance + composition cycles)
Core services: TemplateService, SharedScriptService, TemplateResolver,
LockEnforcer, CollisionDetector, CycleDetector. 358 tests pass.
2026-03-16 20:10:34 -04:00

164 lines
6.2 KiB
C#

using ScadaLink.Commons.Entities.Scripts;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types;
namespace ScadaLink.TemplateEngine;
/// <summary>
/// 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).
/// </summary>
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<Result<SharedScript>> CreateSharedScriptAsync(
string name,
string code,
string? parameterDefinitions,
string? returnDefinition,
string user,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
return Result<SharedScript>.Failure("Shared script name is required.");
if (string.IsNullOrWhiteSpace(code))
return Result<SharedScript>.Failure("Shared script code is required.");
// Check unique name
var existing = await _repository.GetSharedScriptByNameAsync(name, cancellationToken);
if (existing != null)
return Result<SharedScript>.Failure($"A shared script named '{name}' already exists.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result<SharedScript>.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<SharedScript>.Success(script);
}
public async Task<Result<SharedScript>> 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<SharedScript>.Failure($"Shared script with ID {scriptId} not found.");
if (string.IsNullOrWhiteSpace(code))
return Result<SharedScript>.Failure("Shared script code is required.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result<SharedScript>.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<SharedScript>.Success(script);
}
public async Task<Result<bool>> DeleteSharedScriptAsync(
int scriptId,
string user,
CancellationToken cancellationToken = default)
{
var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
if (script == null)
return Result<bool>.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<bool>.Success(true);
}
public async Task<SharedScript?> GetSharedScriptByIdAsync(int scriptId, CancellationToken cancellationToken = default)
{
return await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
}
public async Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
{
return await _repository.GetAllSharedScriptsAsync(cancellationToken);
}
/// <summary>
/// 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.
/// </summary>
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;
}
}