Skip to content

jzswag#158

Merged
josephbirkner merged 65 commits into
release/2.0.0from
jzswag
Jun 19, 2026
Merged

jzswag#158
josephbirkner merged 65 commits into
release/2.0.0from
jzswag

Conversation

@fklebert

@fklebert fklebert commented May 5, 2026

Copy link
Copy Markdown
Contributor

Changes

Introduces native JAVA libs (jvm and android) at full parity with the C++ and Python clients.

Modules (grouped under libs/jzswag/):

  • jzswag-api — platform-agnostic interfaces and value types
  • jzswag-shared — OpenAPI dispatch core, parameter encoding, OAuth2/OAuth1 token flow, YAML loader
  • jzswag-jvm — JVM client (JDK 11 HttpClient, OS keychain)
  • jzswag-android — Android client (OkHttp + Android Keystore + AES-GCM SharedPreferences)

Both clients implement zserio's ServiceClientInterface, so the idiomatic MyService.MyServiceClient(transport) usage works identically to the C++ and Python flavours.

Side effects to review carefully:

  • README and docs/ restructured into per-language pages (python.md, cpp.md, java.md, openapi-generator.md). The shared HTTP_SETTINGS_FILE format stays in the README so the devportal pymdown markers ([env], [settings]) keep working at their original source path.
  • Gradle wrapper checked in; Java toolchain pinned to Temurin 17 via Foojay — any JDK 17+ on PATH is enough to launch.

CI/coverage: new jzswag.yml workflow with JaCoCo + Codecov upload (flag unittests-java) on Linux + macOS. 212 unit tests across the four Java modules — jzswag-api at ~99% line coverage, the others 60%+. The existing C++/Python workflows are untouched.

fklebert and others added 10 commits May 5, 2026 10:19
Covers ParameterEncoder (all styles/formats/edge cases), OAuth2Handler
(token acquisition, caching, threading, refresh, errors), OpenAPIParser
(JSON/YAML, server URLs, security schemes, operations), and
DesktopHttpClient (HTTP methods, headers, query params, auth, cookies,
SSL, response handling) using JUnit 5 + Mockito + AssertJ + MockWebServer.

Splits the bundled junit-jupiter dep into api/params/engine + adds
mockito-junit-jupiter and junit-platform-launcher so the test classes
can use @nested, @ParameterizedTest, and Mockito's JUnit5 integration.
settings.gradle includes both modules but neither had a tracked file, so
git did not track the directories. After ./gradlew clean (or on a fresh
checkout) Gradle aborted with "Configuring project ... without an
existing directory is not allowed".

Adds minimal java-library stubs that build cleanly without an Android
SDK. They will be replaced with com.android.library / com.android.application
once Phase 2 / Phase 3 of NEXT_STEPS.md is picked up.
Adds a Session Handoff section at the top with current branch state,
verified build commands, and a concrete ordered list of immediate next
actions (re-run integration tests → open draft PR → start Phase 2).

Also updates the progress tracker: unit-test coverage, the rebase onto
1.11.1 main, and the placeholder build files for jzswag-android and
jzswag-aaos are now marked complete.
The heart of the Java port: ZswagClient now implements
zserio.runtime.service.ServiceClientInterface so users can write the same
idiom as Python (services.MyService.Client(OAClient(...))) and C++
(MyService::Client(openApiClient)):

    ZswagClient transport = new ZswagClient(openApiUrl);
    Calculator.CalculatorClient calc = new Calculator.CalculatorClient(transport);
    Double r = calc.powerMethod(request);

Built up in layers:

* HTTP config split: HttpConfig (per-request adhoc, mirrors httpcl::Config /
  Python HTTPConfig) and HttpSettings (multi-scope persistent registry,
  mirrors httpcl::Settings). HttpSettingsLoader loads the canonical
  http-settings: - scope: ... YAML so the same file works with all three
  clients.

* OpenAPIParser extended to full spec: x-zserio-request-part on parameters,
  application/x-zserio-object request bodies, root-level default security,
  OAuth2 flows.clientCredentials parsing (rejects other flows), security
  alternatives preserved as List<SecurityRequirement>, format: byte alias,
  style/location validation. PATCH operations are intentionally ignored.

* ParameterEncoder split by location: encodeForPath returns the styled
  string; encodeForQuery returns a list of (name, value) pairs so
  style: form + explode: true correctly emits ?id=1&id=2&id=3;
  encodeForHeader / encodeForCookie return single values.

* ZserioReflection resolves x-zserio-request-part dotted paths against the
  typed zserio request object via JavaBean getter reflection (zserio Java
  has no IReflectableView equivalent), unwraps zserio enums via
  ZserioEnum.getGenericValue(), serializes nested compounds via
  Writer.write(BitStreamWriter).

* DesktopOpenAPIClient.callMethod(methodIdent, zserioRequest) is now the
  canonical entry point; it dispatches via x-zserio-request-part with
  application/x-zserio-object Content-Type and Accept, strict 200-only
  success check, throws on unfilled path placeholders.

* DesktopHttpClient applies HTTP_SSL_STRICT and proxyUrl (previously TODOs),
  scope-merges persistent + adhoc per request, default timeout 60s
  (matching C++).

* Integration test rewritten: uses Calculator.CalculatorClient(zswagClient)
  with no manual extractParameters; all 10 tests pass through the actual
  zswag flow rather than test-harness camouflage.

