Skip to content

v0.24.17

Choose a tag to compare

@Goldziher Goldziher released this 13 Jun 11:48
· 164 commits to main since this release
v0.24.17
d623712

Added

  • Phase C cross-cutting plumbing: per-language override resolution, surface scan helpers, emitter traits, and backend stubs. Four items land together as the seam framework that wave-2 per-backend specialists drop real Jinja-template work into:

    1. RegistrationVariant::resolved_for(language, base_handler_shape) -> ResolvedVariant<'_> (and companion method_name_for) on src/core/ir/service.rs. The helper performs a single authoritative lookup into variant.language_overrides[language] and returns the effective (style, handler_shape, method_prefix) triple — per-language override wins, missing fields fall through to variant-level and registration-level defaults transparently. The PyO3 (registration_variants.rs) and NAPI (typescript.rs) backends are updated to call resolved_for("python"/"napi", reg.handler_shape) instead of reading variant.style directly; all other backends continue to work unchanged since no language override is yet configured for them.

    2. ApiSurface gate predicates (src/core/ir/surface.rs): has_lifecycle_hooks() -> bool, has_websocket_routes() -> bool, has_sse_routes() -> bool, has_error_types() -> bool. Each wraps a simple !self.<collection>.is_empty() so backends can guard emission blocks without duplicating the same collection check everywhere.

    3. Emitter traits (src/core/ir/codegen_hooks.rs): LifecycleHookEmitter, WebSocketRouteEmitter, SseRouteEmitter, ErrorTypeEmitter. Each declares one method (emit_lifecycle_hooks, emit_websocket_routes, emit_sse_routes, emit_error_types) with documented stub contract (log a tracing::debug!, walk without panicking, return String::new()). Backends implement these when they write real emission.

    4. Per-backend stub modules (new_ir_stubs.rs for subdirectory backends, inline stubs for flat-file backends): pyo3, napi, magnus, php, rustler, go, jni, csharp, kotlin, dart, swift, zig. Each backend exposes an emit_new_ir_sections(api: &ApiSurface) -> String aggregate that forwards all four IR sections and returns empty output until a Phase-C specialist replaces the stub body. #[allow(dead_code)] suppresses the unused-function lint until callers are wired in.

    Unit tests: resolved_for tests cover the per-language override path, the fallback-to-variant-defaults path, partial overrides (style-only, shape-only), and the method_name_for prefix concatenation. Surface predicate tests assert empty-collection → false, single-item → true for all four predicates. Stub tests assert that all four stubs return "" for the empty surface and do not panic on non-empty inputs.

  • PHP backend Phase C: fluent verb-decorator API with lifecycle hooks. The ext-php-rs service-API codegen (src/backends/php/gen_bindings/service_api/php.rs) now emits an idiomatic Slim-style fluent App class aligned with the verb_decorator + request_response handler shape: zero-arg constructor, HTTP verb methods (get, post, put, patch, delete, head, options) that return $this for method chaining, config(ServerConfig $config): self to set server configuration, five lifecycle hook methods (onRequest, preValidation, preHandler, onResponse, onError) forwarding to native, and run(): void entrypoint. Generated class is final with declare(strict_types=1). Foundation for Request/Response helper classes, ServerConfig DTO, error hierarchy, attribute scanner, and WebSocket/SSE routes established but deferred to follow-on iterations. (src/backends/php/gen_bindings/service_api/php.rs)

  • NAPI backend Phase C: config method forwarding, lifecycle hook registration, and skeleton for future phases. The TypeScript service-API codegen now emits special handling for the config method to forward configuration to the native runtime via JSON serialization (fixes the long-standing config no-op bug). All lifecycle hooks declared in the IR now generate app.onRequest(fn), app.preValidation(fn), app.preHandler(fn), app.onResponse(fn), app.onError(fn) registration methods that forward to native. Async-by-default hook handlers use ThreadsafeFunction internally. Three new templates (service_ts_configurator_config_forward, service_ts_lifecycle_hook) emit the wrappers. Foundation for Context-object handler shape, WebSocket/SSE routes, and error classes established but deferred to follow-on Phase C iterations per language. (src/backends/napi/gen_bindings/service_api/typescript.rs, src/backends/napi/template_env.rs)

  • Go backend Phase C: chi-style fluent API with config struct, error types, lifecycle hooks, WebSocket/SSE routes, and helpers. The cgo service-API codegen (src/backends/go/gen_bindings/phase_c.rs) now emits full Phase C surface for verb_decorator + request_response shape: (1) Config struct with all ServerConfig fields (Host, Port, Workers, Compression, Cors, RateLimit, RequestTimeout, MaxBodySize, JwtAuth, ApiKeyAuth, StaticFiles, EnableRequestId, EnableHttpTrace, GracefulShutdown, ShutdownTimeout); (2) error types for each ErrorTypeDef in the IR, each implementing error interface with StatusCode() int and ToProblemDetails() map[string]interface{} methods (e.g. NotFoundError, ValidationError); (3) lifecycle hook registration methods app.OnRequest(fn), app.PreValidation(fn), app.PreHandler(fn), app.OnResponse(fn), app.OnError(fn) forwarding to native; (4) skeleton WebSocket(path, handler) and SSE(path, producer) methods for future implementation; (5) app.Run(config Config) error method with defaults and C entrypoint call; (6) helper functions PathParam(), QueryParam(), HeaderParam(), BindJSON(), RespondJSON() for chi-style HTTP handler convenience. New module refactored to keep service_api.rs under line-count ceiling. (src/backends/go/gen_bindings/phase_c.rs, five new Jinja templates under src/backends/go/templates/)

  • C# backend Phase C: lifecycle hooks, WebSocket/SSE routes, and error exception hierarchy. The P/Invoke service-API codegen (src/backends/csharp/gen_bindings/service_api.rs) now emits Phase C surface alongside the existing registration and entrypoint methods: (1) exception classes for each ErrorTypeDef in the IR under the Spikard.Errors namespace, each extending SpikardException base with StatusCode property returning the HTTP status code and ToProblemDetailsJson() method returning RFC 9457 ProblemDetails JSON (e.g. NotFoundError, ValidationError, UnauthorizedError, etc.); (2) lifecycle hook registration methods injected into the service class (OnRequest, PreValidation, PreHandler, OnResponse, OnError) each accepting Action<RequestData> and forwarding via P/Invoke to the native layer with GCHandle marshalling; (3) WebSocket(string path, Func<WebSocketConnection, Task> handler) and Sse(string path, Func<IAsyncEnumerable<SseEvent>> producer) route registration methods marshalling async handlers via GCHandle. Error type file is emitted separately under Spikard/Errors/SpikardException.cs for clean namespace segregation. (src/backends/csharp/gen_bindings/service_api.rs)

