[AB-307] enabling dependent features and including them into the setup wizard

this comes with the usual limitation, that dependent features must be referenced via importing
This commit is contained in:
Sheldan
2021-11-28 13:19:17 +01:00
parent c10296251c
commit f2aa7035aa
5 changed files with 103 additions and 35 deletions

View File

@@ -13,18 +13,22 @@ import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;
import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.models.database.AServer; import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.template.commands.EnableModel; import dev.sheldan.abstracto.core.models.template.commands.FeatureSwitchModel;
import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.FeatureConfigService; import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService; import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService; import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component @Component
public class DisableFeature extends AbstractConditionableCommand { public class DisableFeature extends AbstractConditionableCommand {
@@ -43,26 +47,46 @@ public class DisableFeature extends AbstractConditionableCommand {
@Autowired @Autowired
private ServerManagementService serverManagementService; private ServerManagementService serverManagementService;
private static final String DISABLE_FEATURE_ALL_FEATURES_RESPONSE_TEMPLATE_KEY = "disableFeature_all_features_response";
private static final String DISABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY = "disableFeature_feature_dependencies_response";
@Override @Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) { public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
if(commandContext.getParameters().getParameters().isEmpty()) { if (commandContext.getParameters().getParameters().isEmpty()) {
EnableModel model = (EnableModel) ContextConverter.fromCommandContext(commandContext, EnableModel.class); FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class);
model.setFeatures(featureConfigService.getAllFeatures()); model.setFeatures(featureConfigService.getAllFeatures());
String response = templateService.renderTemplate("disable_features_response", model, commandContext.getGuild().getIdLong()); MessageToSend messageToSend = templateService.renderEmbedTemplate(DISABLE_FEATURE_ALL_FEATURES_RESPONSE_TEMPLATE_KEY, model, commandContext.getGuild().getIdLong());
return channelService.sendTextToChannel(response, commandContext.getChannel()) return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(aVoid -> CommandResult.fromSuccess()); .thenApply(message -> CommandResult.fromIgnored());
} else { } else {
String flagKey = (String) commandContext.getParameters().getParameters().get(0); String flagKey = (String) commandContext.getParameters().getParameters().get(0);
FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(flagKey); FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(flagKey);
featureFlagService.disableFeature(feature, commandContext.getGuild().getIdLong()); featureFlagService.disableFeature(feature, commandContext.getGuild().getIdLong());
if(feature.getDependantFeatures() != null) { List<FeatureConfig> featureDependencies = new ArrayList<>();
if (feature.getDependantFeatures() != null) {
AServer server = serverManagementService.loadServer(commandContext.getGuild()); AServer server = serverManagementService.loadServer(commandContext.getGuild());
feature.getDependantFeatures().forEach(featureDisplay -> feature.getDependantFeatures().forEach(featureDisplay -> {
featureFlagService.disableFeature(featureDisplay, server) if (featureFlagService.isFeatureEnabled(featureDisplay, server)) {
featureFlagService.disableFeature(featureDisplay, server);
featureDependencies.add(featureDisplay);
}
}
); );
} }
if (featureDependencies.isEmpty()) {
return CompletableFuture.completedFuture(CommandResult.fromSuccess()); return CompletableFuture.completedFuture(CommandResult.fromSuccess());
} else {
List<String> additionalFeatures = featureDependencies
.stream()
.map(featureDef -> featureDef.getFeature().getKey()).
collect(Collectors.toList());
FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class);
model.setFeatures(additionalFeatures);
MessageToSend messageToSend = templateService.renderEmbedTemplate(DISABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY,
model, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(message -> CommandResult.fromIgnored());
}
} }
} }

View File

