
software.amazon.smithy.model.knowledge.ServiceIndex Maven / Gradle / Ivy
Show all versions of smithy-model Show documentation
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.smithy.model.knowledge;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.AuthDefinitionTrait;
import software.amazon.smithy.model.traits.AuthTrait;
import software.amazon.smithy.model.traits.OptionalAuthTrait;
import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
import software.amazon.smithy.utils.MapUtils;
/**
* An index that resolves service protocols and auth schemes.
*
* This index can be used to get all of the protocol traits applied to
* a service, to get all of the auth defining traits applied to a service,
* to get the effective authentication schemes of a service, and to get the
* effective authentication schemes of an operation bound within the
* closure of a service.
*/
public final class ServiceIndex implements KnowledgeIndex {
private final WeakReference model;
private final Set protocolTraits = new HashSet<>();
private final Set authTraits = new HashSet<>();
public ServiceIndex(Model model) {
this.model = new WeakReference<>(model);
for (Shape shape : model.getShapesWithTrait(ProtocolDefinitionTrait.class)) {
protocolTraits.add(shape.getId());
}
for (Shape shape : model.getShapesWithTrait(AuthDefinitionTrait.class)) {
authTraits.add(shape.getId());
}
}
public static ServiceIndex of(Model model) {
return model.getKnowledge(ServiceIndex.class, ServiceIndex::new);
}
/**
* Defines the type of auth schemes returned by {@link #getEffectiveAuthSchemes}.
*/
public enum AuthSchemeMode {
/**
* Use only the modeled auth schemes. This is the default.
*/
MODELED,
/**
* Use the modeled auth schemes, as well as the synthetic {@link NoAuthTrait} where applicable.
*
* The Smithy Reference Architecture recommends using the {@code smithy.api#noAuth} auth scheme to represent
* no authentication which is available as the {@link NoAuthTrait}.
*/
NO_AUTH_AWARE;
}
/**
* Get all protocol traits attached to a service.
*
*
A protocol trait is a trait that is marked with
* the {@code smithy.api#protocolDefinition} trait.
*
*
An empty map is returned if {@code service} cannot be found in the
* model or is not a service shape.
*
* @param service Service to get the protocols of.
* @return Returns a map of the protocol trait ID to the trait.
*/
public Map getProtocols(ToShapeId service) {
return getTraitMapInSet(service, protocolTraits);
}
private Map getTraitMapInSet(ToShapeId service, Set haystack) {
return getModel()
.getShape(service.toShapeId())
.flatMap(Shape::asServiceShape)
.map(shape -> {
Map result = new TreeMap<>();
for (Trait trait : shape.getAllTraits().values()) {
if (haystack.contains(trait.toShapeId())) {
result.put(trait.toShapeId(), trait);
}
}
return result;
})
.orElse(Collections.emptyMap());
}
private Model getModel() {
return Objects.requireNonNull(model.get(), "The dereferenced WeakReference is null");
}
/**
* Get all auth defining traits attached to a service or operation.
*
* An auth defining trait is a trait that is marked with
* the {@code smithy.api#authDefinition} trait.
*
*
The returned map is ordered alphabetically by absolute shape ID.
*
*
An empty map is returned if {@code id} cannot be found in the
* model or is not a service shape.
*
* @param service Service to get the auth schemes of.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map getAuthSchemes(ToShapeId service) {
return getTraitMapInSet(service, authTraits);
}
/**
* Gets a list of effective authentication schemes applied to a service.
*
* An effective authentication scheme is derived from the
* {@code smithy.api#auth} trait and the auth defining traits applied
* to a service. If the service has the {@code smithy.api#auth} trait,
* then a map is returned that contains the traits applied to the service
* that matches the values in the auth trait. If no auth trait is applied,
* then all of the auth defining traits on the service are returned.
*
*
The returned map is provided in the same order as the values in the
* {@code auth} trait if an auth trait is present, otherwise the result
* returned is ordered alphabetically by absolute shape ID.
*
*
An empty map is returned if {@code service} cannot be found in the
* model or is not a service shape.
*
* @param service Service to get the effective authentication schemes of.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map getEffectiveAuthSchemes(ToShapeId service) {
return getModel()
.getShape(service.toShapeId())
.flatMap(Shape::asServiceShape)
.map(shape -> {
Map result = getAuthTraitValues(shape, shape);
if (result == null) {
result = new TreeMap<>();
for (Map.Entry traitEntry : shape.getAllTraits().entrySet()) {
if (authTraits.contains(traitEntry.getKey())) {
result.put(traitEntry.getKey(), traitEntry.getValue());
}
}
}
return result;
})
.orElse(Collections.emptyMap());
}
/**
* Gets a list of effective authentication schemes applied to a service, based on the AuthSchemeMode.
*
* If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
* {@link #getEffectiveAuthSchemes(ToShapeId)}.
*
*
If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, except that if the service has no effective
* auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth} auth scheme. It avoids having to
* special case handling an empty result. The returned map will always contain at least 1 entry.
*
* @param service Service to get the effective authentication schemes of.
* @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map getEffectiveAuthSchemes(ToShapeId service, AuthSchemeMode authSchemeMode) {
Map authSchemes = getEffectiveAuthSchemes(service);
if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
if (authSchemes.isEmpty()) {
authSchemes = MapUtils.of(NoAuthTrait.ID, new NoAuthTrait());
}
}
return authSchemes;
}
/**
* Gets a list of effective authentication schemes applied to an operation
* bound within a service.
*
* If the given operation defines that {@code smithy.api#auth} trait,
* then a map is returned that consists of the traits applied to the
* service that match the values of the {@code smithy.api#auth} trait. If
* the operation does not define an {@code smithy.api#auth} trait, then
* the effective auth schemes of the service is returned (that is, the
* return value of {@link #getEffectiveAuthSchemes(ToShapeId)}).
*
*
The returned map is provided in the same order as the values in the
* {@code auth} trait if an auth trait is present, otherwise the result
* returned is ordered alphabetically by absolute shape ID.
*
*
An empty map is returned if {@code service} shape cannot be found
* in the model or is not a service shape. An empty map is returned if
* {@code operation} cannot be found in the model or is not an operation
* shape.
*
* @param service Service the operation is within.
* @param operation Operation to get the effective authentication schemes of.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map getEffectiveAuthSchemes(ToShapeId service, ToShapeId operation) {
Shape serviceShape = getModel()
.getShape(service.toShapeId())
.flatMap(Shape::asServiceShape)
.orElse(null);
if (serviceShape == null) {
return Collections.emptyMap();
}
return getModel()
.getShape(operation.toShapeId())
.flatMap(Shape::asOperationShape)
.map(operationShape -> {
Map result = getAuthTraitValues(serviceShape, operationShape);
return result != null ? result : getEffectiveAuthSchemes(service);
})
.orElse(Collections.emptyMap());
}
/**
* Gets a list of effective authentication schemes applied to an operation
* bound within a service, based on the AuthSchemeMode.
*
* If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
* {@link #getEffectiveAuthSchemes(ToShapeId, ToShapeId)}.
*
*
If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, with the following differences:
* If the operation has no effective auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth}
* auth scheme.
* If the operation has the {@code smithy.api#optionalAuth} trait, it adds {@code smithy.api#noAuth} to the end.
*
*
Using {@code NO_AUTH_AWARE} accounts for {@code smithy.api#optionalAuth} and avoids having to special case
* handling an empty result. The returned map will always contain at least 1 entry.
*
*
The {@code smithy.api#noAuth} scheme, if present, is always the last scheme.
*
* @param service Service the operation is within.
* @param operation Operation to get the effective authentication schemes of.
* @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
* @return Returns a map of the trait shape ID to the auth trait itself.
*/
public Map getEffectiveAuthSchemes(ToShapeId service,
ToShapeId operation,
AuthSchemeMode authSchemeMode) {
Map authSchemes = getEffectiveAuthSchemes(service, operation);
if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
if (authSchemes.isEmpty() || hasOptionalAuth(operation)) {
authSchemes = new LinkedHashMap<>(authSchemes);
authSchemes.put(NoAuthTrait.ID, new NoAuthTrait());
}
}
return authSchemes;
}
private boolean hasOptionalAuth(ToShapeId operation) {
return getModel()
.getShape(operation.toShapeId())
.filter(shape -> shape.hasTrait(OptionalAuthTrait.class))
.isPresent();
}
private static Map getAuthTraitValues(Shape service, Shape subject) {
if (!subject.hasTrait(AuthTrait.class)) {
return null;
}
AuthTrait authTrait = subject.expectTrait(AuthTrait.class);
Map result = new LinkedHashMap<>();
for (ShapeId value : authTrait.getValueSet()) {
service.findTrait(value).ifPresent(trait -> result.put(value, trait));
}
return result;
}
}