Skip to content

Release 2.0.1 - #163

Merged
josephbirkner merged 80 commits into
mainfrom
release/2.0.0
Jul 6, 2026
Merged

Release 2.0.1#163
josephbirkner merged 80 commits into
mainfrom
release/2.0.0

Conversation

@josephbirkner

@josephbirkner josephbirkner commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Release 2.0.1

Major zswag release for the upcoming MapViewer 2026.3.1 dependency chain.

v2.0.0 was already tagged and published from an earlier release-branch commit. Since PyPI versions are immutable, the finalized release branch is prepared as v2.0.1 instead.

Release Notes

What's Changed

Python/C++ transport

  • Replaced the Python native HTTP transport with the libcurl-based client, including HTTP/2 support.
  • Added a multiplexing regression test proving concurrent requests share a single HTTP/2 TLS connection.
  • Fixed a Windows shutdown crash in the libcurl completion path after HTTP/2 transfers.
  • Added environment-variable references in HTTP settings.
  • Improved Python API documentation/docstrings for both native and pure-Python zswag APIs.

Java client

  • Added the jzswag Java client stack and brought it to parity with the C++/Python OpenAPI client surface.
  • Added Java support for relative OpenAPI servers[].url handling.
  • Added Java HTTP settings, OAuth2/authentication, keychain/error handling, hot reload, logging, gzip handling, Android/JVM split, and coverage/test infrastructure.

Docs and release infrastructure

  • Restructured docs into focused language pages.
  • Documented server URL forms, shared HTTP settings, CI/CD, and release process.
  • Updated C++ integration examples to use v2.0.1.

Important: v2.0.0 was already tagged/published from an earlier release-branch commit. This release supersedes it and contains the final release-branch fixes.

Full Changelog: v1.14.0...v2.0.1

TODOs

  • Land the remaining zswag 2.0.x feature work.
  • Finalize the zswag 2.0.1 release notes.
  • Verify PR CI is green.
  • Merge this PR.
  • Tag and publish v2.0.1 after merge.

fklebert and others added 30 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.
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%
Documents the new shared-and-platform split throughout the project:

- README.md: Components table now lists all four Java modules with their
  roles. Quickstart split into JVM and Android variants (the only
  public-API difference is the Context parameter on the Android
  ZswagClient constructor).
- docs/java.md: full module table, dual-platform code samples.
- CLAUDE.md: per-module bullet list, build commands run all four module
  tests, "Working with the Java client" points reviewers at jzswag-shared
  for changes that affect parity with C++/Python, plus a note about the
  Android-on-aarch64 build trade-off.
- libs/jzswag-shared/README.md: new file documenting the shared core.
- libs/jzswag-android/README.md: new file with full build trade-off
  rationale + pointers to the platform-specific bits.
- libs/jzswag-jvm/README.md: trimmed to JVM-specific content; cross-
  platform pieces now point to jzswag-shared.
- libs/jzswag-api/README.md: refreshed to reflect that all three
  downstream modules consume from here, plus the new IKeychain interface.

CI workflow:
- Build & test runs all four Java modules now (api, shared, jvm,
  android) plus the existing jzswag-test:assemble.
- Coverage upload covers all four jacocoTestReport.xml files; min-
  coverage threshold ratcheted from 25 → 60 to match the parity goal
  (every module ships ≥60% line coverage).
- JaCoCo HTML artifact pattern broadened to libs/jzswag-*/...
Maintainer feedback: the five jzswag-* modules at the libs/ root pollute
the directory next to the C++/Python siblings (httpcl, zswagcl, zswag,
pyzswagcl). Group them under a single libs/jzswag/ folder so the layout
becomes:

    libs/
      httpcl/  zswagcl/  zswag/  pyzswagcl/      # existing C++/Python
      jzswag/
        jzswag-api/  jzswag-shared/  jzswag-jvm/
        jzswag-android/  jzswag-test/

Module names keep the jzswag- prefix inside the new folder so they line
up 1:1 with their Maven artifactIds (jzswag-api, jzswag-jvm, ...) — no
publication coordinates change.

