diff --git a/deployment/hasura/metadata/actions.graphql b/deployment/hasura/metadata/actions.graphql index a5975744b1..a75616d162 100644 --- a/deployment/hasura/metadata/actions.graphql +++ b/deployment/hasura/metadata/actions.graphql @@ -101,6 +101,18 @@ type Query { ): [EffectiveArgumentsResponse!]! } +type Query { + getConstraintProcedureEffectiveArgumentsBulk( + arguments: [ProcedureEffectiveArgumentsInput!]! + ): [ProcedureEffectiveArgumentsResponse!]! +} + +type Query { + getSchedulingProcedureEffectiveArgumentsBulk( + arguments: [ProcedureEffectiveArgumentsInput!]! + ): [ProcedureEffectiveArgumentsResponse!]! +} + type Query { getActivityTypeScript(missionModelId: Int!, activityTypeName: String!): DslTypescriptResponse } @@ -213,6 +225,20 @@ type EffectiveArgumentsResponse { typeName: String } +input ProcedureEffectiveArgumentsInput { + id: Int! + revision: Int! + arguments: ProcedureArguments! +} + +type ProcedureEffectiveArgumentsResponse { + success: Boolean! + arguments: ProcedureArguments! + errors: [String!] + id: Int! + revision: Int! +} + type AddExternalDatasetResponse { datasetId: Int! } @@ -341,6 +367,8 @@ scalar ModelArguments scalar ActivityArguments +scalar ProcedureArguments + scalar ProfileSet scalar SchedulingFailureReason diff --git a/deployment/hasura/metadata/actions.yaml b/deployment/hasura/metadata/actions.yaml index e78f24227b..9e0c4d71d3 100644 --- a/deployment/hasura/metadata/actions.yaml +++ b/deployment/hasura/metadata/actions.yaml @@ -98,6 +98,15 @@ actions: - role: aerie_admin - role: user - role: viewer + - name: getConstraintProcedureEffectiveArgumentsBulk + definition: + kind: "" + handler: "{{AERIE_MERLIN_URL}}/getConstraintProcedureEffectiveArgumentsBulk" + timeout: 300 + permissions: + - role: aerie_admin + - role: user + - role: viewer - name: getActivityTypeScript definition: kind: "" @@ -157,6 +166,15 @@ actions: permissions: - role: aerie_admin - role: user + - name: getSchedulingProcedureEffectiveArgumentsBulk + definition: + kind: "" + handler: "{{AERIE_SCHEDULER_URL}}/getSchedulingProcedureEffectiveArgumentsBulk" + timeout: 300 + permissions: + - role: aerie_admin + - role: user + - role: viewer - name: constraintsDslTypescript definition: kind: "" diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 0068fe7917..4d923c6ccb 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -60,6 +60,7 @@ dependencies { annotationProcessor project(':procedural:processor') implementation project(":procedural:scheduling") + implementation project(":procedural:constraints") implementation project(":procedural:timeline") implementation project(':merlin-sdk') implementation project(':type-utils') @@ -104,7 +105,7 @@ tasks.create("generateProcedureJarTasks") { } files.toList().each { file -> - final nameWithoutExtension = file.name.replace(".java", "") + final nameWithoutExtension = file.name.replace(".java", "").replace("Mapper", "") final taskName = "buildProcedureJar_${nameWithoutExtension}" println "Generating ${taskName} task, which will build ${nameWithoutExtension}.jar" diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java index 7085d0f640..8a0802eabe 100644 --- a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults; import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; import gov.nasa.ammos.aerie.procedural.scheduling.Goal; import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; @@ -17,7 +18,7 @@ * one every 6hrs. */ @SchedulingProcedure -public record DumbRecurrenceGoal(int quantity) implements Goal { +public record DumbRecurrenceGoal(int quantity, int biteSize) implements Goal { @Override public void run(@NotNull final EditablePlan plan) { final var firstTime = Duration.hours(24); @@ -27,7 +28,7 @@ public void run(@NotNull final EditablePlan plan) { for (var i = 0; i < quantity; i++) { plan.create( new NewDirective( - new AnyDirective(Map.of("biteSize", SerializedValue.of(1))), + new AnyDirective(Map.of("biteSize", SerializedValue.of(biteSize))), "It's a bite banana activity", "BiteBanana", new DirectiveStart.Absolute(currentTime) @@ -37,4 +38,11 @@ public void run(@NotNull final EditablePlan plan) { } plan.commit(); } + + /** + * Default parameters. Quantity is provided but biteSize is not so it is required. + */ + public static @WithDefaults class Defaults { + public int quantity = 360; + } } diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoalWithTemplateDefaults.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoalWithTemplateDefaults.java new file mode 100644 index 0000000000..cd4e9e51d3 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoalWithTemplateDefaults.java @@ -0,0 +1,46 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.Template; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.NewDirective; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Waits 24hrs into the plan, then places `quantity` number of BiteBanana activities, + * one every 6hrs. + */ +@SchedulingProcedure +public record DumbRecurrenceGoalWithTemplateDefaults(int quantity, int biteSize) implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + final var firstTime = Duration.hours(24); + final var step = Duration.hours(6); + + var currentTime = firstTime; + for (var i = 0; i < quantity; i++) { + plan.create( + new NewDirective( + new AnyDirective(Map.of("biteSize", SerializedValue.of(biteSize))), + "It's a bite banana activity", + "BiteBanana", + new DirectiveStart.Absolute(currentTime) + ) + ); + currentTime = currentTime.plus(step); + } + plan.commit(); + } + + public static @Template DumbRecurrenceGoalWithTemplateDefaults create() { + return new DumbRecurrenceGoalWithTemplateDefaults(3, 5); + } +} + diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/FruitThresholdConstraint.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/FruitThresholdConstraint.java new file mode 100644 index 0000000000..d5da47ef55 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/FruitThresholdConstraint.java @@ -0,0 +1,29 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.constraints.Constraint; +import gov.nasa.ammos.aerie.procedural.constraints.Violations; +import gov.nasa.ammos.aerie.procedural.constraints.annotations.ConstraintProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults; +import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real; +import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan; +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults; +import org.jetbrains.annotations.NotNull; + +@ConstraintProcedure +public record FruitThresholdConstraint(int lowerBound, int upperBound) implements Constraint { + @NotNull + @Override + public Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults) { + final var fruit = simResults.resource("/fruit", Real.deserializer()); + + return Violations.on( + fruit.lessThan(upperBound).and(fruit.greaterThan(lowerBound)), + false + ); + } + + @WithDefaults + public static class Template{ + public int lowerBound = 5; + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/BasicConstraintTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/BasicConstraintTests.java new file mode 100644 index 0000000000..9b238a8108 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/constraints/BasicConstraintTests.java @@ -0,0 +1,82 @@ +package gov.nasa.jpl.aerie.e2e.procedural.constraints; + +import gov.nasa.jpl.aerie.e2e.procedural.scheduling.ProceduralSchedulingSetup; +import gov.nasa.jpl.aerie.e2e.types.ConstraintInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BasicConstraintTests extends ProceduralSchedulingSetup { + private ConstraintInvocationId fruitTresholdConstraintId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + final int fruitTresholdConstraintJarId = gateway.uploadJarFile("build/libs/FruitThresholdConstraint.jar"); + // Add Scheduling Procedure + fruitTresholdConstraintId = hasura.createConstraintSpecProcedure( + "Test Constraint Procedure 1", + fruitTresholdConstraintJarId, + planId + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteConstraint(fruitTresholdConstraintId.id()); + } + + /** + * Run a spec with one procedure in it with required params but no args set + * Should fail because one argument is provided in the template but not the other + */ + @Test + void executeConstraintRunWithoutArguments() throws IOException { + hasura.awaitSimulation(planId); + final var resp = hasura.checkConstraints(planId); + assertEquals(1, resp.constraintsRun().size()); + assertEquals(1, resp.constraintsRun().getFirst().errors().size()); + resp.constraintsRun().getFirst().errors().getFirst().message().contains("gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException: Invalid arguments for input type \"FruitThresholdConstraint\": extraneous arguments: [], unconstructable arguments: [], missing arguments: [MissingArgument[parameterName=upperBound, schema=IntSchema[]]], valid arguments: [ValidArgument[parameterName=lowerBound, serializedValue=NumericValue[value=5]]]"); + } + + /** + * Run a constraint that has one template argument and requires one other argument + */ + @Test + void executeConstraintRunWithArguments() throws IOException { + final var args = Json.createObjectBuilder().add("upperBound", 10).build(); + hasura.updateConstraintArguments(fruitTresholdConstraintId.invocationId(), args); + hasura.awaitSimulation(planId); + final var resp = hasura.checkConstraints(planId); + assertTrue(resp.constraintsRun().getFirst().success()); + } + + /** + * Queries the procedural constraints arguments. + */ + @Test + void effectiveArgumentsQuery() throws IOException { + final var effectiveArgs = hasura.getEffectiveProceduralConstraintsArgumentsBulk( + List.of(Pair.of(fruitTresholdConstraintId.id(), Json.createObjectBuilder().add("upperBound", 10).build()))); + assertEquals(1, effectiveArgs.size()); + assertTrue(effectiveArgs.get(0).success()); + assertTrue(effectiveArgs.get(0).arguments().isPresent()); + assertTrue(effectiveArgs.get(0).errors().isEmpty()); + + // Check returned Arguments + final var args = effectiveArgs.get(0).arguments().get(); + assertEquals(2, args.size()); + assertEquals(10, args.getInt("upperBound")); + assertEquals(5, args.getInt("lowerBound")); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicSchedulingTests.java similarity index 72% rename from e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java rename to e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicSchedulingTests.java index 6a8b8d7f04..5554c074dd 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicSchedulingTests.java @@ -13,18 +13,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class BasicTests extends ProceduralSchedulingSetup { - private int procedureJarId; - private GoalInvocationId procedureId; +public class BasicSchedulingTests extends ProceduralSchedulingSetup { + private int dumbRecurrenceGoalJarId; + private GoalInvocationId dumbRecurrenceGoalId; @BeforeEach void localBeforeEach() throws IOException { try (final var gateway = new GatewayRequests(playwright)) { - procedureJarId = gateway.uploadJarFile("build/libs/DumbRecurrenceGoal.jar"); + dumbRecurrenceGoalJarId = gateway.uploadJarFile("build/libs/DumbRecurrenceGoal.jar"); // Add Scheduling Procedure - procedureId = hasura.createSchedulingSpecProcedure( - "Test Scheduling Procedure", - procedureJarId, + dumbRecurrenceGoalId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure 1", + dumbRecurrenceGoalJarId, specId, 0 ); @@ -33,7 +33,7 @@ void localBeforeEach() throws IOException { @AfterEach void localAfterEach() throws IOException { - hasura.deleteSchedulingGoal(procedureId.goalId()); + hasura.deleteSchedulingGoal(dumbRecurrenceGoalId.goalId()); } /** @@ -44,7 +44,7 @@ void proceduralUploadWorks() throws IOException { final var ids = hasura.getSchedulingSpecGoalIds(specId); assertEquals(1, ids.size()); - assertEquals(procedureId.goalId(), ids.getFirst()); + assertEquals(dumbRecurrenceGoalId.goalId(), ids.getFirst()); } /** @@ -55,7 +55,7 @@ void proceduralUploadWorks() throws IOException { void executeSchedulingRunWithoutArguments() throws IOException { final var resp = hasura.awaitFailingScheduling(specId); final var message = resp.reason().getString("message"); - assertTrue(message.contains("java.lang.RuntimeException: Record missing key Component[name=quantity")); + assertTrue(message.contains("java.lang.RuntimeException: gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException: Invalid arguments for input type \"DumbRecurrenceGoal\": extraneous arguments: [], unconstructable arguments: [], missing arguments: [MissingArgument[parameterName=biteSize, schema=IntSchema[]]], valid arguments: [ValidArgument[parameterName=quantity, serializedValue=NumericValue[value=360]]]")); } /** @@ -63,9 +63,9 @@ void executeSchedulingRunWithoutArguments() throws IOException { */ @Test void executeSchedulingRunWithArguments() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); + final var args = Json.createObjectBuilder().add("quantity", 2).add("biteSize", 1).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + hasura.updateSchedulingSpecGoalArguments(dumbRecurrenceGoalId.invocationId(), args); hasura.awaitScheduling(specId); @@ -88,10 +88,10 @@ void executeSchedulingRunWithArguments() throws IOException { */ @Test void executeMultipleInvocationsOfSameProcedure() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + final var args = Json.createObjectBuilder().add("quantity", 2).add("biteSize", 1).build(); + hasura.updateSchedulingSpecGoalArguments(dumbRecurrenceGoalId.invocationId(), args); - final var secondInvocationId = hasura.insertGoalInvocation(procedureId.goalId(), specId); + final var secondInvocationId = hasura.insertGoalInvocation(dumbRecurrenceGoalId.goalId(), specId); hasura.updateSchedulingSpecGoalArguments(secondInvocationId.invocationId(), args); hasura.awaitScheduling(specId); @@ -107,12 +107,12 @@ void executeMultipleInvocationsOfSameProcedure() throws IOException { */ @Test void executeMultipleProcedures() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + final var args = Json.createObjectBuilder().add("quantity", 2).add("biteSize", 1).build(); + hasura.updateSchedulingSpecGoalArguments(dumbRecurrenceGoalId.invocationId(), args); final var secondProcedure = hasura.createSchedulingSpecProcedure( "Test Scheduling Procedure 2", - procedureJarId, + dumbRecurrenceGoalJarId, specId, 1); @@ -131,8 +131,8 @@ void executeMultipleProcedures() throws IOException { */ @Test void executeEDSLAndProcedure() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 4).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + final var args = Json.createObjectBuilder().add("quantity", 4).add("biteSize", 1).build(); + hasura.updateSchedulingSpecGoalArguments(dumbRecurrenceGoalId.invocationId(), args); final String recurrenceGoalDefinition = """ @@ -161,8 +161,8 @@ export default function myGoal() { */ @Test void saveActivityName() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + final var args = Json.createObjectBuilder().add("quantity", 2).add("biteSize", 1).build(); + hasura.updateSchedulingSpecGoalArguments(dumbRecurrenceGoalId.invocationId(), args); hasura.awaitScheduling(specId); diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateDefaultsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateDefaultsTests.java new file mode 100644 index 0000000000..f99af58818 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateDefaultsTests.java @@ -0,0 +1,116 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import java.io.IOException; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TemplateDefaultsTests extends ProceduralSchedulingSetup { + private int procedureJarId; + private GoalInvocationId procedureId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + procedureJarId = gateway.uploadJarFile("build/libs/DumbRecurrenceGoalWithTemplateDefaults.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + } + + /** + * Upload a procedure jar and add to spec + */ + @Test + void proceduralUploadWorks() throws IOException { + final var ids = hasura.getSchedulingSpecGoalIds(specId); + + assertEquals(1, ids.size()); + assertEquals(procedureId.goalId(), ids.getFirst()); + } + + /** + * Running without argument works because of template defaults + */ + @Test + void executeSchedulingRunWithoutArguments() throws IOException { + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(3, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "24:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 5))); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "30:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 5))); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "36:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 5))); + } + + /** + * Run a spec with one procedure in it and just one of the argument. The other should be provided by the template. + */ + @Test + void executeSchedulingRunWithOneArgument() throws IOException { + final var args = Json.createObjectBuilder().add("biteSize", 2).build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(3, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "24:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 2))); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "30:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 2))); + + assertTrue(activities.stream().anyMatch( + it -> + Objects.equals(it.type(), "BiteBanana") && + Objects.equals(it.startOffset(), "36:00:00") && + Objects.equals(it.arguments().getInt("biteSize"), 2))); + } + +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateParametersTest.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateParametersTest.java new file mode 100644 index 0000000000..ed58cf0582 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/TemplateParametersTest.java @@ -0,0 +1,101 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import javax.json.JsonValue; +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TemplateParametersTest extends ProceduralSchedulingSetup { + private GoalInvocationId dumbRecurrenceWithTemplateDefaultsGoalId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + final int dumbRecurrenceWithTemplateDefaultsGoalJarId = gateway.uploadJarFile( + "build/libs/DumbRecurrenceGoalWithTemplateDefaults.jar"); + // Add Scheduling Procedure + dumbRecurrenceWithTemplateDefaultsGoalId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure 1", + dumbRecurrenceWithTemplateDefaultsGoalJarId, + specId, + 0 + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(dumbRecurrenceWithTemplateDefaultsGoalId.goalId()); + } + + /** + * Run a spec with one procedure in it with required params but no args set + * Should succeed because of default arguments + */ + @Test + void executeSchedulingRunWithoutArguments() throws IOException { + hasura.awaitScheduling(specId); + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(3, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.startOffset(), "24:00:00") + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.startOffset(), "30:00:00") + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.startOffset(), "36:00:00") + )); + } + + @Test + void defaultArgsTotal() throws IOException { + final var effectiveArgs = hasura.getEffectiveProceduralGoalsArgumentsBulk( + List.of(Pair.of(dumbRecurrenceWithTemplateDefaultsGoalId.goalId(), JsonValue.EMPTY_JSON_OBJECT))); + assertEquals(1, effectiveArgs.size()); + assertTrue(effectiveArgs.get(0).success()); + assertTrue(effectiveArgs.get(0).arguments().isPresent()); + assertTrue(effectiveArgs.get(0).errors().isEmpty()); + + // Check returned Arguments + final var args = effectiveArgs.get(0).arguments().get(); + assertEquals(2, args.size()); + assertEquals(5, args.getInt("biteSize")); + assertEquals(3, args.getInt("quantity")); + } + + @Test + void defaultArgsPartial() throws IOException { + final var effectiveArgs = hasura.getEffectiveProceduralGoalsArgumentsBulk( + List.of(Pair.of( + dumbRecurrenceWithTemplateDefaultsGoalId.goalId(), + Json.createObjectBuilder().add("quantity", 4).build()))); + assertEquals(1, effectiveArgs.size()); + assertTrue(effectiveArgs.get(0).success()); + assertTrue(effectiveArgs.get(0).arguments().isPresent()); + assertTrue(effectiveArgs.get(0).errors().isEmpty()); + + // Check returned Arguments + final var args = effectiveArgs.get(0).arguments().get(); + assertEquals(2, args.size()); + assertEquals(5, args.getInt("biteSize")); + assertEquals(4, args.getInt("quantity")); + } + +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java index b8077f6670..bce34d338f 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintError.java @@ -1,8 +1,9 @@ package gov.nasa.jpl.aerie.e2e.types; import javax.json.JsonObject; +import java.util.Optional; -public record ConstraintError(String message, String stack, Location location ){ +public record ConstraintError(String message, String stack, Optional location ){ record Location(int column, int line){ public static Location fromJSON(JsonObject json){ return new Location(json.getJsonNumber("column").intValue(), json.getJsonNumber("line").intValue()); @@ -10,6 +11,11 @@ public static Location fromJSON(JsonObject json){ }; public static ConstraintError fromJSON(JsonObject json){ - return new ConstraintError(json.getString("message"),json.getString("stack"),Location.fromJSON(json.getJsonObject("location"))); + return new ConstraintError( + json.getString("message"), + json.getString("stack"), + json.getJsonObject("location").isEmpty() ? + Optional.empty() : + Optional.of(Location.fromJSON(json.getJsonObject("location")))); } }; diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/EffectiveProceduralArguments.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/EffectiveProceduralArguments.java new file mode 100644 index 0000000000..144177ac2f --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/EffectiveProceduralArguments.java @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.e2e.types; + +import javax.json.JsonObject; +import javax.json.JsonValue; +import java.util.Optional; + +public record EffectiveProceduralArguments( + int goalId, + boolean success, + Optional arguments, + Optional errors) +{ + public static EffectiveProceduralArguments fromJSON(JsonObject json) { + return new EffectiveProceduralArguments( + json.getInt("id"), + json.getBoolean("success"), + json.containsKey("arguments") ? Optional.of(json.getJsonObject("arguments")) : Optional.empty(), + json.containsKey("errors") ? Optional.of(json.get("errors")) : Optional.empty()); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index d51cc3f32a..fd25988e02 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -332,6 +332,28 @@ query GetEffectiveActivityArgumentsBulk($modelId: Int!, $activities: [EffectiveA success } }"""), + GET_EFFECTIVE_PROCEDURAL_GOALS_ARGUMENTS_BULK(""" + query GetSchedulingProcedureEffectiveArgumentsBulk($arguments: [ProcedureEffectiveArgumentsInput!]!) { + getSchedulingProcedureEffectiveArgumentsBulk( + arguments: $arguments + ) { + success + arguments + errors + id + } + }"""), + GET_EFFECTIVE_PROCEDURAL_CONSTRAINTS_ARGUMENTS_BULK(""" + query GetConstraintProcedureEffectiveArgumentsBulk($arguments: [ProcedureEffectiveArgumentsInput!]!) { + getConstraintProcedureEffectiveArgumentsBulk( + arguments: $arguments + ) { + success + arguments + errors + id + } + }"""), GET_ACTIVITY_VALIDATIONS(""" query GetActivityValidations($planId: Int!) { activity_directive_validations(where: {plan_id: {_eq: $planId}}) { @@ -732,6 +754,15 @@ mutation updateSchedulingSpecGoalArguments($goal_invocation_id: Int!, $arguments arguments } }"""), + UPDATE_CONSTRAINT_ARGUMENTS(""" + mutation updateConstraintArguments($constraint_id: Int!, $arguments: jsonb!) { + update_constraint_specification(where: {constraint_id: {_eq: $constraint_id}}, _set: {arguments: $arguments}){ + returning { + arguments + } + } + } + """), UPDATE_SCHEDULING_SPEC_GOALS_ENABLED(""" mutation updateSchedulingSpecGoalVersion($goal_invocation_id: Int!, $enabled: Boolean!) { update_scheduling_specification_goals_by_pk( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index 9b6f27c4fd..de5f0839e6 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -287,6 +287,40 @@ public EffectiveActivityArguments getEffectiveActivityArguments( return effectiveArgs.get(0); } + + public List getEffectiveProceduralGoalsArgumentsBulk( + List> proceduralGoalIds + ) throws IOException { + final var proceduresBuilder = Json.createArrayBuilder(); + proceduralGoalIds.forEach(goal -> proceduresBuilder.add(Json.createObjectBuilder() + .add("id", goal.getLeft()) + .add("revision", 0) + .add("arguments", goal.getRight()))); + final var variables = Json.createObjectBuilder() + .add("arguments", proceduresBuilder.build()) + .build(); + return makeRequest(GQL.GET_EFFECTIVE_PROCEDURAL_GOALS_ARGUMENTS_BULK, variables) + .getJsonArray("getSchedulingProcedureEffectiveArgumentsBulk") + .getValuesAs(EffectiveProceduralArguments::fromJSON); + } + + + public List getEffectiveProceduralConstraintsArgumentsBulk( + List> proceduralGoalIds + ) throws IOException { + final var proceduresBuilder = Json.createArrayBuilder(); + proceduralGoalIds.forEach(goal -> proceduresBuilder.add(Json.createObjectBuilder() + .add("id", goal.getLeft()) + .add("revision", 0) + .add("arguments", goal.getRight()))); + final var variables = Json.createObjectBuilder() + .add("arguments", proceduresBuilder.build()) + .build(); + return makeRequest(GQL.GET_EFFECTIVE_PROCEDURAL_CONSTRAINTS_ARGUMENTS_BULK, variables) + .getJsonArray("getConstraintProcedureEffectiveArgumentsBulk") + .getValuesAs(EffectiveProceduralArguments::fromJSON); + } + public List getEffectiveActivityArgumentsBulk( int modelId, List> activities @@ -723,6 +757,35 @@ public GoalInvocationId createSchedulingSpecProcedure( return createSchedulingSpecProcedure(name, jarId, specificationId, priority, true); } + public ConstraintInvocationId createConstraintSpecProcedure( + String name, + int jarId, + int planId + ) throws IOException { + final var specGoalBuilder = Json.createObjectBuilder() + .add("constraint_metadata", + Json.createObjectBuilder() + .add("data", + Json.createObjectBuilder() + .add("name", name) + .add("description", "") + .add("versions", + Json.createObjectBuilder() + .add("data", + Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("type", "JAR") + .add("uploaded_jar_id", jarId) + ))))) + .add("plan_id", planId); + final var variables = Json.createObjectBuilder().add("constraint", specGoalBuilder).build(); + final var resp = makeRequest(GQL.INSERT_PLAN_SPEC_CONSTRAINT, variables) + .getJsonObject("constraint"); + return new ConstraintInvocationId( + resp.getInt("constraint_id"), + resp.getInt("invocation_id") + ); + } public GoalInvocationId createSchedulingSpecProcedure( String name, @@ -827,6 +890,14 @@ public int updateGoalDefinition(int goalId, String definition) throws IOExceptio return makeRequest(GQL.UPDATE_GOAL_DEFINITION, variables).getJsonObject("definition").getInt("revision"); } + public void updateConstraintArguments(int constraintId, JsonObject arguments) throws IOException { + final var variables = Json.createObjectBuilder() + .add("constraint_id", constraintId) + .add("arguments", arguments) + .build(); + makeRequest(GQL.UPDATE_CONSTRAINT_ARGUMENTS, variables); + } + public void updateSchedulingSpecGoalArguments(int invocationId, JsonObject arguments) throws IOException { final var variables = Json.createObjectBuilder() .add("goal_invocation_id", invocationId) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java index ca538013d9..4062667aec 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java @@ -67,6 +67,26 @@ private static JsonParser> hasura ) ); + public static JsonParser> constraintArgumentsP() { + return hasuraActionF( + productP + .field("arguments", listP(constraintArgumentsItemP()) + .map( + untuple(HasuraAction.ConstraintArguments::new), + constraintArguments -> tuple(constraintArguments.items())))); + } + + public static JsonParser constraintArgumentsItemP() { + return productP + .field("id", longP) + .field("revision", longP) + .field("arguments", mapP(serializedValueP)) + .map( + untuple(HasuraAction.ConstraintArgumentItem::new), + constraintArgumentItem -> tuple(constraintArgumentItem.constraintId(), constraintArgumentItem.revision(), constraintArgumentItem.arguments())); + } + + public static final JsonParser> hasuraConstraintsViolationsActionP = hasuraActionF( productP diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java index ba03e35a83..19ffba3a77 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.stream.Collectors; +import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.constraintArgumentsP; import static gov.nasa.jpl.aerie.merlin.server.http.MerlinParsers.parseJson; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraActivityActionP; @@ -108,6 +109,7 @@ public void apply(final Javalin javalin) { path("extendExternalDataset", () -> post(this::extendExternalDataset)); path("constraintsDslTypescript", () -> post(this::getConstraintsDslTypescript)); path("refreshConstraintProcedureParameterTypes", () -> post(this::refreshConstrainProcedureParameterTypes)); + path("getConstraintProcedureEffectiveArgumentsBulk", () -> post(this::getConstraintProcedureEffectiveArgumentsBulk)); path("health", () -> get(ctx -> ctx.status(200))); }); @@ -439,6 +441,20 @@ private void getActivityEffectiveArgumentsBulk(final Context ctx) { } } + private void getConstraintProcedureEffectiveArgumentsBulk(final Context ctx) { + try { + final var input = parseJson(ctx.body(), constraintArgumentsP()); + final var responses = this.constraintAction.getConstraintProcedureEffectiveArgumentsBulk(input.input()); + ctx.result(ResponseSerializers.serializeIterable( + ResponseSerializers::serializeConstraintBulkEffectiveArgumentResponse, responses).toString()); + } catch (final InvalidJsonException ex) { + ctx.status(400).result(ResponseSerializers.serializeInvalidJsonException(ex).toString()); + } catch (final InvalidEntityException ex) { + ctx.status(400).result(ResponseSerializers.serializeInvalidEntityException(ex).toString()); + } + } + + private void addExternalDataset(final Context ctx) { try { final var body = parseJson(ctx.body(), hasuraUploadExternalDatasetActionP); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinParsers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinParsers.java index e6a78c5d71..43174bb9b4 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinParsers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinParsers.java @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.json.SchemaCache; import gov.nasa.jpl.aerie.merlin.driver.SimulationFailure; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.DatasetId; import gov.nasa.jpl.aerie.merlin.server.models.PlanId; import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; @@ -107,6 +108,16 @@ public JsonValue unparse(final Timestamp value) { failure -> tuple(failure.type(), failure.message(), failure.data(), Optional.ofNullable(failure.trace()), new Timestamp(failure.timestamp())) ); + public static JsonParser constraintIdP() { + return productP + .field("constraint_id", longP) + .field("revision", longP) + .map( + untuple(ConstraintId::new), + constraintArguments -> tuple(constraintArguments.id(), constraintArguments.revision())); + } + + public static T parseJson(final String subject, final JsonParser parser) throws InvalidJsonException, InvalidEntityException { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java index c9e258a560..c7805c376c 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java @@ -3,6 +3,7 @@ import gov.nasa.jpl.aerie.constraints.InputMismatchException; import gov.nasa.jpl.aerie.constraints.model.ConstraintResult; import gov.nasa.jpl.aerie.json.JsonParseResult.FailureReason; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; @@ -15,7 +16,9 @@ import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintsCompilationError; +import gov.nasa.jpl.aerie.merlin.server.models.ProcedureLoader; import gov.nasa.jpl.aerie.merlin.server.remotes.MissionModelAccessException; +import gov.nasa.jpl.aerie.merlin.server.services.BulkConstraintEffectiveArgumentResponse; import gov.nasa.jpl.aerie.merlin.server.services.GetSimulationResultsAction; import gov.nasa.jpl.aerie.merlin.server.services.LocalMissionModelService; import gov.nasa.jpl.aerie.merlin.server.services.MissionModelService; @@ -117,6 +120,56 @@ public static JsonValue serializeBulkEffectiveArgumentResponseList(final List effectiveArguments)) { + return Json.createObjectBuilder() + .add("id", constraintId.id()) + .add("revision", constraintId.revision()) + .add("success", JsonValue.TRUE) + .add("arguments", + serializeMap( + ResponseSerializers::serializeArgument, + effectiveArguments)) + .build(); + } else if (response instanceof BulkConstraintEffectiveArgumentResponse.TypeFailure(ConstraintId constraintId)) { + return Json.createObjectBuilder() + .add("id", constraintId.id()) + .add("revision", constraintId.revision()) + .add("success", JsonValue.FALSE) + .add("errors", "Constraint is not procedural") + .build(); + } else if (response instanceof BulkConstraintEffectiveArgumentResponse.InstantiationFailure( + ConstraintId constraintId, + InstantiationException ex)) { + return Json.createObjectBuilder( + serializeInstantiationException(ex).asJsonObject()) + .add("success", JsonValue.FALSE) + .add("id", constraintId.id()) + .add("revision", constraintId.revision()) + .build(); + } else if (response instanceof BulkConstraintEffectiveArgumentResponse.NoConstraintFailure(ConstraintId constraintId)) { + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("id", constraintId.id()) + .add("revision", constraintId.revision()) + .add("errors", "There is no constraint with this id") + .build(); + } else if (response instanceof BulkConstraintEffectiveArgumentResponse.ProcedureLoadFailure( + ConstraintId constraintId, ProcedureLoader.ProcedureLoadException ex)) { + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("id", constraintId.id()) + .add("revision", constraintId.revision()) + .add("errors", "Error when loading the procedure jar") + .build(); + } + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("errors", String.format("Internal error: %s", response)) + .build(); + } + public static JsonValue serializeBulkEffectiveArgumentResponse(BulkEffectiveArgumentResponse response) { // TODO use pattern matching in switch statement with JDK 21 if (response instanceof BulkEffectiveArgumentResponse.Success s) { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ConstraintId.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ConstraintId.java new file mode 100644 index 0000000000..68f14a5551 --- /dev/null +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ConstraintId.java @@ -0,0 +1,3 @@ +package gov.nasa.jpl.aerie.merlin.server.models; + +public record ConstraintId(long id, long revision) { } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java index 15977f4f52..b46c20b127 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/ExecutableConstraint.java @@ -5,7 +5,7 @@ import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.model.Violation; import gov.nasa.jpl.aerie.constraints.tree.Expression; -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import org.jetbrains.annotations.NotNull; import gov.nasa.ammos.aerie.procedural.constraints.ProcedureMapper; @@ -84,9 +84,14 @@ public ProceduralConstraintResult run( throw new RuntimeException(e); } - final var violations = Violation.fromProceduralViolations(procedureMapper - .deserialize(SerializedValue.of(record.arguments())) - .run(plan, simResults), merlinResults); + final List violations; + try { + violations = Violation.fromProceduralViolations(procedureMapper + .getInputType().instantiate(record.arguments()) + .run(plan, simResults), merlinResults); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } return new ProceduralConstraintResult(violations, record); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java index 6a40bfb5c2..7eca18d65b 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java @@ -36,4 +36,6 @@ public record ExtendExternalDatasetInput(DatasetId datasetId, public record ConstraintsInput(MissionModelId missionModelId, Optional planId) implements Input {} public record NewConstraintRevisionEvent(long constraintId, long revision) implements Input {} + public record ConstraintArgumentItem(long constraintId, long revision, Map arguments) implements Input {} + public record ConstraintArguments(List items) implements Input {} } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java index 1798499729..1e40040656 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchConstraintException; import gov.nasa.jpl.aerie.merlin.server.http.Fallible; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintType; import gov.nasa.jpl.aerie.merlin.server.models.DBConstraintResult; import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; @@ -20,4 +21,5 @@ int insertConstraintRuns(final ConstraintRequestConfiguration requestConfigurati Map getValidConstraintRuns(List constraints, SimulationDatasetId simulationDatasetId); ConstraintType getConstraintType(final long constraintId, final long revision) throws NoSuchConstraintException; void updateConstraintParameterSchema(final long constraintId, final long revision, final ValueSchema schema); -} + Map getConstraints(final List constraintIds); + } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetConstraintAction.java new file mode 100644 index 0000000000..a5128a3d5e --- /dev/null +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetConstraintAction.java @@ -0,0 +1,93 @@ +package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; + +import gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.server.http.InvalidEntityException; +import gov.nasa.jpl.aerie.merlin.server.http.InvalidJsonException; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintType; +import org.intellij.lang.annotations.Language; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static gov.nasa.jpl.aerie.merlin.server.http.MerlinParsers.parseJson; + +/** + * Gets a constraint from its id and revision + */ +public class GetConstraintAction implements AutoCloseable { + private static final @Language("SQL") String sql = """ + select + cd.type, + cd.definition, + cs.priority, + cs.invocation_id , + cm.name, + cm.description, + cd.type, + cs.arguments, + encode(f.path, 'escape') as path + from merlin.constraint_metadata as cm + left join merlin.constraint_definition cd on cm.id = cd.constraint_id + left join merlin.constraint_specification cs on cm.id = cs.constraint_id + left join merlin.uploaded_file f on cd.uploaded_jar_id = f.id + where cm.id = ? + and cd.revision = ?; + """; + + private final PreparedStatement statement; + + public GetConstraintAction(final Connection connection) throws SQLException { + this.statement = connection.prepareStatement(sql); + } + + public Map get(List ids) throws SQLException { + final var constraints = new HashMap(); + for(var id: ids) { + this.statement.setLong(1, id.id()); + this.statement.setLong(2, id.revision()); + + try (final var results = this.statement.executeQuery()) { + if (!results.next()) return constraints; + final var constraintTypeString = results.getString("type"); + Optional type = Optional.empty(); + switch (constraintTypeString) { + case "EDSL" -> { + type = Optional.of(new ConstraintType.EDSL(results.getString("definition"))); + } + case "JAR" -> { + type = Optional.of(new ConstraintType.JAR(results.getString("path"))); + } + default -> throw new SQLException("Invalid value in 'type' column of 'constraint_definition': " + + constraintTypeString); + } + final var c = new ConstraintRecord( + results.getLong("priority"), + results.getLong("invocation_id"), + id.id(), + id.revision(), + results.getString("name"), + results.getString("description"), + type.get(), + parseJson(results.getString("arguments"), new SerializedValueJsonParser()).asMap().orElse(Map.of()) + ); + constraints.put(id, c); + } catch (InvalidJsonException | InvalidEntityException e) { + throw new SQLException(e); + } + } + return constraints; + } + + @Override + public void close() throws SQLException { + this.statement.close(); + } +} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java index dfbaf79cad..486538d606 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchConstraintException; import gov.nasa.jpl.aerie.merlin.server.http.Fallible; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintType; import gov.nasa.jpl.aerie.merlin.server.models.DBConstraintResult; @@ -61,6 +62,16 @@ public ConstraintType getConstraintType(final long constraintId, final long revi } } + @Override + public Map getConstraints(List ids) { + try(final var connection = this.dataSource.getConnection(); + final var getConstraintAction = new GetConstraintAction(connection)) { + return getConstraintAction.get(ids); + } catch (SQLException ex) { + throw new DatabaseException("Failed to get constraints", ex); + } + } + @Override public void updateConstraintParameterSchema(final long constraintId, final long revision, final ValueSchema schema) { try (final var connection = this.dataSource.getConnection()) { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/BulkConstraintEffectiveArgumentResponse.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/BulkConstraintEffectiveArgumentResponse.java new file mode 100644 index 0000000000..498e6b270b --- /dev/null +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/BulkConstraintEffectiveArgumentResponse.java @@ -0,0 +1,16 @@ +package gov.nasa.jpl.aerie.merlin.server.services; + +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; +import gov.nasa.jpl.aerie.merlin.server.models.ProcedureLoader; + +import java.util.Map; + +public sealed interface BulkConstraintEffectiveArgumentResponse { + record Success(ConstraintId constraintId, Map effectiveArguments) implements BulkConstraintEffectiveArgumentResponse { } + record NoConstraintFailure(ConstraintId constraintId) implements BulkConstraintEffectiveArgumentResponse { } + record InstantiationFailure(ConstraintId constraintId, InstantiationException ex) implements BulkConstraintEffectiveArgumentResponse { } + record TypeFailure(ConstraintId constraintId) implements BulkConstraintEffectiveArgumentResponse { } + record ProcedureLoadFailure(ConstraintId constraintId, ProcedureLoader.ProcedureLoadException ex) implements BulkConstraintEffectiveArgumentResponse { } +} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index e1e90c313c..6ae7bad3e6 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -1,9 +1,11 @@ package gov.nasa.jpl.aerie.merlin.server.services; +import gov.nasa.ammos.aerie.procedural.constraints.ProcedureMapper; import gov.nasa.jpl.aerie.constraints.InputMismatchException; import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; import gov.nasa.jpl.aerie.constraints.model.*; import gov.nasa.jpl.aerie.constraints.tree.Expression; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException; import gov.nasa.jpl.aerie.merlin.server.http.Fallible; @@ -11,6 +13,7 @@ import gov.nasa.jpl.aerie.types.MissionModelId; import org.apache.commons.lang3.tuple.Pair; +import java.nio.file.Path; import java.util.*; public class ConstraintAction { @@ -40,6 +43,39 @@ public void refreshConstraintProcedureParameterTypes(long constraintId, long rev constraintService.refreshConstraintProcedureParameterTypes(constraintId, revision); } + public List getConstraintProcedureEffectiveArgumentsBulk( + HasuraAction.ConstraintArguments procedureArgumentsList) { + final var responses = new ArrayList(); + final var constraints = this.constraintService.getConstraintsById( + procedureArgumentsList.items().stream().map( + p -> new ConstraintId(p.constraintId(), p.revision())).toList()); + for (final var procedureArguments : procedureArgumentsList.items()) { + final var constraintIdRecord = new ConstraintId(procedureArguments.constraintId(), procedureArguments.revision()); + final var constraint = constraints.get(constraintIdRecord); + switch (constraint.type()) { + case ConstraintType.EDSL e -> { + responses.add(new BulkConstraintEffectiveArgumentResponse.TypeFailure(constraintIdRecord)); + } + case ConstraintType.JAR j -> { + final ProcedureMapper procedureMapper; + try { + procedureMapper = ProcedureLoader.loadProcedure(Path.of("/usr/src/app/merlin_file_store", j.path().toString())); + + responses.add(new BulkConstraintEffectiveArgumentResponse.Success( + constraintIdRecord, + procedureMapper.getInputType().getEffectiveArguments(procedureArguments.arguments()))); + + } catch (InstantiationException e) { + responses.add(new BulkConstraintEffectiveArgumentResponse.InstantiationFailure(constraintIdRecord, e)); + } catch (ProcedureLoader.ProcedureLoadException e) { + responses.add(new BulkConstraintEffectiveArgumentResponse.ProcedureLoadFailure(constraintIdRecord, e)); + } + } + } + } + return responses; + } + /** * Check the constraints on a plan's specification for violations. * diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java index 06c073c047..52ec4691f5 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java @@ -2,6 +2,7 @@ import gov.nasa.jpl.aerie.constraints.model.ConstraintResult; import gov.nasa.jpl.aerie.merlin.server.http.Fallible; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.DBConstraintResult; import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; @@ -14,4 +15,5 @@ int createConstraintRuns(final ConstraintRequestConfiguration requestConfigurati final Map>> constraintToResultsMap); Map getValidConstraintRuns(List constraints, SimulationDatasetId simulationDatasetId); void refreshConstraintProcedureParameterTypes(long constraintId, long revision); + Map getConstraintsById(List constraintIds); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java index c4e1d026b2..d6d3c2ae09 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.constraints.model.ConstraintResult; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchConstraintException; import gov.nasa.jpl.aerie.merlin.server.http.Fallible; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintType; import gov.nasa.jpl.aerie.merlin.server.models.DBConstraintResult; @@ -59,4 +60,9 @@ public void refreshConstraintProcedureParameterTypes(final long constraintId, fi } } } + + @Override + public Map getConstraintsById(final List constraintIds) { + return constraintRepository.getConstraints(constraintIds); + } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java index 7fa40e084a..792aff1898 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanDatasetException; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.DatasetId; import gov.nasa.jpl.aerie.merlin.server.models.PlanId; diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/PlanService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/PlanService.java index ec43a59d73..6675edcde1 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/PlanService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/PlanService.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanDatasetException; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.DatasetId; import gov.nasa.jpl.aerie.merlin.server.models.PlanId; diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubPlanService.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubPlanService.java index 60617a3686..b03d2db37f 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubPlanService.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubPlanService.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.merlin.server.models.ConstraintId; import gov.nasa.jpl.aerie.merlin.server.models.ConstraintRecord; import gov.nasa.jpl.aerie.merlin.server.models.DatasetId; import gov.nasa.jpl.aerie.merlin.server.models.PlanId; @@ -81,6 +82,7 @@ public List getConstraintsForPlan(final PlanId planId) { return List.of(); } + @Override public long addExternalDataset( final PlanId planId, diff --git a/procedural/constraints/src/main/kotlin/gov/nasa/ammos/aerie/procedural/constraints/ProcedureMapper.kt b/procedural/constraints/src/main/kotlin/gov/nasa/ammos/aerie/procedural/constraints/ProcedureMapper.kt index 93c47e43e5..ce7389d76e 100644 --- a/procedural/constraints/src/main/kotlin/gov/nasa/ammos/aerie/procedural/constraints/ProcedureMapper.kt +++ b/procedural/constraints/src/main/kotlin/gov/nasa/ammos/aerie/procedural/constraints/ProcedureMapper.kt @@ -1,10 +1,9 @@ package gov.nasa.ammos.aerie.procedural.constraints -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema interface ProcedureMapper { fun valueSchema(): ValueSchema - fun serialize(procedure: T): SerializedValue - fun deserialize(arguments: SerializedValue): T + fun getInputType(): InputType } diff --git a/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/constraints/FruitThreshold.java b/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/constraints/FruitThreshold.java index 76933e7c5d..5ee6fe4c73 100644 --- a/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/constraints/FruitThreshold.java +++ b/procedural/examples/banana-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/bananaprocedures/constraints/FruitThreshold.java @@ -3,6 +3,7 @@ import gov.nasa.ammos.aerie.procedural.constraints.Constraint; import gov.nasa.ammos.aerie.procedural.constraints.annotations.ConstraintProcedure; import gov.nasa.ammos.aerie.procedural.constraints.Violations; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults; import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real; import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan; import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults; @@ -20,4 +21,9 @@ public Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults) false ); } + + @WithDefaults + public static class Template{ + public int threshold = 5; + } } diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/AllStaticallyDefinedMethodMaker.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/AllStaticallyDefinedMethodMaker.java new file mode 100644 index 0000000000..5d8cdea649 --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/AllStaticallyDefinedMethodMaker.java @@ -0,0 +1,87 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.Template; +import gov.nasa.jpl.aerie.merlin.protocol.types.UnconstructableArgumentException; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** Method maker for defaults style where all default arguments are provided within a @Template static method. */ +/*package-private*/ final class AllStaticallyDefinedMethodMaker extends MapperMethodMaker { + + public AllStaticallyDefinedMethodMaker(final InputTypeRecord inputType) { + super(inputType); + } + + @Override + public MethodSpec makeInstantiateMethod() { + final var activityTypeName = inputType.declaration().getSimpleName().toString(); + + var methodBuilder = MethodSpec.methodBuilder("instantiate") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(inputType.declaration().asType())) + .addException(InstantiationException.class) + .addParameter( + ParameterizedTypeName.get( + java.util.Map.class, + String.class, + gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.class), + "arguments", + Modifier.FINAL); + + for (final var element : inputType.declaration().getEnclosedElements()) { + if (element.getKind() != ElementKind.METHOD && element.getKind() != ElementKind.CONSTRUCTOR) continue; + if (element.getAnnotation(Template.class) == null) continue; + var templateName = element.getSimpleName().toString(); + methodBuilder = methodBuilder.addStatement("final var template = $L.$L()", activityTypeName, templateName); + + methodBuilder = methodBuilder.addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "$T $L = $T$L", + new TypePattern.ClassPattern( + ClassName.get(Optional.class), + List.of(TypePattern.from(parameter.type))).render(), + parameter.name, + Optional.class, + ".ofNullable(template." + parameter.name + "())" + ) + ) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()).addCode("\n"); + + methodBuilder = makeArgumentAssignments(methodBuilder, (builder, parameter) -> builder + .addStatement( + "$L = $L(this.mapper_$L.deserializeValue($L.getValue())$W.getSuccessOrThrow(failure -> new $T(\"$L\", failure)))", + parameter.name, + "Optional.ofNullable", + parameter.name, + "entry", + UnconstructableArgumentException.class, + parameter.name)); + break; + } + + // Add return statement with instantiation of class with parameters + methodBuilder = methodBuilder.addStatement( + "return new $T($L)", + inputType.declaration(), + inputType.parameters().stream().map( + parameter -> parameter.name + ".get()").collect(Collectors.joining(", "))); + + return methodBuilder.build(); + } +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ExportDefaultsStyle.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ExportDefaultsStyle.java new file mode 100644 index 0000000000..70a842327c --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ExportDefaultsStyle.java @@ -0,0 +1,11 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +/** + * Export defaults "style" refers to how an exporter's + * default arguments have been defined within the mission model. + */ +public enum ExportDefaultsStyle { + AllStaticallyDefined, // All default arguments provided within @Template static method + SomeStaticallyDefined, // Some arguments provided within @WithDefaults static class + NoneDefined // No default arguments provided +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/InputTypeRecord.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/InputTypeRecord.java new file mode 100644 index 0000000000..2954d7e7b3 --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/InputTypeRecord.java @@ -0,0 +1,12 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import javax.lang.model.element.TypeElement; +import java.util.List; + +public record InputTypeRecord( + String name, + TypeElement declaration, + List parameters, + MapperRecord mapper, + ExportDefaultsStyle defaultsStyle +) {} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperMethodMaker.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperMethodMaker.java new file mode 100644 index 0000000000..a68233e0cc --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperMethodMaker.java @@ -0,0 +1,255 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; +import gov.nasa.jpl.aerie.merlin.protocol.types.UnconstructableArgumentException; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +/** + * Mapper method generator for all export types (activities and configurations). + * Generates common methods between all export types. + */ +public abstract sealed class MapperMethodMaker + permits AllStaticallyDefinedMethodMaker, + NoneDefinedMethodMaker, SomeStaticallyDefinedMethodMaker +{ + /*package-private*/ final InputTypeRecord inputType; + + public MapperMethodMaker(final InputTypeRecord inputType) { + this.inputType = inputType; + } + + public abstract MethodSpec makeInstantiateMethod(); + + public /*non-final*/ List getParametersWithDefaults() { + return inputType.parameters().stream().map(p -> p.name).toList(); + } + + public /*non-final*/ MethodSpec makeGetParametersMethod() { + return MethodSpec.methodBuilder("getParameters") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get( + ArrayList.class, + InputType.Parameter.class)) + .addStatement( + "final var $L = new $T()", + "parameters", + ParameterizedTypeName.get( + ArrayList.class, + InputType.Parameter.class)) + .addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "$L.add(new $T($S, this.mapper_$L.getValueSchema()))", + "parameters", + InputType.Parameter.class, + parameter.name, + parameter.name)) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()) + .addStatement( + "return $L", + "parameters") + .build(); + } + + public /*non-final*/ MethodSpec makeGetArgumentsMethod() { + return MethodSpec + .methodBuilder("getArguments") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get( + Map.class, + String.class, + gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.class)) + .addParameter( + TypeName.get(inputType.declaration().asType()), + "input", + Modifier.FINAL) + .addStatement( + "final var $L = new $T()", + "arguments", + ParameterizedTypeName.get( + java.util.HashMap.class, + String.class, + gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.class)) + .addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "$L.put($S, this.mapper_$L.serializeValue($L.$L()))", + "arguments", + parameter.name, + parameter.name, + "input", + parameter.name + )) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()) + .addStatement( + "return $L", + "arguments") + .build(); + } + + public final MethodSpec makeGetRequiredParametersMethod() { + final var optionalParams = getParametersWithDefaults(); + final var requiredParams = inputType.parameters().stream().filter(p -> !optionalParams.contains(p.name)).toList(); + + return MethodSpec.methodBuilder("getRequiredParameters") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get( + List.class, + String.class)) + .addStatement( + "return $T.of($L)", + List.class, + requiredParams.stream().map(p -> "\"%s\"".formatted(p.name)).collect(Collectors.joining(", "))) + .build(); + } + + protected final MethodSpec.Builder makeArgumentAssignments( + final MethodSpec.Builder methodBuilder, + final BiFunction makeArgumentAssignment) + { + var mb = methodBuilder; + + // Condition must be checked for otherwise a try/catch without an exception thrown + // will not pass compilation + final var shouldExpectArguments = !inputType.parameters().isEmpty(); + + mb = mb + .addStatement( + "final var $L = new $T(\"$L\")", + "instantiationExBuilder", + gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException.Builder.class, + inputType.name()) + .addCode("\n") + .beginControlFlow( + "for (final var $L : $L.entrySet())", + "entry", + "arguments"); + + if (shouldExpectArguments) { + mb = mb.beginControlFlow("try"); + } + + mb = mb + .beginControlFlow("switch ($L.getKey())", "entry") + .addCode( + inputType.parameters() + .stream() + .map(parameter -> { + final var caseBuilder = CodeBlock.builder() + .add("case $S:\n", parameter.name) + .indent(); + return makeArgumentAssignment.apply(caseBuilder, parameter) + .addStatement("break") + .unindent(); + }) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()) + .addCode( + CodeBlock + .builder() + .add("default:\n") + .indent() + .addStatement( + "$L.withExtraneousArgument($L.getKey())", + "instantiationExBuilder", + "entry") + .unindent() + .build()) + .endControlFlow(); + + if (shouldExpectArguments) { + mb = mb + .nextControlFlow("catch (final $T e)", UnconstructableArgumentException.class) + .addStatement( + "$L.withUnconstructableArgument(e.parameterName, e.failure)", + "instantiationExBuilder" + ) + .endControlFlow(); + } + + mb = mb + .endControlFlow() + .addCode("\n"); + + return makeMissingArgumentsCheck(mb); + } + + public final MethodSpec makeGetValidationFailuresMethod() { + return MethodSpec + .methodBuilder("getValidationFailures") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get( + java.util.List.class, + InputType.ValidationNotice.class)) + .addParameter( + TypeName.get(inputType.declaration().asType()), + "input", + Modifier.FINAL) + .addStatement( + "final var $L = new $T()", + "notices", + ParameterizedTypeName.get( + java.util.ArrayList.class, + InputType.ValidationNotice.class)) + .addStatement( + "return $L", + "notices") + .build(); + } + + private MethodSpec.Builder makeMissingArgumentsCheck(final MethodSpec.Builder methodBuilder) { + // Ensure all parameters are non-null + return methodBuilder + .addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + // Re-serialize value since provided `arguments` map may not contain the value (when using `@WithDefaults` templates) + "$L.ifPresentOrElse($Wvalue -> $L.withValidArgument(\"$L\", this.mapper_$L.serializeValue(value)),$W() -> $L.withMissingArgument(\"$L\", this.mapper_$L.getValueSchema()))", + parameter.name, + "instantiationExBuilder", + parameter.name, + parameter.name, + "instantiationExBuilder", + parameter.name, + parameter.name)) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()) + .addCode("\n") + .addStatement( + "$L.throwIfAny()", + "instantiationExBuilder"); + } + + static MapperMethodMaker make(final InputTypeRecord inputType) { + return switch (inputType.defaultsStyle()) { + case AllStaticallyDefined -> new AllStaticallyDefinedMethodMaker(inputType); + case NoneDefined -> new NoneDefinedMethodMaker(inputType); + case SomeStaticallyDefined -> new SomeStaticallyDefinedMethodMaker(inputType); + }; + } +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperRecord.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperRecord.java new file mode 100644 index 0000000000..f4da012d9c --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/MapperRecord.java @@ -0,0 +1,33 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import com.squareup.javapoet.ClassName; + +import javax.lang.model.element.PackageElement; +import java.util.Objects; + +public final class MapperRecord { + public final ClassName name; + + public MapperRecord(final ClassName name) { + this.name = Objects.requireNonNull(name); + } + + public static MapperRecord + generatedFor(final ClassName procedureTypeName, final PackageElement jarElement) { + final var jarPackage = jarElement.getQualifiedName().toString(); + final var procedurePackage = procedureTypeName.packageName(); + + final String generatedSuffix; + if ((procedurePackage + ".").startsWith(jarPackage + ".")) { + generatedSuffix = procedurePackage.substring(jarPackage.length()); + } else { + generatedSuffix = procedurePackage; + } + + final var mapperName = ClassName.get( + jarPackage + ".generated" + generatedSuffix, + procedureTypeName.simpleName() + "Mapper"); + + return new MapperRecord(mapperName); + } +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/NoneDefinedMethodMaker.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/NoneDefinedMethodMaker.java new file mode 100644 index 0000000000..8edc0affdc --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/NoneDefinedMethodMaker.java @@ -0,0 +1,74 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.UnconstructableArgumentException; + +import javax.lang.model.element.Modifier; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** Method maker for defaults style where no default arguments are provided (for example, a record class). */ +/*package-private*/ final class NoneDefinedMethodMaker extends MapperMethodMaker { + + public NoneDefinedMethodMaker(final InputTypeRecord inputType) { + super(inputType); + } + + @Override + public MethodSpec makeInstantiateMethod() { + var methodBuilder = MethodSpec.methodBuilder("instantiate") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(inputType.declaration().asType())) + .addException(InstantiationException.class) + .addParameter( + ParameterizedTypeName.get( + java.util.Map.class, + String.class, + gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.class), + "arguments", + Modifier.FINAL); + + methodBuilder = methodBuilder.addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "$T $L = $T$L", + new TypePattern.ClassPattern( + ClassName.get(Optional.class), + List.of(TypePattern.from(parameter.type))).render(), + parameter.name, + Optional.class, + ".empty()" + ) + ) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()).addCode("\n"); + + methodBuilder = makeArgumentAssignments(methodBuilder, (builder, parameter) -> builder + .addStatement( + "$L = Optional.ofNullable(this.mapper_$L.deserializeValue($L.getValue())$W.getSuccessOrThrow(failure -> new $T(\"$L\", failure)))", + parameter.name, + parameter.name, + "entry", + UnconstructableArgumentException.class, + parameter.name)); + + // Add return statement with instantiation of class with parameters + methodBuilder = methodBuilder.addStatement( + "return new $T($L)", + inputType.declaration(), + inputType.parameters().stream().map( + parameter -> parameter.name + ".get()").collect(Collectors.joining(", "))); + + return methodBuilder.build(); + } +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ParameterRecord.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ParameterRecord.java new file mode 100644 index 0000000000..6eba4d67c1 --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ParameterRecord.java @@ -0,0 +1,16 @@ +package gov.nasa.ammos.aerie.procedural.processor; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; +import java.util.Objects; + +public final class ParameterRecord { + public final String name; + public final TypeMirror type; + public final Element element; + + public ParameterRecord(final String name, final TypeMirror type, final Element element) { + this.name = Objects.requireNonNull(name); + this.type = Objects.requireNonNull(type); + this.element = Objects.requireNonNull(element); + } +} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureProcessor.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureProcessor.java index c94aa27c0c..fac38b6599 100644 --- a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureProcessor.java +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureProcessor.java @@ -1,13 +1,19 @@ package gov.nasa.ammos.aerie.procedural.processor; +import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.Template; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults; import gov.nasa.jpl.aerie.merlin.framework.ValueMapper; -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; + +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.ammos.aerie.procedural.scheduling.ProcedureMapper; import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; @@ -39,8 +45,10 @@ import java.lang.annotation.Repeatable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -111,18 +119,22 @@ public boolean process(final Set annotations, final Round for (final var factory : mapperClassElements) { typeRules.addAll(parseValueMappers(factory)); } + //we now have all value mappers final var schedulingProcedures = roundEnv.getElementsAnnotatedWith(SchedulingProcedure.class); final var constraintProcedures = roundEnv.getElementsAnnotatedWith(ConstraintProcedure.class); final var generatedClassName = ClassName.get(packageElement.getQualifiedName() + ".generated", "AutoValueMappers"); + final var procedureToTypeRecord = new HashMap(); for (final var procedure : schedulingProcedures) { final var procedureElement = (TypeElement) procedure; + procedureToTypeRecord.put(procedure, parseProcedureType(packageElement, procedureElement)); typeRules.add(AutoValueMappers.recordTypeRule(procedureElement, generatedClassName)); } for (final var procedure : constraintProcedures) { final var procedureElement = (TypeElement) procedure; + procedureToTypeRecord.put(procedure, parseProcedureType(packageElement, procedureElement)); typeRules.add(AutoValueMappers.recordTypeRule(procedureElement, generatedClassName)); } @@ -140,10 +152,12 @@ public boolean process(final Set annotations, final Round .applyRules(new TypePattern.ClassPattern(ClassName.get(ValueMapper.class), List.of(new TypePattern.ClassPattern((ClassName) procedureType, List.of())))); if (valueMapperCode.isEmpty()) throw new Error("Could not generate a valuemapper for procedure " + procedure.getSimpleName()); + final var typeSpec = generateInputType(packageElement, procedureToTypeRecord.get(procedure).inputType(), "InputMapper", typeRules); generatedFiles.add(JavaFile - .builder(generatedClassName.packageName() + ".procedures", TypeSpec - .classBuilder(procedure.getSimpleName().toString()) + .builder(procedureToTypeRecord.get(procedure).inputType().mapper().name.packageName(), TypeSpec + .classBuilder(procedureToTypeRecord.get(procedure).inputType().mapper().name) + .addType(typeSpec.get()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addSuperinterface(ParameterizedTypeName.get(ClassName.get(ProcedureMapper.class), procedureType)) .addMethod(MethodSpec @@ -154,20 +168,13 @@ public boolean process(final Set annotations, final Round .addStatement("return $L.getValueSchema()", valueMapperCode.get()) .build()) .addMethod(MethodSpec - .methodBuilder("serialize") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addParameter(procedureType, "procedure") - .returns(SerializedValue.class) - .addStatement("return $L.serializeValue(procedure)", valueMapperCode.get()) - .build()) - .addMethod(MethodSpec - .methodBuilder("deserialize") + .methodBuilder("getInputType") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) - .addParameter(SerializedValue.class, "value") - .returns(procedureType) - .addStatement("return $L.deserializeValue(value).getSuccessOrThrow(e -> new $T(e))", valueMapperCode.get(), RuntimeException.class) + .returns(ParameterizedTypeName.get( + ClassName.get(InputType.class), + ClassName.get(procedureToTypeRecord.get(procedure).inputType().declaration()))) + .addStatement("return new $T()", procedureToTypeRecord.get(procedure).inputType().mapper().name.nestedClass(typeSpec.get().name)) .build()) .build()) .skipJavaLangImports(true) @@ -186,12 +193,15 @@ public boolean process(final Set annotations, final Round .applyRules(new TypePattern.ClassPattern(ClassName.get(ValueMapper.class), List.of(new TypePattern.ClassPattern((ClassName) procedureType, List.of())))); if (valueMapperCode.isEmpty()) throw new Error("Could not generate a valuemapper for procedure " + procedure.getSimpleName()); + final var typeSpec = generateInputType(packageElement, procedureToTypeRecord.get(procedure).inputType(), "InputMapper", typeRules); + generatedFiles.add(JavaFile - .builder(generatedClassName.packageName() + ".procedures", TypeSpec - .classBuilder(procedure.getSimpleName().toString()) + .builder(procedureToTypeRecord.get(procedure).inputType().mapper().name.packageName(), TypeSpec + .classBuilder(procedureToTypeRecord.get(procedure).inputType().mapper().name) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addSuperinterface(ParameterizedTypeName.get(ClassName.get(gov.nasa.ammos.aerie.procedural.constraints.ProcedureMapper.class), procedureType)) + .addType(typeSpec.get()) .addMethod(MethodSpec .methodBuilder("valueSchema") .addModifiers(Modifier.PUBLIC) @@ -200,20 +210,13 @@ public boolean process(final Set annotations, final Round .addStatement("return $L.getValueSchema()", valueMapperCode.get()) .build()) .addMethod(MethodSpec - .methodBuilder("serialize") + .methodBuilder("getInputType") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) - .addParameter(procedureType, "procedure") - .returns(SerializedValue.class) - .addStatement("return $L.serializeValue(procedure)", valueMapperCode.get()) - .build()) - .addMethod(MethodSpec - .methodBuilder("deserialize") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addParameter(SerializedValue.class, "value") - .returns(procedureType) - .addStatement("return $L.deserializeValue(value).getSuccessOrThrow(e -> new $T(e))", valueMapperCode.get(), RuntimeException.class) + .returns(ParameterizedTypeName.get( + ClassName.get(InputType.class), + ClassName.get(procedureToTypeRecord.get(procedure).inputType().declaration()))) + .addStatement("return new $T()", procedureToTypeRecord.get(procedure).inputType().mapper().name.nestedClass(typeSpec.get().name)) .build()) .build()) .skipJavaLangImports(true) @@ -235,6 +238,115 @@ public boolean process(final Set annotations, final Round return false; } + private ProcedureTypeRecord parseProcedureType(final PackageElement jarElement, final TypeElement procedureElement) + { + final var fullyQualifiedClassName = procedureElement.getQualifiedName(); + final var name = procedureElement.getSimpleName().toString(); + final MapperRecord mapper = MapperRecord.generatedFor(ClassName.get(procedureElement), jarElement); + final List parameters = this.getExportParameters(procedureElement); + + /* + The following parameter was created as a result of AERIE-1295/1296/1297 on JIRA + In order to allow for optional/required parameters, the processor + must extract the factory method call that creates the default + template values for some activity. Additionally, a helper method + is used to determine whether some activity is written as a + class (old-style) or as a record (new-style) by determining + whether there are @Parameter tags (old-style) or not + */ + final var defaultsStyle = this.getExportDefaultsStyle(procedureElement); + + return new ProcedureTypeRecord( + fullyQualifiedClassName.toString(), + name, + new InputTypeRecord(name, procedureElement, parameters, mapper, defaultsStyle)); + } + + /** Parse a list of parameters from an export type element, depending on the export defaults style in use. */ + private List getExportParameters(final TypeElement exportTypeElement) + { + return exportTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD) // Element must be a field + .map(e -> new ParameterRecord(e.getSimpleName().toString(), e.asType(), e)) + .toList(); + } + + private ExportDefaultsStyle getExportDefaultsStyle(final TypeElement exportTypeElement) + { + for (final var element : exportTypeElement.getEnclosedElements()) { + if (element.getAnnotation(Template.class) != null) + return ExportDefaultsStyle.AllStaticallyDefined; + if (element.getAnnotation(WithDefaults.class) != null) + return ExportDefaultsStyle.SomeStaticallyDefined; + } + return ExportDefaultsStyle.NoneDefined; // No default arguments provided + } + + /** Generate an `InputType` implementation. */ + public Optional generateInputType( + PackageElement packageElement, + final InputTypeRecord inputType, + final String name, + final List typeRules) { + final var mapperBlocks$ = generateParameterMapperBlocks(typeRules, inputType); + if (mapperBlocks$.isEmpty()) return Optional.empty(); + final var mapperBlocks = mapperBlocks$.get(); + + final var mapperMethodMaker = MapperMethodMaker.make(inputType); + return Optional.of(TypeSpec + .classBuilder(name) + .addOriginatingElement(packageElement) + // The fields and methods of the activity determines the overall behavior of this class. + .addOriginatingElement(inputType.declaration()) + .addSuperinterface(ParameterizedTypeName.get( + ClassName.get(gov.nasa.jpl.aerie.merlin.protocol.model.InputType.class), + ClassName.get(inputType.declaration()))) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addFields( + inputType.parameters() + .stream() + .map(parameter -> FieldSpec + .builder( + ParameterizedTypeName.get( + ClassName.get(gov.nasa.jpl.aerie.merlin.framework.ValueMapper.class), + TypeName.get(parameter.type).box()), + "mapper_" + parameter.name) + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()) + .collect(Collectors.toList())) + .addMethod( + MethodSpec + .constructorBuilder() + .addModifiers(Modifier.PUBLIC) + /* Suppress unchecked warnings because the resolver has to + put some big casting in for Class parameters + */ + .addAnnotation( + AnnotationSpec + .builder(SuppressWarnings.class) + .addMember("value", "$S", "unchecked") + .build()) + .addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "this.mapper_$L =\n$L", + parameter.name, + mapperBlocks.get(parameter.name))) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()) + .build()) + .addMethod(mapperMethodMaker.makeGetRequiredParametersMethod()) + .addMethod(mapperMethodMaker.makeGetParametersMethod()) + .addMethod(mapperMethodMaker.makeGetArgumentsMethod()) + .addMethod(mapperMethodMaker.makeInstantiateMethod()) + .addMethod(mapperMethodMaker.makeGetValidationFailuresMethod()) + .build()); + } + + @Override public Iterable getCompletions( final Element element, @@ -245,6 +357,28 @@ public Iterable getCompletions( return Collections::emptyIterator; } + private Optional> generateParameterMapperBlocks(List typeRules, final InputTypeRecord inputType) + { + final var resolver = new Resolver(this.typeUtils, this.elementUtils, typeRules); + var failed = false; + final var mapperBlocks = new HashMap(); + + for (final var parameter : inputType.parameters()) { + final var mapperBlock = resolver.instantiateNullableMapperFor(parameter.type); + if (mapperBlock.isPresent()) { + mapperBlocks.put(parameter.name, mapperBlock.get()); + } else { + failed = true; + messager.printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate value mapper for parameter", + parameter.element); + } + } + + return failed ? Optional.empty() : Optional.of(mapperBlocks); + } + private static Optional getAnnotationAttribute(final AnnotationMirror annotationMirror, final String attributeName) { for (final var entry : annotationMirror.getElementValues().entrySet()) { diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureTypeRecord.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureTypeRecord.java new file mode 100644 index 0000000000..ccf72af2aa --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/ProcedureTypeRecord.java @@ -0,0 +1,6 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +public record ProcedureTypeRecord( + String fullyQualifiedClass, + String name, + InputTypeRecord inputType) {} diff --git a/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/SomeStaticallyDefinedMethodMaker.java b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/SomeStaticallyDefinedMethodMaker.java new file mode 100644 index 0000000000..3cd0e82b34 --- /dev/null +++ b/procedural/processor/src/main/java/gov/nasa/ammos/aerie/procedural/processor/SomeStaticallyDefinedMethodMaker.java @@ -0,0 +1,122 @@ +package gov.nasa.ammos.aerie.procedural.processor; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithDefaults; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.UnconstructableArgumentException; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** Method maker for defaults style where some arguments are provided within an @WithDefaults static class. */ +/*package-private*/ final class SomeStaticallyDefinedMethodMaker extends MapperMethodMaker { + + public SomeStaticallyDefinedMethodMaker(final InputTypeRecord inputType) { + super(inputType); + } + + @Override + public MethodSpec makeInstantiateMethod() { + var activityTypeName = inputType.declaration().getSimpleName().toString(); + + var methodBuilder = MethodSpec.methodBuilder("instantiate") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(inputType.declaration().asType())) + .addException(InstantiationException.class) + .addParameter( + ParameterizedTypeName.get( + java.util.Map.class, + String.class, + gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue.class), + "arguments", + Modifier.FINAL); + + for (final var element : inputType.declaration().getEnclosedElements()) { + if (element.getAnnotation(WithDefaults.class) == null) continue; + var defaultsName = element.getSimpleName().toString(); + methodBuilder = methodBuilder.addStatement( + "final var defaults = new $L.$L()", + activityTypeName, + defaultsName); + + methodBuilder = methodBuilder.addCode( + inputType.parameters() + .stream() + .map(parameter -> CodeBlock + .builder() + .addStatement( + "$T $L = $T$L", + new TypePattern.ClassPattern( + ClassName.get(Optional.class), + List.of(TypePattern.from(parameter.type))).render(), + parameter.name, + Optional.class, + ".empty()" + ) + ) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())) + .build()).addCode("\n"); + + methodBuilder = produceParametersFromDefaultsClass(methodBuilder); + + methodBuilder = makeArgumentAssignments(methodBuilder, (builder, parameter) -> builder + .addStatement( + "$L = Optional.ofNullable(this.mapper_$L.deserializeValue($L.getValue())$W.getSuccessOrThrow(failure -> new $T(\"$L\", failure)))", + parameter.name, + parameter.name, + "entry", + UnconstructableArgumentException.class, + parameter.name)); + } + + // Add return statement with instantiation of class with parameters + methodBuilder = methodBuilder.addStatement( + "return new $T($L)", + inputType.declaration(), + inputType.parameters().stream().map(parameter -> parameter.name + ".get()").collect(Collectors.joining(", "))); + + return methodBuilder.build(); + } + + @Override + public List getParametersWithDefaults() { + Optional defaultsClass = Optional.empty(); + for (final var element : inputType.declaration().getEnclosedElements()) { + if (element.getAnnotation(WithDefaults.class) == null) continue; + defaultsClass = Optional.of(element); + } + + final var fieldNameList = new ArrayList(); + defaultsClass.ifPresent(c -> { + for (final Element fieldElement : c.getEnclosedElements()) { + if (fieldElement.getKind() != ElementKind.FIELD) continue; + fieldNameList.add(fieldElement.getSimpleName().toString()); + } + }); + + return fieldNameList; + } + + private MethodSpec.Builder produceParametersFromDefaultsClass(final MethodSpec.Builder methodBuilder) + { + return methodBuilder.addCode(getParametersWithDefaults().stream() + .map(fieldName -> CodeBlock + .builder() + .addStatement( + "$L = Optional.ofNullable($L.$L)", + fieldName, + "defaults", + fieldName)) + .reduce(CodeBlock.builder(), (x, y) -> x.add(y.build())).build()).addCode("\n"); + } +} diff --git a/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/Template.java b/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/Template.java new file mode 100644 index 0000000000..61545d644e --- /dev/null +++ b/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/Template.java @@ -0,0 +1,12 @@ +package gov.nasa.ammos.aerie.procedural.scheduling.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// Marks the default scheduling procedure whose arguments are all defaulted +// Primarily used for All Optional Parameter types +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Template {} diff --git a/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/WithDefaults.java b/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/WithDefaults.java new file mode 100644 index 0000000000..184f93ee0f --- /dev/null +++ b/procedural/scheduling/src/main/java/gov/nasa/ammos/aerie/procedural/scheduling/annotations/WithDefaults.java @@ -0,0 +1,12 @@ +package gov.nasa.ammos.aerie.procedural.scheduling.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// Declares Defaults method for instantiation +// Primarily used for Some Optional Parameter types +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface WithDefaults {} diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ProcedureMapper.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ProcedureMapper.kt index 3340317cb3..a0e0730679 100644 --- a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ProcedureMapper.kt +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ProcedureMapper.kt @@ -1,10 +1,9 @@ package gov.nasa.ammos.aerie.procedural.scheduling -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema interface ProcedureMapper { fun valueSchema(): ValueSchema - fun serialize(procedure: T): SerializedValue - fun deserialize(arguments: SerializedValue): T + fun getInputType(): InputType } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java index 19d76ad089..da0564c735 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java @@ -5,6 +5,7 @@ import gov.nasa.ammos.aerie.procedural.scheduling.utils.DefaultEditablePlanDriver; import gov.nasa.ammos.aerie.procedural.timeline.payloads.ExternalEvent; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.ammos.aerie.procedural.scheduling.ProcedureMapper; import gov.nasa.ammos.aerie.procedural.scheduling.plan.Edit; @@ -157,7 +158,11 @@ private void instantiateGoal() { } catch (ProcedureLoader.ProcedureLoadException e) { throw new RuntimeException(e); } - this.goal = procedureMapper.deserialize(SerializedValue.of(this.args)); + try { + this.goal = procedureMapper.getInputType().instantiate(this.args); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/BulkEffectiveArgumentResponse.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/BulkEffectiveArgumentResponse.java new file mode 100644 index 0000000000..248364f010 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/BulkEffectiveArgumentResponse.java @@ -0,0 +1,17 @@ +package gov.nasa.jpl.aerie.scheduler.server.http; + +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.scheduler.ProcedureLoader; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; +import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; + +import java.util.Map; + +public sealed interface BulkEffectiveArgumentResponse { + record Success(GoalId goalId, Map effectiveArguments) implements BulkEffectiveArgumentResponse { } + record NoGoalFailure(GoalId goalId, NoSuchSchedulingGoalException ex) implements BulkEffectiveArgumentResponse { } + record InstantiationFailure(GoalId goalId, InstantiationException ex) implements BulkEffectiveArgumentResponse { } + record TypeFailure(GoalId goalId) implements BulkEffectiveArgumentResponse { } + record ProcedureLoadFailure(GoalId goalId, ProcedureLoader.ProcedureLoadException ex) implements BulkEffectiveArgumentResponse { } +} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ProcedureArguments.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ProcedureArguments.java new file mode 100644 index 0000000000..1aace710f8 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ProcedureArguments.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.scheduler.server.http; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; + +import java.util.Map; + +public record ProcedureArguments(GoalId goalId, Map arguments) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java index af99dbd96c..2655060489 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java @@ -8,8 +8,12 @@ import java.util.stream.Collectors; import gov.nasa.jpl.aerie.json.JsonParseResult; import gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import gov.nasa.jpl.aerie.scheduler.ProcedureLoader; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; @@ -18,6 +22,8 @@ import gov.nasa.jpl.aerie.scheduler.server.services.UnexpectedSubtypeError; import org.apache.commons.lang3.tuple.Pair; +import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; + /** * json serialization methods for data entities used in the scheduler response bodies */ @@ -81,6 +87,101 @@ public static JsonValue serializeScheduleResultsResponse(final ScheduleAction.Re } } + public static JsonValue serializeArgument(final SerializedValue parameter) { + if (parameter == null) return JsonValue.NULL; + return serializedValueP.unparse(parameter); + } + + public static JsonValue serializeBulkEffectiveArgumentResponseList(final List responses) { + return serializeIterable(ResponseSerializers::serializeBulkEffectiveArgumentResponse, responses); + } + + public static JsonValue serializeBulkEffectiveArgumentResponse(BulkEffectiveArgumentResponse response) { + // TODO use pattern matching in switch statement with JDK 21 + if (response instanceof BulkEffectiveArgumentResponse.Success( + GoalId goalId, Map effectiveArguments)) { + return Json.createObjectBuilder() + .add("id", goalId.id()) + .add("revision", goalId.revision()) + .add("success", JsonValue.TRUE) + .add("arguments", + serializeMap( + ResponseSerializers::serializeArgument, + effectiveArguments)) + .build(); + } else if (response instanceof BulkEffectiveArgumentResponse.TypeFailure(GoalId goalId)) { + return Json.createObjectBuilder() + .add("id", goalId.id()) + .add("revision", goalId.revision()) + .add("success", JsonValue.FALSE) + .add("errors", "Goal is not procedural") + .build(); + } else if (response instanceof BulkEffectiveArgumentResponse.InstantiationFailure( + GoalId goalId, + InstantiationException ex)) { + return Json.createObjectBuilder(serializeInstantiationException(ex).asJsonObject()) + .add("id", goalId.id()) + .add("revision", goalId.revision()) + .build(); + } + else if (response instanceof BulkEffectiveArgumentResponse.NoGoalFailure( + GoalId goalId, + NoSuchSchedulingGoalException ex)) { + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("id", goalId.id()) + .add("revision", goalId.revision()) + .add("errors", "There is no goal with this id") + .build(); + } + else if (response instanceof BulkEffectiveArgumentResponse.ProcedureLoadFailure( + GoalId goalId, + ProcedureLoader.ProcedureLoadException ex)) { + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("id", goalId.id()) + .add("revision", goalId.revision()) + .add("errors", "Error when loading the procedure jar") + .build(); + } + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("errors", String.format("Internal error: %s", response)) + .build(); + } + + public static JsonValue serializeInstantiationException(final gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException ex) { + return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("errors", Json.createObjectBuilder() + .add("extraneousArguments", serializeStringList(ex.extraneousArguments.stream().map(a -> a.parameterName()).toList())) + .add("unconstructableArguments", serializeIterable(ResponseSerializers::serializeUnconstructableArgument, ex.unconstructableArguments)) + .add("missingArguments", serializeStringList(ex.missingArguments.stream().map(a -> a.parameterName()).toList())) + .build()) + .add("arguments", serializeMap(ResponseSerializers::serializeArgument, ex.validArguments.stream().collect(Collectors.toMap( + gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException.ValidArgument::parameterName, + InstantiationException.ValidArgument::serializedValue)))) + .build(); + } + + public static JsonValue serializeStringList(final List elements) { + return serializeIterable(ResponseSerializers::serializeString, elements); + } + + public static JsonValue serializeString(final String value) { + if (value == null) return JsonValue.NULL; + return Json.createValue(value); + } + + private static JsonValue serializeUnconstructableArgument( + final InstantiationException.UnconstructableArgument argument) + { + return Json.createObjectBuilder() + .add("name", argument.parameterName()) + .add("failure", argument.failure()) + .build(); + } + /** * serialize the provided scheduling result summary to json * diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java index eba7905773..333089130b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java @@ -6,7 +6,9 @@ import java.io.StringReader; import java.util.List; import java.util.Objects; + import static gov.nasa.jpl.aerie.scheduler.server.http.ResponseSerializers.*; +import static gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers.hasuraBulkProcedureArgumentsP; import static gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers.hasuraSchedulingDSLTypescriptActionP; import static gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers.hasuraSchedulingGoalEventTriggerP; import static gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers.hasuraSpecificationActionP; @@ -69,6 +71,7 @@ public void apply(final Javalin javalin) { path("health", () -> get(ctx -> ctx.status(200))); path("schedulingDslTypescript", () -> post(this::getSchedulingDslTypescript)); path("refreshSchedulingProcedureParameterTypes", () -> post(this::refreshSchedulingProcedureParameterTypes)); + path("getSchedulingProcedureEffectiveArgumentsBulk", () -> post(this::getSchedulingProcedureEffectiveArgumentsBulk)); }); } @@ -178,6 +181,19 @@ private void refreshSchedulingProcedureParameterTypes(final Context ctx) { } } + private void getSchedulingProcedureEffectiveArgumentsBulk(final Context ctx) { + try { + final var input = parseJson(ctx.body(), hasuraBulkProcedureArgumentsP()); + + final var responses = this.specificationService.getSchedulingProcedureEffectiveArguments(input.input().items()); + ctx.result(ResponseSerializers.serializeBulkEffectiveArgumentResponseList(responses).toString()); + } catch (final InvalidJsonException ex) { + ctx.status(400).result(ResponseSerializers.serializeInvalidJsonException(ex).toString()); + } catch (final InvalidEntityException ex) { + ctx.status(400).result(ResponseSerializers.serializeInvalidEntityException(ex).toString()); + } + } + /** * parses the provided json string into the object type understood by the given parser * diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java index cad88bc85f..ea007836e9 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.http; import gov.nasa.jpl.aerie.json.JsonParser; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.HasuraAction; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; @@ -15,12 +16,15 @@ import java.util.Optional; import static gov.nasa.jpl.aerie.json.BasicParsers.anyP; +import static gov.nasa.jpl.aerie.json.BasicParsers.listP; import static gov.nasa.jpl.aerie.json.BasicParsers.longP; +import static gov.nasa.jpl.aerie.json.BasicParsers.mapP; import static gov.nasa.jpl.aerie.json.BasicParsers.nullableP; import static gov.nasa.jpl.aerie.json.BasicParsers.productP; import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; import static gov.nasa.jpl.aerie.json.Uncurry.tuple; import static gov.nasa.jpl.aerie.json.Uncurry.untuple; +import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; import static gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresParsers.pgTimestampP; /** @@ -48,6 +52,25 @@ private SchedulerParsers() {} PlanId::new, PlanId::id); + public static JsonParser> hasuraBulkProcedureArgumentsP() { + return hasuraActionF( + productP + .field("arguments", listP(procedureArgumentsP())) + .map( + untuple((items) -> new HasuraAction.HasuraBulkEffectiveArguments(items)), + procedureArguments -> tuple(procedureArguments.items()))); + } + + public static JsonParser procedureArgumentsP() { + return productP + .field("id", longP) + .field("revision", longP) + .field("arguments", mapP(serializedValueP)) + .map( + untuple((id, revision, arguments) -> new ProcedureArguments(new GoalId(id, revision), arguments)), + procedureArguments -> tuple(procedureArguments.goalId().id(), procedureArguments.goalId().revision(), procedureArguments.arguments())); + } + public static final JsonParser scheduleFailureP = productP .field("type", stringP) .field("message", stringP) diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java index 49bbb33d1a..328bc9cd83 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java @@ -1,16 +1,19 @@ package gov.nasa.jpl.aerie.scheduler.server.models; +import gov.nasa.jpl.aerie.scheduler.server.http.ProcedureArguments; import gov.nasa.jpl.aerie.types.MissionModelId; +import java.util.List; import java.util.Optional; public record HasuraAction(String name, I input, Session session) { public record Session(String hasuraRole, String hasuraUserId) { } - public sealed interface Input permits SpecificationInput, MissionModelIdInput { } + public sealed interface Input permits SpecificationInput, MissionModelIdInput, HasuraBulkEffectiveArguments{ } public record SpecificationInput(SpecificationId specificationId) implements Input { } public record MissionModelIdInput(MissionModelId missionModelId, Optional planId) implements Input { } public record HasuraSchedulingGoalEvent(long goalId, long revision) { } + public record HasuraBulkEffectiveArguments(List items) implements Input { } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java index d0a6dc27a4..b5396f0ded 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java @@ -1,11 +1,14 @@ package gov.nasa.jpl.aerie.scheduler.server.services; import gov.nasa.ammos.aerie.procedural.scheduling.ProcedureMapper; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.scheduler.ProcedureLoader; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; import gov.nasa.jpl.aerie.scheduler.model.GoalId; +import gov.nasa.jpl.aerie.scheduler.server.http.BulkEffectiveArgumentResponse; +import gov.nasa.jpl.aerie.scheduler.server.http.ProcedureArguments; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; @@ -13,6 +16,8 @@ import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; public record SpecificationService(SpecificationRepository specificationRepository) { // Queries @@ -28,6 +33,36 @@ public SpecificationRevisionData getSpecificationRevisionData(final Specificatio return specificationRepository.getSpecificationRevisionData(specificationId); } + public List getSchedulingProcedureEffectiveArguments( + List procedureArgumentsList) + { + final var responses = new ArrayList(); + for (final var procedureArguments : procedureArgumentsList) { + final GoalType goal; + try { + goal = specificationRepository.getGoal(procedureArguments.goalId()); + switch (goal) { + case GoalType.EDSL edsl -> responses.add(new BulkEffectiveArgumentResponse.TypeFailure( + procedureArguments.goalId())); + case GoalType.JAR jar -> responses.add(new BulkEffectiveArgumentResponse.Success( + procedureArguments.goalId(), + ProcedureLoader + .loadProcedure(Path.of("/usr/src/app/merlin_file_store", jar.path().toString())) + .getInputType() + .getEffectiveArguments(procedureArguments.arguments()))); + } + } catch (NoSuchSchedulingGoalException e) { + responses.add(new BulkEffectiveArgumentResponse.NoGoalFailure(procedureArguments.goalId(), e)); + } + catch (InstantiationException e) { + responses.add(new BulkEffectiveArgumentResponse.InstantiationFailure(procedureArguments.goalId(), e)); + } catch (ProcedureLoader.ProcedureLoadException e) { + responses.add(new BulkEffectiveArgumentResponse.ProcedureLoadFailure(procedureArguments.goalId(), e)); + } + } + return responses; + } + public void refreshSchedulingProcedureParameterTypes(long goalId, long revision) { final GoalType goal; try {