@@ -14,19 +14,23 @@ import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.models.FeatureValidationResult; import dev.sheldan.abstracto.core.models.FeatureValidationResult;
import dev.sheldan.abstracto.core.models.database.AServer; import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.template.commands.EnableModel; import dev.sheldan.abstracto.core.models.template.commands.FeatureSwitchModel;
import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.FeatureConfigService; import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService; import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService; import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
@@ -47,32 +51,52 @@ public class EnableFeature extends AbstractConditionableCommand {
@Autowired @Autowired
private ServerManagementService serverManagementService; private ServerManagementService serverManagementService;
private static final String ENABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY = "enableFeature_feature_dependencies_response";
private static final String ENABLE_FEATURE_ALL_FEATURES_RESPONSE = "enableFeature_all_features_response";
@Override @Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) { public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
if(commandContext.getParameters().getParameters().isEmpty()) { if(commandContext.getParameters().getParameters().isEmpty()) {
EnableModel model = (EnableModel) ContextConverter.fromCommandContext(commandContext, EnableModel.class); FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class);
model.setFeatures(featureConfigService.getAllFeatures()); model.setFeatures(featureConfigService.getAllFeatures());
String response = templateService.renderTemplate("enable_features_response", model, commandContext.getGuild().getIdLong()); MessageToSend messageToSend = templateService.renderEmbedTemplate(ENABLE_FEATURE_ALL_FEATURES_RESPONSE, model, commandContext.getGuild().getIdLong());
return channelService.sendTextToChannel(response, commandContext.getChannel()) return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(message -> CommandResult.fromSuccess()); .thenApply(message -> CommandResult.fromIgnored());
} else { } else {
String flagKey = (String) commandContext.getParameters().getParameters().get(0); String featureKey = (String) commandContext.getParameters().getParameters().get(0);
FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(flagKey); FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(featureKey);
AServer server = serverManagementService.loadServer(commandContext.getUserInitiatedContext().getGuild().getIdLong()); AServer server = serverManagementService.loadServer(commandContext.getUserInitiatedContext().getGuild().getIdLong());
FeatureValidationResult featureSetup = featureConfigService.validateFeatureSetup(feature, server); FeatureValidationResult featureSetup = featureConfigService.validateFeatureSetup(feature, server);
if(Boolean.FALSE.equals(featureSetup.getValidationResult())) { if(Boolean.FALSE.equals(featureSetup.getValidationResult())) {
log.info("Feature {} has failed the setup validation. Notifying user.", flagKey); log.info("Feature {} has failed the setup validation. Notifying user.", featureKey);
channelService.sendTextToChannelNotAsync(templateService.renderTemplatable(featureSetup, commandContext.getGuild().getIdLong()), channelService.sendTextToChannelNotAsync(templateService.renderTemplatable(featureSetup, commandContext.getGuild().getIdLong()),
commandContext.getChannel()); commandContext.getChannel());
} }
featureFlagService.enableFeature(feature, server); featureFlagService.enableFeature(feature, server);
List<FeatureConfig> featureDependencies = new ArrayList<>();
if(feature.getRequiredFeatures() != null) { if(feature.getRequiredFeatures() != null) {
feature.getRequiredFeatures().forEach(featureDisplay -> { feature.getRequiredFeatures().forEach(featureDisplay -> {
log.info("Also enabling required feature {}.", featureDisplay.getFeature().getKey()); log.info("Also enabling required feature {}.", featureDisplay.getFeature().getKey());
if(!featureFlagService.isFeatureEnabled(featureDisplay, server)) {
featureFlagService.enableFeature(featureDisplay, server); featureFlagService.enableFeature(featureDisplay, server);
featureDependencies.add(featureDisplay);
}
}); });
} }
if(featureDependencies.isEmpty()) {
return CompletableFuture.completedFuture(CommandResult.fromSuccess()); return CompletableFuture.completedFuture(CommandResult.fromSuccess());
} else {
List<String> additionalFeatures = featureDependencies
.stream()
.map(featureDef -> featureDef.getFeature().getKey()).
collect(Collectors.toList());
FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class);
model.setFeatures(additionalFeatures);
MessageToSend messageToSend = templateService.renderEmbedTemplate(ENABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY,
model, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(message -> CommandResult.fromIgnored());
}
} }
} }

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.core.commands.config; package dev.sheldan.abstracto.core.commands.config.features;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration; import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
@@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition;
import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult; import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;
import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.FeatureNotFoundException; import dev.sheldan.abstracto.core.exception.FeatureNotFoundException;

View File

