From 33291f444dba0500fc600d5779505a344bf4d982 Mon Sep 17 00:00:00 2001 From: Alexander M Greer Date: Mon, 22 Jan 2024 13:38:41 -0800 Subject: [PATCH] new auto-gen activity value mappers to use activity types as parameters. --- .../activities/RussianBanana.java | 43 +++++ .../jpl/aerie/banananation/package-info.java | 3 +- .../merlin/processor/AutoValueMappers.java | 19 +++ .../merlin/processor/MissionModelParser.java | 39 ++++- .../processor/MissionModelProcessor.java | 16 +- .../generator/MissionModelGenerator.java | 159 +++++++++++------- .../metamodel/ActivityMapperRecord.java | 39 +++++ .../metamodel/ActivityValueMapperRecord.java | 39 +++++ .../processor/metamodel/InputTypeRecord.java | 3 +- .../metamodel/MissionModelRecord.java | 4 + .../merlin/framework/ActivityValueMapper.java | 40 +++++ 11 files changed, 331 insertions(+), 73 deletions(-) create mode 100644 examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/RussianBanana.java create mode 100644 merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityMapperRecord.java create mode 100644 merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityValueMapperRecord.java create mode 100644 merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ActivityValueMapper.java diff --git a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/RussianBanana.java b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/RussianBanana.java new file mode 100644 index 0000000000..0331d2f998 --- /dev/null +++ b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/RussianBanana.java @@ -0,0 +1,43 @@ +package gov.nasa.jpl.aerie.banananation.activities; + +import gov.nasa.jpl.aerie.banananation.Mission; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.List; + +import static gov.nasa.jpl.aerie.banananation.generated.ActivityActions.call; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; + +/** + * Russian Banana Encloses Banana + * + * This activity causes a piece of banana to be bitten off and consumed. + * + * @subsystem fruit + * @contact John Doe + */ +@ActivityType("RussianBanana") +public final class RussianBanana { + + @Export.Parameter + public List testints; + + @Export.Parameter + public List biteBananaActivity; + + @Export.Parameter + public PeelBananaActivity peelBananaActivity; + + + @ActivityType.EffectModel + public void run(final Mission mission) { + for (final var bite : biteBananaActivity) { + call(mission, bite); + delay(Duration.of(30, Duration.MINUTE)); + } + call(mission, peelBananaActivity); + } + +} diff --git a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/package-info.java b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/package-info.java index 1f27ab1fb0..d14ea407de 100644 --- a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/package-info.java +++ b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/package-info.java @@ -26,7 +26,7 @@ @WithActivityType(ControllableDurationActivity.class) @WithActivityType(RipenBananaActivity.class) @WithActivityType(ExceptionActivity.class) - +@WithActivityType(RussianBanana.class) package gov.nasa.jpl.aerie.banananation; import gov.nasa.jpl.aerie.banananation.activities.BakeBananaBreadActivity; @@ -46,6 +46,7 @@ import gov.nasa.jpl.aerie.banananation.activities.PeelBananaActivity; import gov.nasa.jpl.aerie.banananation.activities.PickBananaActivity; import gov.nasa.jpl.aerie.banananation.activities.RipenBananaActivity; +import gov.nasa.jpl.aerie.banananation.activities.RussianBanana; import gov.nasa.jpl.aerie.banananation.activities.ThrowBananaActivity; import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel; diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/AutoValueMappers.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/AutoValueMappers.java index 10be45490b..7bf18b52ae 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/AutoValueMappers.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/AutoValueMappers.java @@ -13,6 +13,7 @@ import gov.nasa.jpl.aerie.contrib.serialization.mappers.RecordValueMapper; import gov.nasa.jpl.aerie.merlin.framework.Result; import gov.nasa.jpl.aerie.merlin.framework.ValueMapper; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; import gov.nasa.jpl.aerie.merlin.framework.annotations.AutoValueMapper; import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.TypeRule; @@ -61,6 +62,24 @@ static TypeRule recordTypeRule(final Element autoValueMapperElement, final Class ClassName.get((TypeElement) autoValueMapperElement).canonicalName().replace(".", "_")); } + static TypeRule activityTypeRule(final Element activityTypeElement, final ClassName generatedClassName) throws InvalidMissionModelException { + if (!(activityTypeElement.getKind().equals(ElementKind.CLASS) || activityTypeElement.getKind().equals(ElementKind.RECORD))) { //todo: check if activities are constrained to class and record + throw new InvalidMissionModelException( + "@%s is only allowed on classes and records".formatted( + ActivityType.class.getSimpleName()), + activityTypeElement); + } + + return new TypeRule( + new TypePattern.ClassPattern( + ClassName.get(ValueMapper.class), + List.of(TypePattern.from(activityTypeElement.asType()))), + Set.of(), + List.of(), + generatedClassName, + ClassName.get((TypeElement) activityTypeElement).canonicalName().replace("activities", "generated_activitiesValueMappers").replace(".", "_") + "ValueMapper"); + } + static TypeRule annotationTypeRule(final Element autoValueMapperElement, final ClassName generatedClassName) throws InvalidMissionModelException { if (!autoValueMapperElement.getKind().equals(ElementKind.ANNOTATION_TYPE)) { throw new InvalidMissionModelException( diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java index 06742b0d1b..a8333b31ba 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java @@ -9,10 +9,11 @@ import gov.nasa.jpl.aerie.merlin.framework.annotations.Export; import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel; import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityTypeRecord; +import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityValueMapperRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.InputTypeRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.EffectModelRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.ExportDefaultsStyle; -import gov.nasa.jpl.aerie.merlin.processor.metamodel.MapperRecord; +import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityMapperRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.ParameterRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.ParameterValidationRecord; @@ -183,9 +184,10 @@ private Optional getMissionModelConfigurationType(final Package final var name = declaration.getSimpleName().toString(); final var parameters = getExportParameters(declaration); final var validations = this.getExportValidations(declaration, parameters); - final var mapper = getExportMapper(missionModelElement, declaration); + final var activityMapper = getExportActivityMapper(missionModelElement, declaration); + final var valueMapper = getExportValueMapper(missionModelElement, declaration); final var defaultsStyle = getExportDefaultsStyle(declaration); - return Optional.of(new InputTypeRecord(name, declaration, parameters, validations, mapper, defaultsStyle)); + return Optional.of(new InputTypeRecord(name, declaration, parameters, validations, activityMapper, valueMapper, defaultsStyle)); } private List getMissionModelMapperClasses(final PackageElement missionModelElement) @@ -381,7 +383,8 @@ private ActivityTypeRecord parseActivityType(final PackageElement missionModelEl { final var fullyQualifiedClassName = activityTypeElement.getQualifiedName(); final var name = this.getActivityTypeName(activityTypeElement); - final var mapper = this.getExportMapper(missionModelElement, activityTypeElement); + final var activityMapper = getExportActivityMapper(missionModelElement, activityTypeElement); + final var valueMapper = getExportValueMapper(missionModelElement, activityTypeElement); final var parameters = this.getExportParameters(activityTypeElement); final var validations = this.getExportValidations(activityTypeElement, parameters); final var effectModel = this.getActivityEffectModel(activityTypeElement); @@ -405,7 +408,7 @@ class (old-style) or as a record (new-style) by determining return new ActivityTypeRecord( fullyQualifiedClassName.toString(), name, - new InputTypeRecord(name, activityTypeElement, parameters, validations, mapper, defaultsStyle), + new InputTypeRecord(name, activityTypeElement, parameters, validations, activityMapper, valueMapper, defaultsStyle), effectModel); } @@ -471,12 +474,12 @@ private String getActivityTypeName(final TypeElement activityTypeElement) return (String) nameAttribute.getValue(); } - private MapperRecord getExportMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement) + private ActivityMapperRecord getExportActivityMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement) throws InvalidMissionModelException { final var annotationMirror = this.getAnnotationMirrorByType(exportTypeElement, ActivityType.WithMapper.class); if (annotationMirror.isEmpty()) { - return MapperRecord.generatedFor( + return ActivityMapperRecord.generatedFor( ClassName.get(exportTypeElement), missionModelElement); } @@ -488,7 +491,27 @@ private MapperRecord getExportMapper(final PackageElement missionModelElement, f annotationMirror.get())) .getValue(); - return MapperRecord.custom( + return ActivityMapperRecord.custom( + ClassName.get((TypeElement) mapperType.asElement())); + } + private ActivityValueMapperRecord getExportValueMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement) + throws InvalidMissionModelException + { + final var annotationMirror = this.getAnnotationMirrorByType(exportTypeElement, ActivityType.WithMapper.class); + if (annotationMirror.isEmpty()) { + return ActivityValueMapperRecord.generatedFor( + ClassName.get(exportTypeElement), + missionModelElement); + } + + final var mapperType = (DeclaredType) getAnnotationAttribute(annotationMirror.get(), "value") + .orElseThrow(() -> new InvalidMissionModelException( + "Unable to get value attribute of annotation", + exportTypeElement, + annotationMirror.get())) + .getValue(); + + return ActivityValueMapperRecord.custom( ClassName.get((TypeElement) mapperType.asElement())); } diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelProcessor.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelProcessor.java index 3daba903b6..72c7f349b8 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelProcessor.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelProcessor.java @@ -1,7 +1,10 @@ package gov.nasa.jpl.aerie.merlin.processor; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.TypeName; import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; import gov.nasa.jpl.aerie.merlin.framework.annotations.AutoValueMapper; +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export; import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel; import gov.nasa.jpl.aerie.merlin.processor.generator.MissionModelGenerator; import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord; @@ -26,6 +29,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -92,9 +96,10 @@ public boolean process(final Set annotations, final Round for (final var element : roundEnv.getElementsAnnotatedWith(MissionModel.class)) { final var recordAutoValueMapperRequests = roundEnv.getElementsAnnotatedWith(AutoValueMapper.Record.class); final var annotationAutoValueMapperRequests = roundEnv.getElementsAnnotatedWith(AutoValueMapper.Annotation.class); + final var packageElement = (PackageElement) element; try { - final var missionModelRecord$ = missionModelParser.parseMissionModel(packageElement); + final var missionModelRecord$ = missionModelParser.parseMissionModel(packageElement); //todo: add typerules for activity parameters final var concatenatedTypeRules = new ArrayList<>(missionModelRecord$.typeRules()); for (final var request : recordAutoValueMapperRequests) { @@ -103,6 +108,9 @@ public boolean process(final Set annotations, final Round for (final var request : annotationAutoValueMapperRequests) { concatenatedTypeRules.add(AutoValueMappers.annotationTypeRule(request, missionModelRecord$.getAutoValueMappersName())); } + for(final var request : this.foundActivityTypes) { + concatenatedTypeRules.add(AutoValueMappers.activityTypeRule(request, missionModelRecord$.getActivityValueMappers())); + } final var missionModelRecord = new MissionModelRecord( missionModelRecord$.$package(), @@ -113,6 +121,7 @@ public boolean process(final Set annotations, final Round missionModelRecord$.activityTypes() ); + final var generatedFiles = new ArrayList<>(List.of( missionModelGen.generateMerlinPlugin(missionModelRecord), missionModelGen.generateSchedulerPlugin(missionModelRecord))); @@ -134,13 +143,16 @@ public boolean process(final Set annotations, final Round annotationAutoValueMapperRequests); generatedFiles.add(autoValueMappers); + for (final var activityRecord : missionModelRecord.activityTypes()) { this.ownedActivityTypes.add(activityRecord.inputType().declaration()); - if (!activityRecord.inputType().mapper().isCustom) { + if (!activityRecord.inputType().activityMapper().isCustom) { missionModelGen.generateActivityMapper(missionModelRecord, activityRecord).ifPresent(generatedFiles::add); } } + generatedFiles.add(missionModelGen.generateActivityValueMappers(missionModelRecord)); + for (final var generatedFile : generatedFiles) { this.messager.printMessage( Diagnostic.Kind.NOTE, diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index be17603c33..217b8ed6c6 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -13,8 +13,10 @@ import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; import gov.nasa.jpl.aerie.merlin.framework.ActivityMapper; +import gov.nasa.jpl.aerie.merlin.framework.ActivityValueMapper; import gov.nasa.jpl.aerie.merlin.framework.EmptyInputType; import gov.nasa.jpl.aerie.merlin.framework.ModelActions; +import gov.nasa.jpl.aerie.merlin.framework.Result; import gov.nasa.jpl.aerie.merlin.framework.ValueMapper; import gov.nasa.jpl.aerie.merlin.processor.MissionModelProcessor; import gov.nasa.jpl.aerie.merlin.processor.Resolver; @@ -123,9 +125,9 @@ public JavaFile generateSchedulerPlugin(final MissionModelRecord missionModel) { /** Generate `ConfigurationMapper` class. */ public Optional generateMissionModelConfigurationMapper(final MissionModelRecord missionModel, final InputTypeRecord configType) { - return generateInputType(missionModel, configType, configType.mapper().name.simpleName()) + return generateInputType(missionModel, configType, configType.activityMapper().name.simpleName()) .map(typeSpec -> JavaFile - .builder(configType.mapper().name.packageName(), typeSpec) + .builder(configType.activityMapper().name.packageName(), typeSpec) .skipJavaLangImports(true) .build()); } @@ -176,12 +178,12 @@ public JavaFile generateModelType(final MissionModelRecord missionModel) { .addAnnotation(Override.class) .returns( missionModel.modelConfigurationType() - .map(configType -> configType.mapper().name) + .map(configType -> configType.activityMapper().name) .orElse(ClassName.get(EmptyInputType.class))) .addStatement( "return new $T()", missionModel.modelConfigurationType() - .map(configType -> configType.mapper().name) + .map(configType -> configType.activityMapper().name) .orElse(ClassName.get(EmptyInputType.class))) .build()) .addMethod( @@ -260,6 +262,7 @@ private static CodeBlock generateMissionModelInstantiation(final MissionModelRec public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { final var typeName = missionModel.getSchedulerModelName(); final var durationValueMapperCodeBlock = generateDurationMapperBlock(missionModel); + final var typeSpec = TypeSpec .classBuilder(typeName) @@ -293,7 +296,7 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { .map($ -> { if ($.durationParameter().isPresent()) return CodeBlock.of("controllable(\"$L\")", $.durationParameter().get()); else if ($.fixedDurationExpr().isPresent()) return CodeBlock.of("fixed($L.$L)", activityTypeRecord.fullyQualifiedClass(), $.fixedDurationExpr().get()); - else if ($.parametricDuration().isPresent()) return CodeBlock.of("parametric($$ -> (new $L().new InputMapper()).instantiate($$).$L())", activityTypeRecord.inputType().mapper().name, $.parametricDuration().get()); + else if ($.parametricDuration().isPresent()) return CodeBlock.of("parametric($$ -> (new $L().new InputMapper()).instantiate($$).$L())", activityTypeRecord.inputType().activityMapper().name, $.parametricDuration().get()); else return CodeBlock.of("uncontrollable()"); }) .orElse(CodeBlock.of("fixed($T.ZERO)", ClassName.get(Duration.class))))) @@ -307,9 +310,9 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { .addAnnotation(Override.class) .returns(SerializedValue.class) .addParameter( - TypeName.get(Duration.class), - "duration", - Modifier.FINAL) + TypeName.get(Duration.class), + "duration", + Modifier.FINAL) .addStatement( "return $L.serializeValue(duration)", durationValueMapperCodeBlock.get() ) @@ -319,15 +322,14 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter( - TypeName.get(SerializedValue.class), - "serializedDuration", - Modifier.FINAL) + TypeName.get(SerializedValue.class), + "serializedDuration", + Modifier.FINAL) .addStatement( "return $L.deserializeValue(serializedDuration).getSuccessOrThrow()", durationValueMapperCodeBlock.get() ) .returns(Duration.class) .build()) - .build(); return JavaFile @@ -374,7 +376,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { "final var $L = $T.$L", "mapper", missionModel.getTypesName(), - entry.inputType().mapper().name.canonicalName().replace(".", "_")) + entry.inputType().activityMapper().name.canonicalName().replace(".", "_")) .addStatement( "$T.spawn($L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, @@ -401,7 +403,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { "final var $L = $T.$L", "mapper", missionModel.getTypesName(), - entry.inputType().mapper().name.canonicalName().replace(".", "_")) + entry.inputType().activityMapper().name.canonicalName().replace(".", "_")) .addStatement( "$T.defer($L, $L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, @@ -457,7 +459,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { "final var $L = $T.$L", "mapper", missionModel.getTypesName(), - entry.inputType().mapper().name.canonicalName().replace(".", "_")) + entry.inputType().activityMapper().name.canonicalName().replace(".", "_")) .addStatement( "$T.call($L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, @@ -492,10 +494,10 @@ public JavaFile generateActivityTypes(final MissionModelRecord missionModel) { .stream() .map(activityType -> FieldSpec .builder( - activityType.inputType().mapper().name, - activityType.inputType().mapper().name.canonicalName().replace(".", "_"), + activityType.inputType().activityMapper().name, + activityType.inputType().activityMapper().name.canonicalName().replace(".", "_"), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .initializer("new $T()", activityType.inputType().mapper().name) + .initializer("new $T()", activityType.inputType().activityMapper().name) .build()) .toList()) .addField( @@ -522,7 +524,7 @@ public JavaFile generateActivityTypes(final MissionModelRecord missionModel) { "\n$T.entry($S, $L)", ClassName.get(Map.class), activityType.name(), - activityType.inputType().mapper().name.canonicalName().replace(".", "_"))) + activityType.inputType().activityMapper().name.canonicalName().replace(".", "_"))) .reduce((x, y) -> x.add(",").add(y.build())) .orElse(CodeBlock.builder()) .build()) @@ -578,14 +580,13 @@ public JavaFile generateActivityTypes(final MissionModelRecord missionModel) { private record ComputedAttributesCodeBlocks(TypeName typeName, FieldSpec fieldDef) {} /** Generate an `InputType` implementation. */ - public Optional generateInputType(final MissionModelRecord missionModel, - final InputTypeRecord inputType, - final String name) { + public Optional generateInputType(final MissionModelRecord missionModel, final InputTypeRecord inputType, final String name) { final var mapperBlocks$ = generateParameterMapperBlocks(missionModel, inputType); if (mapperBlocks$.isEmpty()) return Optional.empty(); final var mapperBlocks = mapperBlocks$.get(); final var mapperMethodMaker = MapperMethodMaker.make(inputType); + return Optional.of(TypeSpec .classBuilder(name) // The location of the missionModel package determines where to put this class. @@ -604,16 +605,16 @@ public Optional generateInputType(final MissionModelRecord missionMode .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())) + .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() @@ -628,15 +629,15 @@ public Optional generateInputType(final MissionModelRecord missionMode .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()) + .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()) @@ -646,6 +647,45 @@ public Optional generateInputType(final MissionModelRecord missionMode .build()); } + /** Generate `ActivityValueMappers` class. */ + public JavaFile generateActivityValueMappers(final MissionModelRecord missionModel) { + final var typeName = missionModel.getTypesName(); + + final var typeSpec = + TypeSpec + .classBuilder("ActivityValueMappers") + .addAnnotation( + AnnotationSpec + .builder(javax.annotation.processing.Generated.class) + .addMember("value", "$S", MissionModelProcessor.class.getCanonicalName()) + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethods( + missionModel.activityTypes() + .stream() + .map(activityType -> MethodSpec + .methodBuilder(activityType.inputType().valueMapper().name.canonicalName().replace(".", "_")) + .returns(ParameterizedTypeName.get( + ClassName.get(gov.nasa.jpl.aerie.merlin.framework.ValueMapper.class), + TypeName.get(activityType.inputType().declaration().asType()))) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addCode(CodeBlock.builder().add("return new $T(new $T(), $S);", + ParameterizedTypeName.get( + ClassName.get(gov.nasa.jpl.aerie.merlin.framework.ActivityValueMapper.class), + TypeName.get(activityType.inputType().declaration().asType())), + activityType.inputType().activityMapper().name, + activityType.name()).build()) + .build()) + .toList() + ) + .build(); + + return JavaFile + .builder(typeName.packageName(), typeSpec) + .skipJavaLangImports(true) + .build(); + } + private Optional getComputedAttributesCodeBlocks( final MissionModelRecord missionModel, final ActivityTypeRecord activityType) @@ -696,10 +736,9 @@ public Optional generateActivityMapper(final MissionModelRecord missio final var inputTypeMapper$ = generateInputType(missionModel, activityType.inputType(), "InputMapper"); if (inputTypeMapper$.isEmpty()) return Optional.empty(); final var inputTypeMapper = inputTypeMapper$.get(); - final var computedAttributesCodeBlocks = computedAttributesCodeBlocks$.get(); final var typeSpec = TypeSpec - .classBuilder(activityType.inputType().mapper().name) + .classBuilder(activityType.inputType().activityMapper().name) // The location of the missionModel package determines where to put this class. .addOriginatingElement(missionModel.$package()) // The fields and methods of the activity determines the overall behavior of this class. @@ -756,7 +795,7 @@ public Optional generateActivityMapper(final MissionModelRecord missio .returns(ParameterizedTypeName.get( ClassName.get(InputType.class), ClassName.get(activityType.inputType().declaration()))) - .addStatement("return new $T()", activityType.inputType().mapper().name.nestedClass(inputTypeMapper.name)) + .addStatement("return new $T()", activityType.inputType().activityMapper().name.nestedClass(inputTypeMapper.name)) .build()) .addMethod(MethodSpec .methodBuilder("getOutputType") @@ -765,7 +804,7 @@ public Optional generateActivityMapper(final MissionModelRecord missio .returns(ParameterizedTypeName.get( ClassName.get(OutputType.class), computedAttributesCodeBlocks.typeName().box())) - .addStatement("return new $T()", activityType.inputType().mapper().name.nestedClass("OutputMapper")) + .addStatement("return new $T()", activityType.inputType().activityMapper().name.nestedClass("OutputMapper")) .build()) .addMethod( MethodSpec @@ -844,7 +883,7 @@ public Optional generateActivityMapper(final MissionModelRecord missio .build(); return Optional.of(JavaFile - .builder(activityType.inputType().mapper().name.packageName(), typeSpec) + .builder(activityType.inputType().activityMapper().name.packageName(), typeSpec) .skipJavaLangImports(true) .build()); } @@ -874,7 +913,19 @@ private static MethodSpec makeSerializeReturnValueMethod(final ActivityTypeRecor "return this." + COMPUTED_ATTRIBUTES_VALUE_MAPPER_FIELD_NAME + ".serializeValue(returnValue)") .build(); } - + private Optional generateDurationMapperBlock(final MissionModelRecord missionModel){ + final var resolver = new Resolver(this.typeUtils, this.elementUtils, missionModel.typeRules()); + final var mapperBlock = resolver.instantiateNullableMapperFor( + elementUtils.getTypeElement(Duration.class.getName()).asType()); + if (mapperBlock.isPresent()) { + return mapperBlock; + } else { + messager.printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate value mapper for Duration"); + return Optional.empty(); + } + } private Optional> generateParameterMapperBlocks(final MissionModelRecord missionModel, final InputTypeRecord inputType) { final var resolver = new Resolver(this.typeUtils, this.elementUtils, missionModel.typeRules()); @@ -896,18 +947,4 @@ private Optional> generateParameterMapperBlocks(final Mis return failed ? Optional.empty() : Optional.of(mapperBlocks); } - - private Optional generateDurationMapperBlock(final MissionModelRecord missionModel){ - final var resolver = new Resolver(this.typeUtils, this.elementUtils, missionModel.typeRules()); - final var mapperBlock = resolver.instantiateNullableMapperFor( - elementUtils.getTypeElement(Duration.class.getName()).asType()); - if (mapperBlock.isPresent()) { - return mapperBlock; - } else { - messager.printMessage( - Diagnostic.Kind.ERROR, - "Failed to generate value mapper for Duration"); - return Optional.empty(); - } - } } diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityMapperRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityMapperRecord.java new file mode 100644 index 0000000000..c2fbcea446 --- /dev/null +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityMapperRecord.java @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.merlin.processor.metamodel; + +import com.squareup.javapoet.ClassName; + +import javax.lang.model.element.PackageElement; +import java.util.Objects; + +public final class ActivityMapperRecord { + public final ClassName name; + public final boolean isCustom; + + public ActivityMapperRecord(final ClassName name, final boolean isCustom) { + this.name = Objects.requireNonNull(name); + this.isCustom = isCustom; + } + + public static ActivityMapperRecord custom(final ClassName name) { + return new ActivityMapperRecord(name, true); + } + + public static ActivityMapperRecord + generatedFor(final ClassName activityTypeName, final PackageElement missionModelElement) { + final var missionModelPackage = missionModelElement.getQualifiedName().toString(); + final var activityPackage = activityTypeName.packageName(); + + final String generatedSuffix; + if ((activityPackage + ".").startsWith(missionModelPackage + ".")) { + generatedSuffix = activityPackage.substring(missionModelPackage.length()); + } else { + generatedSuffix = activityPackage; + } + + final var mapperName = ClassName.get( + missionModelPackage + ".generated" + generatedSuffix, + activityTypeName.simpleName() + "Mapper"); + + return new ActivityMapperRecord(mapperName, false); + } +} diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityValueMapperRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityValueMapperRecord.java new file mode 100644 index 0000000000..4459d2073e --- /dev/null +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/ActivityValueMapperRecord.java @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.merlin.processor.metamodel; + +import com.squareup.javapoet.ClassName; + +import javax.lang.model.element.PackageElement; +import java.util.Objects; + +public final class ActivityValueMapperRecord { + public final ClassName name; + public final boolean isCustom; + + public ActivityValueMapperRecord(final ClassName name, final boolean isCustom) { + this.name = Objects.requireNonNull(name); + this.isCustom = isCustom; + } + + public static ActivityValueMapperRecord custom(final ClassName name) { + return new ActivityValueMapperRecord(name, true); + } + + public static ActivityValueMapperRecord + generatedFor(final ClassName activityTypeName, final PackageElement missionModelElement) { + final var missionModelPackage = missionModelElement.getQualifiedName().toString(); + final var activityPackage = activityTypeName.packageName() + "ValueMappers"; + + final String generatedSuffix; + if ((activityPackage + ".").startsWith(missionModelPackage + ".")) { + generatedSuffix = activityPackage.substring(missionModelPackage.length()); + } else { + generatedSuffix = activityPackage; + } + + final var mapperName = ClassName.get( + missionModelPackage + ".generated" + generatedSuffix, + activityTypeName.simpleName() + "ValueMapper"); + + return new ActivityValueMapperRecord(mapperName, false); + } +} diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/InputTypeRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/InputTypeRecord.java index c5e58b1413..f4627d7816 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/InputTypeRecord.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/InputTypeRecord.java @@ -8,6 +8,7 @@ public record InputTypeRecord( TypeElement declaration, List parameters, List validations, - MapperRecord mapper, + ActivityMapperRecord activityMapper, + ActivityValueMapperRecord valueMapper, ExportDefaultsStyle defaultsStyle ) {} diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java index 17ffdd4e6b..6c49426738 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java @@ -35,6 +35,10 @@ public ClassName getActivityActionsName() { return ClassName.get(this.$package.getQualifiedName() + ".generated", "ActivityActions"); } + public ClassName getActivityValueMappers() { + return ClassName.get(this.$package.getQualifiedName() + ".generated", "ActivityValueMappers"); + } + public ClassName getTypesName() { return ClassName.get(this.$package.getQualifiedName() + ".generated", "ActivityTypes"); } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ActivityValueMapper.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ActivityValueMapper.java new file mode 100644 index 0000000000..f8ecf54dee --- /dev/null +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ActivityValueMapper.java @@ -0,0 +1,40 @@ +package gov.nasa.jpl.aerie.merlin.framework; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; + +import java.util.Map; + +public record ActivityValueMapper(ActivityMapper activityMapper, String ActivityName) implements ValueMapper { + @Override + public ValueSchema getValueSchema() { + return ValueSchema.withMeta("activity", + SerializedValue.of(Map.of("value", SerializedValue.of(ActivityName))), + activityMapper.getInputAsOutput().getSchema()); + } + + public Result deserialize( + final Map arguments) { + try { + return Result.success(activityMapper.getInputType().instantiate(arguments)); + } catch (Exception e) { + return Result.failure("Failed to instantiate activity DecomposingSpawnChild. Error: %s".formatted(e.getMessage())); + } + } + + @Override + public Result deserializeValue( + final SerializedValue serializedValue) { + return serializedValue + .asMap() + .map(Map::copyOf) + .map(arguments -> deserialize(arguments)) + .orElseGet(() -> Result.failure("Expected map from string to serialized value, but got: " + serializedValue)); + } + + @Override + public SerializedValue serializeValue( + final T value) { + return activityMapper.getInputAsOutput().serialize(value); + } +}