001/*
002 * Copyright (c) 2026 Singular
003 * SPDX-License-Identifier: MIT
004 */
005package ai.singlr.runtime;
006
007import io.helidon.common.GenericType;
008import io.helidon.common.media.type.MediaTypes;
009import io.helidon.http.Headers;
010import io.helidon.http.HttpMediaType;
011import io.helidon.http.WritableHeaders;
012import io.helidon.http.media.EntityReader;
013import io.helidon.http.media.EntityWriter;
014import io.helidon.http.media.MediaSupport;
015import tools.jackson.databind.ObjectMapper;
016import tools.jackson.databind.json.JsonMapper;
017
018/**
019 * Helidon {@link MediaSupport} backed by Jackson 3.x. Wire via {@code WebServer.builder()
020 * .mediaContext(it -> it.addMediaSupport(JacksonSupport.create(mapper)))}.
021 */
022public final class JacksonSupport implements MediaSupport {
023
024  private final ObjectMapper objectMapper;
025  private final JacksonReader<?> reader;
026  private final JacksonWriter<?> writer;
027
028  private JacksonSupport(ObjectMapper objectMapper) {
029    this.objectMapper = objectMapper;
030    this.reader = new JacksonReader<>(objectMapper);
031    this.writer = new JacksonWriter<>(objectMapper);
032  }
033
034  /**
035   * Create a {@code JacksonSupport} backed by a default {@link JsonMapper}.
036   *
037   * @return a fresh instance
038   */
039  public static JacksonSupport create() {
040    return new JacksonSupport(JsonMapper.builder().build());
041  }
042
043  /**
044   * Create a {@code JacksonSupport} backed by the caller's {@link ObjectMapper}.
045   *
046   * @param objectMapper non-null mapper
047   * @return a fresh instance
048   */
049  public static JacksonSupport create(ObjectMapper objectMapper) {
050    return new JacksonSupport(objectMapper);
051  }
052
053  /**
054   * The wrapped mapper. Useful for handlers that need to (de)serialize directly without going
055   * through Helidon's media context.
056   *
057   * @return the mapper this support owns
058   */
059  public ObjectMapper objectMapper() {
060    return objectMapper;
061  }
062
063  @Override
064  public String name() {
065    return "jackson";
066  }
067
068  @Override
069  public String type() {
070    return "jackson";
071  }
072
073  @SuppressWarnings("unchecked")
074  @Override
075  public <T> ReaderResponse<T> reader(GenericType<T> type, Headers headers) {
076    if (isJsonContentType(headers)) {
077      return new ReaderResponse<>(SupportLevel.SUPPORTED, () -> (EntityReader<T>) reader);
078    }
079    if (isSupportedType(type)) {
080      return new ReaderResponse<>(SupportLevel.COMPATIBLE, () -> (EntityReader<T>) reader);
081    }
082    return ReaderResponse.unsupported();
083  }
084
085  @SuppressWarnings("unchecked")
086  @Override
087  public <T> WriterResponse<T> writer(
088      GenericType<T> type, Headers requestHeaders, WritableHeaders<?> responseHeaders) {
089    if (isJsonAccepted(requestHeaders)) {
090      return new WriterResponse<>(SupportLevel.SUPPORTED, () -> (EntityWriter<T>) writer);
091    }
092    if (isSupportedType(type)) {
093      return new WriterResponse<>(SupportLevel.COMPATIBLE, () -> (EntityWriter<T>) writer);
094    }
095    return WriterResponse.unsupported();
096  }
097
098  @SuppressWarnings("unchecked")
099  @Override
100  public <T> ReaderResponse<T> reader(
101      GenericType<T> type, Headers requestHeaders, Headers responseHeaders) {
102    if (isJsonContentType(responseHeaders)) {
103      return new ReaderResponse<>(SupportLevel.SUPPORTED, () -> (EntityReader<T>) reader);
104    }
105    if (isSupportedType(type)) {
106      return new ReaderResponse<>(SupportLevel.COMPATIBLE, () -> (EntityReader<T>) reader);
107    }
108    return ReaderResponse.unsupported();
109  }
110
111  @SuppressWarnings("unchecked")
112  @Override
113  public <T> WriterResponse<T> writer(GenericType<T> type, WritableHeaders<?> requestHeaders) {
114    if (isSupportedType(type)) {
115      return new WriterResponse<>(SupportLevel.COMPATIBLE, () -> (EntityWriter<T>) writer);
116    }
117    return WriterResponse.unsupported();
118  }
119
120  private boolean isJsonContentType(Headers headers) {
121    return headers.contentType().map(ct -> ct.test(MediaTypes.APPLICATION_JSON)).orElse(false);
122  }
123
124  private boolean isJsonAccepted(Headers headers) {
125    for (HttpMediaType acceptedType : headers.acceptedTypes()) {
126      if (acceptedType.test(MediaTypes.APPLICATION_JSON)) {
127        return true;
128      }
129    }
130    return false;
131  }
132
133  private <T> boolean isSupportedType(GenericType<T> type) {
134    var rawType = type.rawType();
135    return !rawType.equals(String.class) && !rawType.equals(byte[].class) && !rawType.isArray();
136  }
137}