Mechanical updates only:
- settings.gradle: include 'libs:jzswag:jzswag-X' instead of 'libs:jzswag-X'
- every project(':libs:jzswag-X') → project(':libs:jzswag:jzswag-X')
- jzswag-test/build.gradle: zserioSourceRoot path adjusted by one level
- test-java-client.bash: project_root computation adjusted by one level
- CI workflow: artifact globs and JaCoCo paths point at libs/jzswag/...
- README.md, CLAUDE.md, docs/java.md, per-module READMEs: paths updated

Java packages and Maven artifactIds are unchanged. All four module test
suites still pass (218 tests, ≥60% line coverage on each).
Both noticed during a doc-audit pass:

- HttpSettings javadoc said HttpSettingsLoader lives in jzswag-jvm, but
  it moved to jzswag-shared during the shared-extraction commit so that
  jzswag-android could reuse it.
- IKeychain javadoc claimed AndroidKeychain uses EncryptedSharedPreferences
  — it doesn't; we explicitly chose raw Android Keystore + manual AES-GCM
  + plain SharedPreferences because EncryptedSharedPreferences is an AAR
  dep we can't consume from the java-library plugin (see
  jzswag-android/build.gradle for the full trade-off).
fklebert and others added 13 commits May 18, 2026 23:27
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.
The OpenAPI Options Interoperability section showed servers as a
single ✔️ row with an example using only the historically-supported
form (absolute URL with path prefix). Expand to show all three forms
from OpenAPI 3.0+ (clarified in 3.2.0 §4.5.2.1) with concrete examples,
and split the matrix row to make clear which forms are supported per
client.

The 'document-relative' row is marked n/a for OAServer and zswag.gen
since neither consumes servers[].url at runtime — OAServer routes
based on operation paths, zswag.gen emits whatever the user supplies.
Rewrites OpenApiClient.resolveBaseUrl to use java.net.URI.resolve,
which natively implements RFC 3986 §5.3 reference resolution.

Previous implementation handled only:
  - Empty server URL  (used spec origin)
  - URL starting with '/' (path-only, combined with spec origin)
  - Absolute URL (returned as-is)

It silently broke for document-relative forms ('.', './v2', '../v2',
bare 'v2') — the else-branch returned them unchanged, producing
nonsense like "." concatenated with the operation path at request time.

The new implementation:
  * Converts spec location to java.net.URI (http(s)://, file://, or local
    path -> file URI)
  * Converts server URL to java.net.URI as a reference
  * Returns specBase.resolve(serverRef)

