Kowalski/Configuration/Core/Processor/TableProcessor.cs
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|