001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.protobuf;
019
020import static java.util.Collections.emptySet;
021
022import com.google.common.collect.ImmutableMap;
023import com.google.common.collect.ImmutableMultimap;
024import com.google.common.collect.ImmutableSet;
025import com.google.common.collect.Multimap;
026import com.google.common.net.MediaType;
027
028import dev.enola.common.io.mediatype.MediaTypeProvider;
029import dev.enola.common.io.mediatype.MediaTypes;
030
031import java.nio.charset.StandardCharsets;
032import java.util.Map;
033import java.util.Optional;
034import java.util.Set;
035
036public class ProtobufMediaTypes implements MediaTypeProvider {
037    // TODO move this class into the common.proto module!
038
039    // The *.proto ("schemas") files, as well as "Text Proto" (and their JSON and YAML equivalent)
040    // are text/* and not application/* whereas the binary serialization wire format is an
041    // "application/*";
042    // based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types.
043
044    /**
045     * MediaType parameter to indicate the resource's root message fully qualified name. Inspired by
046     * https://protobuf.dev/reference/protobuf/textformat-spec/#header.
047     */
048    public static final String PARAMETER_PROTO_MESSAGE = "proto-message";
049
050    public static MediaType setProtoMessageFQN(MediaType mediaType, String protoFQN) {
051        return mediaType.withParameter(PARAMETER_PROTO_MESSAGE, protoFQN);
052    }
053
054    // TODO After move: public static MediaType setProtoMessage(MediaType mediaType, Descriptor
055    // descriptor) {
056    //     return mediaType.withParameter(PARAMETER_PROTO_MESSAGE, descriptor.getFullName());
057    // }
058
059    public static Optional<String> getProtoMessageFQN(MediaType mediaType) {
060        return MediaTypes.parameter(mediaType, PARAMETER_PROTO_MESSAGE);
061    }
062
063    // TODO Support "sniffing" proto-message from header in comment of ReadableResource
064
065    public static final MediaType PROTO_UTF_8 =
066            MediaType.create("text", "proto").withCharset(StandardCharsets.UTF_8);
067
068    public static final MediaType PROTOBUF_TEXTPROTO_UTF_8 =
069            MediaType.create("text", "protobuf").withCharset(StandardCharsets.UTF_8);
070
071    /** "ProtoBuf as JSON" - which is different from e.g. a JSON-LD representation of RDF. */
072    public static final MediaType PROTOBUF_JSON_UTF_8 =
073            MediaType.create("text", "protobuf+json").withCharset(StandardCharsets.UTF_8);
074
075    /** "ProtoBuf as YAML" - which is different from e.g. a YAML-LD representation of RDF. */
076    public static final MediaType PROTOBUF_YAML_UTF_8 =
077            MediaType.create("text", "protobuf+yaml").withCharset(StandardCharsets.UTF_8);
078
079    // https://datatracker.ietf.org/doc/html/draft-rfernando-protocol-buffers-00
080    public static final MediaType PROTOBUF_BINARY = MediaType.create("application", "protobuf");
081
082    @Override
083    public Map<MediaType, Set<MediaType>> knownTypesWithAlternatives() {
084        return ImmutableMap.of(
085                PROTO_UTF_8,
086                emptySet(),
087                PROTOBUF_TEXTPROTO_UTF_8,
088                emptySet(),
089                PROTOBUF_JSON_UTF_8,
090                emptySet(),
091                PROTOBUF_YAML_UTF_8,
092                emptySet(),
093                PROTOBUF_BINARY,
094                // https://stackoverflow.com/questions/30505408/what-is-the-correct-protobuf-content-type
095                ImmutableSet.of(
096                        MediaType.create("application", "x-protobuf"),
097                        MediaType.create("application", "vnd.google.protobuf")));
098    }
099
100    @Override
101    public Multimap<String, MediaType> extensionsToTypes() {
102        // https://protobuf.dev/programming-guides/techniques/#suffixes
103        return ImmutableMultimap.<String, MediaType>builder()
104                .put(".proto", ProtobufMediaTypes.PROTO_UTF_8)
105                .put(
106                        ".proto.binpb",
107                        // TODO This parameter isn't actually used for anything... yet.
108                        ProtobufMediaTypes.PROTOBUF_BINARY.withParameter(
109                                PARAMETER_PROTO_MESSAGE, "google.protobuf.FileDescriptorSet"))
110                .put(".binpb", ProtobufMediaTypes.PROTOBUF_BINARY)
111                .put(".pb", ProtobufMediaTypes.PROTOBUF_BINARY)
112                .put(".textproto", ProtobufMediaTypes.PROTOBUF_TEXTPROTO_UTF_8)
113                .put(".txtpb", ProtobufMediaTypes.PROTOBUF_TEXTPROTO_UTF_8)
114                .build();
115    }
116}