diff --git a/src/core/EdFi.DataManagementService.Core.Tests.Unit/ApiSchema/DependencyCalculatorTests.cs b/src/core/EdFi.DataManagementService.Core.Tests.Unit/ApiSchema/DependencyCalculatorTests.cs index 6523fa35c..265a692fd 100644 --- a/src/core/EdFi.DataManagementService.Core.Tests.Unit/ApiSchema/DependencyCalculatorTests.cs +++ b/src/core/EdFi.DataManagementService.Core.Tests.Unit/ApiSchema/DependencyCalculatorTests.cs @@ -66,7 +66,7 @@ public void Setup() [Test] public void It_should_calculate_dependencies() { - var dependencies = _dependencyCalculator!.GetDependencies(); + var dependencies = _dependencyCalculator!.GetDependenciesFromResourceSchema(); dependencies.Should().NotBeEmpty(); dependencies.Count.Should().Be(1); @@ -194,7 +194,7 @@ public void Setup() [Test] public void It_should_calculate_dependencies() { - var dependencies = _dependencyCalculator!.GetDependencies(); + var dependencies = _dependencyCalculator!.GetDependenciesFromResourceSchema(); dependencies.Should().NotBeEmpty(); var expectedDependencies = JsonNode.Parse(_expectedDescriptor)!.AsArray(); @@ -244,6 +244,27 @@ public class Given_A_Sample_ApiSchema_With_Superclass_Reference() : DependencyCa "isSubclass": false, "isSchoolYearEnumeration": false, "resourceName": "OpenStaffPosition" + }, + "localEducationAgencies": { + "documentPathsMapping": { + "EducationOrganizationCategoryDescriptor": { + "isDescriptor": true, + "isReference": true, + "projectName": "Ed-Fi", + "resourceName": "EducationOrganizationCategoryDescriptor" + }, + "ParentLocalEducationAgency": { + "isReference": true, + "projectName": "Ed-Fi", + "resourceName": "LocalEducationAgency" + } + }, + "isSubclass": true, + "isSchoolYearEnumeration": false, + "resourceName": "LocalEducationAgency", + "subclassType": "domainEntity", + "superclassProjectName": "Ed-Fi", + "superclassResourceName": "EducationOrganization" }, "schools": { "documentPathsMapping": { @@ -281,7 +302,7 @@ public class Given_A_Sample_ApiSchema_With_Superclass_Reference() : DependencyCa ] }, { - "resource": "/ed-fi/schools", + "resource": "/ed-fi/localEducationAgencies", "order": 2, "operations": [ "Create", @@ -289,12 +310,20 @@ public class Given_A_Sample_ApiSchema_With_Superclass_Reference() : DependencyCa ] }, { - "resource": "/ed-fi/openStaffPositions", + "resource": "/ed-fi/schools", "order": 3, "operations": [ "Create", "Update" ] + }, + { + "resource": "/ed-fi/openStaffPositions", + "order": 4, + "operations": [ + "Create", + "Update" + ] } ] """; @@ -309,7 +338,7 @@ public void Setup() [Test] public void It_should_calculate_dependencies() { - var dependencies = _dependencyCalculator!.GetDependencies(); + var dependencies = _dependencyCalculator!.GetDependenciesFromResourceSchema(); dependencies.Should().NotBeEmpty(); var expectedDependencies = JsonNode.Parse(_expectedDescriptor)!.AsArray(); @@ -318,6 +347,7 @@ public void It_should_calculate_dependencies() .IgnoringCyclicReferences()); } } + [TestFixture] public class Given_A_Sample_ApiSchema_Missing_ProjectSchemas() : DependencyCalculatorTests { @@ -340,9 +370,68 @@ public void Setup() [Test] public void It_should_throw_invalid_operation() { - Action act = () => _dependencyCalculator!.GetDependencies(); + Action act = () => _dependencyCalculator!.GetDependenciesFromResourceSchema(); act.Should().Throw(); } } + + [TestFixture] + public class Given_A_Dependency_Calculator() : DependencyCalculatorTests + { + [Test] + public void It_should_return_proper_ordered_dependencies1() + { + Dictionary> resources = new Dictionary> + { + { "A", ["B"] }, + { "B", [] }, + { "C", ["B"] }, + }; + + var dependencies = DependencyCalculator.GetDependencies(resources); + + dependencies["A"].Should().Be(2); + dependencies["B"].Should().Be(1); + dependencies["C"].Should().Be(2); + } + + [Test] + public void It_should_return_proper_ordered_dependencies2() + { + Dictionary> resources = new Dictionary> + { + { "A", ["B"] }, + { "B", ["C", "D"] }, + { "C", [] }, + { "D", [] } + }; + + var dependencies = DependencyCalculator.GetDependencies(resources); + + dependencies["A"].Should().Be(3); + dependencies["B"].Should().Be(2); + dependencies["C"].Should().Be(1); + dependencies["D"].Should().Be(1); + } + + [Test] + public void It_should_handle_circular_dependencies() + { + Dictionary> resources = new Dictionary> + { + { "EOCD", [] }, + { "OSP", ["S"] }, + { "LEA", ["EOCD", "LEA"] }, + { "S", ["EOCD", "LEA"] } + }; + + var dependencies = DependencyCalculator.GetDependencies(resources); + + dependencies["EOCD"].Should().Be(1); + dependencies["LEA"].Should().Be(2); + dependencies["S"].Should().Be(3); + dependencies["OSP"].Should().Be(4); + } + } } diff --git a/src/core/EdFi.DataManagementService.Core/ApiSchema/DependencyCalculator.cs b/src/core/EdFi.DataManagementService.Core/ApiSchema/DependencyCalculator.cs index a210586ca..abf25c63f 100644 --- a/src/core/EdFi.DataManagementService.Core/ApiSchema/DependencyCalculator.cs +++ b/src/core/EdFi.DataManagementService.Core/ApiSchema/DependencyCalculator.cs @@ -10,7 +10,7 @@ namespace EdFi.DataManagementService.Core.ApiSchema; internal class DependencyCalculator(JsonNode _apiSchemaRootNode, ILogger _logger) { - public JsonArray GetDependencies() + public JsonArray GetDependenciesFromResourceSchema() { var apiSchemaDocument = new ApiSchemaDocument(_apiSchemaRootNode, _logger); var dependenciesJsonArray = new JsonArray(); @@ -18,17 +18,14 @@ public JsonArray GetDependencies() { var resourceSchemas = projectSchemaNode["resourceSchemas"]?.AsObject().Select(x => new ResourceSchema(x.Value!)).ToList()!; - Dictionary> dependencies = + Dictionary> resources = resourceSchemas .Where(rs => !rs.IsSchoolYearEnumeration) - .ToDictionary(rs => rs.ResourceName.Value, rs => rs.DocumentPaths.Where(d => d.IsReference).Select(d => d.ResourceName.Value).ToList()); + .ToDictionary( + rs => rs.ResourceName.Value, + rs => rs.DocumentPaths.Where(d => d.IsReference && d.ResourceName.Value != "SchoolYearType").Select(d => ReplaceAbstractResourceNames(d.ResourceName.Value)).ToList()); - Dictionary orderedResources = dependencies.ToDictionary(d => d.Key, _ => 0); - Dictionary visitedResources = []; - foreach (var dependency in dependencies.OrderBy(d => d.Value.Count).ThenBy(d => d.Key).Select(d => d.Key)) - { - RecursivelyDetermineDependencies(dependency, 0); - } + var orderedResources = GetDependencies(resources); string ResourceNameMapping(string resourceName) { @@ -58,55 +55,69 @@ string ResourceNameMapping(string resourceName) dependenciesJsonArray.Add(new { resource = $"/{projectSchemaNode!.GetPropertyName()}/{resourceName}", order = orderedResource.Value, operations = new[] { "Create", "Update" } }); } + } - int RecursivelyDetermineDependencies(string resourceName, int depth) - { - // Code Smell here: - // These resources are similar to abstract base classes, so they are not represented in the resourceSchemas - // portion of the schema document. This is a rudimentary replacement with the most specific version of the resource - if (resourceName == "EducationOrganization") - { - resourceName = "School"; - } + return dependenciesJsonArray; + } - if (resourceName == "GeneralStudentProgramAssociation") - { - resourceName = "StudentProgramAssociation"; - } + private static string ReplaceAbstractResourceNames(string resourceName) + { + // Code Smell here: + // These resources are similar to abstract base classes, so they are not represented in the resourceSchemas + // portion of the schema document. This is a rudimentary replacement with the most specific version of the resource + if (resourceName == "EducationOrganization") + { + resourceName = "School"; + } - if (!visitedResources.ContainsKey(resourceName)) - { - visitedResources.Add(resourceName, 0); - } + if (resourceName == "GeneralStudentProgramAssociation") + { + resourceName = "StudentProgramAssociation"; + } - var maxDepth = depth; - if (dependencies.ContainsKey(resourceName)) + return resourceName; + } + + public static Dictionary GetDependencies(Dictionary> resources) + { + Dictionary orderedResources = resources.ToDictionary(d => d.Key, _ => 0); + Dictionary visitedResources = []; + + foreach (var dependency in resources.OrderBy(d => d.Value.Count).ThenBy(d => d.Key).Select(d => d.Key)) + { + RecursivelyDetermineDependencies(dependency, 0); + } + + int RecursivelyDetermineDependencies(string resourceName, int depth) + { + if (orderedResources[resourceName] > 0) + { + return orderedResources[resourceName]; + } + + visitedResources.TryAdd(resourceName, 0); + var maxDepth = depth; + foreach (var dependency in resources[resourceName]) + { + if (visitedResources.ContainsKey(dependency)) { - foreach (var dependency in dependencies[resourceName]) + if (visitedResources[dependency] > maxDepth) { - if (visitedResources.ContainsKey(dependency)) - { - if (visitedResources[dependency] > maxDepth) - { - maxDepth = visitedResources[dependency]; - } - } - else - { - var level = RecursivelyDetermineDependencies(dependency, depth); - if (level > maxDepth) - maxDepth = level; - } + maxDepth = visitedResources[dependency]; } - - orderedResources[resourceName] = maxDepth + 1; - visitedResources[resourceName] = maxDepth + 1; } - - return maxDepth + 1; + else + { + var level = RecursivelyDetermineDependencies(dependency, depth); + if (level > maxDepth) + maxDepth = level; + } } + orderedResources[resourceName] = maxDepth + 1; + visitedResources[resourceName] = maxDepth + 1; + return maxDepth + 1; } - return dependenciesJsonArray; + return orderedResources; } } diff --git a/src/core/EdFi.DataManagementService.Core/ApiService.cs b/src/core/EdFi.DataManagementService.Core/ApiService.cs index d552c294c..527ddfe7a 100644 --- a/src/core/EdFi.DataManagementService.Core/ApiService.cs +++ b/src/core/EdFi.DataManagementService.Core/ApiService.cs @@ -259,6 +259,6 @@ public IList GetDataModelInfo() public JsonArray GetDependencies() { var dependencyCalculator = new DependencyCalculator(_apiSchemaProvider.ApiSchemaRootNode, _logger); - return dependencyCalculator.GetDependencies(); + return dependencyCalculator.GetDependenciesFromResourceSchema(); } }