Kowalski/Configuration/Core/Processor/TableProcessor.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
namespace Kowalski.Configuration.Core
{
public class TableProcessor
{
public enum State
{
Initial,
ImportedFields,
ImportedData,
ValidatedData,
}
public const string MetaRowPrefix = "#";
private const string OptionSeparator = "#";
private const string NameMark = "#name";
private const string TypeMark = "#type";
private const string OptionMark = "#option";
private State m_State;
private readonly TableConfig m_Table;
private readonly DataSheet m_Sheet;
private readonly Dictionary<string, Action<DataRow>> m_MetaRowImporters;
private readonly FieldFactory m_FieldFactory;
private bool m_NameRowImported = false;
private bool m_TypeRowImported = false;
private bool m_OptionRowImported = false;
private int m_DataRowStartIndex = -1;
private int[] m_FieldDataMapping;
public TableProcessor(TableConfig table, DataSheet sheet, FieldFactory factory)
{
m_State = State.Initial;
m_Table = table;
m_Sheet = sheet;
m_FieldFactory = factory;
m_MetaRowImporters = new Dictionary<string, Action<DataRow>>();
m_MetaRowImporters.Add(NameMark, ImportNameRow);
m_MetaRowImporters.Add(TypeMark, ImportTypeRow);
m_MetaRowImporters.Add(OptionMark, ImportOptionRow);
}
public void ImportFields(HashSet<string> tags)
{
if (m_State != State.Initial || m_Table.FieldList != null)
{
throw new InvalidOperationException($"Cannot import fields in sheet \"{m_Table.Path}\"");
}
int fieldCount = m_Sheet.ColumnCount - 1;
m_Table.FieldList = new List<FieldConfig>(fieldCount);
for (int i = 0; i < fieldCount; ++i)
{
var field = new FieldConfig();
m_Table.FieldList.Add(field);
}
int rowIndex = -1;
m_DataRowStartIndex = -1;
foreach (var row in m_Sheet)
{
rowIndex++;
string mark = Utils.ToString(row[0]);
if ((mark is null) || !mark.StartsWith(MetaRowPrefix, StringComparison.Ordinal))
{
// Reached data row, stop finding meta row
m_DataRowStartIndex = rowIndex;
break;
}
if (m_MetaRowImporters.TryGetValue(mark, out var importAction))
{
importAction(row);
}
else
{
throw new InvalidDataException($"Unknown meta row in sheet \"{m_Table.Path}\": \"{mark}\"");
}
}
AssertRequiredRowImported();
// Ensure option list is not null to reduce code in later steps
foreach (var fieldConfig in m_Table.FieldList)
{
if (fieldConfig.Options is null)
{
fieldConfig.Options = new List<IFieldOption>();
}
}
ValidateFields(tags);
ProcessFields();
m_State = State.ImportedFields;
}
public void ImportData()
{
if (m_State != State.ImportedFields)
{
throw new InvalidOperationException($"Cannot import data from sheet \"{m_Table.Path}\"");
}
// Check if data is empty
if (m_DataRowStartIndex == -1)
{
m_State = State.ImportedData;
return;
}
int rowIndex = -1;
foreach (var row in m_Sheet)
{
rowIndex++;
if (rowIndex < m_DataRowStartIndex)
{
continue;
}
// The first cell is not valid data and unused currently
if (!m_Table.TableData.AddRow(row.AsVirtual(1, m_FieldDataMapping), out string errorString))
{
throw new InvalidDataException($"Cannot import row from sheet \"{m_Table.Path}\": {errorString}");
}
}
m_State = State.ImportedData;
}
public void ValidateData()
{
if (m_State != State.ImportedData)
{
throw new InvalidOperationException($"Cannot validate data from sheet \"{m_Table.Path}\"");
}
if (!m_Table.TableData.EndDataProcess(out string errorString))
{
throw new InvalidDataException($"Data validation of sheet \"{m_Table.Path}\" failed: {errorString}");
}
m_State = State.ValidatedData;
}
private void ImportNameRow(DataRow row)
{
if (m_NameRowImported)
{
throw new InvalidDataException($"Duplicate meta row in sheet \"{m_Table.Path}\": \"{NameMark}\"");
}
int fieldCount = m_Table.FieldList.Count;
for (int i = 0; i < fieldCount; ++i)
{
string name = Utils.ToString(row[i + 1]);
if (!Utils.IsValidName(name))
{
throw new InvalidDataException($"Invalid field name in sheet \"{m_Table.Path}\": \"{name}\"");
}
m_Table.FieldList[i].Name = new NameString(name);
}
m_NameRowImported = true;
}
private void ImportTypeRow(DataRow row)
{
if (m_TypeRowImported)
{
throw new InvalidDataException($"Duplicate meta row in sheet \"{m_Table.Path}\": \"{TypeMark}\"");
}
int fieldCount = m_Table.FieldList.Count;
for (int i = 0; i < fieldCount; ++i)
{
string typeString = Utils.ToString(row[i + 1]);
if (string.IsNullOrEmpty(typeString))
{
throw new InvalidDataException($"Empty field type in sheet \"{m_Table.Path}\"");
}
string[] definitionArray = typeString.Split(OptionSeparator);
var fieldType = m_FieldFactory.CreateFieldType(definitionArray[0], definitionArray.AsSpan(1));
if (null == fieldType)
{
fieldType = new InvalidFieldType(typeString);
}
m_Table.FieldList[i].Type = fieldType;
}
m_TypeRowImported = true;
}
private void ImportOptionRow(DataRow row)
{
if (m_OptionRowImported)
{
throw new InvalidDataException($"Duplicate meta row in sheet \"{m_Table.Path}\": \"{OptionMark}\"");
}
int fieldCount = m_Table.FieldList.Count;
for (int i = 0; i < fieldCount; ++i)
{
string optionString = Utils.ToString(row[i + 1]);
var optionList = new List<IFieldOption>();
if (!string.IsNullOrEmpty(optionString))
{
string[] definitionArray = optionString.Split(OptionSeparator);
foreach (string definition in definitionArray)
{
var fieldOption = m_FieldFactory.CreateFieldOption(definition);
if (null == fieldOption)
{
throw new InvalidDataException(
$"Invalid field option in sheet \"{m_Table.Path}\": \"{definition}\"");
}
optionList.Add(fieldOption);
}
}
m_Table.FieldList[i].Options = optionList;
}
m_OptionRowImported = true;
}
private void AssertRequiredRowImported()
{
if (!m_NameRowImported)
{
throw new InvalidDataException($"Meta row not found in sheet \"{m_Table.Path}\": \"{NameMark}\"");
}
if (!m_TypeRowImported)
{
throw new InvalidDataException($"Meta row not found in sheet \"{m_Table.Path}\": \"{TypeMark}\"");
}
}
private void ValidateFields(HashSet<string> tags)
{
var context = new FieldValidationContext();
context.Tags = tags;
var nameSet = new HashSet<string>();
int enabledCount = 0;
foreach (var fieldConfig in m_Table.FieldList)
{
context.Config = fieldConfig;
string simpleName = fieldConfig.Name.ToSimplified();
if (nameSet.Contains(simpleName))
{
throw new InvalidDataException(
$"Duplicate field name in sheet \"{m_Table.Path}\": \"{fieldConfig.Name}\"");
}
nameSet.Add(simpleName);
foreach (var fieldOption in fieldConfig.Options)
{
// Field option can disable field
if (!fieldConfig.IsEnabled)
{
break;
}
if (!fieldOption.OnValidateField(context, out string errorString))
{
throw new InvalidDataException($"Invalid field option of field \"{fieldConfig.Name}\" " +
$"in sheet \"{m_Table.Path}\": {errorString}");
}
}
if (fieldConfig.IsEnabled)
{
enabledCount++;
if (fieldConfig.Type is InvalidFieldType invalidType)
{
throw new InvalidDataException($"Invalid field type in sheet \"{m_Table.Path}\": " +
$"\"{invalidType.Description}\"");
}
}
}
int p = 0;
m_FieldDataMapping = new int[enabledCount];
for (int i = 0; i < m_Table.FieldList.Count; ++i)
{
if (m_Table.FieldList[i].IsEnabled)
{
m_FieldDataMapping[p] = i;
p++;
}
}
if (p != enabledCount)
{
throw new InvalidDataException($"Unexpected field mapping in sheet \"{m_Table.Path}\"");
}
for (int i = m_Table.FieldList.Count - 1; i >= 0; i--)
{
if (!m_Table.FieldList[i].IsEnabled)
{
m_Table.FieldList.RemoveAt(i);
}
}
}
private void ProcessFields()
{
string errorString;
int fieldCount = m_Table.FieldList.Count;
m_Table.TableData = CreateTableDataContainer(m_Table.Type, m_Table.FieldList);
for (int i = 0; i < fieldCount; ++i)
{
var field = m_Table.FieldList[i];
field.Index = i;
foreach (var option in field.Options)
{
if (option is KeyFieldOption)
{
if (!m_Table.TableData.AddKeyField(i, out errorString))
{
throw new InvalidDataException(
$"Failed to process fields in sheet \"{m_Table.Path}\": {errorString}");
}
}
if (option is ICellValueProcessor cellValueProcessor)
{
m_Table.TableData.AddCellValueProcessor(i, cellValueProcessor);
}
}
}
if (!m_Table.TableData.EndFieldProcess(out errorString))
{
throw new InvalidDataException($"Failed to process fields in sheet \"{m_Table.Path}\": {errorString}");
}
}
private ITableData CreateTableDataContainer(TableConfig.TableType tableType, List<FieldConfig> fieldList)
{
switch (tableType)
{
case TableConfig.TableType.Map:
return new MapTableData(fieldList);
case TableConfig.TableType.List:
return new ListTableData(fieldList);
case TableConfig.TableType.Singleton:
return new SingletonTableData(fieldList);
default:
throw new NotSupportedException($"Not supported table type: {tableType}");
}
}
}
}