@Nonnull private ValidationReport validateHeader(final ApiOperation apiOperation, final String headerName, final Header apiHeader, final Collection<String> propertyValues) { if (propertyValues.isEmpty() && TRUE.equals(apiHeader.getRequired())) { return ValidationReport.singleton( messages.get("validation.response.header.missing", headerName, apiOperation.getApiPath().original()) ); } return propertyValues .stream() .map(v -> schemaValidator.validate(v, apiHeader.getSchema(), "response.header")) .reduce(ValidationReport.empty(), ValidationReport::merge); } }
@Nonnull private ValidationReport validatePathParameters(final ApiOperation apiOperation) { ValidationReport validationReport = empty(); final NormalisedPath requestPath = apiOperation.getRequestPath(); for (int i = 0; i < apiOperation.getApiPath().numberOfParts(); i++) { if (!apiOperation.getApiPath().hasParams(i)) { continue; } final ValidationReport pathPartValidation = apiOperation .getApiPath() .paramValues(i, requestPath.part(i)) .entrySet() .stream() .map(param -> validatePathParameter(apiOperation, param.getKey(), param.getValue())) .reduce(empty(), ValidationReport::merge); validationReport = validationReport.merge(pathPartValidation); } return validationReport; }
/** * Tries to find the best fitting API path matching the given path and request method. * * @param path the requests path to find in API definition * @param method the {@link Request.Method} for the request * @return a {@link ApiOperationMatch} containing the information if the path is defined, the operation * is allowed and having the necessary {@link ApiOperation} if applicable */ @Nonnull public ApiOperationMatch findApiOperation(final String path, final Request.Method method) { // try to find possible matching paths regardless of HTTP method final NormalisedPath requestPath = new NormalisedPathImpl(path, apiPrefix); final List<ApiPath> matchingPaths = apiPathsGroupedByNumberOfParts .getOrDefault(requestPath.numberOfParts(), emptyList()).stream() .filter(p -> p.matches(requestPath)) .collect(toList()); if (matchingPaths.isEmpty()) { return ApiOperationMatch.MISSING_PATH; } // try to find the operation which fits the HTTP method, // choosing the most 'specific' path match from the candidates final PathItem.HttpMethod httpMethod = PathItem.HttpMethod.valueOf(method.name()); final Optional<ApiPath> matchingPathAndOperation = matchingPaths.stream() .filter(apiPath -> operations.contains(apiPath.original(), httpMethod)) .max(comparingInt(ApiOperationResolver::specificityScore)); return matchingPathAndOperation .map(match -> new ApiOperationMatch(new ApiOperation(match, requestPath, httpMethod, operations.get(match.original(), httpMethod)))) .orElse(ApiOperationMatch.NOT_ALLOWED_OPERATION); }
/** * Get the 'specificity' score of the provided API path. This is used when selecting an API operation to validate against - * where an incoming request matches multiple paths the 'most specific' one should win. * <p> * Note: This score is essentially meaningless across different paths - it should only be used to differentiate paths * that could be equivalent. For example, '{@code /{id}}' and '{@code /{id}.json}' could both match an incoming request on path * '{@code /foo.json}'; in that case we should match on '{@code /{id}.json}' as it is the most 'specific' match. * * @return a score >= 0 that indicates how 'specific' the path definition is. Higher numbers indicate more specific * definitions (e.g. fewer path variables). */ private static int specificityScore(final ApiPath apiPath) { // Return the length of the path, with path vars counting as 1. return apiPath.normalised().replaceAll("\\{.+?}", "").length(); }
/** * Matches operations that contain the given regular expression in their API path. * <p> * The tested path does not have parameters materialized, but is taken from the API * definition, e.g. "/store/order/{orderId}". */ public static WhitelistRule pathContains(final String regexp) { return new PrintableWhitelistRule( "Api path contains: '" + regexp + "'", (message, operation, request, response) -> operation != null && regexpContain(operation.getApiPath().normalised(), regexp)); }
public Optional<OpenApiOperation> findApiOperation(String method, String path) { String relativePath = UrlUtils.extractPath(path); Request.Method requestMethod = Enum.valueOf(Request.Method.class, method); ApiOperationMatch apiOperation = apiOperationResolver.findApiOperation(relativePath, requestMethod); if (! apiOperation.isPathFound() || ! apiOperation.isOperationAllowed()) { return Optional.empty(); } return Optional.of(new OpenApiOperation(method, combineWithBasePath(apiOperation.getApiOperation().getApiPath().original()))); }
@Nonnull private ValidationReport validateParameter(final ApiOperation apiOperation, final Parameter parameter, final Collection<String> parameterValues, final String missingKey) { final ValidationReport.MessageContext context = ValidationReport.MessageContext.create().withParameter(parameter).build(); if (parameterValues.isEmpty() && TRUE.equals(parameter.getRequired())) { return ValidationReport.singleton( messages.get(missingKey, parameter.getName(), apiOperation.getApiPath().original()) ).withAdditionalContext(context); } return parameterValues .stream() .map(v -> parameterValidator.validate(v, parameter)) .reduce(empty(), ValidationReport::merge); }
return ValidationReport.singleton( messages.get("validation.response.body.missing", apiOperation.getMethod(), apiOperation.getApiPath().original()) );
return ValidationReport.singleton( messages.get("validation.response.status.unknown", response.getStatus(), apiOperation.getApiPath().original()) ).withAdditionalContext(contextBuilder.build());