* Old unit tests removed (they tested the pre-port API surface and would
  not compile against the new types). New tests will land alongside L11.

OAuth2 wiring, full auth completeness, OS keychain, env-var plumbing
remain to be done in subsequent commits.
Completes the parity surface beyond the dispatch core:

* OAuth2Handler rewritten with full feature parity to C++ openapi-oauth.cpp:
  - Process-wide token cache keyed by (tokenUrl, clientId, audience, scope)
    so multiple OAuth2 schemes don't collide; per-key locking serialises
    mint/refresh attempts.
  - Refresh-token reuse on expiry; falls back to fresh mint on refresh
    failure (preserving the old refresh_token if not re-issued).
  - rfc6749-client-secret-basic (default) and rfc5849-oauth1-signature
    (HMAC-SHA256) token-endpoint authentication methods, the latter via a
    new OAuth1Signature port of httpcl::oauth1::*.
  - audience parameter and public-client (client_id-in-body) support.

* DesktopOpenAPIClient.applySecurity walks the OR-of-AND security
  alternatives, picking the first satisfiable one. For OAuth2 schemes it
  resolves tokenUrl/refreshUrl/scopes per the settings-vs-spec precedence
  rules and injects Authorization: Bearer; for API-key schemes it routes
  the merged config's api-key field to header/query/cookie based on the
  scheme's `in:`. Throws a descriptive error if no alternative can be
  satisfied (matches openapi-security.cpp behavior).

* JzswagLogging hooks HTTP_LOG_LEVEL up to logback's root logger
  programmatically (graceful no-op if logback isn't the active SLF4J
  binding). Initialised on every DesktopHttpClient construction.

* New unit tests (55 across 5 classes, all passing):
  - HttpSettingsLoaderTest (16): YAML schema parity with C++/Python,
    including all oauth2 sub-fields, basic-auth/proxy keychain forms,
    legacy list root, scope vs url forms, and validation errors.
  - HttpConfigAndSettingsTest (9): mergedWith multi-valued union,
    other-wins-on-set semantics, OAuth2 sub-field merge, scope glob
    compilation, multi-scope forUrl merging.
  - ParameterEncoderTest (14): style x location x format combinations
    including the previously-broken style:form/explode:true case.
  - ZserioReflectionTest (7): POJO getter resolution, snake_case to
    lowerCamel normalisation, ZserioEnum unwrap to genericValue,
    descriptive error on missing getter.
  - OAuth1SignatureTest (9): RFC 3986 percent-encoding boundaries,
    nonce-length bounds, RFC 5849 signature base string format,
    Authorization header structure.

Integration test still 10/10. Notable parity gaps still open:
HTTP_LOG_FILE rotation, settings-file reload-on-change watchdog,
Windows Credential Manager keychain (Linux secret-tool / macOS
security work today).
The README had grown to 1328 lines covering Python, C++, Java, server, and
generator in one file, with the per-language sections drifting out of
sync — most visibly the Java section, which referenced the pre-port API
and listed Java nowhere in the OpenAPI Options Interoperability matrix.

Restructured to a slim README + focused docs/<lang>.md sub-pages:

* README.md (1328 -> 306 lines): intro/components, ten-line quickstarts
  per language linking to the focused docs, CI/release notes, and the
  full feature interop matrix (now with Java alongside C++ / Python /
  OAServer / zswag.gen).

* docs/java.md (new): canonical Java guide. The
  Calculator.CalculatorClient(new ZswagClient(url)) idiom; HttpConfig
  vs HttpSettings model; persistent + adhoc merge rule; OAuth2 wiring
  including rfc5849-oauth1-signature; how x-zserio-request-part is
  resolved via POJO reflection; environment vars; troubleshooting.
  Replaces the 456-line GETTING_STARTED_JAVA.md (deleted) which
  documented the pre-port API and was misleading.

* docs/python.md (new): client + server usage extracted from README.
* docs/cpp.md (new): client integration + CMake from README.
* docs/openapi-generator.md (new): zswag.gen CLI reference from README.
* docs/http-settings.md (new): the shared HTTP_SETTINGS_FILE YAML format
  in one place — referenced from each language doc, eliminating the
  ~150-line per-language duplication that previously lived in README.

* libs/jzswag-api/README.md (71 -> 23 lines): describes the module's
  role and contents only; usage examples removed (they reference removed
  builder methods anyway). Points at docs/java.md.

* libs/jzswag-desktop/README.md (148 -> 46 lines): module layout +
  dependency list + pointer to docs/java.md. Out-of-date pre-port code
  examples removed.

* libs/jzswag-test/README.md (187 -> 63 lines): test coverage matrix +
  how to run; removed stale "Known Issues" / "Next Steps" sections that
  pre-dated the parity work.

Internal markdown links verified.
@fklebert

fklebert commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@petrhons-tomtom, added you to the review as I remember you may be interested in that.

fklebert and others added 15 commits May 5, 2026 18:06
The jzswag-api module had no tests of its own; HttpConfig and HttpSettings
were only exercised transitively via jzswag-desktop, so JaCoCo reported 0%
coverage for the api jar itself.

Adds a dedicated test source dir with five focused suites — HttpConfig,
HttpSettings, HttpRequest/HttpResponse/HttpException, OpenAPIParameter,
SecurityScheme/SecurityRequirement — covering 394/395 lines.

Also adds the missing junit-platform-launcher runtime dep so the new test
task can start under JUnit 5.
Adds tests for the previously-untested classes that account for most of
the desktop module's line count:

