NaHAL is a graphical navigator for HAL (Hypertext Application Language) APIs, plus the navigation layer it is built on, for Kotlin Multiplatform. Point it at a HAL endpoint and walk the API by following links, expanding URI templates, inspecting embedded resources and building requests — on the desktop, in a browser, or embedded in your own application.
|
Note
|
The HAL client library and the example plugins are not part of this repository.
This repository builds NaHAL on top of both: |
| Module | Artifact | Role |
|---|---|---|
|
|
Navigation layer over Haldish — |
|
|
The Compose Multiplatform navigator GUI: desktop, browser, iOS. |
|
|
Kotlin test DSL for driving HAL APIs in tests. |
|
|
Groovy/Spock bindings for the testkit. |
| Platform | What you get | How it is delivered |
|---|---|---|
macOS (JVM) |
Desktop app |
|
macOS (native) |
Desktop app, no JVM required |
|
Linux |
Desktop app |
|
Windows |
Desktop app |
|
Browser (JS) |
Web app |
|
Browser (Wasm) |
— not shipped yet |
The |
iOS |
Embeddable view controller |
|
Linux / macOS / Windows © |
Shared library |
|
./gradlew :ui:jvmRun # opens a 1280×820 windowInstallers are built by Compose Desktop, which only produces the host format — a full set needs a CI matrix across the three operating systems:
./gradlew :ui:packageDistributionForCurrentOS
# output: ui/build/compose/binaries/main/{dmg,deb,msi}/
# e.g. on macOS: NahalNavigator-<version>.dmgThe macOS targets also build a standalone executable and wrap it in an app bundle:
./gradlew :ui:runMacosArm64App # Apple silicon — builds NaHAL.app and opens it
./gradlew :ui:runMacosX64App # Intel
# bundle only, no launch: ./gradlew :ui:bundleMacosArm64App
# output: ui/build/NaHAL.app|
Note
|
|
Build the bundle, or take the released zip:
./gradlew :ui:jsBrowserDistribution
# output: ui/build/dist/js/productionExecutable/unzip nahal-ui-web-<version>.zip -d nahal-web
cd nahal-web
python3 -m http.server 8080
# open http://localhost:8080/The bundle contains index.html, the compiled ui.js, the Skia runtime (skiko.js, skiko.wasm) and composeResources/. Three things to know:
-
It must be served over HTTP.
file://fails, becauseskiko.jsfetchesskiko.wasm. -
The server must send
application/wasmfor.wasm(Python’shttp.serverand modern nginx already do; older nginx needstypes { application/wasm wasm; }). -
Paths in
index.htmlare relative, so hosting under a sub-path works unchanged.
Because a browser build is subject to the same-origin policy, every API you navigate to must return
Access-Control-Allow-Origin for your host. APIs that work in the desktop build can still fail in
the browser for this reason alone.
:ui publishes iOS klibs and exposes a Compose entry point; this build produces no .app or
.ipa. Embed it from an Xcode project:
// SwiftUI
struct NaHalView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController { MainViewController() }
func updateUIViewController(_ vc: UIViewController, context: Context) {}
}NaHAL ships without plugins, and activates nothing unless a configuration source names it — a plugin sitting on the classpath or in a drop-in directory stays inert on its own.
| Runtime | How plugins get in | Ordering and properties |
|---|---|---|
JVM |
Drop |
|
Native |
No reflection: plugins are compiled in and registered by class name before the UI starts. |
|
Browser |
Registered by the hosting page. |
|
Any, single artifact |
|
Chain several by pointing at a chain artifact. Note the native C ABI carries no properties. |
To run the UI with the example plugins chained (CURIE expansion → base-URL rewriting → request logging):
(cd ../HALDiSh_Plugins && ./gradlew :chain:jvmRun) # desktop, JVM
(cd ../HALDiSh_Plugins && ./gradlew :chain:runMacosX64App) # desktop, native macOSThe plugin contract itself — hooks, lifecycle, the C ABI, packaging — is specified in PLUGIN_CONTRACT.md.
// build.gradle.kts
dependencies {
implementation("com.helpchoice.nahal:nahal-core:<version>")
}import com.helpchoice.nahal.core.HalNavigator
import com.helpchoice.nahal.core.LinkSelector
import com.helpchoice.nahal.core.RequestSpec
HalNavigator().use { navigator ->
// Fetch a starting document
val root = navigator.send(RequestSpec(url = "https://api.example.com/")).document!!
// Follow a link by rel — plugins' preLink hook runs, the URI template is expanded,
// the response is parsed
val orders = navigator.navigate(root, LinkSelector.TopLevel("orders"))
println("${orders.statusCode} ${orders.url}")
// Links inside embedded resources and collection items
navigator.navigate(root, LinkSelector.InEmbedded("ea:order", linkRel = "self"))
navigator.navigate(root, LinkSelector.InItems(itemIndex = 0, linkRel = "self"))
}DocLinkResolver resolves a rel’s documentation URL from the HAL curies relation, walking
outward through enclosing resources.
The :core JS target publishes a library with an exported navigator facade:
./gradlew :core:jsNodeProductionLibraryDistributionconst nav = new JsCoreNavigator();
nav.linkHref(halJson, 'orders'); // href for a top-level rel
nav.embeddedLinkHref(halJson, 'ea:order', 'self'); // href inside an embedded resourceOn Wasm the same operations are module-level functions (coreLinkHref, coreEmbeddedLinkHref),
because Kotlin/Wasm restricts @JsExport to functions.
./gradlew :core:linkReleaseSharedMacosArm64 # or …LinuxX64, …MingwX64, …MacosX64
# output: core/build/bin/<target>/releaseShared/
# libnahal_core.dylib (.so / .dll)
# libnahal_core_api.hconst char* core_link_href(const char* halJson, const char* rel);
const char* core_embedded_link_href(const char* halJson, const char* embeddedRel,
const char* linkRel);The full HAL client C API — HTTP verbs, headers, multipart, URI templates — belongs to Haldish and is documented in its repository.
./gradlew build # all modules, host platform targets
./gradlew :core:jvmTest # core tests (JVM is the only target with a standard runner)
./gradlew :core:coverageReportBecause Maven Central currently serves only Haldish 1.0.1, building against the current 2.0.0 needs
it published locally first — settings.gradle.kts adds mavenLocal() for exactly this:
(cd ../HALDiSh_KMP && ./gradlew publishToMavenLocal -PRELEASE_SIGNING_ENABLED=false)
./gradlew publishToMavenLocal -PRELEASE_SIGNING_ENABLED=false
(cd ../HALDiSh_Plugins && ./gradlew publishToMavenLocal -PRELEASE_SIGNING_ENABLED=false)That is also the bootstrap order between the three repositories: Haldish → this build → plugins
(:testkit here consumes the CURIE plugin).
./gradlew stageReleaseArtifacts # collects into build/release/, uploads nothing| Asset | Contents |
|---|---|
|
Desktop installer — host OS format only |
|
Browser app bundle (JS) |
|
|
|
Checksum for every asset above |
Attach all of them to the GitHub release. Klibs, sources and javadoc are not duplicated here — Maven Central serves those for Gradle and Maven users.
-
HALDiSh_KMP — the Haldish HAL client library this navigator is built on
-
HALDiSh_Plugins — example plugins, and the tasks that run NaHAL with them active
-
HALDiSh — a BASH script implementation of HAL client functionality
-
MockingHAL — a mock HAL server useful for testing plugins