UpsertInventoryUseCase.cs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. using System.ComponentModel.DataAnnotations;
  2. using System.Text.Json;
  3. using System.Text.Json.Serialization;
  4. using RackPeek.Domain.Persistence;
  5. using RackPeek.Domain.Persistence.Yaml;
  6. using RackPeek.Domain.Resources;
  7. using YamlDotNet.Serialization;
  8. using YamlDotNet.Serialization.NamingConventions;
  9. namespace RackPeek.Domain.Api;
  10. public class UpsertInventoryUseCase(
  11. IResourceCollection repo,
  12. IResourceYamlMigrationService migrationService)
  13. : IUseCase {
  14. private static readonly JsonSerializerOptions _jsonOptions = new() {
  15. PropertyNameCaseInsensitive = true,
  16. WriteIndented = false,
  17. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  18. ReferenceHandler = ReferenceHandler.IgnoreCycles,
  19. TypeInfoResolver = ResourcePolymorphismResolver.Create()
  20. };
  21. public async Task<ImportYamlResponse> ExecuteAsync(ImportYamlRequest request) {
  22. if (request == null)
  23. throw new ValidationException("Invalid request.");
  24. if (string.IsNullOrWhiteSpace(request.Yaml) && request.Json == null)
  25. throw new ValidationException("Either 'yaml' or 'json' must be provided.");
  26. if (!string.IsNullOrWhiteSpace(request.Yaml) && request.Json != null)
  27. throw new ValidationException("Provide either 'yaml' or 'json', not both.");
  28. YamlRoot incomingRoot;
  29. string yamlInput;
  30. if (!string.IsNullOrWhiteSpace(request.Yaml)) {
  31. yamlInput = request.Yaml!;
  32. incomingRoot = await migrationService.DeserializeAsync(yamlInput)
  33. ?? throw new ValidationException("Invalid YAML structure.");
  34. }
  35. else {
  36. if (request.Json is not JsonElement element)
  37. throw new ValidationException("Invalid JSON payload.");
  38. var rawJson = element.GetRawText();
  39. incomingRoot = JsonSerializer.Deserialize<YamlRoot>(
  40. rawJson,
  41. _jsonOptions)
  42. ?? throw new ValidationException("Invalid JSON structure.");
  43. // Generate YAML only for persistence layer
  44. ISerializer yamlSerializer = new SerializerBuilder()
  45. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  46. .WithTypeConverter(new StorageSizeYamlConverter())
  47. .WithTypeConverter(new NotesStringYamlConverter())
  48. .ConfigureDefaultValuesHandling(
  49. DefaultValuesHandling.OmitNull |
  50. DefaultValuesHandling.OmitEmptyCollections)
  51. .Build();
  52. yamlInput = yamlSerializer.Serialize(incomingRoot);
  53. }
  54. if (incomingRoot.Resources == null)
  55. throw new ValidationException("Missing 'resources' section.");
  56. // 2️Compute Diff
  57. List<Resource>? incomingResources = incomingRoot.Resources;
  58. IReadOnlyList<Resource> currentResources = await repo.GetAllOfTypeAsync<Resource>();
  59. IGrouping<string, Resource>? duplicate = incomingResources
  60. .GroupBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
  61. .FirstOrDefault(g => g.Count() > 1);
  62. if (duplicate != null)
  63. throw new ValidationException($"Duplicate resource name: {duplicate.Key}");
  64. var currentDict = currentResources
  65. .ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase);
  66. ISerializer serializerDiff = new SerializerBuilder()
  67. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  68. .ConfigureDefaultValuesHandling(
  69. DefaultValuesHandling.OmitNull |
  70. DefaultValuesHandling.OmitEmptyCollections)
  71. .Build();
  72. var oldSnapshots = currentResources
  73. .ToDictionary(
  74. r => r.Name,
  75. r => serializerDiff.Serialize(r),
  76. StringComparer.OrdinalIgnoreCase);
  77. List<Resource> mergedResources = ResourceCollectionMerger.Merge(
  78. currentResources,
  79. incomingResources,
  80. request.Mode);
  81. var mergedDict = mergedResources
  82. .ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase);
  83. var response = new ImportYamlResponse();
  84. foreach (Resource incoming in incomingResources) {
  85. if (!mergedDict.TryGetValue(incoming.Name, out Resource? merged))
  86. continue;
  87. var newYaml = serializerDiff.Serialize(merged);
  88. response.NewYaml[incoming.Name] = newYaml;
  89. if (!currentDict.ContainsKey(incoming.Name)) {
  90. response.Added.Add(incoming.Name);
  91. continue;
  92. }
  93. var oldYaml = oldSnapshots[incoming.Name];
  94. response.OldYaml[incoming.Name] = oldYaml;
  95. Resource existing = currentDict[incoming.Name];
  96. if (request.Mode == MergeMode.Replace ||
  97. existing.GetType() != incoming.GetType())
  98. response.Replaced.Add(incoming.Name);
  99. else if (oldYaml != newYaml) response.Updated.Add(incoming.Name);
  100. }
  101. if (!request.DryRun) await repo.Merge(yamlInput, request.Mode);
  102. return response;
  103. }
  104. }