Works for all three URL forms against both HTTP and local-file spec
locations. The earlier "absolute server URL with local-file spec"
behaviour is preserved via an explicit check (avoids producing a
file:// base URL when the server URL is absolute).

OpenApiClientBaseUrlTest covers each reference form via direct URI
resolution (decoupling the test from the spec-fetch path) plus two
end-to-end tests against a real OpenApiClient using a temp-file spec.

Closes #159 (Java side; C++/Python handled in the preceding commit).
codecov/patch on PR #160 flagged 36.36% patch coverage on
OpenApiClient.java because the previous test class verified
java.net.URI.resolve() in isolation — the production method's
branches (file:// fallback, URISyntaxException paths, etc.) weren't
hit by any test.

Refactor:
* Extract resolveBaseUrl(specLocation, serverUrl) as a static
  package-private helper. The instance method delegates to it.
* OpenApiClientBaseUrlTest now calls the static helper directly,
  exercising every branch including the file:// + relative warning,
  the malformed-URI path, and the file URI + document-relative case.
* Keep one end-to-end test that constructs a real OpenApiClient from
  a temp-file spec so the wiring stays verified.

Tests in this class: 9 -> 13. All 236 Java tests still passing.
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Java Coverage (api / shared / jvm / android)

Overall Project 72.64% -27.36% 🍏
Files changed 72.64% 🍏

Module Coverage
jzswag-api 95.79% -4.21% 🍏
jzswag-shared 76.14% -23.86% 🍏
jzswag-android 58.83% -41.17% 🍏
jzswag-jvm 45.15% -54.85%
Files
Module File Coverage
jzswag-api SecuritySchemeType.java 100% 🍏
ParameterFormat.java 100% 🍏
ParameterStyle.java 100% 🍏
HttpSettings.java 100% 🍏
HttpException.java 100% 🍏
ParameterLocation.java 100% 🍏
SecurityScheme.java 100% 🍏
HttpResponse.java 100% 🍏
SecurityRequirement.java 100% 🍏
HttpRequest.java 99.13% -0.87% 🍏
OpenAPIParameter.java 98.51% -1.49% 🍏
HttpConfig.java 94.29% -5.71% 🍏
IHttpClient.java 0%
KeychainException.java 0%
jzswag-shared OAuth1Signature.java 97.41% -2.59% 🍏
OpenAPIParser.java 91.79% -8.21% 🍏
HttpSettingsLoader.java 79.46% -20.54% 🍏
OAuth2Handler.java 78.42% -21.58% 🍏
ZserioReflection.java 67.82% -32.18% 🍏
ParameterEncoder.java 65.44% -34.56% 🍏
OpenApiClient.java 61.4% -38.6% 🍏
jzswag-android AndroidHttpClient.java 73.2% -26.8% 🍏
AndroidLogging.java 67.5% -32.5% 🍏
OAClient.java 35.14% -64.86%
AndroidKeychain.java 25.57% -74.43%
jzswag-jvm JvmHttpClient.java 73.77% -26.23% 🍏
Keychain.java 53.99% -46.01% 🍏
JzswagLogging.java 5.5% -94.5%
OAClient.java 0%

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.79281% with 702 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.79%. Comparing base (fc5924c) to head (f57db65).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
...va/io/github/ndsev/zswag/shared/OpenApiClient.java 49.30% 82 Missing and 28 partials ⚠️
.../java/io/github/ndsev/zswag/jvm/JzswagLogging.java 10.00% 78 Missing and 3 partials ⚠️
...io/github/ndsev/zswag/shared/ParameterEncoder.java 58.82% 48 Missing and 29 partials ⚠️
.../github/ndsev/zswag/shared/HttpSettingsLoader.java 73.77% 36 Missing and 28 partials ⚠️
.../java/io/github/ndsev/zswag/jvm/JvmHttpClient.java 70.71% 42 Missing and 11 partials ⚠️
.../github/ndsev/zswag/android/AndroidHttpClient.java 69.33% 35 Missing and 11 partials ⚠️
...io/github/ndsev/zswag/android/AndroidKeychain.java 27.41% 43 Missing and 2 partials ⚠️
...va/io/github/ndsev/zswag/shared/OpenAPIParser.java 81.19% 22 Missing and 22 partials ⚠️
...va/io/github/ndsev/zswag/shared/OAuth2Handler.java 67.20% 23 Missing and 18 partials ⚠️
...ain/java/io/github/ndsev/zswag/api/HttpConfig.java 84.25% 1 Missing and 33 partials ⚠️
... and 11 more
Additional details and impacted files
@@              Coverage Diff              @@
##               main     #163       +/-   ##
=============================================
+ Coverage     39.97%   66.79%   +26.81%     
- Complexity        0      484      +484     
=============================================
  Files            20       29        +9     
  Lines          1936     2114      +178     
  Branches       1142      417      -725     
=============================================
+ Hits            774     1412      +638     
- Misses          404      509      +105     
+ Partials        758      193      -565     
Flag Coverage Δ
unittests ?
unittests-java 66.79% <66.79%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@josephbirkner
josephbirkner marked this pull request as ready for review July 6, 2026 13:34
@josephbirkner josephbirkner changed the title Release 2.0.0 Release 2.0.1 Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Package Line Rate Branch Rate Health
libs.httpcl.include.httpcl 81% 50%
libs.httpcl.src 73% 48%
libs.zswagcl.include.zswagcl.private 29% 14%
libs.zswagcl.src 80% 46%
Summary 74% (1762 / 2395) 46% (2111 / 4571)

Minimum allowed line rate is 65%

@josephbirkner
josephbirkner merged commit 52c103c into main Jul 6, 2026
28 checks passed
@josephbirkner
josephbirkner deleted the release/2.0.0 branch July 6, 2026 15:05
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