- OpenAPIParserTest — YAML parsing, x-zserio-request-part, OAuth2 flow
  validation, style/location validation, default operationId synthesis,
  PATCH-skip behaviour (0% → 91.4%).
- DesktopHttpClientTest — uses MockWebServer to verify all HTTP methods,
  header/cookie/query/basic-auth merging, per-request header precedence,
  scope-matched persistent settings (0% → 79.2%).
- ZswagServiceClientTest — Mockito-based tests for path construction,
  getter reflection, and exception propagation (0% → 89.7%).
- KeychainTest — exercises the empty-service guard, OS-detection
  branches, and exception forms (0% → 40.9%).
- JzswagLoggingTest — idempotent init paths (0% → 40.0%).
- HttpSettingsLoaderFileEnvTest — file-based loading and env-var/null/
  scalar root handling (76.3% → 85.2%).

Module total now at 723/1167 lines covered.
The "desktop" name was misleading — this module's defining trait is
"uses the JDK 11 java.net.http.HttpClient", which works equally well on
servers, lambdas, CLIs, and desktops. The split that actually matters is
JVM vs Android (which lacks java.net.http and will need OkHttp + Android
Keystore + a different logger). Renaming aligns with the Kotlin/JVM
ecosystem convention of "-jvm" / "-android" artifact suffixes.

This commit only renames the module directory and updates external
references (settings.gradle, sibling-module project deps, CI workflow,
maven artifactId). Java package and class names stay as
com.ndsev.zswag.desktop / Desktop* in this commit and are renamed in
the follow-ups so each step is independently reviewable.
Shifts the package namespace from com.ndsev.zswag.* to io.github.ndsev.zswag.*
to match how the zserio runtime — which jzswag depends on — is published on
Maven Central. Two reasons:

1. zserio publishes as io.github.ndsev:zserio-runtime via Sonatype's
   GitHub-org namespace verification; jzswag uses the same GitHub org
   (ndsev) and is conceptually a sibling project, so the family
   relationship is now visible to consumers.

2. com.ndsev.zswag would have failed Maven Central namespace verification
   anyway: ndsev.com is not the NDS Association's domain, and Sonatype
   requires either DNS proof or a matching GitHub org. io.github.ndsev
   takes the latter route cleanly.

Mechanical change only: every .java file moves from src/.../java/com/ndsev/...
to src/.../java/io/github/ndsev/..., and the package + import declarations
follow. Also updates the root group, mainClass entries in
examples/jzswag-cli and libs/jzswag-test build files.
Final piece of the rename: the io.github.ndsev.zswag.desktop sub-package
becomes io.github.ndsev.zswag.jvm, and the two public classes that
carried the now-stale "Desktop" prefix follow:

- DesktopHttpClient   → JvmHttpClient
- DesktopOpenAPIClient → JvmOpenAPIClient
- DesktopHttpClientTest → JvmHttpClientTest

These are public API but have no external consumers yet (jzswag has not
been published), so a hard rename is preferable to a deprecation period.

The canonical entry point ZswagClient is unchanged — that is the class
end users actually instantiate (Calculator.CalculatorClient(zswagClient)),
so this rename does not affect the typical usage idiom.
Sweep of every doc and javadoc that named the old module, package, or
classes:

- README.md (top-level): module table, Java quickstart Gradle/import snippet
- docs/java.md: intro paragraph, module table, build/import code blocks
- libs/jzswag-jvm/README.md: title, intro, module-layout class names,
  testing command
- libs/jzswag-api/README.md: cross-references to the jvm module
- CLAUDE.md: project guide (module list, build commands, Java entry
  points)
- HttpSettings.java javadoc: cross-reference to HttpSettingsLoader
- JvmHttpClient.java javadoc: opening sentence ("Desktop" → "JVM")
- ExampleCli.java javadoc: opening sentence
Adds a third module for the platform-agnostic core so that an Android
implementation can reuse the same OpenAPI dispatch / parsing / OAuth2 /
keychain-loader logic without duplicating it. New layout:

    jzswag-api      contracts: HttpConfig, HttpSettings, OpenAPIParameter,
                    SecurityScheme, IHttpClient, IKeychain (new), ...
    jzswag-shared   portable core: OpenAPIClient (formerly JvmOpenAPIClient),
                    OpenAPIParser, ParameterEncoder, ZserioReflection,
                    OAuth1Signature, OAuth2Handler, HttpSettingsLoader,
                    ZswagServiceClient
    jzswag-jvm      platform-specific: JvmHttpClient, Keychain, JzswagLogging,
                    ZswagClient (constructs the right HTTP client + keychain)

Required changes to make the core platform-agnostic:

- New IKeychain interface in jzswag-api decouples OAuth2Handler from the
  JVM-specific Keychain class. JvmHttpClient now takes an IKeychain too,
  defaulting to a fresh Keychain instance for back-compat.
- IHttpClient gains a getPersistentSettings() method (default returns
  empty) so OpenAPIClient.mergedConfigFor(url) doesn't have to downcast
  to JvmHttpClient any more.
- OpenAPIClient and OAuth2Handler take an IKeychain in their constructors;
  ZswagClient (jvm) wires up Keychain + JvmHttpClient + OpenAPIClient.
- Static Keychain.load() is removed; only the instance method remains
  (KeychainTest updated accordingly).
- ZswagServiceClient.create() static factories removed — they instantiated
  JvmHttpClient directly (now a layering violation). Constructors stay.

