-
Notifications
You must be signed in to change notification settings - Fork 97
Expand file tree
/
Copy pathSchemaValidator.cs
More file actions
164 lines (140 loc) · 5.41 KB
/
SchemaValidator.cs
File metadata and controls
164 lines (140 loc) · 5.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using autoShell.Logging;
namespace autoShell;
/// <summary>
/// Reads <c>.pas.json</c> schema files produced by the TypeAgent action schema compiler
/// and extracts action names. Used at startup to cross-validate that every schema-defined
/// action has a registered C# handler and vice versa.
/// </summary>
internal class SchemaValidator
{
/// <summary>
/// Default path from the autoShell binary to the desktop agent's dist folder.
/// </summary>
internal static readonly string DefaultSchemaRelativePath =
Path.Combine("..", "..", "..", "..", "ts", "packages", "agents", "desktop", "dist");
private readonly ILogger _logger;
public SchemaValidator(ILogger logger)
{
_logger = logger;
}
/// <summary>
/// Extracts action names from all <c>.pas.json</c> files in the given directory.
/// Each action type in the schema has an <c>actionName</c> field whose
/// <c>typeEnum</c> array contains the action name as its single element.
/// </summary>
/// <returns>A set of action names found across all schema files, or an empty set if the directory is missing.</returns>
public HashSet<string> LoadActionNames(string schemaDirectory)
{
var actionNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!Directory.Exists(schemaDirectory))
{
_logger.Info($"Schema directory not found: {schemaDirectory}. Skipping validation.");
return actionNames;
}
var schemaFiles = Directory.GetFiles(schemaDirectory, "*.pas.json");
if (schemaFiles.Length == 0)
{
_logger.Info($"No .pas.json files found in {schemaDirectory}. Skipping validation.");
return actionNames;
}
foreach (var filePath in schemaFiles)
{
try
{
var names = ExtractActionNames(File.ReadAllText(filePath));
actionNames.UnionWith(names);
}
catch (Exception ex)
{
_logger.Warning($"Failed to parse schema file {Path.GetFileName(filePath)}: {ex.Message}");
}
}
return actionNames;
}
/// <summary>
/// Extracts action names from a single <c>.pas.json</c> JSON string.
/// Deserializes into <see cref="ActionSchemaFile"/> and finds types whose
/// <c>actionName</c> field has a <c>string-union</c> type with a <c>typeEnum</c> array.
/// </summary>
internal static HashSet<string> ExtractActionNames(string json)
{
var actionNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var schema = JsonSerializer.Deserialize<ActionSchemaFile>(json);
if (schema?.Types == null)
{
return actionNames;
}
foreach (var schemaType in schema.Types.Values)
{
// Only object types can have an actionName field
if (schemaType.Type?.Kind != "object" || schemaType.Type.Fields == null)
{
continue;
}
if (!schemaType.Type.Fields.TryGetValue("actionName", out var actionNameField))
{
continue;
}
var typeEnum = actionNameField.Type?.TypeEnum;
if (typeEnum == null)
{
continue;
}
foreach (var name in typeEnum)
{
actionNames.Add(name);
}
}
return actionNames;
}
/// <summary>
/// Compares schema-defined action names against registered handler actions
/// and logs warnings for any mismatches.
/// </summary>
/// <param name="schemaActions">Action names from .pas.json files.</param>
/// <param name="registeredActions">Action names from handler SupportedActions.</param>
public void ValidateWiring(HashSet<string> schemaActions, IEnumerable<string> registeredActions)
{
var (missingHandlers, missingSchemas) = FindMismatches(schemaActions, registeredActions);
foreach (var action in missingHandlers)
{
_logger.Warning($"Schema action '{action}' has no registered C# handler.");
}
foreach (var action in missingSchemas)
{
_logger.Warning($"Handler action '{action}' has no matching schema definition.");
}
}
/// <summary>
/// Returns the set of schema actions without handlers and handler actions without schemas.
/// Useful for testing that wiring is complete.
/// </summary>
internal static (List<string> MissingHandlers, List<string> MissingSchemas) FindMismatches(
HashSet<string> schemaActions, IEnumerable<string> registeredActions)
{
var registered = new HashSet<string>(registeredActions, StringComparer.OrdinalIgnoreCase);
var missingHandlers = new List<string>();
var missingSchemas = new List<string>();
foreach (var action in schemaActions)
{
if (!registered.Contains(action))
{
missingHandlers.Add(action);
}
}
foreach (var action in registered)
{
if (!schemaActions.Contains(action))
{
missingSchemas.Add(action);
}
}
return (missingHandlers, missingSchemas);
}
}