Grimm refuses per-request classpath scanning: all the work happens once, at CDI container startup, and the result is cached. This page describes the exact pipeline sequence, the role of the Vauban BCE, and the threading invariants.
grimm-core / grimm-cdi-vauban separation
grimm-core is plain Java. It knows nothing about CDI or JAX-RS runtime — only the spec types (@Path is read as an annotation, not as a handler). It is usable outside any container, for example by a Maven tool that produces an OpenAPI document at build time.
grimm-cdi-vauban is the integration layer: it plugs grimm-core into the Vauban Build Compatible Extension, exposes the /openapi resource, and supplies the Supplier<OpenAPI> consumed by downstream tools.
Build pipeline
Six steps, executed once when the container activates:
-
StaticFileReader— searches forMETA-INF/openapi.yaml,.yml, then.json. Read through the current classloader. ReturnsOptional<OpenAPI>. -
ModelReaderInvoker— ifmp.openapi.model.readeris set, instantiates the class (CDI lookup preferred, no-arg constructor as fallback) and callsbuildModel(). -
AnnotationScanner— walks the@Pathclasses of the current CDI module, reads the MP OpenAPI annotations (@Operation,@Parameter,@Schema, etc.), and builds a partialOpenAPI. The output is tagged byAnnotationSource(sealed sub-type ofModelSource). -
ModelMerger— merges the threeModelSourceinstances (sealed:StaticFileSource,ReaderSource,AnnotationSource) into a singleOpenAPI. Strategy: for operations, annotations win; forComponents, deduplicated union byname; forservers, seeConfigApplier. -
FilterInvoker— ifmp.openapi.filteris set, loads theOASFilter(CDI lookup or no-arg constructor) and walks it across the tree. The filter is dispatched recursively via a typed visitor that `switch`es on the sealed model types. -
GrimmModelCache— wraps the finalOpenAPI. Lock-free reads, write-once initialisation.
Vauban BCE: GrimmExtension
The orchestration lives in io.vidocq.grimm.cdi.GrimmExtension, a Build Compatible Extension:
// Pseudo-declaration — the real BCE uses CDI 4.1 Lite's
// @Discovery, @Enhancement, @Registration, @Synthesis hooks.
@Discovery
public void declareScanRoots(ScannedTypes types) { /* ... */ }
@Synthesis
public void synthesizeOpenApiBean(SyntheticComponents synth) {
// Synthesises an ApplicationScoped bean for GrimmModelCache,
// populated at @PostConstruct by the full pipeline.
}
GrimmModelCache is @ApplicationScoped. Its @PostConstruct runs the pipeline; getDocument() simply returns the cached reference.
Threading model
No hot locking
GrimmModelCache stores the document in a final reference, initialised once in @PostConstruct. Every subsequent read is lock-free: no synchronized, no ReentrantLock, no volatile in any critical happens-before sense — the final field of the record provides the required JMM semantics.
Virtual threads for potentially blocking operations
Invoking an application-supplied OASModelReader or OASFilter may, in theory, trigger blocking calls (file or database reads). When the host runtime (Cassini + Chappe) runs on the default virtual executor (Executors.newVirtualThreadPerTaskExecutor()), these invocations inherit the virtual context — Grimm creates no platform-thread pool of its own.
YAML and JSON serialization
grimm-core ships two hand-written serializers (no SnakeYAML, no Jackson):
-
JsonSerializer— emits raw JSON 2020-12, dependency-free. -
YamlSerializer— emits OpenAPI 3.1-compatible YAML 1.2 with two-space indentation and block scalars for multi-line strings.
Symmetrically, JsonDeserializer and YamlDeserializer consume the static files.
OpenApiModelMapper and OpenApiValueMapper translate between the serialised tree and the internal OpenAPI model.
/openapi endpoint
OpenApiResource is an @ApplicationScoped JAX-RS bean in grimm-cdi-vauban. Its constructor injects GrimmModelCache. The getOpenApi(format, accept) method:
-
Resolves the format via
?format=(priority, spec §2.3) thenAccept(spec §2.2, YAML default). -
Reads the document from the cache.
-
Serialises it to YAML or JSON depending on the resolved format.
-
Returns a
Responsewith the matchingContent-Type.
No lock is taken; serialisation itself can parallelise safely.