The primary motivation for the port is compatibilty with babashka. Beyond compatibility, I also consider maintainability (ease of keeping the library up-to-date with changes to aws-api), performance, ease-of-use, and personal aesthetics (am I happy with the resulting library?).
The aws-api library isn't compatible with babashka, as it uses a number of classes not included in babahska and depends on Cognitect's com.cognitect/http-client, a wrapper around the Jetty HTTP Client.
The java.lang.ThreadLocal
class, used in aws-api, is not included in
babashka. ThreadLocal
is used in cognitect.aws.util
to make
java.text.SimpleDateFormat
thread-safe. As
I'm not concerned with supporting pre-Java 8 versions, I've decided to
use the thread-safe java.time.format.DateTimeFormatter
rather than
drop thread-safety workarounds for SimpleDateFormat
or implement
them in some other way.
The cognitect.aws.util
namespace is used throughout the aws-api
library, either directly or transitively.
There are a few other compatiblity issues, such as the use of
java.lang.ClassLoader::getResources
in
cognitect.aws.http/configured-client
, and replacing ^int x
hinting
with explicit (int x)
casts.
At the time of the initial release of awyeah-api, babashka did not include classes required to implement the auto-refresh credentials functionality. Michiel is always improving babashka, and has since included these classes so I've been able to implement credentials auto-refresh as of v0.8.82/5ecad02.
The aws-api library defines a protocol
(cognitect.aws.http/HttpClient
) to provide an interface between the
aws-api data transformation logic and the specific HTTP client
implementation. The aws-api includes an implementation for the
com.cognitect/http-client
library. Interfaces are great: we can
provide our own implementations of the cognitect.aws.http/HttpClient
interface based on the various HTTP clients included in babashka.
The java.net.http
package, introduced in Java 11, includes
java.net.http.HttpClient
, a very nice HTTP client implementation. It's
also included in babashka. The aws-api sets the host
header and the
host
header is included in the signature for signed AWS API requests.
The java.net.http.HttpClient
considers host
a restricted header and
does not allow it to be set. Java 12 added the
jdk.httpclient.allowRestrictedHeaders
System property to allow
host
to be set.
Currently, babashka is built using Java 11 GraalVM. Building a custom babashaka binary using a Java 17 GraalVM 22.2.0. Currently this is only available in a dev build.
The jdk.httpclient.allowRestrictedHeaders
property must be set at
JVM startup. It cannot be set at run-time after the JVM has
started. You can pass properties to babashka at the command line just
like you can to to java
.
bb -Djdk.httpclient.allowRestrictedHeaders=host ...
That's not a very pretty solution, however, having to set a system property at the command line.
The solution I've arrived at is to drop host
from the headers in the
http-client itself and not set it. The java.net.http.HttpRequest
builder sets the host from the value in the URI, which is the value we
want anyway. The com.grzm.awyeah.client/Client
instance builds a
request map including host
in the headers, creates the appropriate
signture taking the host
header into account, submits the request
map to the com.grzm.awyeah.http-client
, and the http-client just
ignores the host
header. The signature header included in the
request map is set, and the host
header is set by the HttpRequest
builder.
From a maintainability perspective, it would be easiest to stop after
making the minimum changes required for babashka compatibility. I
could then use text tools like diff
to easily identify differences
between the source code in aws-api and awyeah-api.
The aws-api library defines the cognitect.dynaload/load-var
function
to dynamically require and resolve the var referenced by a given
symbol. Clojure 1.10 provides the same functionality with the
requiring-resolve
function. Given that requiring-resolve
is
compiled into the babashka image, I've chosen to replace load-var
with requiring-resolve
rather than relying on sci to interpret
load-var
at run-time.
The aws-api library depends on
clojure.data.json
for JSON serialization and
deserialization, a pure Clojure library. Babashka includes
Cheshire for JSON support and not clojure.data.json
.
The Clojure source of clojure.data.json
can be interpreted by sci,
so I could include clojure.data.json
as a dependency and use it
as-is. The clojure.data.json
usage in aws-api is easily replaced by
Cheshire. Replacing clojure.data.json
with Cheshire means one less
dependency to include, and we can leverage compiled code rather than
interpreted. To isolate the library choice, I've extracted the
library-specific calls in the com.grzm.awyeah.json
namespace.
One thought would be to make the choice of JSON library available to the awyeah-api library user, defining a protocol for the necessary functions. While interesting, that seems overkill for the desired use case at this time.
Changes such as whitespace formatting or removing unused bindings, or reordering requires don't affect the functionality of the code. However, these are important to me as a developer from a fit and finish perspective.
Frankly, I do struggle a bit with whether I should value these as highly as I do, putting them above making maintenance as easy as possible. That said, the impact of these changes should be minimal with respect to maintainability, and one I'm willing to live with, at least for now. I can always revisit this decision and with a little work, reverse it.
It would be an interesting challenge to determine if I could—instead of maintaining these changes by hand—write transformation rules that not only include the changes necessary for babashka compatibility, but also the formatting and linting changes I'd like. Perhaps someday.