diff --git a/SessionZero.SzfCli/Program.cs b/SessionZero.SzfCli/Program.cs index eecb11b..250be58 100644 --- a/SessionZero.SzfCli/Program.cs +++ b/SessionZero.SzfCli/Program.cs @@ -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" + @@ -11,19 +42,32 @@ AnsiConsole.MarkupLine("[deepskyblue4_1]\n\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("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); } \ No newline at end of file diff --git a/SessionZero.SzfLib/Helpers/SzfHelper.cs b/SessionZero.SzfLib/Helpers/SzfHelper.cs new file mode 100644 index 0000000..fb835ac --- /dev/null +++ b/SessionZero.SzfLib/Helpers/SzfHelper.cs @@ -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()?.TypeIdentifier == szfType); + + if (obj is null) return null; + + return obj.IsAbstract || obj.IsInterface ? null : obj; + } +} \ No newline at end of file diff --git a/SessionZero.SzfLib/Objects/ISzfObject.cs b/SessionZero.SzfLib/Objects/ISzfObject.cs index e4fbf4a..e557dd9 100644 --- a/SessionZero.SzfLib/Objects/ISzfObject.cs +++ b/SessionZero.SzfLib/Objects/ISzfObject.cs @@ -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 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(); } \ No newline at end of file diff --git a/SessionZero.SzfLib/Objects/SzfObject.cs b/SessionZero.SzfLib/Objects/SzfObject.cs index f7e8231..92edb7b 100644 --- a/SessionZero.SzfLib/Objects/SzfObject.cs +++ b/SessionZero.SzfLib/Objects/SzfObject.cs @@ -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 Sections { get; set; } = new(); + + + /// + /// Handles specific parsing logic for the SzfObject that overrides this method. + /// + /// + public virtual ISzfObject? Parse() + { + return this; + } + + /// + /// Gets the value of a field in the Metadata section. + /// + /// + /// + /// + 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); + } + + /// + /// Gets the value of a field in a specific section. Can handle nested sections using dot notation (e.g., "Section.Subsection"). + /// + /// + /// + /// + /// + 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; + } + + } \ No newline at end of file diff --git a/SessionZero.SzfLib/Parser/ISzfParser.cs b/SessionZero.SzfLib/Parser/ISzfParser.cs index f737f15..1b0f75d 100644 --- a/SessionZero.SzfLib/Parser/ISzfParser.cs +++ b/SessionZero.SzfLib/Parser/ISzfParser.cs @@ -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); } \ No newline at end of file diff --git a/SessionZero.SzfLib/Parser/SzfParser.cs b/SessionZero.SzfLib/Parser/SzfParser.cs index a0bf989..c54d39b 100644 --- a/SessionZero.SzfLib/Parser/SzfParser.cs +++ b/SessionZero.SzfLib/Parser/SzfParser.cs @@ -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) + /// + /// Parses the given SZF content and returns the corresponding ISzfObject. + /// + /// + /// + 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(); + + 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; + } + + if (line.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase)) + { + 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(); + + 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) + public ISzfObject? Parse(ISzfFile file) { - throw new NotImplementedException(); + 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 fields, Dictionary 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 Fields { get; set; } = new(); + public Dictionary 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, } \ No newline at end of file