Fixed

  • JNI codegen: Vec<String> parameters now respect vec_inner_is_ref and emit a Vec<&str> materialisation when the core function takes &[&str]. Previously the JNI function/method shims unconditionally emitted &{name} for Vec<String> slots, which coerces to &[String] but not &[&str]. Core fns declared as download(names: &[&str]) failed to compile with E0308 expected reference &[&str], found reference &Vec<String>. The shim now checks p.vec_inner_is_ref && Vec<String> and emits &{name}.iter().map(|s| s.as_str()).collect::<Vec<_>>(), mirroring the existing Dart codegen branch. (src/backends/jni/gen_shims/function_shims.rs, src/backends/jni/gen_shims/method_shims.rs)

  • JNI return-marshal: Option<T> for any inner type now short-circuits None to a null jstring. Previously only Option<String> had a dedicated return_optional_string.rs.jinja branch; every other Option<T> (e.g. Option<EmbeddingPreset> from get_embedding_preset) fell through to the generic return_json.rs.jinja arm, which serialises None to the four-character JSON literal "null". Kotlin then saw a non-null String containing those four characters, so consumer tests like assertTrue(result == null) failed even when the Rust side returned None. The new TypeRef::Optional(_) arm in emit_return_marshal_with_indent wraps the serialise step in a match v { None => std::ptr::null_mut(), Some(inner) => /* serialise inner */ }, so any Option<T> return now produces a real null at the JVM boundary. (src/backends/jni/gen_shims/marshalling.rs)

  • Kotlin-Android wrapper object name drops the appended Converter suffix. kotlin_android_wrapper_object_name used to return <Crate>Converter (so kreuzbergKreuzbergConverter), but the bridge file emitted by the JNI scaffold is <Crate>Bridge (KreuzbergBridge) and every e2e test calls the wrapper as <Crate>.method(...) (Kreuzberg.extractFile(...)). The mismatch produced Unresolved reference 'KreuzbergConverterBridge' in every wrapper that delegated to the bridge, and Unresolved reference 'Kreuzberg' in every alef-emitted test that used the public surface. Return the bare PascalCase stem (still stripping the Rs binding-crate suffix) so the wrapper and bridge file names line up. (src/codegen/naming.rs)

  • Struct-field doc strings now flow through sanitize_rust_idioms before reaching the generated Rust source. The shared gen_struct codegen in src/codegen/generators/structs.rs (three call sites at lines 179, 293, 369) and the NAPI-specific struct emitter in src/backends/napi/gen_bindings/types.rs:195 were passing field.doc verbatim to StructBuilder::add_field_with_doc, bypassing the prose sanitizer that strips [X](crate::X) explicit-link targets. The core crate's rustdoc carried valid crate::DataNode / crate::ProcessResult::data intra-doc links — propagated as-is into ts-pack-core-node, ts-pack-core-py, and ts-pack-core-php, those references became broken_intra_doc_links / redundant_explicit_links errors because crate:: resolves in the originating crate but not in the binding crate. A new sanitize_field_doc helper applies the same target-agnostic transform that method/struct-level docs already go through; the NAPI emitter does the same inline. Method/struct-level docs already routed through sanitize_rust_idioms, so this closes the field-doc gap. (src/codegen/generators/structs.rs, src/backends/napi/gen_bindings/types.rs)

  • Dart FRB bridge crate now emits #![allow(missing_docs)]. Every #[frb]-attributed function in packages/<crate>/dart/rust/src/lib.rs is named from a paired Dart binding and carries no human-authored docstring — the name and the bridge contract are the spec. Without the allow, the shared rustdoc-lint pre-commit hook (cargo doc -- -D missing-docs) blocked every consumer commit that regenerated the dart bridge. Mirrors the same fix landed earlier for the JNI shim. (src/backends/dart/gen_rust_crate/mod.rs)