Test counts after the split: api 59, shared 83, jvm 45 (187 total, all
passing). Line coverage: api 99.5%, shared 62.8%, jvm 61.0%.
Replaces the placeholder jzswag-android with a real build setup that
depends on jzswag-shared and on OkHttp / slf4j-android, ready for the
Android-specific implementations to land in subsequent commits.

Trade-off documented in the build file: this module uses the plain
`java-library` plugin instead of `com.android.library`. Reason:

  Google currently ships only x86_64 Linux aapt2 binaries. On aarch64
  Linux build hosts the AGP-driven build fails with "AAPT2 daemon
  startup failed" on `verifyReleaseResources` /
  `processReleaseUnitTestResources`, even for resource-free library
  modules. There is no community aarch64 build of aapt2 either.

Effect of the trade-off:
  - Output is a JAR rather than an AAR (Android consumers can still
    depend on it, just less idiomatically than an AAR);
  - AndroidX dependencies are unavailable (java-library can't consume
    AAR deps), so AndroidKeychain will use the raw Android Keystore
    APIs + AES + SharedPreferences instead of EncryptedSharedPreferences;
  - android.* references compile against `org.robolectric:android-all`
    (a stub jar of the Android framework), with the real framework
    provided at runtime by the consuming app.

On an x86_64 build host the module can be flipped back to
`com.android.library` for proper AAR output with no source changes.

Other plumbing in this commit:
  - AGP classpath bumped from 8.2.2 → 8.7.2 (kept for the future flip
    back to `com.android.library`; harmless no-op while we are on
    `java-library`).
  - Root `gradle.properties` enables `android.useAndroidX=true` (still
    needed if someone flips the plugin back) and bumps Gradle daemon
    JVM heap to 2 GB.
  - `.gitignore` adds `local.properties`, `*.aar`, `*.apk`, `.cxx/`.
The Android counterpart to JvmHttpClient. Mirrors its behaviour exactly
so a request configured the same way produces the same wire-level traffic
on either platform:

- persistent HttpSettings (URL-scope-matched) merged with the per-call
  adhoc HttpConfig;
- per-request headers (case-insensitive) suppress duplicate merged-config
  entries — prevents OkHttp from emitting double Authorization / Cookie
  headers when both layers configure them;
- basic-auth resolved from cleartext password OR an injected IKeychain
  (no static Keychain fallback like the JVM version had);
- per-URL proxy config builds a one-shot OkHttpClient with a
  proxyAuthenticator (matches JvmHttpClient's "rare path" approach);
- HTTP_SSL_STRICT env var + HttpConfig.isSslStrict() drive a
  TrustEverythingManager when relaxed mode is required;
- HTTP_TIMEOUT env var sets connect / read / write timeouts (default 60s,
  matching the C++/JVM clients).

Removes the BuildMarker placeholder.
…Preferences)

The Android counterpart to the JVM's Keychain. Implements IKeychain so
OAuth2Handler and AndroidHttpClient consume both interchangeably.

Storage strategy:

- A symmetric AES-256-GCM key is generated in the platform Keystore on
  first use, aliased "io.github.ndsev.zswag.keychain.master". The key
  never leaves the secure hardware (TEE / StrongBox where available); we
  only ever hold a Cipher handle.
- Per-credential entries (one per service|user pair) are encrypted with
  that key and stored in a private SharedPreferences file. The on-disk
  blob is base64(iv_len_byte | iv | ciphertext_with_gcm_tag).

Public API:

- load(service, user)  — IKeychain contract, throws if entry absent.
- store(service, user, secret)  — for app-side onboarding.
- delete(service, user).

Why not androidx.security:security-crypto / EncryptedSharedPreferences?
That library is distributed as an AAR, which the java-library-based build
of this module cannot consume (see this module's build.gradle for the
aapt2-on-arm trade-off). Doing the AES/GCM dance manually keeps us inside
Java APIs that work both at compile time (against the Robolectric
android.jar stub) and at runtime (on a real device).

Will get full unit-test coverage in the upcoming Robolectric-tests commit.
Completes the Android module's user-facing API surface:

- AndroidLogging.init() — symmetric to JzswagLogging.init() but a near-noop:
  on Android, log filtering is controlled by logcat tag levels (setprop
  log.tag.<TAG>), not programmatically by the application. We surface
  HTTP_LOG_LEVEL once if set so the developer can confirm the value the
  JVM modules would have used.

- ZswagClient — implements zserio's ServiceClientInterface; the only
  public-API difference from the JVM port is a Context parameter on the
  convenience constructors (needed so AndroidKeychain can reach
  SharedPreferences for credential storage). After construction, the
  call-site is identical to the JVM port:

      ZswagClient transport = new ZswagClient(context, openApiUrl);
      Calculator.CalculatorClient calc = new Calculator.CalculatorClient(transport);
      Double r = calc.powerMethod(new BaseAndExponent(...));
Adds three test classes covering the largest part of the Android port:

- AndroidHttpClientTest (17 tests) — full coverage via OkHttp's
  MockWebServer, mirroring JvmHttpClientTest. AndroidHttpClient happens
  to be a pure-Java class (only OkHttp + java.net + javax.net.ssl, no
  android.* refs), so plain JUnit + MockWebServer is sufficient.

- AndroidKeychainTest (5 tests) — input validation + missing-entry
  paths, using Mockito to fake Context / SharedPreferences. The
  encrypt/decrypt round trip and the platform Keystore key generation
  need either Robolectric or an Android device; tracking that as a
  follow-up gap (see below).

