Files
meeting-assistant/MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs
T

244 lines
9.4 KiB
C#

using MeetingAssistant.MeetingNotes;
namespace MeetingAssistant.Workflow;
internal static class MeetingWorkflowRulesValidator
{
public static async Task<IReadOnlyList<string>> ValidateAsync(MeetingWorkflowRulesFile rulesFile)
{
var errors = new List<string>();
var templateRenderer = new MeetingWorkflowTemplateRenderer();
var templateModel = CreateValidationTemplateModel();
for (var ruleIndex = 0; ruleIndex < rulesFile.Rules.Count; ruleIndex++)
{
var rule = rulesFile.Rules[ruleIndex];
var ruleName = GetRuleName(rule, ruleIndex);
if (rule.On.Count == 0)
{
errors.Add($"{ruleName} must define at least one trigger in 'on'. Supported triggers: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedTriggerKeys)}.");
}
for (var triggerIndex = 0; triggerIndex < rule.On.Count; triggerIndex++)
{
ValidateTrigger(rule.On[triggerIndex], ruleName, triggerIndex, errors);
}
for (var conditionIndex = 0; conditionIndex < rule.If.Count; conditionIndex++)
{
ValidateCondition(rule.If[conditionIndex], ruleName, $"if[{conditionIndex + 1}]", errors);
}
if (rule.Steps.Count == 0)
{
errors.Add($"{ruleName} must define at least one step. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
}
for (var stepIndex = 0; stepIndex < rule.Steps.Count; stepIndex++)
{
await ValidateStepAsync(
rule.Steps[stepIndex],
rule,
ruleName,
stepIndex,
errors,
templateRenderer,
templateModel);
}
}
return errors;
}
private static void ValidateTrigger(
MeetingWorkflowTrigger trigger,
string ruleName,
int triggerIndex,
List<string> errors)
{
var configuredCount = CountConfigured(
trigger.Created is not null,
trigger.StateTransition is not null,
trigger.SpeakerIdentified is not null,
trigger.TranscriptLine is not null,
trigger.AttendeeAdded is not null);
if (configuredCount == 0)
{
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain one supported trigger: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedTriggerKeys)}.");
}
else if (configuredCount > 1)
{
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain only one trigger.");
}
}
private static void ValidateCondition(
MeetingWorkflowCondition condition,
string ruleName,
string path,
List<string> errors)
{
var configuredCount = CountConfigured(
!string.IsNullOrWhiteSpace(condition.Condition),
condition.And is { Count: > 0 },
condition.Or is { Count: > 0 },
condition.Not is not null);
if (configuredCount == 0)
{
errors.Add($"{ruleName} {path} must contain one supported condition key: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedConditionKeys)}.");
return;
}
if (configuredCount > 1)
{
errors.Add($"{ruleName} {path} must contain only one condition key.");
}
if (condition.And is { Count: > 0 } andConditions)
{
for (var index = 0; index < andConditions.Count; index++)
{
ValidateCondition(andConditions[index], ruleName, $"{path}.and[{index + 1}]", errors);
}
}
if (condition.Or is { Count: > 0 } orConditions)
{
for (var index = 0; index < orConditions.Count; index++)
{
ValidateCondition(orConditions[index], ruleName, $"{path}.or[{index + 1}]", errors);
}
}
if (condition.Not is not null)
{
ValidateCondition(condition.Not, ruleName, $"{path}.not", errors);
}
}
private static async Task ValidateStepAsync(
MeetingWorkflowStep step,
MeetingWorkflowRule rule,
string ruleName,
int stepIndex,
List<string> errors,
MeetingWorkflowTemplateRenderer templateRenderer,
MeetingWorkflowTemplateModel templateModel)
{
var stepPath = $"{ruleName} steps[{stepIndex + 1}]";
var uses = step.Uses.Trim();
if (string.IsNullOrWhiteSpace(uses))
{
errors.Add($"{stepPath} must define 'uses'. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
return;
}
if (!MeetingWorkflowRuleSchema.SupportedStepUses.Contains(uses, StringComparer.OrdinalIgnoreCase))
{
errors.Add($"{stepPath} uses unsupported step '{uses}'. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
return;
}
if (HasAttendeeAddedTrigger(rule) &&
!IsAttendeeNameSetPropertyStep(step))
{
errors.Add($"{stepPath} on attendee_added rules can only transform the attendee with 'set_property' property 'attendee.name'.");
}
if (MeetingWorkflowRuleSchema.IsStep(uses, MeetingWorkflowRuleSchema.StepSetProperty))
{
var property = step.Property ?? step.Name;
if (string.IsNullOrWhiteSpace(property))
{
errors.Add($"{stepPath} must define 'property' or 'name' for set_property. Supported properties: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedSetPropertyNames)}.");
}
else if (!MeetingWorkflowRuleSchema.SupportedSetPropertyNames.Contains(property.Trim(), StringComparer.OrdinalIgnoreCase))
{
errors.Add($"{stepPath} uses unsupported property '{property}'. Supported properties: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedSetPropertyNames)}.");
}
else if (MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyTranscriptLine) &&
!HasTranscriptLineTrigger(rule))
{
errors.Add($"{stepPath} can set 'transcript.line' only on rules with a transcript_line trigger.");
}
else if (MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyAttendeeName) &&
!HasAttendeeAddedTrigger(rule))
{
errors.Add($"{stepPath} can set 'attendee.name' only on rules with an attendee_added trigger.");
}
}
if (step.Value is { } value &&
MeetingWorkflowTemplateRenderer.ContainsRazorTemplateSyntax(value))
{
try
{
_ = await templateRenderer.RenderAsync(value, templateModel);
}
catch (Exception exception)
{
errors.Add($"{stepPath} has invalid Razor template in 'value' ({value}). {FormatRazorValidationMessage(exception)}");
}
}
}
private static string GetRuleName(MeetingWorkflowRule rule, int ruleIndex)
{
return string.IsNullOrWhiteSpace(rule.Name)
? $"rule[{ruleIndex + 1}]"
: $"rule '{rule.Name}'";
}
private static bool HasTranscriptLineTrigger(MeetingWorkflowRule rule)
{
return rule.On.Any(static trigger => trigger.TranscriptLine is not null);
}
private static bool HasAttendeeAddedTrigger(MeetingWorkflowRule rule)
{
return rule.On.Any(static trigger => trigger.AttendeeAdded is not null);
}
private static bool IsAttendeeNameSetPropertyStep(MeetingWorkflowStep step)
{
return MeetingWorkflowRuleSchema.IsStep(step.Uses, MeetingWorkflowRuleSchema.StepSetProperty) &&
MeetingWorkflowRuleSchema.IsProperty(step.Property ?? step.Name, MeetingWorkflowRuleSchema.PropertyAttendeeName);
}
private static int CountConfigured(params bool[] values)
{
return values.Count(static value => value);
}
private static string JoinAllowed(IEnumerable<string> values)
{
return string.Join(", ", values);
}
private static MeetingWorkflowTemplateModel CreateValidationTemplateModel()
{
return new MeetingWorkflowTemplateModel(
new MeetingWorkflowMeetingModel(
"Validation Meeting",
["Ada Lovelace"],
["Validation Project"],
MeetingWorkflowStateNames.ToRuleName(AssistantContextState.Transcribing)),
new MeetingWorkflowEventModel(
MeetingWorkflowEventType.StateTransition.ToString(),
MeetingWorkflowStateNames.ToRuleName(AssistantContextState.CollectingMetadata),
MeetingWorkflowStateNames.ToRuleName(AssistantContextState.Transcribing)),
new MeetingWorkflowSpeakerModel("Ada Lovelace"),
new MeetingWorkflowTranscriptModel(
"[00:00:01] Ada Lovelace: Validation line.",
"Ada Lovelace"),
new MeetingWorkflowAttendeeModel("Ada Lovelace"));
}
private static string FormatRazorValidationMessage(Exception exception)
{
var message = exception.Message.Split(Environment.NewLine)[0].Trim();
return string.IsNullOrWhiteSpace(message)
? "The template could not be compiled or rendered with the workflow model."
: message;
}
}