TypeRegistry.java

/*
 * Copyright 2015-2016 Providence 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.providence.types;

import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.descriptor.PAnnotation;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDeclaredDescriptor;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PDescriptorProvider;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceProvider;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.util.collect.UnmodifiableMap;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

/**
 * Base class for type registry. The type registry holds type information, possibly
 * with some context, and has methods to get type information out from the registry.
 */
public abstract class TypeRegistry {
    protected TypeRegistry() {}

    /**
     * Get the declared type with the given name and program context.
     *
     * @param reference The reference to the type.
     * @return Optional type descriptor.
     */
    @Nonnull
    public abstract Optional<PDeclaredDescriptor<?>> getDeclaredType(@Nonnull TypeReference reference);

    /**
     * Get a service definition from the name and program context.
     *
     * @param reference The service reference.
     * @return Optional service descriptor.
     */
    @Nonnull
    public abstract Optional<PService> getService(@Nonnull TypeReference reference);

    /**
     * This is to check to find a constant given the const name and program context. This will
     * return empty if the constant does not exist.
     *
     * @param reference The constant reference.
     * @return The optional const value.
     * @param <T> The returned value type.
     */
    @Nonnull
    public abstract <T> Optional<T> getConstantValue(@Nonnull TypeReference reference);

    /**
     * Get a type-definition for the given reference.
     *
     * @param reference The typedef reference.
     * @return Optional typedef definition.
     */
    @Nonnull
    public abstract Optional<TypeReference> getTypedef(@Nonnull TypeReference reference);

    public abstract List<PDeclaredDescriptor<?>> getDeclaredTypes();

    /**
     * @param program The program name.
     * @return True if the program is known in this registry.
     */
    public abstract boolean isKnownProgram(@Nonnull String program);

    // ---- Default Methods ---

    /**
     * Get the declared type with the given name and program context.
     *
     * @param reference The reference to the type.
     * @return The declared type descriptor.
     */
    @Nonnull
    public PDeclaredDescriptor requireDeclaredType(@Nonnull TypeReference reference) {
        if (reference.isNativeType()) {
            throw new IllegalArgumentException("Not a valid type name '" + reference + "'");
        }
        final TypeReference finalReference = finalTypeReference(reference);
        if (finalReference.isNativeType()) {
            throw new IllegalArgumentException("Declared name '" + reference.typeName + "' is defined to native type in program '" + reference + "'");
        }
        if (getService(finalReference).isPresent() ||
            getConstantValue(finalReference).isPresent()) {
            if (reference.equals(finalReference)) {
                throw new IllegalArgumentException(
                        "Declared name '" + finalReference.typeName + "' is not a type in program '" +
                        finalReference.programName + "'");
            } else {
                throw new IllegalArgumentException(
                        "Declared name '" + finalReference.typeName + "' is not a type in program '" +
                        finalReference.programName + "' from typedef '" + reference + "'");
            }
        }
        return getDeclaredType(finalReference).orElseThrow(
                () -> {
                    if (isKnownProgram(finalReference.programName)) {
                        return new IllegalArgumentException("No type '" + finalReference.typeName + "' in program '" + finalReference.programName + "'");
                    }
                    return new IllegalArgumentException("No program '" + finalReference.programName + "' for type '" + finalReference + "'");
                });
    }

    /**
     * Get the declared message type.
     *
     * @param reference The type reference.
     * @param <M> The message type.
     * @return The message type descriptor.
     */
    @Nonnull
    @SuppressWarnings("unchecked")
    public  <M extends PMessage<M>>
    PMessageDescriptor<M> requireMessageType(@Nonnull TypeReference reference) {
        PDeclaredDescriptor descriptor = requireDeclaredType(reference);
        if (descriptor instanceof PMessageDescriptor) {
            return (PMessageDescriptor) descriptor;
        }
        throw new IllegalStateException("Not a message type " + reference + ", was " + descriptor.getType());
    }

    /**
     * Get the declared enum type.
     *
     * @param reference The type reference.
     * @param <E> The enum value type.
     * @return The enum type descriptor.
     */
    @Nonnull
    @SuppressWarnings("unchecked")
    public <E extends PEnumValue<E>>
    PEnumDescriptor<E> requireEnumType(@Nonnull TypeReference reference) {
        PDeclaredDescriptor descriptor = requireDeclaredType(reference);
        if (descriptor instanceof PEnumDescriptor) {
            return (PEnumDescriptor) descriptor;
        }
        throw new IllegalStateException("Not an enum type " + reference + ", was " + descriptor.getType());
    }