Added

  • Service-API IR: three new RegistrationVariantStyle variants — Decorator, Attribute, Dsl. The pre-existing Hybrid/VerbDecorator/Builder enum is extended so backends can emit per-language idiomatic surfaces beyond the original method-pair shape: Decorator is the Python-style overloaded method that doubles as both direct registration (app.get(path, handler)) and decorator factory (@app.get(path)) by returning a callable when the handler argument is absent; Attribute emits annotation/attribute types (e.g. #[Get('/path')], @GetMapping(...), [HttpGet(...)]) paired with an app.mount(ControllerClass) reflection scanner; Dsl emits a nested routing-DSL entry point (e.g. Kotlin Ktor routing {}, Elixir use Router macro block). Default remains Hybrid so consumers without explicit style declarations get the previous behaviour. Backends that cannot express a requested style (Attribute in reflection-less languages, Dsl in non-block-friendly targets) should fall back to Hybrid. (src/core/ir/service.rs)
  • Service-API IR: HandlerShape enum on RegistrationDef. A new field controls how the generated wrapper binds the incoming request DTO to the host-language callback. Four variants: BareCallable (default — preserves existing behaviour: handler receives the full request DTO directly), ContextObject (handler receives a single ergonomic context handle exposing typed accessors — idiomatic in Hono, Vapor, Javalin, ASP.NET Minimal API), RequestResponse (handler receives separate request and response objects, with the response written to rather than returned — idiomatic in Express, chi), and IntrospectParams (handler signature is introspected at registration time so type-annotated parameters bind from path/query/body by name — idiomatic in FastAPI / Litestar). Backends dispatch on the shape when selecting the emission template. (src/core/ir/service.rs)
  • Service-API IR: PathConstraint with parse_path normalization. Three input syntaxes are now accepted and normalized uniformly to the canonical {name} form: {name:type} (typed), {name} (untyped), and :name (colon-prefix). A new field path_param_constraints: Vec<PathConstraint> on RegistrationDef carries (name, type_constraint) pairs extracted at extract time, while the normalized path string is emitted to the router unchanged. Supported type_constraint values: "int", "uuid", "slug", "path" (greedy), and pass-through custom regexes; None means no structural constraint. Backends that don't support a particular constraint fall back to a plain parameter binding and let the native layer validate. (src/core/ir/service.rs)
  • Service-API IR: per-language style overrides via RegistrationVariantLanguageOverride. A new language_overrides: HashMap<String, RegistrationVariantLanguageOverride> map on RegistrationVariant lets each backend look up its canonical language name and override the variant-global style, handler_shape, and method_prefix. The override mechanism is purely additive — absent keys fall through to the variant-global defaults. Enables alef.toml entries like [crates.services.registrations.variants.languages.csharp] style = "attribute" method_prefix = "Map" to produce MapGet/MapPost for ASP.NET Minimal-style emission, or […languages.kotlin] style = "dsl" for Ktor-style routing blocks, without disturbing other backends. (src/core/ir/service.rs)
  • Application IR: LifecycleHookDef for callback-slot registration. A new top-level IR entry describes a named lifecycle hook the service owner exposes to host-language code (e.g. on_request, pre_validation, pre_handler, on_response, on_error). Each hook carries a name, a callback_contract cross-referencing an existing HandlerContractDef::trait_name, an is_async flag controlling whether async-first languages emit async callback types, and documentation. Backends generate the matching app.on_<name>(fn) registration method (or its idiomatic equivalent) alongside the existing service entrypoints. (src/core/ir/application.rs)
  • Application IR: first-class WebSocketRouteDef and SseRouteDef. Promotes WebSocket and SSE handler contracts from the previously-skipped category to first-class IR entries. Because both contracts use impl Future return types that are not object-safe (and so cannot be wrapped in Arc<dyn Trait>), each entry names a concrete wrapper struct type (handler_wrapper_type for WebSocket, producer_wrapper_type for SSE) so backends emit a concrete monomorphisation instead of a trait-object bridge. The socket type and event type are also named so backends generate typed annotations in the handler signature. (src/core/ir/application.rs)
  • Application IR: cross-binding ErrorTypeDef + HttpStatus enum. A new top-level IR section describes the exception hierarchy that every backend must emit as native error classes. Each ErrorTypeDef carries a PascalCase name (e.g. NotFoundError, ValidationError), an HttpStatus discriminating the response status (with named variants for the common 4xx/5xx codes plus a Custom(u16) escape hatch), an optional RFC 9457 ProblemDetails type URI, and documentation. Backends generate native exception classes whose serialization produces the standardized ProblemDetails JSON body, so handler-thrown errors round-trip uniformly across every language binding. (src/core/ir/application.rs)
  • IR module split. The previously monolithic src/core/ir.rs is reorganized into a module: application.rs (lifecycle hooks, WebSocket/SSE, error types), service.rs (service definitions, registrations, handler shapes, path constraints), items.rs (the pre-existing TypeDef/EnumDef/FunctionDef/MethodDef/etc.), metadata.rs (deprecation, version, default-value annotations), surface.rs (ApiSurface), type_ref.rs (TypeRef + PrimitiveType). Re-exports in mod.rs preserve the existing public API exactly — no consumer touch required. (src/core/ir/)

Fixed

  • NAPI options-field bridge: drop unused mut on the Option<JsOptions>.map(|o| …) closure binding. The generated convert(html, options) shim moved o straight into o.into() without mutating the binding, so the mut keyword in the template triggered warn(unused_mut) on the consumer crate (e.g. crates/html-to-markdown-node/src/lib.rs:1775). The mut was a copy-paste from the wasm-side template, where the closure does mutate o.<field> before the into() call. Template: drop mut from the napi closure binding; the wasm template is unchanged. (src/backends/napi/templates/options_field_bridge_body.jinja)

  • Magnus Ruby gemspec: required_ruby_version now emits the array form [">= 3.2.0", "< 4.0"] instead of the comma-joined single string ">= 3.2.0, < 4.0". RubyGems' Gem::Requirement.parse treats a single requirement string as one expression and rejects embedded commas with Gem::Requirement::BadRequirementError: Illformed requirement [">= 3.2.0, < 4.0"], so bundle install / gem build against the v0.24.16-generated gemspec failed before reaching any source. The array form is the canonical RubyGems syntax for multiple bounds and parses cleanly on Ruby 3.2–3.5 as well as 4.x preview builds. The platform-gemspec propagation path (read_required_ruby_version + generate_platform_gemspec) was rewritten to capture the raw RHS (single-string or array literal) and re-emit it verbatim, so the upper-bound constraint is preserved on cross-compiled platform gems. Closes liter-llm v1.5.1 prek failure. (src/scaffold/languages/ruby.rs, src/publish/package/ruby.rs)