- AndroidLoggingTest (2 tests) — exercises the HTTP_LOG_LEVEL-unset
  path of init(); the env-var-set branch routes through android.util.Log
  and needs a device to run.

- ZswagClientTest (4 tests) — uses a mock OpenAPIClient to exercise
  delegation, ZserioError wrapping, and the missing-zserio-object guard.
  The Context-taking convenience constructors are tested only via
  device instrumentation tests (out of this PR's scope).

Why no Robolectric: Robolectric pulls in Conscrypt for SSL, which has
no aarch64-linux-native binary. On the aarch64 Linux build host this
fails with UnsatisfiedLinkError before any test code runs. Robolectric
also requires androidx.test:monitor — distributed only as an AAR which
the java-library plugin cannot consume directly. On an x86_64 host
both restrictions go away and the suite can be expanded to cover
AndroidKeychain's encrypt/decrypt path and AndroidLogging's
log-level-routing path.

Build wiring needed for the test classpath:
  - testImplementation 'org.robolectric:android-all' so test sources
    can import android.content.Context for Mockito mocks (Mockito
    intercepts calls so the stub's "Stub!" method bodies don't matter).
  - testRuntimeClasspath excludes 'uk.uuid.slf4j:slf4j-android' so
    the JVM-side test runtime uses logback-classic (slf4j-android
    references android.util.Log at class-load time and won't load on
    plain JVM).

Coverage summary (line, all modules):
  api      99.5%   shared   62.8%   jvm      61.0%   android  64.3%
fklebert added 5 commits May 15, 2026 16:09
The PNG (and its StarUML .mdj source) was generated before the Java
port and didn't show any of the four jzswag modules. It also drifts
silently — there's no CI gate that the image matches the code.

Mermaid renders natively on GitHub and is text-editable, so the diagram
is maintained alongside the code it documents.

The new diagram shows:
* the four client implementations (Python, C++, Java JVM, Java Android)
* the C++ core (zswagcl, httpcl) shared with Python via pybind11
* the Java core (jzswag-shared, jzswag-api) shared by both Java
  platform clients
* OAServer (Python only, the lone server today)
* the OpenAPI spec as the common contract

Removed docs/assets/zswag-architecture.{png,mdj}; the asset dir is
empty so it's gone too.
Default Mermaid theme on GitHub renders fairly flat. Switching to the
'base' theme + per-language classDef styling produces visibly distinct
node colors (Python green, C++ blue, Java orange, spec yellow) and a
cleaner system-font label rendering — closer to the look of the old
hand-drawn PNG, but still text-editable and free of external tooling.

Solid arrows for "calls into", dotted arrows for "reads / exposes" the
spec, separating dependency direction from data flow.
Mermaid's default dagre renderer produces meandering bezier-ish edges
and uneven node placement. Switching to the elk renderer enables
orthogonal edge routing (90-degree turns) and tighter alignment. The
`curve: 'stepAfter'` setting forces step edges as a fallback in
environments that don't ship elk.

Also normalised node label lengths to 3 lines each so boxes come out
closer to the same width.
The elk renderer was packing too tightly — Java core subgraph border
sliced through jzswag-shared, OAServer floated alone outside any
group, and five "reads" + one "exposes" dotted arrows piled up around
the spec node making it look interrogated.

Rework:
* drop elk, keep stepAfter curves on default dagre (which handles
  this graph shape better)
* fold spec-reading/exposing into the spec node's label, drop the 6
  redundant arrows
* spec now sits at the top with two clean arrows down to clients
  and server subgraphs
* OAServer moves into its own Server subgraph for symmetry with
  Clients
* more nodeSpacing + rankSpacing + padding so the cluster borders
  don't clip node text
Comparing the Mermaid to the recovered original PNG surfaced gaps:

* zswag.gen — the Python CLI that generates the OpenAPI spec from a
  zserio service — was missing entirely.
* External libraries (cpp-httplib, keychain, Flask/Connexion, OkHttp/
  Android Keystore) were shown as small boxes in the original; only
  named in prose here.
* zserio itself wasn't visible as the foundation it actually is.
* The "Python reuses C++ via pybind11" relationship was implicit in the
  py --> zswagcl arrow but not labelled.

Added:
* zserio --> gen --> spec --> {clients, server} flow at the top
* External libraries subgraph at the bottom with arrows from the
  cores/server that use them
* Cluster label "C++ core (shared with Python via pybind11)" now
  describes the binding explicitly
@fklebert

Copy link
Copy Markdown
Contributor Author

We should rename ZswagClient to something matching the C++/Python clients. Current suggestion during internal discussions was OpenApiClient (in case this is not already taken by any open lib that we are using or anything)

fklebert added 14 commits May 18, 2026 10:40
PR review feedback: the user-facing Java client should match the C++
and Python user-facing client names. C++ has 'zswagcl::OAClient', Python
has 'zswag.OAClient' — both are the typed zserio service client that
implements the canonical idiom 'MyService.Client(transport)'.

Java's ZswagClient is semantically identical (implements
zserio.runtime.service.ServiceClientInterface, takes a spec URL,
delegates to OpenAPIClient) so the renaming is a pure naming
alignment. No behaviour change.

