- 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.
164 lines
6.2 KiB
C#
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;
|
|
}
|
|
}
|