SzfParser can now parse szf files. Specific parsing rules can be set up in each object.

This commit is contained in:
Chris Bell 2025-07-17 00:07:20 -05:00
parent d86c9db513
commit 69df933609
6 changed files with 331 additions and 21 deletions

View File

@ -1,6 +1,37 @@
// See https://aka.ms/new-console-template for more information
using Spectre.Console;
using SessionZero.SzfLib.Parser;
using System.IO;
using Spectre.Console;
void PrintSection(dynamic section, int indent = 0)
{
string indentStr = new string(' ', indent * 2);
AnsiConsole.MarkupLine($"\n{indentStr}[bold yellow]Section:[/] {section.Name}");
if (section.Fields.Count == 0)
{
AnsiConsole.MarkupLine($"{indentStr}[italic]No fields[/]");
}
else
{
var table = new Table();
table.AddColumn("Field Name");
table.AddColumn("Type");
table.AddColumn("Value");
foreach (var field in section.Fields.Values)
{
var typedField = (SzfField)field;
table.AddRow(typedField.Name, typedField.SzfType.ToString(), typedField.Value ?? "");
}
AnsiConsole.Write(table);
}
if (section.Subsections.Count > 0)
{
AnsiConsole.MarkupLine($"{indentStr}[italic]Subsections:[/]");
foreach (var sub in section.Subsections.Values)
{
PrintSection(sub, indent + 1);
}
}
}
AnsiConsole.MarkupLine("[deepskyblue4_1]\n\n" +
" █████████ ███████████ ███████████ █████████ █████ █████\n" +
@ -12,18 +43,31 @@ AnsiConsole.MarkupLine("[deepskyblue4_1]\n\n" +
"░░█████████ ███████████ █████ ░░█████████ ███████████ █████\n" +
" ░░░░░░░░░ ░░░░░░░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░░ ░░░░░ \n\n[/]"
);
AnsiConsole.MarkupLine("[deepskyblue4_2]A tool for testing the SessionZero SZF Parser[/]");
AnsiConsole.MarkupLine("See [blue][link]https://sessionzero.app/szf-docs.html[/][/] for SZF documentation.");
struct SZColors
var filePath = AnsiConsole.Ask<string>("Enter the path to the [green]SZF file[/]:");
if (!File.Exists(filePath))
{
public static readonly Color BackgroundColor = Color.Grey19;
public static readonly Color PrimaryColor = Color.SteelBlue;
public static readonly Color PrimaryColorLight = Color.SlateBlue1;
public static readonly Color SecondaryColor = Color.NavyBlue;
public static readonly Color AccentColor = Color.DeepSkyBlue4_2;
public static readonly Color TextColor = Color.White;
public static readonly Color HeadingColor = Color.LightSteelBlue;
AnsiConsole.MarkupLine("[red]File not found![/]");
return;
}
string szfContent = File.ReadAllText(filePath);
var parser = new SzfParser();
var szfObject = parser.Parse(szfContent);
if (szfObject == null)
{
AnsiConsole.MarkupLine("[red]Failed to parse SZF file.[/]");
return;
}
AnsiConsole.MarkupLine($"[bold]SZF Type:[/] {szfObject.SzfType}");
AnsiConsole.MarkupLine($"[bold]SZF Version:[/] {szfObject.SzfVersion}");
AnsiConsole.MarkupLine($"[bold]Number of Sections:[/] {szfObject.Sections.Count}");
foreach (var section in szfObject.Sections)
{
PrintSection(section);
}

View File

@ -0,0 +1,19 @@
using System.Reflection;
using SessionZero.SzfLib.Objects;
namespace SessionZero.SzfLib.Helpers;
public static class SzfHelper
{
public static Type? FindSzfObjectTypeByAttributeType(string szfType)
{
var obj = Assembly
.GetExecutingAssembly()
.GetTypes()
.FirstOrDefault(t => t.GetCustomAttribute(typeof(SzfObjectAttribute)) is not null && t.GetCustomAttribute<SzfObjectAttribute>()?.TypeIdentifier == szfType);
if (obj is null) return null;
return obj.IsAbstract || obj.IsInterface ? null : obj;
}
}

View File

@ -1,6 +1,16 @@
using SessionZero.SzfLib.Parser;
namespace SessionZero.SzfLib.Objects;
public interface ISzfObject
{
public string SzfType { get; set; }
public string SzfVersion { get; set; }
public List<SzfSection> Sections { get; set; }
public string GetMetadataField(string fieldName);
public string GetFieldValue(string sectionName, string fieldName);
public string FindFieldValueInSection(SzfSection section, string[] path, int depth, string fieldName);
public ISzfObject? Parse();
}

View File

@ -1,6 +1,87 @@
using SessionZero.SzfLib.Parser;
namespace SessionZero.SzfLib.Objects;
public abstract class SzfObject : ISzfObject
{
public abstract string SzfType { get; set; }
public virtual string SzfType { get; set; }
public string SzfVersion { get; set; } = "1.0.0";
public List<SzfSection> Sections { get; set; } = new();
/// <summary>
/// Handles specific parsing logic for the SzfObject that overrides this method.
/// </summary>
/// <returns></returns>
public virtual ISzfObject? Parse()
{
return this;
}
/// <summary>
/// Gets the value of a field in the Metadata section.
/// </summary>
/// <param name="fieldName"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public string GetMetadataField(string fieldName)
{
if (string.IsNullOrEmpty(fieldName))
{
throw new ArgumentException("Field name cannot be null or empty.", nameof(fieldName));
}
return GetFieldValue("Metadata", fieldName);
}
/// <summary>
/// Gets the value of a field in a specific section. Can handle nested sections using dot notation (e.g., "Section.Subsection").
/// </summary>
/// <param name="sectionName"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public string GetFieldValue(string sectionName, string fieldName)
{
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentException("Section name cannot be null or empty.", nameof(sectionName));
}
if (string.IsNullOrEmpty(fieldName))
{
throw new ArgumentException("Field name cannot be null or empty.", nameof(fieldName));
}
var sectionPath = sectionName.Split('.');
foreach (var section in Sections)
{
var result = FindFieldValueInSection(section, sectionPath, 0, fieldName);
if (result != string.Empty) return result;
}
return string.Empty;
}
public string FindFieldValueInSection(SzfSection section, string[] path, int depth, string fieldName)
{
if (section.Name != path[depth]) return string.Empty;
if (depth == path.Length - 1)
{
if (section.Fields.TryGetValue(fieldName, out var field) && field is SzfField szfField) return szfField.Value ?? string.Empty;
return string.Empty;
}
foreach (var subsection in section.Subsections.Values)
{
var result = FindFieldValueInSection(subsection, path, depth + 1, fieldName);
if (result != string.Empty) return result;
}
return string.Empty;
}
}