Renamed:
* libs/jzswag/jzswag-jvm/.../ZswagClient.java     -> OAClient.java
* libs/jzswag/jzswag-android/.../ZswagClient.java -> OAClient.java
* libs/jzswag/jzswag-android/.../ZswagClientTest.java -> OAClientTest.java
* All references updated across docs/java.md, the architecture
  diagram in README, the per-module READMEs, integration test client.
…++ parity

PR review feedback: align Java's low-level dispatch-class name with
C++'s zswagcl::OpenApiClient. The Java OPP-ACCRONYM-IN-CAPS convention
(URLConnection, XMLParser) is fine but here the C++ counterpart uses
camelCase 'OpenApiClient' so matching exactly removes a casing-only
inconsistency between languages.

Renamed:
* libs/jzswag/jzswag-api/.../IOpenAPIClient.java          -> IOpenApiClient.java
* libs/jzswag/jzswag-shared/.../OpenAPIClient.java        -> OpenApiClient.java
* libs/jzswag/jzswag-shared/.../OpenAPIClientSecurityTest -> OpenApiClientSecurityTest

All references updated across both platform OAClient impls,
docs/java.md, jzswag-jvm README, jzswag-shared README.
Restores the gh-pages deploy that 3609041 ('Simplify coverage workflow')
removed, and extends it to cover Java JaCoCo HTML too. After this:

  https://ndsev.github.io/zswag/         landing page
  ├── cpp/                               gcovr report (httpcl + zswagcl)
  └── java/
      ├── api/                           JaCoCo — jzswag-api
      ├── shared/                        JaCoCo — jzswag-shared
      ├── jvm/                           JaCoCo — jzswag-jvm
      └── android/                       JaCoCo — jzswag-android

Implementation:
* coverage.yml: stages site/cpp/ from gcovr output + writes the root
  index.html. Adds an index.html redirect inside cpp/ so gcovr's
  coverage.html entrypoint works from the directory URL.
* jzswag.yml: stages site/java/<module>/ from each JaCoCo html dir
  and writes site/java/index.html.
* Both workflows: peaceiris/actions-gh-pages@v4, publish_dir: site,
  keep_files: true so they coexist on the gh-pages branch.
* Triggers now listen on push to main AND master (was master only —
  the deploy guard was unreachable since main is the default branch).
* docs/cpp.md and docs/java.md re-add the public URL link.

