SessionZeroWasm/SessionZero/Data/SzfParser.cs

358 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection; // Required for Reflection
namespace SessionZero.Data;
public class SzfParser
{
// A static map to cache SzfObject types by their TypeIdentifier
private static readonly Dictionary<string, Type> _szfObjectTypeMap =
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
private static readonly object _typeMapLock = new object(); // For thread safety during map population
public SzfParser()
{
// Ensure the type map is populated when a parser instance is created
EnsureTypeMapPopulated();
}
/// <summary>
/// Ensures the internal map of SzfObject types and their string identifiers is populated.
/// This uses reflection to find all concrete SzfObject classes with the SzfObjectAttribute.
/// </summary>
private static void EnsureTypeMapPopulated()
{
if (_szfObjectTypeMap.Any()) return; // Already populated
lock (_typeMapLock)
{
if (_szfObjectTypeMap.Any()) return; // Double-check after acquiring lock
// Find all concrete (non-abstract) types that inherit from SzfObject
// and have the SzfObjectAttribute defined.
var szfObjectTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(SzfObject).IsAssignableFrom(p) && !p.IsAbstract &&
p.IsDefined(typeof(SzfObjectAttribute), false));
foreach (var type in szfObjectTypes)
{
var attribute = type.GetCustomAttribute<SzfObjectAttribute>();
if (attribute != null)
{
// Add to the map using the TypeIdentifier from the attribute
_szfObjectTypeMap[attribute.TypeIdentifier] = type;
}
}
}
}
/// <summary>
/// Parses an SZF content string into a specific SzfObject type.
/// </summary>
/// <typeparam name="T">The expected type of SzfObject.</typeparam>
/// <param name="szfContent">The SZF content string.</param>
/// <returns>An instance of the parsed SzfObject.</returns>
/// <exception cref="SzfParseException">Thrown if parsing fails or types mismatch.</exception>
public T Parse<T>(string szfContent) where T : SzfObject
{
var parseResult =
ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList());
// Validate that the parsed type identifier matches the expected type T
ValidateExpectedType<T>(parseResult.ObjectTypeIdentifier);
// Create an instance of the specific SzfObject type using the parsed identifier
var obj = CreateInstance<T>(parseResult.ObjectTypeIdentifier);
PopulateSzfObject(obj, parseResult);
return obj;
}
/// <summary>
/// Parses an SZF content string into a generic SzfObject. The concrete type will be determined from the content.
/// </summary>
/// <param name="szfContent">The SZF content string.</param>
/// <returns>An instance of the parsed SzfObject.</returns>
/// <exception cref="SzfParseException">Thrown if parsing fails.</exception>
public SzfObject Parse(string szfContent)
{
var parseResult =
ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList());
// Create an instance of the specific SzfObject type using the parsed identifier
var obj = CreateInstanceFromTypeIdentifier(parseResult.ObjectTypeIdentifier);
PopulateSzfObject(obj, parseResult);
return obj;
}
/// <summary>
/// Creates an instance of the specified SzfObject type, ensuring it's assignable to T.
/// </summary>
/// <typeparam name="T">The type to cast the created instance to.</typeparam>
/// <param name="typeIdentifier">The string identifier of the SzfObject type.</param>
/// <returns>A new instance of the SzfObject.</returns>
/// <exception cref="SzfParseException">Thrown if the type is unknown or cannot be instantiated.</exception>
private T CreateInstance<T>(string typeIdentifier) where T : SzfObject
{
EnsureTypeMapPopulated();
if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type))
{
throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
}
if (!typeof(T).IsAssignableFrom(type))
{
throw new SzfParseException(
$"Requested type '{typeof(T).Name}' is not assignable from parsed type '{type.Name}'.");
}
// Use Activator.CreateInstance to create an instance of the found type
return (T)Activator.CreateInstance(type)!;
}
/// <summary>
/// Creates a generic SzfObject instance from its string identifier.
/// </summary>
/// <param name="typeIdentifier">The string identifier of the SzfObject type.</param>
/// <returns>A new instance of SzfObject.</returns>
/// <exception cref="SzfParseException">Thrown if the type is unknown or cannot be instantiated.</exception>
public SzfObject CreateInstanceFromTypeIdentifier(string typeIdentifier)
{
EnsureTypeMapPopulated();
if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type))
{
throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
}
return (SzfObject)Activator.CreateInstance(type)!;
}
/// <summary>
/// Parses the raw SZF content lines into a structured SzfParseResult.
/// </summary>
public SzfParseResult ParseSzfStructure(List<string> lines)
{
var result = new SzfParseResult();
var currentSectionLines = new List<string>();
string? currentSectionName = null;
foreach (var line in lines)
{
var trimmedLine = line.Trim();
// Handle headers (!type:, !schema:)
if (trimmedLine.StartsWith("!type:", StringComparison.OrdinalIgnoreCase))
{
result.ObjectTypeIdentifier = trimmedLine.Substring("!type:".Length).Trim();
continue;
}
if (trimmedLine.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase))
{
result.SchemaVersion = trimmedLine.Substring("!schema:".Length).Trim();
continue;
}
if (trimmedLine.StartsWith("!", StringComparison.Ordinal)) // Other !headers
{
var parts = trimmedLine.Split(new[] { ':' }, 2);
if (parts.Length == 2)
{
result.Headers[parts[0].TrimStart('!').Trim()] = parts[1].Trim();
}
continue;
}
// Handle section headers ([Section Name])
if (trimmedLine.StartsWith("[") && trimmedLine.EndsWith("]"))
{
// If there's an active section, process it before starting a new one
if (currentSectionName != null)
{
ProcessSection(result, currentSectionName, currentSectionLines);
currentSectionLines.Clear();
}
currentSectionName = trimmedLine.Substring(1, trimmedLine.Length - 2).Trim();
continue;
}
// Handle empty lines (ignored within sections for parsing structure)
if (string.IsNullOrWhiteSpace(trimmedLine))
{
continue;
}
// Otherwise, it's a field line within the current section
if (currentSectionName != null)
{
currentSectionLines.Add(trimmedLine);
}
else
{
// Fields found before any section header, treat as global/unassigned, or throw error
// For now, we'll ignore them or consider them an error
// You might want to throw an exception here depending on strictness
}
}
// Process the last section if any
if (currentSectionName != null)
{
ProcessSection(result, currentSectionName, currentSectionLines);
}
return result;
}
/// <summary>
/// Processes lines belonging to a single section, parsing fields and adding to the result.
/// </summary>
private void ProcessSection(SzfParseResult result, string sectionName, List<string> sectionLines)
{
var szfSection = new SzfSection { Name = sectionName };
foreach (var line in sectionLines)
{
// Expected format: Name (type) = Value
var parts = line.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
var nameAndTypePart = parts[0].Trim();
var valueString = parts[1].Trim();
string fieldName;
string fieldType = "text"; // Default type if not specified
// Check for (type) in name part
var typeStart = nameAndTypePart.LastIndexOf('(');
var typeEnd = nameAndTypePart.LastIndexOf(')');
if (typeStart != -1 && typeEnd != -1 && typeEnd > typeStart)
{
fieldName = nameAndTypePart.Substring(0, typeStart).Trim();
fieldType = nameAndTypePart.Substring(typeStart + 1, typeEnd - typeStart - 1).Trim();
}
else
{
fieldName = nameAndTypePart;
}
szfSection.Fields.Add(new SzfField
{
Name = fieldName,
Type = fieldType,
Value = ParseFieldValue(valueString, fieldType)
});
}
// Else, malformed field line, ignore or log error
}
result.Sections.Add(szfSection);
}
/// <summary>
/// Parses a field value string into an appropriate object type based on the field type.
/// </summary>
private object? ParseFieldValue(string valueString, string fieldType)
{
return fieldType.ToLowerInvariant() switch
{
"number" => ParseNumber(valueString),
"bool" => ParseBoolean(valueString),
"text-field" => valueString, // Text-fields are multi-line, keep as string
_ => valueString // Default to string for "text" and unknown types
};
}
private object ParseNumber(string value)
{
if (int.TryParse(value, out var i)) return i;
if (double.TryParse(value, out var d)) return d;
return 0; // Default or throw error
}
private bool ParseBoolean(string value)
{
return bool.TryParse(value, out var b) && b;
}
/// <summary>
/// Validates that the parsed object type identifier matches the expected type T.
/// </summary>
/// <typeparam name="T">The expected SzfObject type.</typeparam>
/// <param name="parsedTypeIdentifier">The string identifier parsed from the SZF content.</param>
/// <exception cref="SzfParseException">Thrown if the types do not match.</exception>
/// <exception cref="InvalidOperationException">Thrown if T is not properly decorated with SzfObjectAttribute.</exception>
private void ValidateExpectedType<T>(string parsedTypeIdentifier) where T : SzfObject
{
var targetAttribute = typeof(T).GetCustomAttribute<SzfObjectAttribute>();
if (targetAttribute == null)
{
throw new InvalidOperationException(
$"Type '{typeof(T).Name}' is missing the SzfObjectAttribute. Cannot validate expected type.");
}
if (!string.Equals(targetAttribute.TypeIdentifier, parsedTypeIdentifier, StringComparison.OrdinalIgnoreCase))
{
throw new SzfParseException(
$"Mismatched SzfObject type. Expected '{targetAttribute.TypeIdentifier}' but found '{parsedTypeIdentifier}'.");
}
}
/// <summary>
/// Populates an SzfObject instance with the data from a parsed SzfParseResult.
/// </summary>
public void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult)
{
// Extract metadata fields into a dictionary for PopulateFromMetadata
var metadataFields = parseResult.Sections
.FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))?
.Fields.ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase)
?? new Dictionary<string, SzfField>();
obj.PopulateFromMetadata(metadataFields);
// Pass all sections (excluding Metadata, which PopulateFromMetadata might handle) to ParseSections
// ParseSections is responsible for building the object's specific internal structure.
obj.ParseSections(parseResult.Sections);
}
}
/// <summary>
/// Represents the parsed structure of an SZF file.
/// </summary>
public class SzfParseResult
{
// Changed from SzfObjectType to string
public string ObjectTypeIdentifier { get; set; } = string.Empty;
public string SchemaVersion { get; set; } = string.Empty;
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
public List<SzfSection> Sections { get; set; } = new List<SzfSection>();
}
/// <summary>
/// Represents a section within an SZF file.
/// </summary>
public class SzfSection
{
public string Name { get; set; } = string.Empty;
public List<SzfField> Fields { get; set; } = new List<SzfField>();
}
/// <summary>
/// Exception thrown during SZF parsing.
/// </summary>
public class SzfParseException : Exception
{
public SzfParseException(string message) : base(message)
{
}
public SzfParseException(string message, Exception innerException) : base(message, innerException)
{
}
}