View File

@ -5,6 +5,6 @@ namespace SessionZero.SzfLib.Parser;
public interface ISzfParser
{
public ISzfFile Parse(string szfContent);
public ISzfObject Parse(ISzfFile file);
public ISzfObject? Parse(string szfContent);
public ISzfObject? Parse(ISzfFile file);
}

View File

@ -1,17 +1,173 @@
using SessionZero.SzfLib.File;
using SessionZero.SzfLib.Helpers;
using SessionZero.SzfLib.Objects;
namespace SessionZero.SzfLib.Parser;
public class SzfParser : ISzfParser
{
public ISzfFile Parse(string szfContent)
/// <summary>
/// Parses the given SZF content and returns the corresponding ISzfObject.
/// </summary>
/// <param name="szfContent"></param>
/// <returns></returns>
public ISzfObject? Parse(string szfContent)
{
throw new NotImplementedException();
var lines = szfContent.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
string szfObjectTypeName = string.Empty;
Type? szfObjectType = null;
string szfVersion = "1.0.0";
var sections = new Dictionary<string, SzfSection>();
for (int idx = 0; idx < lines.Length; idx++)
{
var line = lines[idx].Trim();
if (line.StartsWith("#") || string.IsNullOrWhiteSpace(line)) continue;
if (line.StartsWith("!type:", StringComparison.OrdinalIgnoreCase))
{
var typeIdentifier = line.Substring("!type:".Length).Trim();
szfObjectTypeName = typeIdentifier;
szfObjectType = SzfHelper.FindSzfObjectTypeByAttributeType(typeIdentifier);
continue;
}
public ISzfObject Parse(ISzfFile file)
if (line.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase))
{
throw new NotImplementedException();
var version = line.Substring("!schema:".Length).Trim();
if (!string.IsNullOrEmpty(version)) szfVersion = version;
continue;
}
if (line.StartsWith("[") && line.EndsWith("]"))
{
var sectionName = line.Substring(1, line.Length - 2).Trim();
var sectionFields = new List<string>();
int i = idx + 1;
while (i < lines.Length && !(lines[i].Trim().StartsWith("[") && lines[i].Trim().EndsWith("]")))
{
var fieldLine = lines[i].Trim();
if (!string.IsNullOrWhiteSpace(fieldLine) && !fieldLine.StartsWith("#")) sectionFields.Add(fieldLine);
i++;
}
idx = i - 1;
CreateSection(sectionName, sectionFields, sections);
}
}
if (szfObjectType != null && sections.Count > 0)
{
var szfObject = (ISzfObject)Activator.CreateInstance(szfObjectType)!;
szfObject.SzfType = szfObjectTypeName;
szfObject.SzfVersion = szfVersion;
// Only add root sections (those without a dot in their name)
foreach (var section in sections.Values.Where(s => !sections.Keys.Any(k => k.EndsWith("." + s.Name))))
{
szfObject.Sections.Add(section);
}
return szfObject.Parse() ?? szfObject;
}
return null;
}
public ISzfObject? Parse(ISzfFile file)
{
if (file == null)
{
throw new ArgumentNullException(nameof(file), "File cannot be null.");
}
if (string.IsNullOrEmpty(file.Content))
{
throw new ArgumentException("File content cannot be null or empty.", nameof(file.Content));
}
return Parse(file.Content);
}
private SzfSection CreateSection(string sectionPath, IEnumerable<string> fields, Dictionary<string, SzfSection> sections)
{
var nameParts = sectionPath.Split('.');
SzfSection? parent = null;
string currentPath = "";
for (int p = 0; p < nameParts.Length; p++)
{
currentPath = currentPath == "" ? nameParts[p] : currentPath + "." + nameParts[p];
if (!sections.TryGetValue(currentPath, out var section))
{
section = new SzfSection(nameParts[p]);
sections[currentPath] = section;
if (parent != null) parent.Subsections[nameParts[p]] = section;
}
parent = section;
}
foreach (var field in fields)
{
var szfField = CreateField(field);
parent!.Fields.Add(szfField.Name, szfField);
}
return parent!;
}
private SzfField CreateField(string field)
{
// Expected format: FieldName (type) = Value
int typeStart = field.IndexOf('(');
int typeEnd = field.IndexOf(')');
if (typeStart < 0 || typeEnd < 0 || typeEnd < typeStart) throw new FormatException($"Invalid field format: {field}");
string fieldName = field.Substring(0, typeStart).Trim();
string fieldType = field.Substring(typeStart + 1, typeEnd - typeStart - 1).Trim().Replace("-", string.Empty);
// Find the first '=' after the closing parenthesis
int eqIndex = field.IndexOf('=', typeEnd + 1);
string fieldValue = eqIndex >= 0 ? field.Substring(eqIndex + 1).Trim() : string.Empty;
if (!Enum.TryParse(fieldType, true, out SzfFieldType type)) throw new FormatException($"Invalid field type: {fieldType}");
SzfField szfField = new(fieldName, type) { Value = fieldValue };
return szfField;
}
}
public class SzfSection(string name)
{
public string Name { get; set; } = name;
public Dictionary<string, SzfField> Fields { get; set; } = new();
public Dictionary<string, SzfSection> Subsections { get; set; } = new();
}
public class SzfField(string name, SzfFieldType type)
{
public string Name { get; set; } = name;
public SzfFieldType SzfType { get; set; } = type;
public string? Value { get; set; }
}
public enum SzfFieldType
{
Text,
TextField,
Number,
Bool,
Calculated,
System,
EntryReference,
EntryReferenceList,
}