From 2e212e2c37175efb4f9594614081878292734f6a Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Mon, 30 Jun 2025 22:51:23 -0500 Subject: [PATCH] Szf parsing --- SessionZero/Data/Dataset.cs | 116 ++++++++- SessionZero/Data/SzfGenerator.cs | 250 +++++++++++++++++++ SessionZero/Data/SzfObject.cs | 115 +++++++++ SessionZero/Data/SzfParser.cs | 412 +++++++++++++++++++++++++++++++ SessionZero/Pages/Test.razor | 183 +++++++++++++- SessionZero/Program.cs | 2 + technical-specifications.md | 4 +- 7 files changed, 1073 insertions(+), 9 deletions(-) create mode 100644 SessionZero/Data/SzfGenerator.cs create mode 100644 SessionZero/Data/SzfObject.cs create mode 100644 SessionZero/Data/SzfParser.cs diff --git a/SessionZero/Data/Dataset.cs b/SessionZero/Data/Dataset.cs index 028d0e8..fbc336f 100644 --- a/SessionZero/Data/Dataset.cs +++ b/SessionZero/Data/Dataset.cs @@ -1,27 +1,133 @@ namespace SessionZero.Data; -public class Dataset +public class Dataset : SzfObject { - public Guid Id { get; set; } = Guid.NewGuid(); - public string Name { get; set; } = string.Empty; public string DatasetType { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; + + // Collection of entries within this dataset + public List Entries { get; set; } = []; + + // Additional metadata fields for .szf compatibility + public Dictionary Metadata { get; set; } = new(); + + public override SzfObjectType ObjectType => SzfObjectType.Dataset; + + public override void PopulateFromMetadata(Dictionary metadata) + { + // Call base method to populate common fields + base.PopulateFromMetadata(metadata); + + // Populate dataset-specific fields + if (metadata.TryGetValue("Type", out var typeField)) + DatasetType = typeField.Value?.ToString() ?? string.Empty; + + if (metadata.TryGetValue("ImageUrl", out var imageField)) + ImageUrl = imageField.Value?.ToString() ?? string.Empty; + } + + public override SzfValidationResult Validate() + { + var result = base.Validate(); + + if (string.IsNullOrWhiteSpace(DatasetType)) + result.AddError("DatasetType is required"); + + // Validate entries + var entryNames = new HashSet(); + foreach (var entry in Entries) + { + if (string.IsNullOrWhiteSpace(entry.Name)) + { + result.AddError("All entries must have a name"); + continue; + } + + if (!entryNames.Add(entry.Name)) + result.AddError($"Duplicate entry name: {entry.Name}"); + } + + return result; + } +} + +public class DatasetEntry +{ + public string Name { get; set; } = string.Empty; public List Fields { get; set; } = []; + public List Sections { get; set; } = []; +} + +public class DatasetSection +{ + public string Name { get; set; } = string.Empty; + public List Fields { get; set; } = []; + public List Subsections { get; set; } = []; } public class DatasetField { - public int Id { get; set; } public string Name { get; set; } = string.Empty; public DatasetFieldType Type { get; set; } public object? Value { get; set; } = null; + public string? Description { get; set; } + public bool IsRequired { get; set; } = false; + public object? DefaultValue { get; set; } + + // For calculated fields + public string? Formula { get; set; } + + // For dataset reference fields + public string? ReferencedDatasetId { get; set; } + public string? ReferencedDatasetType { get; set; } + public bool AllowMultiple { get; set; } = false; + + // For group fields + public List GroupFields { get; set; } = []; } public enum DatasetFieldType { Text, + TextField, Number, Boolean, Group, + Calculated, + System, + DatasetReference, + DatasetType, + DatasetReferenceMultiple, + DatasetTypeMultiple +} + +// Supporting classes for dataset management +public class DatasetReference +{ + public string Name { get; set; } = string.Empty; + public Guid DatasetId { get; set; } + public string Version { get; set; } = string.Empty; + + public override string ToString() => $"{Name}|{DatasetId}|{Version}"; + + public static DatasetReference? Parse(string reference) + { + var parts = reference.Split('|'); + if (parts.Length != 3 || !Guid.TryParse(parts[1], out var id)) + return null; + + return new DatasetReference + { + Name = parts[0], + DatasetId = id, + Version = parts[2] + }; + } +} + +public class DatasetValidationResult +{ + public bool IsValid { get; set; } + public List Errors { get; set; } = []; + public List Warnings { get; set; } = []; } \ No newline at end of file diff --git a/SessionZero/Data/SzfGenerator.cs b/SessionZero/Data/SzfGenerator.cs new file mode 100644 index 0000000..b7ec4ee --- /dev/null +++ b/SessionZero/Data/SzfGenerator.cs @@ -0,0 +1,250 @@ +using System.Text; + +namespace SessionZero.Data; + +public class SzfGenerator +{ + private readonly SzfGeneratorOptions _options; + + public SzfGenerator(SzfGeneratorOptions? options = null) + { + _options = options ?? new SzfGeneratorOptions(); + } + + /// + /// Generate SZF content from any SzfObject + /// + public string Generate(SzfObject obj) + { + var builder = new StringBuilder(); + + // Generate header + GenerateHeader(builder, obj); + + // Generate metadata section + GenerateMetadataSection(builder, obj); + + // Generate type-specific content + GenerateTypeSpecificContent(builder, obj); + + return builder.ToString(); + } + + /// + /// Generate the SZF header (!type: and !schema:) + /// + private void GenerateHeader(StringBuilder builder, SzfObject obj) + { + var objectTypeString = obj.ObjectType switch + { + SzfObjectType.Dataset => "dataset", + SzfObjectType.CharacterTemplate => "character_template", + SzfObjectType.Character => "character", + SzfObjectType.Session => "session", + _ => throw new SzfGenerateException($"Unsupported object type: {obj.ObjectType}") + }; + + builder.AppendLine($"!type: {objectTypeString}"); + builder.AppendLine($"!schema: {obj.SchemaVersion}"); + builder.AppendLine(); + } + + /// + /// Generate the common metadata section + /// + private void GenerateMetadataSection(StringBuilder builder, SzfObject obj) + { + builder.AppendLine("[Metadata]"); + + // Standard metadata fields + WriteField(builder, "Name", "text", obj.Name); + WriteField(builder, "Version", "text", obj.Version); + WriteField(builder, "GUID", "text", obj.Id.ToString()); + WriteField(builder, "Description", "text-field", obj.Description); + WriteField(builder, "Author", "text", obj.Author); + + // Type-specific metadata + GenerateTypeSpecificMetadata(builder, obj); + + builder.AppendLine(); + } + + /// + /// Generate type-specific metadata fields + /// + private void GenerateTypeSpecificMetadata(StringBuilder builder, SzfObject obj) + { + switch (obj) + { + case Dataset dataset: + WriteField(builder, "Type", "text", dataset.DatasetType); + if (!string.IsNullOrEmpty(dataset.ImageUrl)) + WriteField(builder, "ImageUrl", "text", dataset.ImageUrl); + break; + // Add other types as they're implemented + } + } + + /// + /// Generate type-specific content sections + /// + private void GenerateTypeSpecificContent(StringBuilder builder, SzfObject obj) + { + switch (obj) + { + case Dataset dataset: + GenerateDatasetContent(builder, dataset); + break; + // Add other types as they're implemented + default: + throw new SzfGenerateException($"Content generation not implemented for type: {obj.GetType().Name}"); + } + } + + /// + /// Generate dataset-specific content (entries, sections, fields) + /// + private void GenerateDatasetContent(StringBuilder builder, Dataset dataset) + { + foreach (var entry in dataset.Entries) + { + // Entry header + builder.AppendLine($"[Entry: {entry.Name}]"); + + // Entry fields + foreach (var field in entry.Fields) + { + WriteDatasetField(builder, field); + } + + // Entry sections + foreach (var section in entry.Sections) + { + GenerateDatasetSection(builder, section, $"Entry.{entry.Name}"); + } + + builder.AppendLine(); + } + } + + /// + /// Generate dataset sections recursively + /// + private void GenerateDatasetSection(StringBuilder builder, DatasetSection section, string parentPath) + { + var sectionPath = $"{parentPath}.{section.Name}"; + builder.AppendLine($"[{sectionPath}]"); + + // Section fields + foreach (var field in section.Fields) + { + WriteDatasetField(builder, field); + } + + // Subsections + foreach (var subsection in section.Subsections) + { + GenerateDatasetSection(builder, subsection, sectionPath); + } + + builder.AppendLine(); + } + + /// + /// Write a dataset field in SZF format + /// + private void WriteDatasetField(StringBuilder builder, DatasetField field) + { + var typeString = field.Type switch + { + DatasetFieldType.Text => "text", + DatasetFieldType.TextField => "text-field", + DatasetFieldType.Number => "number", + DatasetFieldType.Boolean => "bool", + DatasetFieldType.Group => "group", + DatasetFieldType.Calculated => "calculated", + DatasetFieldType.System => "system", + DatasetFieldType.DatasetReference => "dataset-reference", + DatasetFieldType.DatasetType => "dataset-type", + DatasetFieldType.DatasetReferenceMultiple => "dataset-reference-multiple", + DatasetFieldType.DatasetTypeMultiple => "dataset-type-multiple", + _ => "text" + }; + + var value = FormatFieldValue(field.Value, field.Type); + WriteField(builder, field.Name, typeString, value); + } + + /// + /// Write a field in SZF format: Name (type) = value + /// + private void WriteField(StringBuilder builder, string name, string type, object? value) + { + var formattedValue = value?.ToString() ?? string.Empty; + + if (_options.IndentFields) + { + builder.AppendLine($"{_options.Indent}{name} ({type}) = {formattedValue}"); + } + else + { + builder.AppendLine($"{name} ({type}) = {formattedValue}"); + } + } + + /// + /// Format field values based on their type + /// + private string FormatFieldValue(object? value, DatasetFieldType fieldType) + { + if (value == null) + return string.Empty; + + return fieldType switch + { + DatasetFieldType.Boolean => (bool)value ? "true" : "false", + DatasetFieldType.Number => value.ToString() ?? "0", + _ => value.ToString() ?? string.Empty + }; + } +} + +/// +/// Configuration options for SZF generation +/// +public class SzfGeneratorOptions +{ + /// + /// Whether to indent field lines within sections + /// + public bool IndentFields { get; set; } = false; + + /// + /// Indent string to use for fields (if IndentFields is true) + /// + public string Indent { get; set; } = " "; + + /// + /// Whether to include empty sections + /// + public bool IncludeEmptySections { get; set; } = false; + + /// + /// Whether to include fields with null/empty values + /// + public bool IncludeEmptyFields { get; set; } = true; +} + +/// +/// Exception thrown during SZF generation +/// +public class SzfGenerateException : Exception +{ + public SzfGenerateException(string message) : base(message) + { + } + + public SzfGenerateException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/SessionZero/Data/SzfObject.cs b/SessionZero/Data/SzfObject.cs new file mode 100644 index 0000000..8669dce --- /dev/null +++ b/SessionZero/Data/SzfObject.cs @@ -0,0 +1,115 @@ +namespace SessionZero.Data; + +public abstract class SzfObject +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + public string Version { get; set; } = "1.0.0"; + public string Description { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + public DateTime ModifiedDate { get; set; } = DateTime.UtcNow; + + public string SchemaVersion { get; set; } = "1.0.0"; + public Dictionary ExtendedMetadata { get; set; } = new(); + + public abstract SzfObjectType ObjectType { get; } + + public virtual SzfValidationResult Validate() + { + var result = new SzfValidationResult { IsValid = true }; + + if (string.IsNullOrWhiteSpace(Name)) + result.AddError("Name is required"); + + if (Id == Guid.Empty) + result.AddError("Valid GUID is required"); + + if (!IsValidSemanticVersion(Version)) + result.AddError($"Invalid version format: {Version}"); + + return result; + } + + /// + /// Parse SZF content and auto-detect the type + /// + public static SzfObject ParseFromSzf(string szfContent) + { + var parser = new SzfParser(); + return parser.Parse(szfContent); + } + + /// + /// Parse SZF content into a specific type + /// + public static T ParseFromSzf(string szfContent) where T : SzfObject + { + var parser = new SzfParser(); + return parser.Parse(szfContent); + } + + public virtual string ToSzfString() + { + var generator = new SzfGenerator(); + return generator.Generate(this); + } + + public virtual void PopulateFromMetadata(Dictionary metadata) + { + if (metadata.TryGetValue("Name", out var nameField)) + Name = nameField.Value?.ToString() ?? string.Empty; + + if (metadata.TryGetValue("Version", out var versionField)) + Version = versionField.Value?.ToString() ?? "1.0.0"; + + if (metadata.TryGetValue("GUID", out var guidField) && + Guid.TryParse(guidField.Value?.ToString(), out var guid)) + Id = guid; + + if (metadata.TryGetValue("Author", out var authorField)) + Author = authorField.Value?.ToString() ?? string.Empty; + + if (metadata.TryGetValue("Description", out var descField)) + Description = descField.Value?.ToString() ?? string.Empty; + } + + private static bool IsValidSemanticVersion(string version) + { + return System.Text.RegularExpressions.Regex.IsMatch( + version, @"^\d+\.\d+\.\d+$"); + } +} + +public enum SzfObjectType +{ + Dataset, + CharacterTemplate, + Character, + Session +} + +public class SzfValidationResult +{ + public bool IsValid { get; set; } = true; + public List Errors { get; set; } = []; + public List Warnings { get; set; } = []; + + public void AddError(string error) + { + Errors.Add(error); + IsValid = false; + } + + public void AddWarning(string warning) + { + Warnings.Add(warning); + } +} + +public class SzfField +{ + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public object? Value { get; set; } +} \ No newline at end of file diff --git a/SessionZero/Data/SzfParser.cs b/SessionZero/Data/SzfParser.cs new file mode 100644 index 0000000..8449674 --- /dev/null +++ b/SessionZero/Data/SzfParser.cs @@ -0,0 +1,412 @@ +using System.Text.RegularExpressions; + +namespace SessionZero.Data; + +public class SzfParser +{ + private static readonly Regex HeaderRegex = new(@"^!(\w+):\s*(.+)$", RegexOptions.Compiled); + private static readonly Regex SectionRegex = new(@"^\[([^\]]+)\]$", RegexOptions.Compiled); + private static readonly Regex FieldRegex = new(@"^(\w+)\s*\(([^)]+)\)\s*=\s*(.*)$", RegexOptions.Compiled); + + /// + /// Parse a SZF file content and return the appropriate SzfObject type + /// + public T Parse(string szfContent) where T : SzfObject + { + if (string.IsNullOrWhiteSpace(szfContent)) + throw new SzfParseException("SZF content cannot be empty"); + + var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrEmpty(line)) + .ToList(); + + var parseResult = ParseSzfStructure(lines); + + // Validate that the parsed type matches the requested type + ValidateObjectType(parseResult.ObjectType); + + var obj = CreateInstance(parseResult.ObjectType); + PopulateSzfObject(obj, parseResult); + + return obj; + } + + /// + /// Parse SZF content without specifying the exact type - auto-detects type + /// + public SzfObject Parse(string szfContent) + { + if (string.IsNullOrWhiteSpace(szfContent)) + throw new SzfParseException("SZF content cannot be empty"); + + var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrEmpty(line)) + .ToList(); + + var parseResult = ParseSzfStructure(lines); + + var obj = CreateInstanceFromType(parseResult.ObjectType); + PopulateSzfObject(obj, parseResult); + + return obj; + } + + /// + /// Create an instance of the appropriate type based on SzfObjectType + /// + private T CreateInstance(SzfObjectType objectType) where T : SzfObject + { + var instance = CreateInstanceFromType(objectType); + if (instance is T typedInstance) + return typedInstance; + + throw new SzfParseException($"Cannot cast {instance.GetType().Name} to {typeof(T).Name}"); + } + + /// + /// Create an instance based on the object type + /// + private SzfObject CreateInstanceFromType(SzfObjectType objectType) + { + return objectType switch + { + SzfObjectType.Dataset => new Dataset(), + // Add other types as they're implemented + // SzfObjectType.CharacterTemplate => new CharacterTemplate(), + // SzfObjectType.Character => new Character(), + // SzfObjectType.Session => new Session(), + _ => throw new SzfParseException($"Unsupported object type: {objectType}") + }; + } + + /// + /// Parse SZF content into a generic structure before type-specific processing + /// + private SzfParseResult ParseSzfStructure(List lines) + { + var result = new SzfParseResult(); + var currentSection = new List(); + var currentSectionName = string.Empty; + + foreach (var line in lines) + { + // Skip comments + if (line.StartsWith("//") || line.StartsWith("#")) + continue; + + // Parse headers (!type:, !schema:) + var headerMatch = HeaderRegex.Match(line); + if (headerMatch.Success) + { + var headerName = headerMatch.Groups[1].Value.ToLower(); + var headerValue = headerMatch.Groups[2].Value.Trim(); + + switch (headerName) + { + case "type": + result.ObjectType = ParseObjectType(headerValue); + break; + case "schema": + result.SchemaVersion = headerValue; + break; + default: + result.Headers[headerName] = headerValue; + break; + } + + continue; + } + + // Parse section headers [Section Name] + var sectionMatch = SectionRegex.Match(line); + if (sectionMatch.Success) + { + // Process previous section if exists + if (!string.IsNullOrEmpty(currentSectionName)) + { + ProcessSection(result, currentSectionName, currentSection); + } + + // Start new section + currentSectionName = sectionMatch.Groups[1].Value.Trim(); + currentSection.Clear(); + continue; + } + + // Add line to current section + if (!string.IsNullOrEmpty(currentSectionName)) + { + currentSection.Add(line); + } + } + + // Process final section + if (!string.IsNullOrEmpty(currentSectionName)) + { + ProcessSection(result, currentSectionName, currentSection); + } + + return result; + } + + /// + /// Process an individual section and its fields + /// + private void ProcessSection(SzfParseResult result, string sectionName, List sectionLines) + { + var section = new SzfSection { Name = sectionName }; + + foreach (var line in sectionLines) + { + var fieldMatch = FieldRegex.Match(line); + if (fieldMatch.Success) + { + var field = new SzfField + { + Name = fieldMatch.Groups[1].Value.Trim(), + Type = fieldMatch.Groups[2].Value.Trim(), + Value = ParseFieldValue(fieldMatch.Groups[3].Value.Trim(), fieldMatch.Groups[2].Value.Trim()) + }; + + section.Fields.Add(field); + } + } + + result.Sections.Add(section); + } + + /// + /// Parse field value based on its declared type + /// + private object? ParseFieldValue(string valueString, string fieldType) + { + if (string.IsNullOrEmpty(valueString)) + return null; + + return fieldType.ToLower() switch + { + "text" => valueString, + "text-field" => valueString, + "number" => ParseNumber(valueString), + "bool" => ParseBoolean(valueString), + "calculated" => valueString, // Store formula as string + "system" => valueString, + "dataset-reference" => valueString, + "dataset-type" => valueString, + "dataset-reference-multiple" => valueString, + "dataset-type-multiple" => valueString, + "group" => ParseBoolean(valueString), // Groups are typically true/false + _ => valueString // Default to string + }; + } + + /// + /// Parse number values (int or double) + /// + private object ParseNumber(string value) + { + if (int.TryParse(value, out var intResult)) + return intResult; + + if (double.TryParse(value, out var doubleResult)) + return doubleResult; + + throw new SzfParseException($"Invalid number format: {value}"); + } + + /// + /// Parse boolean values + /// + private bool ParseBoolean(string value) + { + return value.ToLower() switch + { + "true" => true, + "false" => false, + "1" => true, + "0" => false, + _ => throw new SzfParseException($"Invalid boolean format: {value}") + }; + } + + /// + /// Parse object type from string + /// + private SzfObjectType ParseObjectType(string typeString) + { + return typeString.ToLower() switch + { + "dataset" => SzfObjectType.Dataset, + "character_template" => SzfObjectType.CharacterTemplate, + "character" => SzfObjectType.Character, + "session" => SzfObjectType.Session, + _ => throw new SzfParseException($"Unknown object type: {typeString}") + }; + } + + /// + /// Validate that the parsed type matches the requested generic type + /// + private void ValidateObjectType(SzfObjectType parsedType) where T : SzfObject + { + var expectedType = typeof(T).Name switch + { + nameof(Dataset) => SzfObjectType.Dataset, + // Add other types as they're implemented + _ => throw new SzfParseException($"Unsupported SzfObject type: {typeof(T).Name}") + }; + + if (parsedType != expectedType) + { + throw new SzfParseException($"Type mismatch: Expected {expectedType}, found {parsedType}"); + } + } + + /// + /// Populate the SzfObject with parsed data + /// + private void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult) + { + // Set schema version + obj.SchemaVersion = parseResult.SchemaVersion; + + // Find and process Metadata section + var metadataSection = + parseResult.Sections.FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase)); + if (metadataSection != null) + { + var metadata = metadataSection.Fields.ToDictionary(f => f.Name, f => f); + obj.PopulateFromMetadata(metadata); + } + + // Process type-specific content + ProcessTypeSpecificContent(obj, parseResult); + } + + /// + /// Process type-specific content based on the object type + /// + private void ProcessTypeSpecificContent(SzfObject obj, SzfParseResult parseResult) + { + switch (obj) + { + case Dataset dataset: + ProcessDatasetContent(dataset, parseResult); + break; + // Add other types as they're implemented + } + } + + /// + /// Process dataset-specific content (entries and their sections) + /// + private void ProcessDatasetContent(Dataset dataset, SzfParseResult parseResult) + { + var entrySections = parseResult.Sections + .Where(s => s.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var entrySection in entrySections) + { + var entryName = entrySection.Name.Substring(6).Trim(); // Remove "Entry: " prefix + var entry = new DatasetEntry { Name = entryName }; + + // Add fields directly on the entry + foreach (var field in entrySection.Fields) + { + entry.Fields.Add(ConvertToDatasetField(field)); + } + + // Find subsections for this entry + var entrySubsections = parseResult.Sections + .Where(s => s.Name.StartsWith($"Entry.{entryName}.", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var subsection in entrySubsections) + { + var sectionName = subsection.Name.Substring($"Entry.{entryName}.".Length); + var datasetSection = new DatasetSection { Name = sectionName }; + + foreach (var field in subsection.Fields) + { + datasetSection.Fields.Add(ConvertToDatasetField(field)); + } + + entry.Sections.Add(datasetSection); + } + + dataset.Entries.Add(entry); + } + } + + /// + /// Convert SzfField to DatasetField + /// + private DatasetField ConvertToDatasetField(SzfField szfField) + { + return new DatasetField + { + Name = szfField.Name, + Type = ParseDatasetFieldType(szfField.Type), + Value = szfField.Value + }; + } + + /// + /// Parse DatasetFieldType from string + /// + private DatasetFieldType ParseDatasetFieldType(string typeString) + { + return typeString.ToLower() switch + { + "text" => DatasetFieldType.Text, + "text-field" => DatasetFieldType.TextField, + "number" => DatasetFieldType.Number, + "bool" => DatasetFieldType.Boolean, + "group" => DatasetFieldType.Group, + "calculated" => DatasetFieldType.Calculated, + "system" => DatasetFieldType.System, + "dataset-reference" => DatasetFieldType.DatasetReference, + "dataset-type" => DatasetFieldType.DatasetType, + "dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple, + "dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple, + _ => DatasetFieldType.Text + }; + } +} + +/// +/// Internal class to hold parsed SZF structure +/// +internal class SzfParseResult +{ + public SzfObjectType ObjectType { get; set; } + public string SchemaVersion { get; set; } = "1.0.0"; + public Dictionary Headers { get; set; } = new(); + public List Sections { get; set; } = new(); +} + +/// +/// Internal class representing a parsed section +/// +internal class SzfSection +{ + public string Name { get; set; } = string.Empty; + public List Fields { get; set; } = new(); +} + +/// +/// Exception thrown during SZF parsing +/// +public class SzfParseException : Exception +{ + public SzfParseException(string message) : base(message) + { + } + + public SzfParseException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/SessionZero/Pages/Test.razor b/SessionZero/Pages/Test.razor index d36e788..9fb6204 100644 --- a/SessionZero/Pages/Test.razor +++ b/SessionZero/Pages/Test.razor @@ -1,4 +1,183 @@ -@page "/Test" +@page "/szf-parser-test" +@using SessionZero.Data +@using Microsoft.Extensions.Logging +@inject SzfParser SzfParser +@inject ILogger Logger -

@_message

+

SZF Parser Test Page

+
+
+
+ +
+ +
+
+ @if (parsedObject != null) + { +

Parsed Object Metadata

+ + + + + + + + + + +
Object Type@parsedObject.ObjectType
Schema Version@parsedObject.SchemaVersion
Name@parsedObject.Name
Version@parsedObject.Version
ID@parsedObject.Id
Author@parsedObject.Author
Description@parsedObject.Description
+ + @if (parsedObject.ExtendedMetadata.Any()) + { +
Extended Metadata
+ + + + + + + + + @foreach (var meta in parsedObject.ExtendedMetadata) + { + + + + + } + +
KeyValue
@meta.Key@meta.Value
+ } + + @if (parsedObject is Dataset dataset && dataset.Entries.Any()) + { +

Entries

+ @foreach (var entry in dataset.Entries) + { +
+
+
@entry.Name
+
+
+ @if (entry.Fields.Any()) + { + + + @foreach (var field in entry.Fields) + { + + + + + } + +
@field.Name@field.Value
+ } + @foreach (var section in entry.Sections) + { + @RenderSection(section) + } +
+
+ } + } + } + @if (errorMessages.Any()) + { +
+

Errors

+
    + @foreach (var error in errorMessages) + { +
  • @error
  • + } +
+
+ } +
+
+ +
+
+

Logs

+
@string.Join("\n", logMessages)
+
+
+ +@code { + private string szfCode = string.Empty; + private SzfObject? parsedObject; + private List logMessages = new List(); + private List errorMessages = new List(); + + private void ParseSzf() + { + logMessages.Clear(); + errorMessages.Clear(); + parsedObject = null; + + Log("Starting SZF parsing..."); + + if (string.IsNullOrWhiteSpace(szfCode)) + { + const string message = "SZF code input is empty."; + LogError(message); + errorMessages.Add(message); + return; + } + + try + { + parsedObject = SzfParser.Parse(szfCode); + Log("SZF content parsed successfully."); + Log($"Object Type: {parsedObject.ObjectType}"); + Log($"Schema Version: {parsedObject.SchemaVersion}"); + } + catch (Exception ex) + { + const string message = "An error occurred during parsing."; + LogError(message, ex); + errorMessages.Add($"{message} See logs for details."); + } + } + + private void Log(string message) + { + Logger.LogInformation(message); + logMessages.Add($"[INFO] {message}"); + } + + private void LogError(string message, Exception? ex = null) + { + Logger.LogError(ex, message); + logMessages.Add($"[ERROR] {message}{(ex is not null ? $": {ex.Message}" : "")}"); + } + + private RenderFragment RenderSection(DatasetSection dataSection) =>@
+
+ @dataSection.Name +
+
+ @if (dataSection.Fields.Any()) + { + + + @foreach (var field in dataSection.Fields) + { + + + + + } + +
@field.Name@field.Value
+ } + @foreach (var subSection in dataSection.Subsections) + { + @RenderSection(subSection) + } +
+
; +} \ No newline at end of file diff --git a/SessionZero/Program.cs b/SessionZero/Program.cs index 049ec91..84e4f6f 100644 --- a/SessionZero/Program.cs +++ b/SessionZero/Program.cs @@ -2,6 +2,7 @@ using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using SessionZero; +using SessionZero.Data; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); @@ -13,6 +14,7 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder. builder.Services.AddBlazoredLocalStorage(); // Register our services +builder.Services.AddScoped(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/technical-specifications.md b/technical-specifications.md index 5cff350..7ea209c 100644 --- a/technical-specifications.md +++ b/technical-specifications.md @@ -289,13 +289,13 @@ Level1Spells (dataset-type-multiple) = spells ## Development Roadmap -### Phase 1: Core Foundation ✅ +### Phase 1: Core Foundation - [] Library system implementation - [] Template Builder functionality - [] Character Manager interface - [] Basic session framework -### Phase 2: Enhanced Sessions (Current) +### Phase 2: Enhanced Sessions - [ ] Dataset loading into sessions - [ ] Advanced session management - [ ] Character-session integration