Requires one-time admin action (out of band): set GitHub Pages source
to the gh-pages branch via repo settings. Currently configured to
serve from main / which is why the path 404s today.
The C++ and Python clients already accept a serverIndex / server_index
parameter to choose between multiple entries in the OpenAPI spec's
servers[] array (issue #113, closed in 1.7.0). The Java port was
hard-coded to servers.get(0), breaking the parity claim in the PR
description.

Adds the parameter to all three relevant constructors:

  * OpenApiClient(spec, http, adhoc, keychain, serverIndex)
  * jvm/OAClient(url, persistent, adhoc, serverIndex)
  * android/OAClient(context, url, persistent, adhoc, serverIndex)

Existing constructors keep the implicit serverIndex=0 default.
Negative values raise IllegalArgumentException; out-of-bounds values
raise IOException with a descriptive message during construction.
Index 0 stays valid when servers[] is empty (per OpenAPI 3.0+ §4.7.5
the empty array implies [{ "url": "/" }]).

Tests in OpenApiClientServerIndexTest cover default behaviour,
explicit index selection across three servers, both error paths, and
the implicit-root edge case.

README and docs/java.md updated with code examples and the matrix
entry distinguishing 'URL pickup' from 'multi-server selection'.
OpenApiClient.dispatch passed PATH-located parameter values through
ParameterEncoder.urlEncode, which is application/x-www-form-urlencoded
semantics. That mangles the sub-delimiters used by OpenAPI path styles:

  Form encoded     vs.   RFC 3986 path-segment
  ';id=42' -> '%3Bid%3D42'  vs.  ';id=42'  (matrix)
  '.42'    -> '.42' (kept)  but  '.1.2'   -> '%2E1%2E2'  in some JDK impls
  ',a,b'   -> '%2Ca%2Cb'    vs.  ',a,b'   (simple array, comma-joined)

The form encoder also emits '+' for space; RFC 3986 wants %20.

Add ParameterEncoder.pathEncode that keeps unreserved + sub-delims
(! $ & ' ( ) * + , ; =) and the pchar additions (: @) verbatim, only
percent-encoding bytes outside that set. Matches C++ httpcl
URIComponents::appendPath behaviour (uri.cpp:337).

Swap the OpenApiClient PATH branch to use pathEncode. Other locations
(QUERY, HEADER, COOKIE) keep their existing encoders.

Tests cover matrix/label preservation, the full pchar set staying
verbatim, segment separators ('/', '?', '#') and space being escaped,
and UTF-8 byte-by-byte percent-encoding.

ParameterEncoderTest: 14 -> 18.
* HTTP/Basic security accepts a pre-set Authorization: Basic header
  (match C++ openapi-security.cpp:22-37). A user who configures their
  own static Authorization header via HttpConfig.header() no longer
  gets a misleading "no basic-auth configured" error when the spec
  requires HTTP Basic.

* OAuth2 spec-fetch fail-mode aligned with C++
  (openapi-oauth.cpp:283-345): when useForSpecFetch=true but
  oauth2.tokenUrl is unset (or mint fails), log a warning and continue
  the spec fetch unauthenticated. Previously Java refused to construct
  the client at all. The downstream OpenAPIParser request will surface
  the real failure if the spec endpoint actually requires auth.

* openIdConnect security scheme rejected at parse time
  (match C++ openapi-parser.cpp). Previously Java accepted the scheme
  and only refused at dispatch, meaning a Java app could construct a
  client against a spec that C++ would reject up-front.

* OAuth2 token cache switched to monotonic clock (System.nanoTime,
  matching C++ std::chrono::steady_clock at openapi-oauth.cpp:56).
  Wall-clock jumps from NTP slews or manual time changes no longer
  retroactively expire valid tokens or extend the lifetime of an
  expired one.

Tests updated:
* OpenApiClientSecurityTest.useForSpecFetchWithoutTokenUrlFallsThroughToUnauthFetch
* OpenAPIParserTest.openIdConnectSchemeRejectedAtParseTime
JDK 11 HttpClient (unlike cpp-httplib and OkHttp) does NOT auto-decompress
gzip-encoded responses. A server that opportunistically gzips
application/x-zserio-object responses left the JVM client looking at raw
gzip bytes — and zserio deserialization saw garbled input.

JvmHttpClient now inspects the Content-Encoding header on responses and
transparently unwraps gzip via GZIPInputStream. Other encodings
(brotli, zstd) are not handled — neither cpp-httplib nor OkHttp's default
config handles them either, so this matches the existing parity surface.

If decompression fails, we log a warning and return the original
(compressed) bytes so the caller can still introspect the problem.

Test in JvmHttpClientTest uses MockWebServer + GZIPOutputStream to
verify the round-trip ends up with the original payload.
C++ wires both env vars to a rotating file appender (log.cpp:35-51);
Java's JzswagLogging only honoured HTTP_LOG_LEVEL with an in-code TODO
acknowledging the gap.

JzswagLogging.init now reads HTTP_LOG_FILE and, if non-empty, attaches
a logback RollingFileAppender to the root logger with:

  * a FixedWindowRollingPolicy (3-file window: FILE / FILE-1 / FILE-2),
    mirroring the C++ rotation scheme
  * a SizeBasedTriggeringPolicy with maxFileSize from HTTP_LOG_FILE_MAXSIZE
    (default 1 GB, matches C++)
  * a PatternLayoutEncoder using a layout close to C++'s default so the
    rendered lines are similar (timestamp / thread / level / logger / msg)

All logback construction is reflective so the JVM module doesn't gain a
compile-time logback dependency. Falls back to a stderr note + best-effort
continue when the active SLF4J binding isn't logback. The earlier TODO
comment is now gone.

Behavioural parity with C++ for log rotation. Unit tests for this path
require a real filesystem write — covered transitively by integration
testing on the calc harness; no new unit test added (rotation hits
real I/O timing).
JvmHttpClient and AndroidHttpClient were constructing a fresh JDK
HttpClient (resp. OkHttpClient) on every request when the merged
HttpConfig selected an HTTP proxy. Both have a non-trivial setup cost
— JDK HttpClient spawns a new executor; OkHttp loses its connection
pool and dispatcher reuse. OkHttp's own docs explicitly recommend
'one client per process'.

Cache the per-proxy clients keyed on host:port|strict|permissive in
a ConcurrentHashMap so concurrent requests through the same proxy
reuse the same underlying client.

No behavioural change for callers; just removes a per-request setup
cost that scaled badly on proxied deployments.
C++ HttpSettings::operator[] checks the source file's mtime on every
call and re-parses on change (http-settings.cpp:520-543) — supports
credential rotation in long-running clients without restart. Java's
HttpSettings was immutable and never reloaded, so a rotated token in
the YAML file would never be picked up.

Adds HttpSettingsLoader.HotReloader: a thread-safe wrapper around an
HttpSettings snapshot + optional source Path. Each current() call
stat()s the file once and reloads via loadFromFile if mtime advanced.

Plumbed through JvmHttpClient and AndroidHttpClient:
* The no-arg constructor (which reads HTTP_SETTINGS_FILE from env)
  now keeps the source path around for hot reload.
* The HttpSettings-taking constructors store a no-source HotReloader
  (no reload — caller-supplied snapshot).
* getPersistentSettings() and the per-request merge call go through
  reloader.current() so spec-fetch and dispatch see the same value.

Failed reloads keep the previous snapshot rather than dropping to
empty (better than losing all credentials mid-flight). Broken YAML
records the mtime so the same broken file isn't reparsed on every
request.

HotReloaderTest covers: initial load, no-change → identity reuse,
mtime-advance → reload, broken YAML → keep-prev, null-source → no-op.
Mirrors C++ Settings::store (http-settings.cpp:484) so tooling can
update credentials programmatically and re-write HTTP_SETTINGS_FILE.
With the HotReloader on the active HTTP client, the new contents are
picked up automatically on the next request — supports rotation
workflows without restart.

The emitter:
* Round-trips through HttpSettings → POJO tree → SnakeYAML Dumper
  (block-style, 2-space indent).
* Omits empty optional fields (no spurious empty basic-auth / proxy /
  oauth2 blocks).
* Flattens single-value headers/query/cookies; preserves list form
  for multi-valued.

Round-trip test verifies a settings object survives writeToFile +
loadFromFile with the relevant fields intact. Minimal-config test
asserts no empty blocks pollute the output.
Two final parity-audit items from the same pass:

PARAMETER-ENCODER MAP SUPPORT
=============================

C++ openapi-parameter-helper handles map-typed parameter values across
all four locations and styles (openapi-parameter-helper.cpp:140-205).
Java's ParameterEncoder previously only supported scalars and arrays;
a Map value silently passed through String.valueOf(...) and emitted
something like "{R=1, G=2}" — server-side dispatch failed without
clear error.

Adds Map handling to encodeForPath, encodeForQuery, encodeForHeader,
encodeForCookie. Style × explode behaviour matches C++:

  query/form, explode=true   ?R=1&G=2
  query/form, explode=false  ?color=R,1,G,2
  path/simple                R,1,G,2
  path/label, explode=true   .R=1.G=2
  path/matrix, explode=true  ;R=1;G=2
  path/matrix, explode=false ;color=R,1,G,2
  header/simple              R,1,G,2
  cookie                     R,1,G,2

No caller produces a Map today (ZserioReflection only emits scalars
and arrays), so this is preparation for a future Java-side
IReflectableView equivalent — but it removes the silent-failure trap
in the meantime.

Tests cover the five primary encoding shapes against deterministic
LinkedHashMap inputs.

WINDOWS KEYCHAIN — documented explicitly
========================================

The C++ httpcl library supports Windows credential manager via the
`keychain` C library (DPAPI). The Java JVM client throws
KeychainException with a previously cryptic message. Now:

* Code: KeychainException message tells the user explicitly that
  Windows isn't supported in Java, names the workarounds (cleartext
  password: or HttpConfig.basicAuth), and notes that C++/Python DO
  support it (so the gap is clearly Java-specific).
* README: keychain table gains C++/Python and Java columns; the
  Java cell explains the limitation and points at workarounds.
* docs/java.md: matching one-line note in the auth section.
* libs/jzswag/jzswag-jvm/README.md: same.

Implementation is out of scope for this PR — needs JNA → DPAPI or
shell-out to cmdkey/vaultcmd. Tracked separately.

Full Java test sweep: 246 tests, 0 failures (was 229 at the start
of the parity audit cycle).
A second-pass parity audit surfaced 5 real issues with the previous
fixes — addressing all before merge.

HOTRELOADER BYPASS VIA OACLIENT
================================
OAClient(String) called HttpSettingsLoader.loadFromEnvironment() and
passed the resulting snapshot to JvmHttpClient(persistent, keychain) —
which constructed a HotReloader with null source, defeating hot-reload
in the most common usage path. Fix: route the env-driven OAClient ctor
through HotReloader.fromEnvironment() so file mtime changes are
picked up on the next request, matching the C++ Settings::operator[]
behaviour. Same change on the Android side.

SPEC FETCH BYPASSED IHTTPCLIENT
================================
OpenAPIParser.loadSpec used raw URLConnection for HTTP(S) spec URLs,
ignoring HTTP_SSL_STRICT, proxy, basic-auth, HTTP_TIMEOUT, and any
persistent headers/cookies/query from http-settings.yaml. C++ routes
through httpcl::IHttpClient (openapi-parser.cpp:499); Java did not.

Adds OpenAPIParser(specLocation, IHttpClient, HttpConfig, extraHeaders)
which builds an HttpRequest and dispatches via the configured client.
OpenApiClient.parseSpec uses this constructor; the OAuth2 useForSpecFetch
Bearer is passed as an extra header instead of via a URLConnection
injector. Local-file specs continue to read straight from the filesystem.

Regression test: OpenApiClientSecurityTest.specFetchRoutesThroughConfiguredIHttpClient
asserts the spec body actually flows through the stub IHttpClient.

HTTP_TIMEOUT CONSISTENCY
========================
HTTP_TIMEOUT was applied to the JDK HttpClient's connect timeout but
not the per-request timeout — that one used HttpConfig.defaultTimeout()
(hardcoded 60s). C++ uses one value end-to-end.

Adds HttpConfig.getTimeoutOrNull() so transports can distinguish
"caller explicitly set 60s" from "caller didn't touch it." JvmHttpClient
and AndroidHttpClient now fall back to the HTTP_TIMEOUT-derived
default for the latter case.

GZIP RESPONSE HEADERS
=====================
After auto-decompression, the returned headers still carried the
original Content-Encoding: gzip and Content-Length (now wrong) — caller
inspection got a stale view. Strip both headers post-decompression.
Test updated to assert their absence.

STALE DOCS
==========
docs/java.md said HTTP_LOG_FILE was "not yet wired in Java" — it was
wired in commit 80fc7fa. docs/java.md said HTTP_SSL_STRICT=0 disables
strict — but the code (per commit b06f699) treats any non-empty value
as enabled. README's HTTP_LOG_FILE row carried the same staleness.
Both fixed.

Java tests: 246 -> 247 (added one for spec-fetch routing); 0 failures.

@josephbirkner josephbirkner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome 💯

@github-actions

Copy link
Copy Markdown
Package Line Rate Branch Rate Health
libs.httpcl.include.httpcl 81% 50%
libs.httpcl.src 85% 53%
libs.zswagcl.include.zswagcl.private 29% 14%
libs.zswagcl.src 80% 46%
Summary 78% (1686 / 2149) 49% (2036 / 4191)

Minimum allowed line rate is 65%

@josephbirkner josephbirkner changed the base branch from main to release/2.0.0 June 19, 2026 14:46
@josephbirkner josephbirkner merged commit 8c61997 into release/2.0.0 Jun 19, 2026
28 checks passed
@josephbirkner josephbirkner deleted the jzswag branch June 19, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants