Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ include("velocity-testkit") // in-memory velocity-spi impl, conformance TCK sce
// Phase 2 — v1 reference backends (tumbling + sliding), the service tier, and the generated client.
include("velocity-backend-jdbi") // Postgres/JDBI backend — v1 tumbling reference
include("velocity-backend-redis") // Redis/Lettuce backend — v1 sliding / hot-path reference
// include("velocity-dropwizard") // Dropwizard bundle, Jersey resources, Dagger wiring, API-key auth
include("velocity-dropwizard") // Dropwizard bundle, Jersey resources, Dagger wiring, API-key auth
// include("velocity-client-java") // Java client generated from OpenAPI via openapi-generator
// include("examples:dropwizard-demo")// runnable reference service (not published)

Expand Down
51 changes: 22 additions & 29 deletions velocity-api/src/main/resources/openapi/velocity-engine-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ paths:
post:
tags: [query]
operationId: queryVelocities
summary: Query current velocities for a batch of tuples (no mutation).
summary: Query current velocities for a subject's named features (no mutation).
description: >-
Returns the current velocity for each requested `(subject, aggregation, window)` tuple
without recording an event (FR-6). Results are aligned **positionally** with the request
tuples: `results[i]` is the outcome for `tuples[i]`, each a value-or-failure `FeatureResult`
(ADR 0009) — a failing tuple never collapses the batch.
Returns the current velocity for each requested feature (by name) for a subject, without
recording an event (FR-6). The response maps each feature name to its per-window
value-or-failure `FeatureResult`s (ADR 0009) — a failing feature never collapses the batch.
The engine resolves a feature name to its aggregation, windows, and backend from the
namespace's feature definitions (feature-name-based query — matches `velocity-core`).
parameters:
- $ref: "#/components/parameters/NamespacePath"
requestBody:
Expand All @@ -111,7 +112,7 @@ paths:
$ref: "#/components/schemas/QueryRequest"
responses:
"200":
description: Per-tuple results, positionally aligned with the request tuples.
description: Each requested feature name mapped to its per-window results.
content:
application/json:
schema:
Expand Down Expand Up @@ -635,41 +636,33 @@ components:
$ref: "#/components/schemas/Window"
additionalProperties: false

QueryTuple:
QueryRequest:
type: object
description: A single read request `(subject, aggregation, window)` (FR-6).
required: [subject, aggregation, window]
description: A subject and the stable feature names to read for it (FR-6/FR-8).
required: [subject, features]
properties:
subject:
$ref: "#/components/schemas/Subject"
aggregation:
$ref: "#/components/schemas/Aggregation"
window:
$ref: "#/components/schemas/Window"
additionalProperties: false

QueryRequest:
type: object
description: A batch of tuples to read in one call (FR-8).
required: [tuples]
properties:
tuples:
features:
type: array
minItems: 1
description: The stable feature names to read (resolved from the namespace's definitions).
items:
$ref: "#/components/schemas/QueryTuple"
type: string
additionalProperties: false

QueryResponse:
type: object
description: Per-tuple results, aligned **positionally** with the request tuples.
required: [results]
description: Each requested feature name mapped to its per-window value-or-failure results.
required: [features]
properties:
results:
type: array
description: "`results[i]` is the outcome for the request's `tuples[i]`."
items:
$ref: "#/components/schemas/FeatureResult"
features:
type: object
description: Feature name to the list of its per-window `FeatureResult`s (one per window).
additionalProperties:
type: array
items:
$ref: "#/components/schemas/FeatureResult"
additionalProperties: false

PurgeRequest:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ void topLevelSchemasArePresent() {
"FeatureResult",
"PerFeature",
"ApplyResult",
"QueryTuple",
"QueryRequest",
"QueryResponse",
"BackendCapabilities",
"FeatureDefinition",
"Problem");
Expand Down
91 changes: 91 additions & 0 deletions velocity-dropwizard/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
plugins {
id("velocity.library-conventions")
id("velocity.test-conventions")
id("velocity.publish-conventions")
}

description =
"velocity-dropwizard: the Dropwizard 5 service tier — Jersey resources over the velocity-core" +
" engine, Dagger 2 wiring, and namespace-scoped API-key auth, serving the velocity-api" +
" OpenAPI contract."

tasks.named<JavaCompile>("compileJava") {
// Dropwizard 5 pulls automatic-module dependencies (jersey, jetty, …) whose `requires` -Werror
// would turn fatal. -Xlint:-processing silences javac's "no processor claimed these annotations"
// hint — Dagger claims only its own annotations; JAX-RS/Jakarta ones are runtime-processed.
options.compilerArgs.addAll(
listOf(
"-Xlint:-requires-automatic",
"-Xlint:-requires-transitive-automatic",
"-Xlint:-processing",
),
)
}

tasks.named<JavaCompile>("compileTestJava") {
options.compilerArgs.addAll(
listOf(
"-Xlint:-requires-automatic",
"-Xlint:-requires-transitive-automatic",
"-Xlint:-processing",
),
)
}

dependencies {
api(project(":velocity-core"))
api(project(":velocity-api"))
api(libs.dropwizard.core)
api(libs.dropwizard.auth)
api(libs.dagger)
api(libs.jakarta.inject.api)

// Dropwizard's transitive Jersey/Jakarta packages reference errorprone annotations; without them
// on the compile classpath javac emits an annotation-not-found warning that -Werror turns fatal.
compileOnly(libs.build.errorprone.annotations)
testCompileOnly(libs.build.errorprone.annotations)

implementation(libs.slf4j.api)

annotationProcessor(libs.dagger.compiler)
testAnnotationProcessor(libs.dagger.compiler)

testImplementation(project(":velocity-testkit"))
testImplementation(libs.dropwizard.testing)
testRuntimeOnly(libs.logback.classic)
}

// Dagger-generated code is excluded from both the report and the coverage gate (NFR-14).
tasks.named<JacocoReport>("jacocoTestReport") {
classDirectories.setFrom(
files(
classDirectories.files.map {
fileTree(it) {
exclude(
"com/codeheadsystems/velocity/dropwizard/dagger/Dagger*",
"**/*_Factory.class",
"**/*_MembersInjector.class",
"**/*_Provide*Factory.class",
)
}
},
),
)
}

tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
classDirectories.setFrom(
files(
classDirectories.files.map {
fileTree(it) {
exclude(
"com/codeheadsystems/velocity/dropwizard/dagger/Dagger*",
"**/*_Factory.class",
"**/*_MembersInjector.class",
"**/*_Provide*Factory.class",
)
}
},
),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.dropwizard;

import com.codeheadsystems.velocity.core.VelocityEngine;
import com.codeheadsystems.velocity.dropwizard.dagger.DaggerVelocityComponent;
import com.codeheadsystems.velocity.dropwizard.dagger.VelocityComponent;
import com.codeheadsystems.velocity.dropwizard.dagger.VelocityModule;
import com.codeheadsystems.velocity.dropwizard.resource.VelocityExceptionMapper;
import com.codeheadsystems.velocity.spi.VelocityBackend;
import io.dropwizard.core.Application;
import io.dropwizard.core.setup.Environment;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
* The Dropwizard 5 service. Builds the Dagger graph, seeds feature definitions, and registers the
* JAX-RS resource behind the {@link com.codeheadsystems.velocity.dropwizard.auth.ApiKeyFilter
* API-key filter} and the {@link VelocityExceptionMapper problem exception mapper}.
*
* <p>The concrete backend is supplied by {@link #createBackend(VelocityConfiguration)} — override
* it (or a subclass wiring a {@code velocity-backend-*} module) to run the service; the default
* throws so a misconfigured deployment fails fast rather than silently serving nothing.
*/
public class VelocityApplication extends Application<VelocityConfiguration> {

/**
* Entry point: starts the service from the given arguments.
*
* @param args CLI arguments.
* @throws Exception if the service fails to start.
*/
public static void main(final String[] args) throws Exception {
new VelocityApplication().run(args);
}

@Override
public String getName() {
return "velocity-engine";
}

@Override
public void run(final VelocityConfiguration configuration, final Environment environment) {
final VelocityBackend backend = createBackend(configuration);
final VelocityComponent component =
DaggerVelocityComponent.builder()
.velocityModule(
new VelocityModule(
backend, configuration.getBackendName(), apiKeySets(configuration)))
.build();
configureEngine(component.engine(), configuration);
environment.jersey().register(component.apiKeyFilter());
environment.jersey().register(component.resource());
environment.jersey().register(new VelocityExceptionMapper());
}

/**
* Supplies the backend the engine routes to. The default is unconfigured.
*
* @param configuration the service configuration.
* @return the backend.
*/
protected VelocityBackend createBackend(final VelocityConfiguration configuration) {
throw new UnsupportedOperationException(
"no backend configured: override createBackend(...) or wire a velocity-backend-* module");
}

/**
* Seeds feature definitions at startup; the default is a no-op.
*
* @param engine the engine to seed.
* @param configuration the service configuration.
*/
protected void configureEngine(
final VelocityEngine engine, final VelocityConfiguration configuration) {
// Default: no feature definitions. A deployment overrides this (or loads YAML) to define them.
}

private static Map<String, Set<String>> apiKeySets(final VelocityConfiguration configuration) {
final Map<String, Set<String>> keys = new LinkedHashMap<>();
configuration.getApiKeys().forEach((key, namespaces) -> keys.put(key, Set.copyOf(namespaces)));
return keys;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.dropwizard;

import io.dropwizard.core.Configuration;
import java.util.List;
import java.util.Map;

/**
* Service configuration. {@code apiKeys} maps each API key to the namespaces it may access
* (namespace-scoped authz, NFR-21); {@code backendName} is the logical name the backend is
* registered under (the {@code /capabilities} endpoint reports it).
*/
public final class VelocityConfiguration extends Configuration {

private String backendName = "default";
private Map<String, List<String>> apiKeys = Map.of();

/**
* Returns the logical backend name.
*
* @return the logical backend name.
*/
public String getBackendName() {
return backendName;
}

/**
* Sets the logical backend name.
*
* @param backendName the logical backend name.
*/
public void setBackendName(final String backendName) {
this.backendName = backendName;
}

/**
* Returns the API-key to allowed-namespaces mapping.
*
* @return the API-key to allowed-namespace-list mapping.
*/
public Map<String, List<String>> getApiKeys() {
return apiKeys;
}

/**
* Sets the API-key to allowed-namespaces mapping.
*
* @param apiKeys the API-key to allowed-namespace-list mapping.
*/
public void setApiKeys(final Map<String, List<String>> apiKeys) {
this.apiKeys = Map.copyOf(apiKeys);
}
}
Loading
Loading