diff --git a/.gitignore b/.gitignore index c1e6c7d27e..6e589bb704 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,9 @@ nb-configuration.xml .env !/docker-compose/.env +# AI Tools +.claude +.bob + # NoSQLBench logs/ \ No newline at end of file diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java index 2357ba741c..e46b64581a 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java @@ -18,9 +18,8 @@ public class ApiFeatures { private final Map fromConfig; private final RequestContext.HttpHeaderAccess httpHeaders; - private ApiFeatures( - Map fromConfig, RequestContext.HttpHeaderAccess httpHeaders) { - this.fromConfig = fromConfig; + private ApiFeatures(Map fromConfig, RequestContext.HttpHeaderAccess httpHeaders) { + this.fromConfig = marshallFromConfig(fromConfig); this.httpHeaders = httpHeaders; } @@ -28,9 +27,44 @@ public static ApiFeatures empty() { return new ApiFeatures(Collections.emptyMap(), null); } + protected Map marshallFromConfig(Map fromConfig) { + if (fromConfig == null || fromConfig.isEmpty()) { + return Collections.emptyMap(); + } + + Map result = new java.util.HashMap<>(); + for (Map.Entry entry : fromConfig.entrySet()) { + String key = entry.getKey(); + ApiFeature feature = findFeatureByName(key); + if (feature == null) { + throw new IllegalArgumentException( + "Invalid feature flag key: '" + + key + + "'. Expected one of: " + + java.util.Arrays.stream(ApiFeature.values()) + .map( + f -> + "STARGATE_FEATURE_FLAGS_" + + f.featureName().toUpperCase().replace('-', '_')) + .collect(java.util.stream.Collectors.joining(", "))); + } + result.put(feature, entry.getValue()); + } + return result; + } + + private ApiFeature findFeatureByName(String name) { + for (ApiFeature feature : ApiFeature.values()) { + if (feature.featureName().equals(name)) { + return feature; + } + } + return null; + } + public static ApiFeatures fromConfigAndRequest( FeaturesConfig config, RequestContext.HttpHeaderAccess httpHeaders) { - Map fromConfig = config.flags(); + Map fromConfig = config.flags(); if (fromConfig == null) { fromConfig = Collections.emptyMap(); } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java index 5e3cd55465..462f655880 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java @@ -7,14 +7,49 @@ * Configuration mapping for Data API Feature flags as read from main application configuration * (with possible property / sysenv overrides). * - *

NOTE: actual keys in YAML or similar configuration file would be {@code - * stargate.feature.}, where {@code } comes from {@link - * ApiFeature#featureName()} (which is always lower-case). So, for example, to enable {@link - * ApiFeature#TABLES} feature, one would use: {@code stargate.feature.flags.tables} as the key. + *

Feature flags can be configured using either property file format (with dots) or environment + * variables (with underscores). The configuration uses the prefix {@code stargate.feature.flags} + * followed by the feature name from {@link ApiFeature#featureName()} (always in lower-case + * kebab-case format). + * + *

Property File Examples (application.yaml, application.properties): + * + *

+ * stargate.feature.flags.lexical=true
+ * stargate.feature.flags.tables=false
+ * stargate.feature.flags.reranking=true
+ * stargate.feature.flags.mcp=false
+ * stargate.feature.flags.request-tracing=true
+ * stargate.feature.flags.billing-events-logging=true
+ * 
+ * + *

Environment Variable Examples: + * + *

+ * export STARGATE_FEATURE_FLAGS_LEXICAL=true
+ * export STARGATE_FEATURE_FLAGS_TABLES=false
+ * export STARGATE_FEATURE_FLAGS_RERANKING=true
+ * export STARGATE_FEATURE_FLAGS_MCP=false
+ * export STARGATE_FEATURE_FLAGS_REQUEST_TRACING=true
+ * export STARGATE_FEATURE_FLAGS_BILLING_EVENTS_LOGGING=true
+ * 
+ * + *

Note: Quarkus/SmallRye Config automatically translates underscores ({@code _}) in + * environment variable names to dots ({@code .}) when matching configuration keys. Feature flag + * values are bound as Strings and converted to Boolean when needed. Null values are not accepted. + * + * @see ApiFeature for available feature flags + * @see ApiFeatures for runtime feature flag evaluation */ @ConfigMapping(prefix = "stargate.feature") public interface FeaturesConfig { - // Quarkus/SmallRye Config won't accept use of `null` values, so we must bind - // as Strings and only convert to Boolean when needed. - Map flags(); + + /** + * Returns a map of feature flag names to their configured values. Keys are feature names in + * kebab-case format (e.g., "lexical", "billing-events-logging"). Values are string + * representations of boolean flags ("true", "false", or blank for undefined). + * + * @return map of feature flag configurations, never null but may be empty + */ + Map flags(); } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/provider/BillingTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/provider/BillingTest.java index 399155d241..56252a5546 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/provider/BillingTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/provider/BillingTest.java @@ -77,7 +77,7 @@ void createReturnsNoOpWhenFeatureDisabled() { @Test void createConfigEnabledIsNotOverriddenByHeader() { var config = mock(FeaturesConfig.class); - when(config.flags()).thenReturn(Map.of(ApiFeature.BILLING_EVENTS_LOGGING, "true")); + when(config.flags()).thenReturn(Map.of("billing-events-logging", "true")); var headers = MultiMap.caseInsensitiveMultiMap(); headers.add(ApiFeature.BILLING_EVENTS_LOGGING.httpHeaderName(), "false"); @@ -126,8 +126,7 @@ private static BillingConfig validConfig() { private static ApiFeatures featuresWithBilling(boolean enabled) { var config = mock(FeaturesConfig.class); - when(config.flags()) - .thenReturn(Map.of(ApiFeature.BILLING_EVENTS_LOGGING, String.valueOf(enabled))); + when(config.flags()).thenReturn(Map.of("billing-events-logging", String.valueOf(enabled))); return ApiFeatures.fromConfigAndRequest(config, null); }