Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
using RobotNet10.CustomConfiguration.Events;
using RobotNet10.CustomConfiguration.Models;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Implementation của IConfigManager
/// Service cho quản lý và truy vấn configuration variables
/// </summary>
public class ConfigManager : IConfigManager, IDisposable
{
private readonly IConfigService _configService;
// ==========================================
// EVENTS
// ==========================================
public event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
/// <summary>
/// Constructor - Subscribe vào ConfigChanged event từ IConfigService
/// </summary>
public ConfigManager(IConfigService configService)
{
_configService = configService ?? throw new ArgumentNullException(nameof(configService));
// Forward events from IConfigService to IConfigManager
_configService.ConfigChanged += OnConfigServiceChanged;
}
/// <summary>
/// Forward ConfigChanged event from IConfigService to IConfigManager subscribers
/// </summary>
private void OnConfigServiceChanged(object? sender, ConfigChangedEventArgs args)
{
ConfigChanged?.Invoke(this, args);
}
// ==========================================
// CONFIG TYPE OPERATIONS
// ==========================================
public async Task<ConfigFile?> GetConfigByTypeAsync(string configType)
{
return await _configService.GetConfigByTypeAsync(configType);
}
public async Task<bool> ConfigTypeExistsAsync(string configType)
{
return await _configService.ConfigTypeExistsAsync(configType);
}
// ==========================================
// VARIABLE EXISTENCE CHECK
// ==========================================
public async Task<bool> VariableExistsAsync(string configType, string variableName)
{
var config = await _configService.GetConfigByTypeAsync(configType);
if (config == null)
return false;
return config.Variables.Any(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
}
public async Task<bool> VariableExistsAsync(Guid configId, string variableName)
{
var config = await _configService.GetConfigByIdAsync(configId);
if (config == null)
return false;
return config.Variables.Any(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
}
// ==========================================
// GET VARIABLE VALUE
// ==========================================
public async Task<object?> GetVariableValueAsync(string configType, string variableName)
{
var variable = await GetVariableAsync(configType, variableName);
return variable?.Value;
}
public async Task<object?> GetVariableValueAsync(Guid configId, string variableName)
{
var variable = await GetVariableAsync(configId, variableName);
return variable?.Value;
}
// ==========================================
// GET VARIABLE(S)
// ==========================================
public async Task<ConfigVariable?> GetVariableAsync(string configType, string variableName)
{
var config = await _configService.GetConfigByTypeAsync(configType);
if (config == null)
return null;
return config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
}
public async Task<List<ConfigVariable>> GetVariablesAsync(string configType)
{
var config = await _configService.GetConfigByTypeAsync(configType);
return config?.Variables ?? [];
}
public async Task<ConfigVariable?> GetVariableAsync(Guid configId, string variableName)
{
var config = await _configService.GetConfigByIdAsync(configId);
if (config == null)
return null;
return config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
}
public async Task<List<ConfigVariable>> GetVariablesAsync(Guid configId)
{
var config = await _configService.GetConfigByIdAsync(configId);
return config?.Variables ?? [];
}
// ==========================================
// GET VARIABLE TYPE
// ==========================================
public async Task<string?> GetVariableTypeAsync(string configType, string variableName)
{
var variable = await GetVariableAsync(configType, variableName);
return variable != null ? ConvertTypeToString(variable.Type) : null;
}
public async Task<string?> GetVariableTypeAsync(Guid configId, string variableName)
{
var variable = await GetVariableAsync(configId, variableName);
return variable != null ? ConvertTypeToString(variable.Type) : null;
}
// ==========================================
// EVENT TRIGGERS
// ==========================================
/// <summary>
/// Trigger ConfigChanged event
/// Method này có thể được gọi từ bên ngoài hoặc từ các service khác khi có thay đổi
/// </summary>
public void OnConfigChanged(string configType, Guid configId, ConfigChangeType changeType, string? variableName = null)
{
var args = new ConfigChangedEventArgs
{
ConfigType = configType,
ConfigId = configId,
ChangeType = changeType,
VariableName = variableName
};
ConfigChanged?.Invoke(this, args);
}
// ==========================================
// DISPOSE
// ==========================================
public void Dispose()
{
_configService.ConfigChanged -= OnConfigServiceChanged;
GC.SuppressFinalize(this);
}
// ==========================================
// HELPER METHODS
// ==========================================
/// <summary>
/// Convert ConfigVariableType enum thành string
/// </summary>
private static string ConvertTypeToString(ConfigVariableType type)
{
return type switch
{
ConfigVariableType.String => "string",
ConfigVariableType.Int => "int",
ConfigVariableType.Double => "double",
ConfigVariableType.Bool => "bool",
ConfigVariableType.Object => "object",
ConfigVariableType.Array => "array",
ConfigVariableType.Enum => "enum",
_ => throw new ArgumentException($"Unknown variable type: {type}")
};
}
}

View File

@@ -0,0 +1,375 @@
using RobotNet10.CustomConfiguration.Events;
using RobotNet10.CustomConfiguration.Helpers;
using RobotNet10.CustomConfiguration.Models;
using RobotNet10.CustomConfiguration.Validators;
using System.Text;
using System.Text.Json;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Implementation của các methods trong ConfigService
/// </summary>
public partial class ConfigService
{
public async Task<ConfigFile> CreateConfigAsync(string configType, List<ConfigVariable> variables, string? description = null)
{
// Validate ConfigType
if (string.IsNullOrWhiteSpace(configType))
{
throw new ArgumentException("ConfigType cannot be empty", nameof(configType));
}
// Check if ConfigType already exists
var existing = await GetMetadataByTypeAsync(configType);
if (existing != null)
{
throw new InvalidOperationException($"Config with type '{configType}' already exists");
}
// Create config file
var config = new ConfigFile
{
Id = Guid.NewGuid(),
ConfigType = configType,
Variables = variables,
Description = description,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
FilePath = $"{ConfigPath}/{configType}{ConfigFileExtension}"
};
// Validate config
var validationResult = ConfigValidator.ValidateConfig(config);
if (!validationResult.IsValid)
{
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
}
// Save config file (includes metadata)
await SaveConfigFileAsync(config);
// Trigger event
OnConfigChanged(config, ConfigChangeType.Created);
return config;
}
public async Task<ConfigFile?> GetConfigByIdAsync(Guid id)
{
// Load all configs and find by ID
var allMetadata = await LoadAllMetadataAsync();
var metadata = allMetadata.FirstOrDefault(m => m.Id == id);
if (metadata == null)
{
return null;
}
return await LoadConfigFileAsync(metadata.ConfigType);
}
public async Task<ConfigFile?> GetConfigByTypeAsync(string configType)
{
return await LoadConfigFileAsync(configType);
}
public async Task<List<ConfigFileMetadata>> GetAllConfigsAsync()
{
var metadata = await LoadAllMetadataAsync();
return [.. metadata.OrderBy(m => m.ConfigType)];
}
public async Task<List<ConfigFileMetadata>> SearchConfigsAsync(string? searchText)
{
var allMetadata = await GetAllConfigsAsync();
if (string.IsNullOrWhiteSpace(searchText))
{
return allMetadata;
}
var searchLower = searchText.ToLower();
return [.. allMetadata.Where(m =>
m.ConfigType.ToLower().Contains(searchLower) ||
(m.Description != null && m.Description.ToLower().Contains(searchLower))
)];
}
public async Task<ConfigFile> UpdateConfigAsync(Guid id, List<ConfigVariable>? variables = null, string? description = null)
{
var config = await GetConfigByIdAsync(id) ?? throw new KeyNotFoundException($"Config with ID '{id}' not found");
// Update variables if provided
if (variables != null)
{
config.Variables = variables;
}
// Update description if provided
if (description != null)
{
config.Description = description;
}
config.UpdatedAt = DateTime.UtcNow;
// Validate config
var validationResult = ConfigValidator.ValidateConfig(config);
if (!validationResult.IsValid)
{
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
}
// Save config file (includes metadata)
await SaveConfigFileAsync(config);
// Trigger event
OnConfigChanged(config, ConfigChangeType.Updated);
return config;
}
public async Task<bool> DeleteConfigAsync(Guid id)
{
// Find config by ID
var allMetadata = await LoadAllMetadataAsync();
var metadata = allMetadata.FirstOrDefault(m => m.Id == id);
if (metadata == null)
{
return false;
}
// Delete config file
// objectName should be {configType}.config so StorageManager adds .json to make {configType}.config.json
var objectName = $"{metadata.ConfigType}.config";
var exists = await _storageManager.ExistsAsync(ConfigPath, objectName, CancellationToken.None);
if (exists)
{
await _storageManager.DeleteAsync(ConfigPath, objectName, CancellationToken.None);
// Trigger event
var deletedConfig = new ConfigFile
{
Id = metadata.Id,
ConfigType = metadata.ConfigType,
CreatedAt = metadata.CreatedAt,
UpdatedAt = metadata.UpdatedAt,
Description = metadata.Description
};
OnConfigChanged(deletedConfig, ConfigChangeType.Deleted);
}
return true;
}
public async Task<bool> ConfigTypeExistsAsync(string configType)
{
var metadata = await GetMetadataByTypeAsync(configType);
return metadata != null;
}
public async Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType)
{
return await ImportConfigFromJsonAsync(jsonStream, configType, null);
}
public async Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType, string? description)
{
// Validate ConfigType
if (string.IsNullOrWhiteSpace(configType))
{
throw new ArgumentException("ConfigType cannot be empty", nameof(configType));
}
// Check if ConfigType already exists
var existing = await GetMetadataByTypeAsync(configType);
if (existing != null)
{
throw new InvalidOperationException($"Config with type '{configType}' already exists");
}
// Try to parse as full config file (new format with metadata)
// Reset stream position first
jsonStream.Position = 0;
string? fileDescription = null;
List<ConfigVariable> variables;
try
{
// Try to parse as ConfigFile (new format)
var configFile = JsonConfigParser.ParseConfigFile(jsonStream);
// If successful, use description from file
fileDescription = configFile.Description;
variables = configFile.Variables;
}
catch (Exception ex) when (ex is JsonException or ArgumentException)
{
// If parsing as ConfigFile fails, try old format (array of variables)
jsonStream.Position = 0;
variables = JsonConfigParser.ParseVariables(jsonStream);
}
// Use description from parameter if provided, otherwise use description from file
var finalDescription = !string.IsNullOrWhiteSpace(description) ? description : fileDescription;
// Create config with description (from parameter or file)
var config = await CreateConfigAsync(configType, variables, finalDescription);
// Note: CreateConfigAsync already triggers Created event, so no need to trigger again
return config;
}
public async Task<Stream> ExportConfigToJsonAsync(Guid id)
{
var config = await GetConfigByIdAsync(id);
return config == null ? throw new KeyNotFoundException($"Config with ID '{id}' not found") : await ExportConfigToJsonAsync(config);
}
public Task<Stream> ExportConfigToJsonAsync(ConfigFile config)
{
// Export với format mới (metadata + variables)
var json = JsonConfigParser.SerializeConfigFile(config);
var bytes = Encoding.UTF8.GetBytes(json);
Stream stream = new MemoryStream(bytes)
{
Position = 0 // Ensure stream is at the beginning
};
return Task.FromResult(stream);
}
public async Task<ConfigFile> UpdateVariableAsync(Guid configId, string variableName, object? value)
{
var config = await GetConfigByIdAsync(configId) ??
throw new KeyNotFoundException($"Config with ID '{configId}' not found");
var variable = config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase)) ??
throw new KeyNotFoundException($"Variable '{variableName}' not found in config");
variable.Value = value;
config.UpdatedAt = DateTime.UtcNow;
// Validate
var validationResult = ConfigValidator.ValidateConfig(config);
if (!validationResult.IsValid)
{
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
}
// Save config file (includes metadata)
await SaveConfigFileAsync(config);
// Trigger event
OnConfigChanged(config, ConfigChangeType.VariableUpdated, variableName);
return config;
}
public async Task<ConfigFile> AddVariableAsync(Guid configId, ConfigVariable variable)
{
var config = await GetConfigByIdAsync(configId) ?? throw new KeyNotFoundException($"Config with ID '{configId}' not found");
// Check if variable name already exists
if (config.Variables.Any(v => v.Name.Equals(variable.Name, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException($"Variable '{variable.Name}' already exists in config");
}
config.Variables.Add(variable);
config.UpdatedAt = DateTime.UtcNow;
// Validate
var validationResult = ConfigValidator.ValidateConfig(config);
if (!validationResult.IsValid)
{
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
}
// Save config file (includes metadata)
await SaveConfigFileAsync(config);
// Trigger event
OnConfigChanged(config, ConfigChangeType.VariableAdded, variable.Name);
return config;
}
public async Task<ConfigFile> RemoveVariableAsync(Guid configId, string variableName)
{
var config = await GetConfigByIdAsync(configId) ??
throw new KeyNotFoundException($"Config with ID '{configId}' not found");
var variable = config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase)) ??
throw new KeyNotFoundException($"Variable '{variableName}' not found in config");
config.Variables.Remove(variable);
config.UpdatedAt = DateTime.UtcNow;
// Validate
var validationResult = ConfigValidator.ValidateConfig(config);
if (!validationResult.IsValid)
{
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
}
// Save config file (includes metadata)
await SaveConfigFileAsync(config);
// Trigger event
OnConfigChanged(config, ConfigChangeType.VariableRemoved, variableName);
return config;
}
// ==========================================
// PRIVATE HELPER METHODS
// ==========================================
/// <summary>
/// Load config file từ StorageManager (format mới với metadata)
/// </summary>
private async Task<ConfigFile?> LoadConfigFileAsync(string configType)
{
try
{
// objectName should be {configType}.config so StorageManager finds {configType}.config.json
var objectName = $"{configType}.config";
var exists = await _storageManager.ExistsAsync(ConfigPath, objectName, CancellationToken.None);
if (!exists)
{
return null;
}
// Get file stream from StorageManager (works with both local and remote storage)
using var stream = await _storageManager.GetFileAsync(ConfigPath, objectName, CancellationToken.None);
// Parse config file directly from stream (format mới với metadata)
return JsonConfigParser.ParseConfigFile(stream);
}
catch (JsonException)
{
// Config file corrupted/invalid format - treat as not found
return null;
}
}
/// <summary>
/// Save config file vào StorageManager (format mới với metadata)
/// </summary>
private async Task SaveConfigFileAsync(ConfigFile config)
{
// Serialize với format mới (metadata + variables)
var json = JsonConfigParser.SerializeConfigFile(config);
var bytes = Encoding.UTF8.GetBytes(json);
// objectName should be {configType}.config so StorageManager adds .json to make {configType}.config.json
var objectName = $"{config.ConfigType}.config";
using var stream = new MemoryStream(bytes);
await _storageManager.UploadAsync(
ConfigPath,
objectName,
stream,
stream.Length,
"application/json",
CancellationToken.None
);
}
}