@@ -2,6 +2,7 @@ package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.command.service.ExceptionService; import dev.sheldan.abstracto.core.command.service.ExceptionService;
import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import dev.sheldan.abstracto.core.exception.ChannelNotInGuildException; import dev.sheldan.abstracto.core.exception.ChannelNotInGuildException;
import dev.sheldan.abstracto.core.interactive.*; import dev.sheldan.abstracto.core.interactive.*;
import dev.sheldan.abstracto.core.models.AServerChannelUserId; import dev.sheldan.abstracto.core.models.AServerChannelUserId;
@@ -14,9 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@Component @Component
@@ -61,8 +60,13 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
log.info("Performing setup of feature {} for user {} in channel {} in server {}.", log.info("Performing setup of feature {} for user {} in channel {} in server {}.",
featureConfig.getFeature().getKey(), user.getUserId(), user.getChannelId(), user.getGuildId()); featureConfig.getFeature().getKey(), user.getUserId(), user.getChannelId(), user.getGuildId());
Optional<TextChannel> textChannelInGuild = channelService.getTextChannelFromServerOptional(user.getGuildId(), user.getChannelId()); Optional<TextChannel> textChannelInGuild = channelService.getTextChannelFromServerOptional(user.getGuildId(), user.getChannelId());
if(textChannelInGuild.isPresent()) { if (textChannelInGuild.isPresent()) {
List<String> requiredSystemConfigKeys = featureConfig.getRequiredSystemConfigKeys(); Set<String> requiredSystemConfigKeys = new HashSet<>();
Set<PostTargetEnum> requiredPostTargets = new HashSet<>();
Set<SetupStep> customSetupSteps = new HashSet<>();
collectRequiredFeatureSteps(featureConfig, requiredSystemConfigKeys, requiredPostTargets, customSetupSteps, new HashSet<>());
List<SetupExecution> steps = new ArrayList<>(); List<SetupExecution> steps = new ArrayList<>();
requiredSystemConfigKeys.forEach(s -> { requiredSystemConfigKeys.forEach(s -> {
log.debug("Feature requires system config key {}.", s); log.debug("Feature requires system config key {}.", s);
@@ -73,7 +77,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
.build(); .build();
steps.add(execution); steps.add(execution);
}); });
featureConfig.getRequiredPostTargets().forEach(postTargetEnum -> { requiredPostTargets.forEach(postTargetEnum -> {
log.debug("Feature requires post target {}.", postTargetEnum.getKey()); log.debug("Feature requires post target {}.", postTargetEnum.getKey());
SetupExecution execution = SetupExecution SetupExecution execution = SetupExecution
.builder() .builder()
@@ -82,7 +86,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
.build(); .build();
steps.add(execution); steps.add(execution);
}); });
featureConfig.getCustomSetupSteps().forEach(setupStep -> { customSetupSteps.forEach(setupStep -> {
log.debug("Feature requires custom setup step {}.", setupStep.getClass().getName()); log.debug("Feature requires custom setup step {}.", setupStep.getClass().getName());
SetupExecution execution = SetupExecution SetupExecution execution = SetupExecution
.builder() .builder()
@@ -94,7 +98,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
for (int i = 0; i < steps.size(); i++) { for (int i = 0; i < steps.size(); i++) {
SetupExecution setupExecution = steps.get(i); SetupExecution setupExecution = steps.get(i);
setupExecution.getParameter().setPreviousMessageId(initialMessageId); setupExecution.getParameter().setPreviousMessageId(initialMessageId);
if(i < steps.size() - 1) { if (i < steps.size() - 1) {
setupExecution.setNextStep(steps.get(i + 1)); setupExecution.setNextStep(steps.get(i + 1));
} }
} }
@@ -117,7 +121,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
@Override @Override
public CompletableFuture<Void> executeFeatureSetup(FeatureConfig featureConfig, List<SetupExecution> steps, AServerChannelUserId user, List<DelayedActionConfig> delayedActionConfigs) { public CompletableFuture<Void> executeFeatureSetup(FeatureConfig featureConfig, List<SetupExecution> steps, AServerChannelUserId user, List<DelayedActionConfig> delayedActionConfigs) {
if(!steps.isEmpty()) { if (!steps.isEmpty()) {
SetupExecution nextStep = steps.get(0); SetupExecution nextStep = steps.get(0);
return executeStep(user, nextStep, delayedActionConfigs, featureConfig); return executeStep(user, nextStep, delayedActionConfigs, featureConfig);
} else { } else {
@@ -130,10 +134,10 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
private CompletableFuture<Void> executeStep(AServerChannelUserId aUserInAServer, SetupExecution execution, List<DelayedActionConfig> delayedActionConfigs, FeatureConfig featureConfig) { private CompletableFuture<Void> executeStep(AServerChannelUserId aUserInAServer, SetupExecution execution, List<DelayedActionConfig> delayedActionConfigs, FeatureConfig featureConfig) {
log.debug("Executing step {} in server {} in channel {} for user {}.", execution.getStep().getClass(), aUserInAServer.getGuildId(), aUserInAServer.getChannelId(), aUserInAServer.getUserId()); log.debug("Executing step {} in server {} in channel {} for user {}.", execution.getStep().getClass(), aUserInAServer.getGuildId(), aUserInAServer.getChannelId(), aUserInAServer.getUserId());
return execution.getStep().execute(aUserInAServer, execution.getParameter()).thenAccept(setpResult -> { return execution.getStep().execute(aUserInAServer, execution.getParameter()).thenAccept(setpResult -> {
if(setpResult.getResult().equals(SetupStepResultType.SUCCESS)) { if (setpResult.getResult().equals(SetupStepResultType.SUCCESS)) {
log.info("Step {} in server {} has been executed successfully. Proceeding.", execution.getStep().getClass(), aUserInAServer.getGuildId()); log.info("Step {} in server {} has been executed successfully. Proceeding.", execution.getStep().getClass(), aUserInAServer.getGuildId());
delayedActionConfigs.addAll(setpResult.getDelayedActionConfigList()); delayedActionConfigs.addAll(setpResult.getDelayedActionConfigList());
if(execution.getNextStep() != null) { if (execution.getNextStep() != null) {
log.debug("Executing next step {}.", execution.getNextStep().getStep().getClass()); log.debug("Executing next step {}.", execution.getNextStep().getStep().getClass());
executeStep(aUserInAServer, execution.getNextStep(), delayedActionConfigs, featureConfig); executeStep(aUserInAServer, execution.getNextStep(), delayedActionConfigs, featureConfig);
} else { } else {
@@ -178,7 +182,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
log.debug("Notifying user {} in channel {} in server {} about completion of setup for feature {}.", log.debug("Notifying user {} in channel {} in server {} about completion of setup for feature {}.",
aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey()); aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey());
String templateKey; String templateKey;
if(result.getResult().equals(SetupStepResultType.CANCELLED)) { if (result.getResult().equals(SetupStepResultType.CANCELLED)) {
templateKey = FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE; templateKey = FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE;
} else { } else {
templateKey = FEATURE_SETUP_COMPLETION_NOTIFICATION_TEMPLATE; templateKey = FEATURE_SETUP_COMPLETION_NOTIFICATION_TEMPLATE;
@@ -202,4 +206,19 @@ public class FeatureSetupServiceBean implements FeatureSetupService {
aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey()); aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey());
notifyUserWithTemplate(aServerChannelUserId, featureConfig, FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE); notifyUserWithTemplate(aServerChannelUserId, featureConfig, FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE);
} }
private void collectRequiredFeatureSteps(FeatureConfig featureConfig, Set<String> requiredSystemConfigKeys,
Set<PostTargetEnum> requiredPostTargets, Set<SetupStep> customSetupSteps,
Set<String> coveredFeatures) {
if (coveredFeatures.contains(featureConfig.getFeature().getKey())) {
return;
}
coveredFeatures.add(featureConfig.getFeature().getKey());
requiredSystemConfigKeys.addAll(featureConfig.getRequiredSystemConfigKeys());
requiredPostTargets.addAll(featureConfig.getRequiredPostTargets());
customSetupSteps.addAll(featureConfig.getCustomSetupSteps());
featureConfig.getRequiredFeatures()
.forEach(requiredFeature -> collectRequiredFeatureSteps(requiredFeature, requiredSystemConfigKeys, requiredPostTargets, customSetupSteps, coveredFeatures));
}
} }

View File

@@ -10,6 +10,6 @@ import java.util.List;
@Getter @Getter
@Setter @Setter
@SuperBuilder @SuperBuilder
public class EnableModel extends UserInitiatedServerContext { public class FeatureSwitchModel extends UserInitiatedServerContext {
private List<String> features; private List<String> features;
} }