UpsertInventoryUseCase.cs 4.7 KB

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