View File

@@ -0,0 +1,86 @@
using RobotNet10.CustomConfiguration.Helpers;
using RobotNet10.CustomConfiguration.Models;
using System.IO;
using System.Text.Json;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Helper methods cho quản lý metadata (load từ config files)
/// </summary>
public partial class ConfigService
{
/// <summary>
/// Load tất cả config files và trả về metadata
/// </summary>
private async Task<List<ConfigFileMetadata>> LoadAllMetadataAsync()
{
var metadataList = new List<ConfigFileMetadata>();
try
{
// List all files in configs directory
var files = await _storageManager.ListAsync(ConfigPath, recursive: false, CancellationToken.None);
// Filter only .config.json files
var configFiles = files.Where(f => f.EndsWith(ConfigFileExtension, StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var fileName in configFiles)
{
try
{
// Extract configType from filename: {configType}.config.json
var configType = fileName.Replace(ConfigFileExtension, "", StringComparison.OrdinalIgnoreCase);
// Load config file to get metadata
var config = await LoadConfigFileAsync(configType);
if (config != null)
{
metadataList.Add(new ConfigFileMetadata
{
Id = config.Id,
ConfigType = config.ConfigType,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
Description = config.Description
});
}
}
catch (JsonException)
{
// Skip corrupted config files
continue;
}
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Storage unavailable - return what we have so far
return metadataList;
}
return metadataList;
}
/// <summary>
/// Get metadata by ConfigType (load từ config file)
/// </summary>
private async Task<ConfigFileMetadata?> GetMetadataByTypeAsync(string configType)
{
var config = await LoadConfigFileAsync(configType);
if (config == null)
{
return null;
}
return new ConfigFileMetadata
{
Id = config.Id,
ConfigType = config.ConfigType,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
Description = config.Description
};
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.Options;
using RobotNet10.CustomConfiguration.Events;
using RobotNet10.CustomConfiguration.Models;
using RobotNet10.StorageManager;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Service implementation cho quản lý configuration files
/// Sử dụng StorageManager để lưu trữ file JSON
/// </summary>
public partial class ConfigService : IConfigService, IDisposable
{
private readonly StorageManager.StorageManager _storageManager;
private const string ConfigPath = "configs"; // Path trong StorageManager
private const string ConfigFileExtension = ".config.json"; // Extension cho config files
private const string StorageConfigsKey = "StorageConfigs"; // Named options key cho IOptionsMonitor
// ==========================================
// EVENTS
// ==========================================
public event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
public ConfigService(IOptionsMonitor<StorageConfig> optionsSnapshot)
{
var config = optionsSnapshot.Get(StorageConfigsKey);
ArgumentNullException.ThrowIfNull(config);
_storageManager = new StorageManager.StorageManager(config);
}
/// <summary>
/// Trigger ConfigChanged event
/// </summary>
protected virtual void OnConfigChanged(ConfigFile config, ConfigChangeType changeType, string? variableName = null)
{
var args = new ConfigChangedEventArgs
{
ConfigType = config.ConfigType,
ConfigId = config.Id,
ChangeType = changeType,
VariableName = variableName
};
ConfigChanged?.Invoke(this, args);
}
public void Dispose()
{
_storageManager?.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,102 @@
using RobotNet10.CustomConfiguration.Events;
using RobotNet10.CustomConfiguration.Models;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Service interface cho quản lý và truy vấn configuration variables
/// </summary>
public interface IConfigManager
{
// ==========================================
// EVENTS
// ==========================================
/// <summary>
/// Event được trigger khi có thay đổi trong config (tạo mới, cập nhật, xóa, hoặc thay đổi variables)
/// </summary>
event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
// ==========================================
// CONFIG TYPE OPERATIONS
// ==========================================
/// <summary>
/// Lấy config theo ConfigType (đầy đủ thông tin bao gồm variables)
/// </summary>
Task<ConfigFile?> GetConfigByTypeAsync(string configType);
/// <summary>
/// Kiểm tra ConfigType có tồn tại không
/// </summary>
Task<bool> ConfigTypeExistsAsync(string configType);
// ==========================================
// VARIABLE EXISTENCE CHECK
// ==========================================
/// <summary>
/// Kiểm tra variable có tồn tại không (theo ConfigType)
/// </summary>
Task<bool> VariableExistsAsync(string configType, string variableName);
/// <summary>
/// Kiểm tra variable có tồn tại không (theo ConfigId)
/// </summary>
Task<bool> VariableExistsAsync(Guid configId, string variableName);
// ==========================================
// GET VARIABLE VALUE
// ==========================================
/// <summary>
/// Lấy giá trị (Value) của variable (theo ConfigType)
/// </summary>
Task<object?> GetVariableValueAsync(string configType, string variableName);
/// <summary>
/// Lấy giá trị (Value) của variable (theo ConfigId)
/// </summary>
Task<object?> GetVariableValueAsync(Guid configId, string variableName);
// ==========================================
// GET VARIABLE(S)
// ==========================================
/// <summary>
/// Lấy một variable cụ thể (theo ConfigType)
/// </summary>
Task<ConfigVariable?> GetVariableAsync(string configType, string variableName);
/// <summary>
/// Lấy tất cả variables của một ConfigType
/// </summary>
Task<List<ConfigVariable>> GetVariablesAsync(string configType);
/// <summary>
/// Lấy một variable cụ thể (theo ConfigId)
/// </summary>
Task<ConfigVariable?> GetVariableAsync(Guid configId, string variableName);
/// <summary>
/// Lấy tất cả variables của một ConfigId
/// </summary>
Task<List<ConfigVariable>> GetVariablesAsync(Guid configId);
// ==========================================
// GET VARIABLE TYPE
// ==========================================
/// <summary>
/// Lấy kiểu dữ liệu của variable dưới dạng string (theo ConfigType)
/// Trả về: "string", "int", "double", "bool", "object", "array", "enum"
/// </summary>
Task<string?> GetVariableTypeAsync(string configType, string variableName);
/// <summary>
/// Lấy kiểu dữ liệu của variable dưới dạng string (theo ConfigId)
/// Trả về: "string", "int", "double", "bool", "object", "array", "enum"
/// </summary>
Task<string?> GetVariableTypeAsync(Guid configId, string variableName);
}

View File

@@ -0,0 +1,50 @@
using RobotNet10.CustomConfiguration.Events;
using RobotNet10.CustomConfiguration.Models;
namespace RobotNet10.CustomConfiguration.Services;
/// <summary>
/// Service interface cho quản lý configuration files
/// </summary>
public interface IConfigService
{
// ==========================================
// EVENTS
// ==========================================
/// <summary>
/// Event được trigger khi có thay đổi trong config (tạo mới, cập nhật, xóa, hoặc thay đổi variables)
/// </summary>
event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
// ==========================================
// CONFIG FILE MANAGEMENT
// ==========================================
Task<ConfigFile> CreateConfigAsync(string configType, List<ConfigVariable> variables, string? description = null);
Task<ConfigFile?> GetConfigByIdAsync(Guid id);
Task<ConfigFile?> GetConfigByTypeAsync(string configType);
Task<List<ConfigFileMetadata>> GetAllConfigsAsync();
Task<List<ConfigFileMetadata>> SearchConfigsAsync(string? searchText);
Task<ConfigFile> UpdateConfigAsync(Guid id, List<ConfigVariable>? variables = null, string? description = null);
Task<bool> DeleteConfigAsync(Guid id);
Task<bool> ConfigTypeExistsAsync(string configType);
// ==========================================
// IMPORT/EXPORT
// ==========================================
Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType);
Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType, string? description);
Task<Stream> ExportConfigToJsonAsync(Guid id);
Task<Stream> ExportConfigToJsonAsync(ConfigFile config);
// ==========================================
// VARIABLE MANAGEMENT
// ==========================================
Task<ConfigFile> UpdateVariableAsync(Guid configId, string variableName, object? value);
Task<ConfigFile> AddVariableAsync(Guid configId, ConfigVariable variable);
Task<ConfigFile> RemoveVariableAsync(Guid configId, string variableName);
}