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:

Diagram
  1. StaticFileReader — searches for META-INF/openapi.yaml, .yml, then .json. Read through the current classloader. Returns Optional<OpenAPI>.

  2. ModelReaderInvoker — if mp.openapi.model.reader is set, instantiates the class (CDI lookup preferred, no-arg constructor as fallback) and calls buildModel().

  3. AnnotationScanner — walks the @Path classes of the current CDI module, reads the MP OpenAPI annotations (@Operation, @Parameter, @Schema, etc.), and builds a partial OpenAPI. The output is tagged by AnnotationSource (sealed sub-type of ModelSource).

  4. ModelMerger — merges the three ModelSource instances (sealed: StaticFileSource, ReaderSource, AnnotationSource) into a single OpenAPI. Strategy: for operations, annotations win; for Components, deduplicated union by name; for servers, see ConfigApplier.

  5. FilterInvoker — if mp.openapi.filter is set, loads the OASFilter (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.

  6. GrimmModelCache — wraps the final OpenAPI. 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.

No ThreadLocal

Per the module’s CLAUDE.md contract, production code uses no ThreadLocal and no synchronized. When shared state is needed, ConcurrentHashMap, ScopedValue or ReentrantLock are preferred.

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:

  1. Resolves the format via ?format= (priority, spec §2.3) then Accept (spec §2.2, YAML default).

  2. Reads the document from the cache.

  3. Serialises it to YAML or JSON depending on the resolved format.

  4. Returns a Response with the matching Content-Type.

No lock is taken; serialisation itself can parallelise safely.

Further reading

  • Concepts — sources, merge, configuration.

  • TCK — execution and status.

  • Reference — annotations and MP Config keys.