All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.knowledge.HttpBindingIndex Maven / Gradle / Ivy

Go to download

This module provides the core implementation of loading, validating, traversing, mutating, and serializing a Smithy model.

There is a newer version: 1.54.0
Show newest version
/*
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.ErrorTrait;
import software.amazon.smithy.model.traits.HttpErrorTrait;
import software.amazon.smithy.model.traits.HttpHeaderTrait;
import software.amazon.smithy.model.traits.HttpLabelTrait;
import software.amazon.smithy.model.traits.HttpPayloadTrait;
import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait;
import software.amazon.smithy.model.traits.HttpQueryParamsTrait;
import software.amazon.smithy.model.traits.HttpQueryTrait;
import software.amazon.smithy.model.traits.HttpResponseCodeTrait;
import software.amazon.smithy.model.traits.HttpTrait;
import software.amazon.smithy.model.traits.MediaTypeTrait;
import software.amazon.smithy.model.traits.StreamingTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;

/**
 * Computes and indexes the explicit and implicit HTTP bindings of a model.
 *
 * 

This index makes it easy to understand how members of the input or output * of a request/response are bound to an HTTP message by providing all of the * bindings in the model as a normalized {@link HttpBinding} object. This can be * used to validate the bindings of an operation, generate code to [de]serialize * shapes, diff models to ensure backward compatibility at the wire level, etc. * *

This index does not perform validation of the underlying model. */ public final class HttpBindingIndex implements KnowledgeIndex { private final WeakReference model; private final Map> requestBindings = new HashMap<>(); private final Map> responseBindings = new HashMap<>(); public HttpBindingIndex(Model model) { this.model = new WeakReference<>(model); OperationIndex opIndex = OperationIndex.of(model); for (OperationShape operation : model.getOperationShapesWithTrait(HttpTrait.class)) { requestBindings.put(operation.getId(), computeRequestBindings(opIndex, operation)); responseBindings.put(operation.getId(), computeResponseBindings(opIndex, operation)); } // Add error structure bindings. for (StructureShape structure : model.getStructureShapesWithTrait(ErrorTrait.class)) { responseBindings.put(structure.getId(), createStructureBindings(structure, false)); } } public static HttpBindingIndex of(Model model) { return model.getKnowledge(HttpBindingIndex.class, HttpBindingIndex::new); } /** * Returns true if a shape has any HTTP request trait bindings. * * @param shape Shape to check. * @return Returns true if the shape is bound to an HTTP header, * payload, prefix headers, query string, or label. */ public static boolean hasHttpRequestBindings(Shape shape) { return shape.hasTrait(HttpHeaderTrait.class) || shape.hasTrait(HttpPrefixHeadersTrait.class) || shape.hasTrait(HttpPayloadTrait.class) || shape.hasTrait(HttpQueryTrait.class) || shape.hasTrait(HttpQueryParamsTrait.class) || shape.hasTrait(HttpLabelTrait.class); } /** * Returns true if a shape has any HTTP response trait bindings. * * @param shape Shape to check. * @return Returns true if the shape is bound to an HTTP header, * payload, of prefix headers. */ public static boolean hasHttpResponseBindings(Shape shape) { return shape.hasTrait(HttpHeaderTrait.class) || shape.hasTrait(HttpPrefixHeadersTrait.class) || shape.hasTrait(HttpPayloadTrait.class) || shape.hasTrait(HttpResponseCodeTrait.class); } private HttpTrait getHttpTrait(ToShapeId operation) { ShapeId id = operation.toShapeId(); return getModel().getShape(id) .orElseThrow(() -> new IllegalArgumentException(id + " is not a valid shape")) .asOperationShape() .orElseThrow(() -> new IllegalArgumentException(id + " is not an operation shape")) .getTrait(HttpTrait.class) .orElseThrow(() -> new IllegalArgumentException(id + " has no http binding trait")); } private Model getModel() { return Objects.requireNonNull(model.get(), "The dereferenced WeakReference is null"); } /** * Gets the computed status code of an operation or error structure. * * @param shapeOrId Operation or error structure shape ID. * @return Returns the computed HTTP status code. * @throws IllegalArgumentException if the given shape is not an operation * or an error structure. */ public int getResponseCode(ToShapeId shapeOrId) { ShapeId id = shapeOrId.toShapeId(); Shape shape = getModel().getShape(id).orElseThrow(() -> new IllegalArgumentException("Shape not found " + id)); if (shape.isOperationShape()) { return getHttpTrait(id).getCode(); } else if (shape.getTrait(HttpErrorTrait.class).isPresent()) { return shape.getTrait(HttpErrorTrait.class).get().getCode(); } else if (shape.getTrait(ErrorTrait.class).isPresent()) { return shape.getTrait(ErrorTrait.class).get().getDefaultHttpStatusCode(); } throw new IllegalStateException(shape + " must be an operation or error structure"); } /** * Gets the request bindings of an operation as a map of member name to * the binding. * * @param operationShapeOrId Operation to get the request bindings for. * @return Map of unmodifiable bindings. */ public Map getRequestBindings(ToShapeId operationShapeOrId) { ShapeId id = operationShapeOrId.toShapeId(); return requestBindings.getOrDefault(id, Collections.emptyList()) .stream() .collect(Collectors.toMap(HttpBinding::getMemberName, Function.identity())); } /** * Gets the request bindings of an operation as a map of member name to * the binding for a specific location type. * * @param operationShapeOrId Operation to get the request bindings for. * @param requestLocation Location of the binding. * @return Map of unmodifiable bindings. */ public List getRequestBindings(ToShapeId operationShapeOrId, HttpBinding.Location requestLocation) { ShapeId id = operationShapeOrId.toShapeId(); return requestBindings.getOrDefault(id, Collections.emptyList()) .stream() .filter(binding -> binding.getLocation() == requestLocation) .collect(Collectors.toList()); } /** * Gets the computed HTTP message response bindings for an operation * or structure with an error trait. * * @param shapeOrId Operation or error structure shape or ID. * @return Map of unmodifiable bindings. */ public Map getResponseBindings(ToShapeId shapeOrId) { ShapeId id = shapeOrId.toShapeId(); return responseBindings.getOrDefault(id, Collections.emptyList()) .stream() .collect(Collectors.toMap(HttpBinding::getMemberName, Function.identity())); } /** * Gets the computed HTTP message response bindings for an operation * or structure with an error trait for a specific binding type. * * @param shapeOrId Operation or error structure shape or ID. * @param bindingLocation Binding location type. * @return List of found bindings. * @throws IllegalArgumentException if the given shape is not an operation * or error structure. */ public List getResponseBindings(ToShapeId shapeOrId, HttpBinding.Location bindingLocation) { ShapeId id = shapeOrId.toShapeId(); return responseBindings.getOrDefault(id, Collections.emptyList()) .stream() .filter(binding -> binding.getLocation() == bindingLocation) .collect(Collectors.toList()); } /** * Determines the appropriate timestamp format for a member shape bound to * a specific location. * * @param member Member to derive the timestamp format. * @param location Location the member is bound to. * @param defaultFormat The format to use for the body or a default. * @return Returns the determined timestamp format. */ public TimestampFormatTrait.Format determineTimestampFormat( ToShapeId member, HttpBinding.Location location, TimestampFormatTrait.Format defaultFormat ) { Model model = getModel(); return model.getShape(member.toShapeId()) // Use the timestampFormat trait on the member or target if present. .flatMap(shape -> shape.getMemberTrait(model, TimestampFormatTrait.class)) .map(TimestampFormatTrait::getFormat) .orElseGet(() -> { // Determine the format based on the location. switch (location) { case PREFIX_HEADERS: case HEADER: return TimestampFormatTrait.Format.HTTP_DATE; case QUERY: case LABEL: return TimestampFormatTrait.Format.DATE_TIME; default: return defaultFormat; } }); } /** * Returns the expected request Content-Type of the given operation. * *

See {@link #determineRequestContentType(ToShapeId, String, String)} * for documentation on how the content-type is resolved. * * @param operation Operation to determine the content-type of. * @param documentContentType Content-Type to use for protocol documents. * @return Returns the optionally resolved content-type of the request. */ public Optional determineRequestContentType(ToShapeId operation, String documentContentType) { return determineRequestContentType(operation, documentContentType, null); } /** * Returns the expected request Content-Type of the given operation. * *

If members are sent in the "document" body, then the default * {@code documentContentType} value is returned. If a member is bound * to the payload, then the following checks are made: * *

    *
  • If the payload has the {@link StreamingTrait}, then the * {@code eventStreamContentType} is returned.
  • *
  • If the targeted shape is a structure or document type, then * the {@code documentContentType} is returned.
  • *
  • If the targeted shape has the {@link MediaTypeTrait}, then * the value of the trait is returned.
  • *
  • If the targeted shape is a blob, then "application/octet-stream" * is returned.
  • *
  • If the targeted shape is a string, then "text/plain" is * returned.
  • *
* *

If no members are sent in the payload, an empty Optional is * returned. * * @param operation Operation to determine the content-type of. * @param documentContentType Content-Type to use for protocol documents. * @param eventStreamContentType Content-Type to use for event streams. * @return Returns the optionally resolved content-type of the request. */ public Optional determineRequestContentType( ToShapeId operation, String documentContentType, String eventStreamContentType ) { Collection bindings = getRequestBindings(operation).values(); return Optional.ofNullable(determineContentType(bindings, documentContentType, eventStreamContentType)); } /** * Returns the expected response Content-Type of the given operation * or error. * *

See {@link #determineResponseContentType(ToShapeId, String, String)} * for documentation on how the content-type is resolved. * * @param operationOrError Operation or error to determine the content-type of. * @param documentContentType Content-Type to use for protocol documents. * @return Returns the optionally resolved content-type of the response. */ public Optional determineResponseContentType(ToShapeId operationOrError, String documentContentType) { return determineResponseContentType(operationOrError, documentContentType, null); } /** * Returns the expected response Content-Type of the given operation * or error. * *

If members are sent in the "document" body, then the default * {@code documentContentType} value is returned. If a member is bound * to the payload, then the following checks are made: * *

    *
  • If the payload has the {@link StreamingTrait}, then the * {@code eventStreamContentType} is returned.
  • *
  • If the targeted shape is a structure or document type, then * the {@code documentContentType} is returned.
  • *
  • If the targeted shape has the {@link MediaTypeTrait}, then * the value of the trait is returned.
  • *
  • If the targeted shape is a blob, then "application/octet-stream" * is returned.
  • *
  • If the targeted shape is a string, then "text/plain" is * returned.
  • *
* *

If no members are sent in the payload, an empty Optional is * returned. * * @param operationOrError Operation or error to determine the content-type of. * @param documentContentType Content-Type to use for protocol documents. * @param eventStreamContentType Content-Type used for event streams. * @return Returns the optionally resolved content-type of the response. */ public Optional determineResponseContentType( ToShapeId operationOrError, String documentContentType, String eventStreamContentType ) { Collection bindings = getResponseBindings(operationOrError).values(); return Optional.ofNullable(determineContentType(bindings, documentContentType, eventStreamContentType)); } private String determineContentType( Collection bindings, String documentContentType, String eventStreamContentType ) { Model model = getModel(); for (HttpBinding binding : bindings) { if (binding.getLocation() == HttpBinding.Location.DOCUMENT) { return documentContentType; } if (binding.getLocation() == HttpBinding.Location.PAYLOAD) { Shape target = model.getShape(binding.getMember().getTarget()).orElse(null); if (target == null) { // Can't determine the content-type because the model is broken :( // Let other parts of the validation system point this out. break; } else if (StreamingTrait.isEventStream(target)) { return eventStreamContentType; } else if (target.isDocumentShape() || target.isStructureShape() || target.isUnionShape() || target.isListShape() || target.isMapShape()) { // Document type and structure targets are always the document content-type. return documentContentType; } else if (target.getTrait(MediaTypeTrait.class).isPresent()) { // Use the @mediaType trait if available. return target.getTrait(MediaTypeTrait.class).get().getValue(); } else if (target.isBlobShape()) { return "application/octet-stream"; } else if (target.isStringShape()) { return "text/plain"; } } } return null; } /** * Returns true if the request has a modeled body. * * @param operation Operation to check. * @return Returns true if the operation has document or payload bindings. */ public boolean hasRequestBody(ToShapeId operation) { return hasPayloadBindings(getRequestBindings(operation).values()); } /** * Returns true if the response has a modeled body. * * @param operation Operation to check. * @return Returns true if the operation has document or payload bindings. */ public boolean hasResponseBody(ToShapeId operation) { return hasPayloadBindings(getResponseBindings(operation).values()); } private boolean hasPayloadBindings(Collection bindings) { for (HttpBinding binding : bindings) { if (binding.getLocation() == HttpBinding.Location.DOCUMENT || binding.getLocation() == HttpBinding.Location.PAYLOAD) { return true; } } return false; } private List computeRequestBindings(OperationIndex opIndex, OperationShape shape) { return createStructureBindings(opIndex.expectInputShape(shape.getId()), true); } private List computeResponseBindings(OperationIndex opIndex, OperationShape shape) { return createStructureBindings(opIndex.expectOutputShape(shape.getId()), false); } private List createStructureBindings(StructureShape struct, boolean isRequest) { List bindings = new ArrayList<>(); List unbound = new ArrayList<>(); boolean foundPayload = false; for (MemberShape member : struct.getAllMembers().values()) { if (member.getTrait(HttpHeaderTrait.class).isPresent()) { HttpHeaderTrait trait = member.getTrait(HttpHeaderTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.HEADER, trait.getValue(), trait)); } else if (member.getTrait(HttpPrefixHeadersTrait.class).isPresent()) { HttpPrefixHeadersTrait trait = member.getTrait(HttpPrefixHeadersTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.PREFIX_HEADERS, trait.getValue(), trait)); } else if (isRequest && member.getTrait(HttpQueryTrait.class).isPresent()) { HttpQueryTrait trait = member.getTrait(HttpQueryTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.QUERY, trait.getValue(), trait)); } else if (isRequest && member.getTrait(HttpQueryParamsTrait.class).isPresent()) { HttpQueryParamsTrait trait = member.getTrait(HttpQueryParamsTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.QUERY_PARAMS, member.getMemberName(), trait)); } else if (member.getTrait(HttpPayloadTrait.class).isPresent()) { foundPayload = true; HttpPayloadTrait trait = member.getTrait(HttpPayloadTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.PAYLOAD, member.getMemberName(), trait)); } else if (isRequest && member.getTrait(HttpLabelTrait.class).isPresent()) { HttpLabelTrait trait = member.getTrait(HttpLabelTrait.class).get(); bindings.add(new HttpBinding(member, HttpBinding.Location.LABEL, member.getMemberName(), trait)); } else if (!isRequest && member.getTrait(HttpResponseCodeTrait.class).isPresent()) { HttpResponseCodeTrait trait = member.getTrait(HttpResponseCodeTrait.class).get(); bindings.add(new HttpBinding( member, HttpBinding.Location.RESPONSE_CODE, member.getMemberName(), trait)); } else { unbound.add(member); } } if (!unbound.isEmpty()) { if (foundPayload) { unbound.forEach(member -> bindings.add( new HttpBinding(member, HttpBinding.Location.UNBOUND, member.getMemberName(), null))); } else { unbound.forEach(member -> bindings.add( new HttpBinding(member, HttpBinding.Location.DOCUMENT, member.getMemberName(), null))); } } return bindings; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy