JsonNameUtil.java

/*
 * Copyright 2022 Proto Utils Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.proto.utils;

import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.ProtocolMessageEnum;
import net.morimekta.collect.SetBuilder;
import net.morimekta.collect.UnmodifiableSet;
import net.morimekta.collect.util.Pair;
import net.morimekta.proto.ProtoEnum;
import net.morimekta.proto.ProtoMessage;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import static net.morimekta.collect.UnmodifiableMap.toMap;
import static net.morimekta.collect.UnmodifiableSet.setOf;
import static net.morimekta.collect.util.SetOperations.product;
import static net.morimekta.proto.ProtoEnum.getEnumDescriptor;

/**
 * Utilities for handling JSON names.
 */
public final class JsonNameUtil {
    /**
     * @param descriptor A proto descriptor.
     * @param <E>        The proto enum value type.
     * @return A map from JSON names to enum value.
     */
    @SuppressWarnings("unchecked,rawtypes")
    public static <E extends Enum<E> & ProtocolMessageEnum>
    Map<String, E> getJsonEnumMap(ProtoEnum<E> descriptor) {
        return (Map<String, E>) (Map) enumNameMaps.computeIfAbsent(
                descriptor,
                d -> descriptor
                        .allValues()
                        .stream()
                        .flatMap(e -> product(getJsonEnumNames(e), setOf(e)).stream())
                        .collect(toMap(Pair::getFirst, Pair::getSecond)));
    }

    /**
     * @param type A proto type class.
     * @param <E>  The proto enum value type.
     * @return A map from JSON names to enum value.
     */
    public static <E extends Enum<E> & ProtocolMessageEnum>
    Map<String, E> getJsonEnumMap(Class<E> type) {
        return getJsonEnumMap(getEnumDescriptor(type));
    }

    /**
     * @param type A message type class.
     * @param <M>  The message type.
     * @return A map of JSON field names to field for.
     */
    public static <M extends Message>
    Map<String, Descriptors.FieldDescriptor> getJsonFieldMap(Class<M> type) {
        return getJsonFieldMap(ProtoMessage.getMessageDescriptor(type));
    }

    /**
     * @param descriptor A message descriptor.
     * @return A map of JSON field names to field for.
     */
    public static Map<String, Descriptors.FieldDescriptor> getJsonFieldMap(Descriptors.Descriptor descriptor) {
        return fieldNameMaps.computeIfAbsent(
                descriptor,
                d -> descriptor.getFields()
                               .stream()
                               .flatMap(f -> product(getJsonFieldNames(f), setOf(f)).stream())
                               .collect(toMap(p -> p.first, p -> p.second)));
    }

    /**
     * @param value An enum value.
     * @return The default JSON name for the value.
     */
    public static String getJsonEnumName(ProtocolMessageEnum value) {
        return value.getValueDescriptor().getName();
    }

    /**
     * @param field An field descriptor.
     * @return The default JSON name for the field.
     */
    public static String getJsonFieldName(Descriptors.FieldDescriptor field) {
        if (field.isExtension()) {
            return field.getFullName();
        } else {
            return field.getName();
        }
    }

    /**
     * @param value A proto enum value.
     * @return A set of possible JSON names for the enum value.
     */
    public static Set<String> getJsonEnumNames(ProtocolMessageEnum value) {
        SetBuilder<String> names = UnmodifiableSet.newBuilder();
        names.add(value.getValueDescriptor().getName());
        if (value.getDescriptorForType().getOptions().getAllowAlias()) {
            for (Descriptors.EnumValueDescriptor desc : value.getDescriptorForType().getValues()) {
                if (desc.getNumber() == value.getNumber()) {
                    names.add(desc.getName());
                }
            }
        }
        return names.build();
    }

    /**
     * @param field An field descriptor.
     * @return The set of possible JSON names for the field.
     */
    public static Set<String> getJsonFieldNames(Descriptors.FieldDescriptor field) {
        SetBuilder<String> names = UnmodifiableSet.newBuilder();
        names.add(field.getName());
        names.add(field.getJsonName());
        return names.build();
    }

    /**
     * A set of field names for the type on serialized unwrapped any types.
     */
    public static final Set<String> ANY_TYPE_FIELDS  = setOf("@type", "__type");
    /**
     * Pattern used to detect numeric field ID string.
     */
    public static final Pattern     NUMERIC_FIELD_ID = Pattern.compile("^[1-9][0-9]{0,8}$");

    // ---- Private ----

    private static final Map<ProtoEnum<?>, Map<String, Enum<?>>>                               enumNameMaps  = new ConcurrentHashMap<>();
    private static final Map<Descriptors.Descriptor, Map<String, Descriptors.FieldDescriptor>> fieldNameMaps = new ConcurrentHashMap<>();

    private JsonNameUtil() {
    }
}