    /**
     * Get the declared type with the given name and program context.
     *
     * @param reference The reference to the type.
     * @return The service descriptor.
     */
    @Nonnull
    public PService requireService(@Nonnull TypeReference reference) {
        if (reference.isNativeType()) {
            throw new IllegalArgumentException("Not a valid service name '" + reference + "'");
        }
        if (getDeclaredType(reference).isPresent() ||
            getTypedef(reference).isPresent()) {
            throw new IllegalArgumentException("Declared name '" + reference.typeName + "' is not a service in program '" + reference.programName + "'");
        }
        return getService(reference).orElseThrow(
                () -> {
                    if (isKnownProgram(reference.programName)) {
                        return new IllegalArgumentException("No service '" + reference.typeName + "' in program '" + reference.programName + "'");
                    }
                    return new IllegalArgumentException("No program '" + reference.programName + "' for service '" + reference + "'");
                });
    }

    // --- Providers ---

    /**
     * Get a type provider for reference.
     * @param reference Type reference to get provider for.
     * @return The type provider.
     */
    @Nonnull
    public PDescriptorProvider getTypeProvider(@Nonnull TypeReference reference) {
        return getTypeProvider(reference, UnmodifiableMap.mapOf());
    }

    @Nonnull
    public PDescriptorProvider getTypeProvider(@Nonnull TypeReference reference,
                                               @Nonnull Map<String, String> annotations) {
        return () -> resolveType(reference, annotations);
    }

    @Nonnull
    public PServiceProvider getServiceProvider(@Nonnull TypeReference reference) {
        return () -> requireService(reference);
    }

    // --- Internal ---

    /**
     * Get the final typename of the given identifier within the context.
     *
     * @param reference The type name.
     * @return The final type reference.
     */
    @Nonnull
    protected TypeReference finalTypeReference(@Nonnull TypeReference reference) {
        if (reference.isMap()) {
            return new TypeReference(reference.programName,
                                     reference.typeName,
                                     finalTypeReference(requireNonNull(reference.keyType)),
                                     finalTypeReference(requireNonNull(reference.valueType)));
        } else if (reference.isContainer()) {
            return new TypeReference(reference.programName,
                                     reference.typeName,
                                     null,
                                     finalTypeReference(requireNonNull(reference.valueType)));
        }
        Optional<TypeReference> def = getTypedef(reference);
        return def.map(this::finalTypeReference).orElse(reference);
    }

    // --- Private ---

    /**
     * Resolve a type reference to the actual type.
     *
     * @param reference The type reference.
     * @param annotations Type annotations.
     * @return The resolved type descriptor.
     */
    private PDescriptor resolveType(TypeReference reference, Map<String, String> annotations) {
        reference = finalTypeReference(reference);

        // Prepend package context to name
        PPrimitive primitive = PPrimitive.findByName(reference.typeName);
        if (primitive != null) {
            return primitive;
        } else if (reference.isMap()) {
            PDescriptorProvider keyType = getTypeProvider(requireNonNull(reference.keyType), annotations);
            PDescriptorProvider valueType = getTypeProvider(requireNonNull(reference.valueType), annotations);

            switch (PContainer.typeForName(annotations.get(PAnnotation.CONTAINER.tag))) {
                case SORTED:
                    return PMap.sortedProvider(keyType, valueType).descriptor();
                case ORDERED:
                    return PMap.orderedProvider(keyType, valueType).descriptor();
                case DEFAULT:
                    return PMap.provider(keyType, valueType).descriptor();
            }
        } else if (reference.isSet()) {
            PDescriptorProvider valueType = getTypeProvider(requireNonNull(reference.valueType), annotations);

            switch (PContainer.typeForName(annotations.get(PAnnotation.CONTAINER.tag))) {
                case SORTED:
                    return PSet.sortedProvider(valueType).descriptor();
                case ORDERED:
                    return PSet.orderedProvider(valueType).descriptor();
                case DEFAULT:
                    return PSet.provider(valueType).descriptor();
            }
        } else if (reference.isList()) {
            PDescriptorProvider valueType = getTypeProvider(requireNonNull(reference.valueType), annotations);
            return PList.provider(valueType).descriptor();
        }

        // Otherwise it's a declared type.
        return requireDeclaredType(reference);
    }
}