358 lines
14 KiB
C#
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)
|
|
{
|
|
}
|
|
} |