Skip to content

Commit

Permalink
Doc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
mizosoft committed Dec 19, 2024
1 parent e8692fb commit 1db655c
Show file tree
Hide file tree
Showing 42 changed files with 1,786 additions and 2,467 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Visit the [project website](https://mizosoft.github.io/methanol) for more info.
*note: documentation may contain updates for 1.8.0, which is not yet released*

# Methanol

Expand All @@ -7,16 +7,15 @@ Visit the [project website](https://mizosoft.github.io/methanol) for more info.
[![Maven Central](https://img.shields.io/maven-central/v/com.github.mizosoft.methanol/methanol?style=flat-square)](https://search.maven.org/search?q=g:%22com.github.mizosoft.methanol%22%20AND%20a:%22methanol%22)
[![Javadoc](https://img.shields.io/maven-central/v/com.github.mizosoft.methanol/methanol?color=blueviolet&label=Javadoc&style=flat-square)](https://mizosoft.github.io/methanol/api/latest/)

Java enjoys a neat, built-in [HTTP client](https://openjdk.java.net/groups/net/httpclient/intro.html).
However, it lacks key HTTP features like multipart uploads, caching and response decompression.
***Methanol*** comes in to fill these gaps. The library comprises a set of lightweight, yet powerful
extensions aimed at making it much easier & more productive to work with `java.net.http`. You can
say it's an `HttpClient` wrapper, but you'll see it almost seamlessly integrates with the standard
API you might already know.
Java enjoys a neat, built-in [HTTP client](https://openjdk.java.net/groups/net/httpclient/intro.html). However, it lacks key HTTP features like multipart uploads, caching and response decompression.
***Methanol*** comes in to fill these gaps. The library comprises a set of lightweight, yet powerful extensions aimed at making it much easier & more productive to work with `java.net.http`.
You can say it's an `HttpClient` wrapper, but you'll see it almost seamlessly integrates with the standard API you might already know.

Methanol isn't invasive. The core library has zero runtime dependencies. However, special attention
is given to object mapping, so integration with libraries like Jackson or Gson becomes a breeze.

[//]: # (There's also a nice DSL for Kotlin lovers!)

## Installation

### Gradle
Expand Down
833 changes: 0 additions & 833 deletions USER_GUIDE.md

This file was deleted.

26 changes: 24 additions & 2 deletions buildSrc/src/main/kotlin/conventions/kotlin-library.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
/*
* Copyright (c) 2024 Moataz Abdelnasser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package conventions

import extensions.javaModuleName
Expand All @@ -24,7 +46,7 @@ tasks.withType<KotlinCompile>().configureEach {
tasks.withType<DokkaTaskPartial> {
try {
moduleName = project.javaModuleName
} catch (e: IllegalStateException) {
project.logger.warn("Couldn't get Java module name for Kotlin project (${project.name})", e)
} catch (_: IllegalStateException) {
project.logger.warn("Couldn't get Java module name for Kotlin project (${project.name})")
}
}
196 changes: 196 additions & 0 deletions docs/adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Adapters

HTTP bodies are often mappable to high-level types that your code understands. Java's HttpClient was designed
with that in mind. However, available `BodyPublisher` & `BodySubscriber` implementations are too basic, and
implementing your own can be tricky. Methanol builds upon these APIs with an extensible object mapping mechanism
that treats your objects as first-citizen HTTP bodies.

## Setup

A serialization library can be integrated with Methanol through a corresponding adapter.
Adapters for the most popular serialization libraries are provided by separate modules.

* [`methanol-gson`](adapters/gson.md): JSON with Gson
* [`methanol-jackson`](adapters/jackson.md): JSON with Jackson (but also XML, protocol buffers and other formats support by Jackson)
* [`methanol-jackson-flux`](adapters/jackson_flux.md): Streaming JSON with Jackson and Reactor
* [`methanol-jaxb`](adapters/jaxb.md): XML with JAXB
* [`methanol-jaxb-jakarta`](adapters/jaxb.md): XML with JAXB (Jakarta version)
* [`methanol-protobuf`](adapters/protobuf.md): Google's Protocol Buffers
* [`methanol-moshi`](adapters/moshi.md): JSON with Moshi, mainly for Kotlin

We'll pick `methanol-jackson` for the examples presented here, which interact with GitHub's REST API.

```java
var mapper = new JsonMapper();
var adapterCodec =
AdapterCodec.newBuilder()
.basic()
.encoder(JacksonAdapterFactory.createEncoder(mapper, MediaType.APPLICATION_JSON))
.decoder(JacksonAdapterFactory.createDecoder(mapper, MediaType.APPLICATION_JSON))
.build();
var client =
Methanol.newBuilder()
.adapterCodec(adapterCodec)
.baseUri("https://api.github.com/")
.defaultHeader("Accept", "application/vnd.github.v3+json")
.build();
```

An `AdapterCodec` groups together one or more adapters, possibly targeting different mapping schemes. It helps `Methanol`
to select the correct adapter based on the request's or response's [`MediaType`](https://mizosoft.github.io/methanol/api/latest/methanol/com/github/mizosoft/methanol/MediaType.html).

The [`basic()`][adaptercodec_basic_javadoc] calls adds the basic adapter, which encodes & decodes basic types like `String` & `InputStream`.

## Receiving Objects

To get an `HttpResponse<T>`, give `Methanol::send` a `T.class`.

```java
@JsonIgnoreProperties(ignoreUnknown = true) // We'll ignore most fields for brevity.
public record GitHubUser(String login, long id, String url) {}

GitHubUser getUser(String username) throws IOException, InterruptedException {
return client.send(MutableRequest.GET("users/" + username), GitHubUser.class).body();
}
```

If you want to get fancier with generics, pass a `TypeRef<T>`.

```java
@JsonIgnoreProperties(ignoreUnknown = true) // We'll ignore most fields for brevity.
public record GitHubIssue(String title, GitHubUser user, String body) {}

List<GitHubIssue> getIssues(String owner, String repo) throws IOException, InterruptedException {
return client.send(
MutableRequest.GET("repos/" + owner + "/" + repo + "/issues"),
new TypeRef<List<GitHubIssue>>() {}).body();
}
```

## Sending Objects

Each `MutableRequest` can have a payload as its body. A payload is an arbitrary object that is not yet resolved into a `BodyPublisher`.
When the request is sent, the payload will be resolved with the client's `AdapterCodec`.

```java
public record Markdown(String text, String context, String mode) {}

String markdownToHtml(String text, String contextRepo) throws IOException, InterruptedException {
return client.send(
MutableRequest.POST("markdown", new Markdown(text, contextRepo, "gfm"), MediaType.APPLICATION_JSON),
String.class).body();
}
```

A payload must be given along with a `MediaType` specifying the format with which it will be resolved.

## Adapters

An adapter provides an [`Encoder`][encoder_javadoc] and/or a [`Decoder`][decoder_javadoc] implementation.
An `Encoder` creates `BodyPublisher` instances that stream a given object's serialized form.
Similarly, a `Decoder` creates `BodySubscriber<T>` instances that convert the response body into `T`.
Encoders & decoders are given [`Hints`][hints_javadoc] to customize their behavior.
One notable hint is the `MediaType`, which can be used to further describe the desired mapping format (e.g. specify a character set).

### Example - An HTML Adapter

Here's an adapter that uses [Jsoup][jsoup] to convert HTML bodies to `Document` objects and vise versa.
When you're writing adapters, it's a good idea to extend from [`AbstractBodyAdapter`][abstractbodyadapter_javadoc].

```java
public abstract class JsoupAdapter extends AbstractBodyAdapter {
JsoupAdapter() {
super(MediaType.TEXT_HTML);
}

@Override
public boolean supportsType(TypeRef<?> type) {
return type.rawType() == Document.class;
}

public static final class Decoder extends JsoupAdapter implements BaseDecoder {
@Override
public <T> BodySubscriber<T> toObject(TypeRef<T> typeRef, Hints hints) {
requireSupport(typeRef, hints);
var charset = hints.mediaTypeOrAny().charsetOrUtf8();
var subscriber = BodySubscribers.mapping(BodySubscribers.ofString(charset), Jsoup::parse);
return BodySubscribers.mapping(subscriber, typeRef.exactRawType()::cast); // Safely cast Document to T.
}
}

public static final class Encoder extends JsoupAdapter implements BaseEncoder {
@Override
public <T> BodyPublisher toBody(T value, TypeRef<T> typeRef, Hints hints) {
requireSupport(typeRef, hints);
var charset = hints.mediaTypeOrAny().charsetOrUtf8();
var publisher = BodyPublishers.ofString(((Document) value).outerHtml(), charset);
return attachMediaType(publisher, hints.mediaTypeOrAny());
}
}
}
```

!!! tip
Make sure your encoders call `AbstractBodyAdapter::attachMediaType` so the created `BodyPublisher` can be converted to a `MimeBodyPublisher`.
That way, requests get the correct `Content-Type` header added by `Methanol`.

## Buffering vs Streaming

Decoders typically load the whole response body into memory before deserialization. If your responses tend to have large bodies,
or you'd prefer the memory efficiency afforded by streaming sources, you can ask to get a `Supplier<T>` instead.

```java
@JsonIgnoreProperties(ignoreUnknown = true) // We'll ignore most fields for brevity.
public record GitHubUser(String login, long id, String url) {}

GitHubUser getUser(String username) throws IOException, InterruptedException {
return client.send(
MutableRequest.GET("user/" + username),
new TypeRef<Supplier<GitHubUser>>() {}).body().get();
}
```

In such case, the response is completed as soon as all headers are read. If he decoder supports
streaming, the supplier will deserialize from a streaming source, typically an `InputStream` or a `Reader`.

The way a `Decoder` implements streaming is by overriding `toDeferredObject` to return a `BodySubscriber<Supplier<T>>`.
Here's how it'd be properly implemented for our HTML adapter's decoder.

```java
@Override
public <T> BodySubscriber<Supplier<T>> toDeferredObject(TypeRef<T> typeRef, Hints hints) {
requireSupport(typeRef, hints);
return BodySubscribers.mapping(
MoreBodySubscribers.ofReader(hints.mediaTypeOrAny().charsetOrUtf8()),
reader ->
() ->
typeRef
.exactRawType() // Get Class<Document> as Class<T>
.cast(
Parser.htmlParser()
.parseInput(
new BufferedReader(reader), ""))); // Note the deferred parsing
}
```

## Legacy Adapters

See [Legacy Adapter](legacy_adapters.md).

[methanol_jackson]: https://github.com/mizosoft/methanol/tree/master/methanol-jackson

[jsoup]: https://jsoup.org/

[encoder_javadoc]: ../api/latest/methanol/com/github/mizosoft/methanol/BodyAdapter.Encoder.html

[decoder_javadoc]: ../api/latest/methanol/com/github/mizosoft/methanol/BodyAdapter.Decoder.html

[bodyadapter_javadoc]: ../api/latest/methanol/com/github/mizosoft/methanol/BodyAdapter.html

[hints_javadoc]: https://mizosoft.github.io/methanol/api/latest/methanol/com/github/mizosoft/methanol/BodyAdapter.Hints.html

[mediatype_javadoc]: https://mizosoft.github.io/methanol/api/latest/methanol/com/github/mizosoft/methanol/MediaType.html

[abstractbodyadapter_javadoc]: https://mizosoft.github.io/methanol/api/latest/methanol/com/github/mizosoft/methanol/adapter/AbstractBodyAdapter.html

[adaptercodec_basic_javadoc]: https://mizosoft.github.io/methanol/api/latest/methanol/com/github/mizosoft/methanol/AdapterCodec.Builder.html#basic()
Loading

0 comments on commit 1db655c

Please sign in to comment.