diff --git a/docs/content/utilities/validation.mdx b/docs/content/utilities/validation.mdx new file mode 100644 index 000000000..c9d60711e --- /dev/null +++ b/docs/content/utilities/validation.mdx @@ -0,0 +1,331 @@ +--- +title: Validation +description: Utility +--- + +import Note from "../../src/components/Note" + +This utility provides JSON Schema validation for payloads held within events and response used in AWS Lambda. + +**Key features** +* Validate incoming events and responses +* Built-in validation for most common events (API Gateway, SNS, SQS, ...) +* JMESPath support validate only a sub part of the event + +## Install + +To install this utility, add the following dependency to your project. + +```xml + + software.amazon.lambda + powertools-validation + 0.5.0-beta + +``` + +And configure the aspectj-maven-plugin to compile-time weave (CTW) the +aws-lambda-powertools-java aspects into your project. You may already have this +plugin in your pom. In that case add the dependency to the `aspectLibraries` +section. + +```xml + + + ... + + org.codehaus.mojo + aspectj-maven-plugin + 1.11 + + 1.8 + 1.8 + 1.8 + + + + software.amazon.lambda + powertools-validation + + + + + + + + compile + + + + + ... + + +``` + +## Validating events + +You can validate inbound and outbound events using `@Validation` annotation. + +You can also use the `Validator#validate()` methods, if you want more control over the validation process such as handling a validation error. + +We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson library](https://github.com/burtcorp/jmespath-java)). + +### @Validation annotation + +`@Validation` annotation is used to validate either inbound events or functions' response. + +It will fail fast with `ValidationException` if an event or response doesn't conform with given JSON Schema. + +While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. + +```java +public class MyFunctionHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json") + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + // ... + return something; + } +} +``` + +**NOTE**: It's not a requirement to validate both inbound and outbound schemas - You can either use one, or both. + +### Validate function + +Validate standalone function is used within the Lambda handler, or any other methods that perform data validation. + +You can also gracefully handle schema validation errors by catching `ValidationException`. + +```java +import static software.amazon.lambda.powertools.validation.Validator.*; + +public class MyFunctionHandler implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + try { + validate(input, "classpath:/schema.json"); + } catch (ValidationException ex) { + // do something before throwing it + throw ex; + } + + // ... + return something; + } +} +``` +**NOTE**: Schemas are stored in memory for reuse, to avoid loading them from file each time. + +## Built-in events and responses + +For the following events and responses, the Validator will automatically perform validation on the content. + +** Events ** + + Type of event | Class | Path to content | + ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- + API Gateway REST | APIGatewayProxyRequestEvent | `body` + API Gateway HTTP | APIGatewayV2HTTPEvent | `body` + Application Load Balancer | ApplicationLoadBalancerRequestEvent | `body` + Cloudformation Custom Resource | CloudFormationCustomResourceEvent | `resourceProperties` + CloudWatch Logs | CloudWatchLogsEvent | `awslogs.powertools_base64_gzip(data)` + EventBridge / Cloudwatch | ScheduledEvent | `detail` + Kafka | KafkaEvent | `records[*][*].value` + Kinesis | KinesisEvent | `Records[*].kinesis.powertools_base64(data)` + Kinesis Firehose | KinesisFirehoseEvent | `Records[*].powertools_base64(data)` + Kinesis Analytics from Firehose | KinesisAnalyticsFirehoseInputPreprocessingEvent | `Records[*].powertools_base64(data)` + Kinesis Analytics from Streams | KinesisAnalyticsStreamsInputPreprocessingEvent | `Records[*].powertools_base64(data)` + SNS | SNSEvent | `Records[*].Sns.Message` + SQS | SQSEvent | `Records[*].body` + +** Responses ** + + Type of response | Class | Path to content (envelope) + ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- + API Gateway REST | APIGatewayProxyResponseEvent} | `body` + API Gateway HTTP | APIGatewayV2HTTPResponse} | `body` + API Gateway WebSocket | APIGatewayV2WebSocketResponse} | `body` + Load Balancer | ApplicationLoadBalancerResponseEvent} | `body` + Kinesis Analytics | KinesisAnalyticsInputPreprocessingResponse} | `Records[*].powertools_base64(data)`` + +## Custom events and responses + +You can also validate any Event or Response type, once you have the appropriate schema. + +Sometimes, you might want to validate only a portion of it - This is where the envelope parameter is for. + +Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extract a portion of JSON you want before applying JSON Schema validation. + +Here is a custom event where we only want to validate each products: + +```json +{ + "basket": { + "products" : [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + }, + { + "id": 765, + "name": "BarBaz AB", + "price": 43.99 + } + ] + } +} +``` + +Here is how you'd use the `envelope` parameter to extract the payload inside the products key before validating: + +```java +public class MyCustomEventHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/my_custom_event_schema.json", + envelope = "basket.products[*]") + public String handleRequest(MyCustomEvent input, Context context) { + return "OK"; + } +} +``` + +This is quite powerful because you can use JMESPath Query language to extract records from +[arrays, slice and dice](https://jmespath.org/tutorial.html#list-and-slice-projections), +to [pipe expressions](https://jmespath.org/tutorial.html#pipe-expressions) +and [function](https://jmespath.org/tutorial.html#functions) expressions, where you'd extract what you need before validating the actual payload. + +## JMESPath functions + +JMESPath functions ensure to make an operation on a specific part of the json.validate + +Powertools provides two built-in functions: + +### powertools_base64 function + +Use `powertools_base64` function to decode any base64 data. + +This sample will decode the base64 value within the data key, and decode the JSON string into a valid JSON before we can validate it: + +```json +{ + "data" : "ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0=" +} +``` + +```java +public class MyEventHandler implements RequestHandler { + + @Override + public String handleRequest(MyEvent myEvent, Context context) { + validate(myEvent, "classpath:/schema.json", "powertools_base64(data)"); + return "OK"; + } +} +``` + +### powertools_base64_gzip function + +Use `powertools_base64_gzip` function to decompress and decode base64 data. + +This sample will decompress and decode base64 data: + +```json +{ + "data" : "H4sIAAAAAAAA/6vmUlBQykxRslIwMTYyMdIBcfMSc1OBAkpu+flOiUUKEZFKYOGCosxkkLiRqQVXLQDnWo6bOAAAAA==" +} +``` + +```java +public class MyEventHandler implements RequestHandler { + + @Override + public String handleRequest(MyEvent myEvent, Context context) { + validate(myEvent, "classpath:/schema.json", "powertools_base64_gzip(data)"); + return "OK"; + } +} +``` + +**NOTE:** You don't need any function to transform a JSON String into a JSON object, powertools-validation will do it for you. +In the 2 previous example, data contains JSON. Just provide the function to transform the base64 / gzipped / ... string into a clear JSON string. + +### Bring your own JMESPath function + + +This should only be used for advanced use cases where you have special formats not covered by the built-in functions. +New functions will be added to the 2 built-in ones. + + + +Your function must extend `io.burt.jmespath.function.BaseFunction`, take a String as parameter and return a String. +You can read the [doc](https://github.com/burtcorp/jmespath-java#adding-custom-functions) for more information. + +Here is an example that takes some xml and transform it into json: +```java +public class XMLFunction extends BaseFunction { + public Base64Function() { + super("powertools_xml", ArgumentConstraints.typeOf(JmesPathType.STRING)); + } + + @Override + protected T callFunction(Adapter runtime, List> arguments) { + T value = arguments.get(0).value(); + String xmlString = runtime.toString(value); + + String jsonString = // ... transform xmlString to json + + return runtime.createString(jsonString); + } +} +``` + +Once your function is created, you need to add it to powertools: + +```java +ValidatorConfig.get().addFunction(new XMLFunction()); +``` + +You can then use it to do your validation: +```java +public class MyXMLEventHandler implements RequestHandler { + + @Override + public String handleRequest(MyEventWithXML myEvent, Context context) { + validate(myEvent, "classpath:/schema.json", "powertools_xml(path.to.xml_data)"); + return "OK"; + } +} +``` +or using annotation: +```java +public class MyXMLEventHandler implements RequestHandler { + + @Override + @Validation(inboundSchema="classpath:/schema.json", envelope="powertools_xml(path.to.xml_data)") + public String handleRequest(MyEventWithXML myEvent, Context context) { + return "OK"; + } +} +``` + +## Change the schema version +By default, powertools-validation is configured with [V7](https://json-schema.org/draft-07/json-schema-release-notes.html). +You can use the `ValidatorConfig` to change that behaviour: + +```java +ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V4); +``` + +## Advanced ObjectMapper settings +If you need to configure the Jackson ObjectMapper, you can use the `ValidatorConfig`: + +```java +ObjectMapper objectMapper= ValidatorConfig.get().getObjectMapper(); +// update (de)serializationConfig or other properties +``` \ No newline at end of file diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index b79126b44..0c9734559 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -31,6 +31,7 @@ module.exports = { 'utilities/sqs_large_message_handling', 'utilities/batch', 'utilities/parameters', + 'utilities/validation' ], }, navConfig: { diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml index 05f73c5cb..b36279fee 100644 --- a/example/HelloWorldFunction/pom.xml +++ b/example/HelloWorldFunction/pom.xml @@ -33,6 +33,11 @@ powertools-parameters 0.5.0-beta + + software.amazon.lambda + powertools-validation + 0.5.0-beta + software.amazon.lambda powertools-sqs @@ -99,6 +104,10 @@ software.amazon.lambda powertools-sqs + + software.amazon.lambda + powertools-validation + diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppValidation.java b/example/HelloWorldFunction/src/main/java/helloworld/AppValidation.java new file mode 100644 index 000000000..40135722d --- /dev/null +++ b/example/HelloWorldFunction/src/main/java/helloworld/AppValidation.java @@ -0,0 +1,52 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import software.amazon.lambda.powertools.validation.Validation; +import software.amazon.lambda.powertools.validation.Validator; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Handler for requests to Lambda function. + */ +public class AppValidation implements RequestHandler { + + @Validation(inboundSchema = "classpath:/schema.json") + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + return response + .withStatusCode(200) + .withBody(output); + } catch (IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/example/HelloWorldFunction/src/main/resources/schema.json b/example/HelloWorldFunction/src/main/resources/schema.json new file mode 100644 index 000000000..f38272f2d --- /dev/null +++ b/example/HelloWorldFunction/src/main/resources/schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "id", + "name", + "price" + ], + "properties": { + "id": { + "$id": "#/properties/id", + "type": "integer", + "title": "Id of the product", + "description": "Unique identifier of the product", + "default": 0, + "examples": [ + 43242 + ] + }, + "name": { + "$id": "#/properties/name", + "type": "string", + "title": "Name of the product", + "description": "Explicit name of the product", + "minLength": 5, + "default": "", + "examples": [ + "FooBar XY" + ] + }, + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/example/template.yaml b/example/template.yaml index 9f279c2bf..f9b6729f8 100644 --- a/example/template.yaml +++ b/example/template.yaml @@ -30,6 +30,21 @@ Resources: Path: /hello Method: get + HelloWorldValidationFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.AppValidation::handleRequest + Runtime: java8 + MemorySize: 512 + Tracing: Active + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: post + HelloWorldStreamFunction: Type: AWS::Serverless::Function Properties: diff --git a/pom.xml b/pom.xml index 6e9db7658..cbd0f6ecd 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ powertools-sqs powertools-metrics powertools-parameters + powertools-validation @@ -70,6 +71,7 @@ 1.6 5.7.0 1.0.1 + 0.5.0 @@ -103,6 +105,11 @@ pom import + + io.burt + jmespath-jackson + ${jmespath.version} + software.amazon.payloadoffloading payloadoffloading-common diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml new file mode 100644 index 000000000..f730908e5 --- /dev/null +++ b/powertools-validation/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + powertools-validation + jar + + + powertools-parent + software.amazon.lambda + 0.5.0-beta + + + AWS Lambda Powertools Java validation library + + Json schema validation for Lambda events and responses + + https://aws.amazon.com/lambda/ + + GitHub Issues + https://github.com/awslabs/aws-lambda-powertools-java/issues + + + + https://github.com/awslabs/aws-lambda-powertools-java.git + + + + AWS Lambda Powertools team + Amazon Web Services + https://aws.amazon.com/ + + + + + + ossrh + https://aws.oss.sonatype.org/content/repositories/snapshots + + + + + + software.amazon.lambda + powertools-core + + + com.amazonaws + aws-lambda-java-events + + + com.amazonaws + aws-lambda-java-core + + + io.burt + jmespath-jackson + + + com.fasterxml.jackson.core + jackson-databind + + + org.aspectj + aspectjrt + + + com.networknt + json-schema-validator + 1.0.43 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.apache.commons + commons-lang3 + test + + + org.mockito + mockito-core + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java new file mode 100644 index 000000000..1c191f031 --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.networknt.schema.SpecVersion.VersionFlag; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.networknt.schema.SpecVersion.VersionFlag.V7; + +/** + * {@link Validation} is used to specify that the annotated method input and/or output needs to be valid.
+ * + *

{@link Validation} should be used on the {@link com.amazonaws.services.lambda.runtime.RequestHandler#handleRequest(Object, Context)} + * or {@link com.amazonaws.services.lambda.runtime.RequestStreamHandler#handleRequest(InputStream, OutputStream, Context)} methods.

+ * + *

Using the Java language, {@link com.amazonaws.services.lambda.runtime.RequestHandler} input and output are already + * strongly typed, and if a json event cannot be deserialize to the specified object, + * invocation will either fail or retrieve a partial event. + * More information in the documentation (java-handler).

+ * + *

But when using built-in types from the + * aws-lambda-java-events library, + * such as {@link com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent} + * or {@link com.amazonaws.services.lambda.runtime.events.SQSEvent}, + * using the {@link Validation} annotation will permit to validate the underlying content, + * for example the body of an API Gateway request, or the records body of an SQS event.

+ * + *

{@link Validation} has built-in validation for the following input types: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Type of eventClassPath to content
API Gateway REST{@link com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent}{@code body}
API Gateway HTTP{@link com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent}{@code body}
Cloudformation Custom Resource{@link com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent}{@code resourceProperties}
CloudWatch Logs{@link com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent}{@code awslogs.powertools_base64_gzip(data)}
EventBridge / Cloudwatch{@link com.amazonaws.services.lambda.runtime.events.ScheduledEvent}{@code detail}
Kafka{@link com.amazonaws.services.lambda.runtime.events.KafkaEvent}{@code records[*][*].value}
Kinesis{@link com.amazonaws.services.lambda.runtime.events.KinesisEvent}{@code Records[*].kinesis.powertools_base64(data)}
Kinesis Firehose{@link com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent}{@code Records[*].powertools_base64(data)}
Kinesis Analytics from Firehose{@link com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent}{@code Records[*].powertools_base64(data)}
Kinesis Analytics from Streams{@link com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent}{@code Records[*].powertools_base64(data)}
Load Balancer{@link com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent}{@code body}
SNS{@link com.amazonaws.services.lambda.runtime.events.SNSEvent}{@code Records[*].Sns.Message}
SQS{@link com.amazonaws.services.lambda.runtime.events.SQSEvent}{@code Records[*].body}
+ *

+ * + *

{@link Validation} has built-in validation for the following output types: + * + * + * + * + * + * + * + * + * + *
Type of responseClassPath to content
API Gateway REST{@link com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent}{@code body}
API Gateway HTTP{@link com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse}{@code body}
API Gateway WebSocket{@link com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketResponse}{@code body}
Load Balancer{@link com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent}{@code body}
Kinesis Analytics{@link com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse}{@code Records[*].powertools_base64(data)}
+ *

+ * + *

+ * You can specify either inboundSchema or outboundSchema or both, depending on what you want to validate.
+ * The schema must be passed as a json string (constant), or using the syntax {@code "classpath:/some/path/to/schema.json" }, + * provided that the schema.json file is available in the classpath at the specified path. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Validation { + /** + * schema used to validate the lambda function input + */ + String inboundSchema() default ""; + + /** + * schema used to validate the lambda function output + */ + String outboundSchema() default ""; + + /** + * path to the subelement + */ + String envelope() default ""; + + /** + * json schema specification version (default is 2019-09) + */ + VersionFlag schemaVersion() default V7; +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java new file mode 100644 index 000000000..cf8ebe02b --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; + +public class ValidationException extends RuntimeException { + + private static final long serialVersionUID = 1133341411263381508L; + + public ValidationException(String message) { + super(message); + } + + public ValidationException(Exception e) { + super(e); + } + + public ValidationException(String message, Exception e) { + super(message, e); + } +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validator.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validator.java new file mode 100644 index 000000000..758b05b6e --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validator.java @@ -0,0 +1,272 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.ValidationMessage; +import io.burt.jmespath.Expression; +import software.amazon.lambda.powertools.validation.internal.ValidationAspect; + +import java.io.InputStream; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Validation utility, used to manually validate Json against Json Schema + */ +public class Validator { + private static final String CLASSPATH = "classpath:"; + + private static final ConcurrentHashMap schemas = new ConcurrentHashMap<>(); + + /** + * Validate part of a json object against a json schema + * + * @param obj The object to validate + * @param jsonSchema The schema used to validate: either the schema itself or a path to file in the classpath: "classpath:/path/to/schema.json" + * @param envelope a path to a sub object within obj + */ + public static void validate(Object obj, String jsonSchema, String envelope) throws ValidationException { + validate(obj, getJsonSchema(jsonSchema), envelope); + } + + /** + * Validate part of a json object against a json schema + * + * @param obj The object to validate + * @param jsonSchema The schema used to validate + * @param envelope a path to a sub object within obj + */ + public static void validate(Object obj, JsonSchema jsonSchema, String envelope) throws ValidationException { + if (envelope == null || envelope.isEmpty()) { + validate(obj, jsonSchema); + return; + } + JsonNode subNode; + try { + JsonNode jsonNode = ValidatorConfig.get().getObjectMapper().valueToTree(obj); + Expression expression = ValidatorConfig.get().getJmesPath().compile(envelope); + subNode = expression.search(jsonNode); + } catch (Exception e) { + throw new ValidationException("Cannot find envelope <"+envelope+"> in the object <"+obj+">", e); + } + if (subNode.getNodeType() == JsonNodeType.ARRAY) { + subNode.forEach(jsonNode -> validate(jsonNode, jsonSchema)); + } else if (subNode.getNodeType() == JsonNodeType.OBJECT) { + validate(subNode, jsonSchema); + } else if (subNode.getNodeType() == JsonNodeType.STRING) { + // try to validate as json string + try { + validate(subNode.asText(), jsonSchema); + } catch (ValidationException e) { + throw new ValidationException("Invalid format for '" + envelope + "': 'STRING' and no JSON found in it."); + } + } else { + throw new ValidationException("Invalid format for '" + envelope + "': '" + subNode.getNodeType() + "'"); + } + } + + /** + * Validate a json object against a json schema + * + * @param obj Object to validate + * @param jsonSchema The schema used to validate: either the schema itself or a path to file in the classpath: "classpath:/path/to/schema.json" + * @throws ValidationException if validation fails + */ + public static void validate(Object obj, String jsonSchema) throws ValidationException { + validate(obj, getJsonSchema(jsonSchema)); + } + + /** + * Validate a json object against a json schema + * + * @param obj Object to validate + * @param jsonSchema The schema used to validate + * @throws ValidationException if validation fails + */ + public static void validate(Object obj, JsonSchema jsonSchema) throws ValidationException { + JsonNode jsonNode; + try { + jsonNode = ValidatorConfig.get().getObjectMapper().valueToTree(obj); + } catch (Exception e) { + throw new ValidationException("Object <"+obj+"> is not valid against the schema provided", e); + } + + validate(jsonNode, jsonSchema); + } + + /** + * Validate a json object (in string format) against a json schema + * + * @param json Json in string format + * @param jsonSchema The schema used to validate: either the schema itself or a path to file in the classpath: "classpath:/path/to/schema.json" + * @throws ValidationException if validation fails + */ + public static void validate(String json, String jsonSchema) throws ValidationException { + validate(json, getJsonSchema(jsonSchema)); + } + + /** + * Validate a json object (in string format) against a json schema + * + * @param json json in string format + * @param jsonSchema the schema used to validate json string + * @throws ValidationException if validation fails + */ + public static void validate(String json, JsonSchema jsonSchema) throws ValidationException { + JsonNode jsonNode; + try { + jsonNode = ValidatorConfig.get().getObjectMapper().readTree(json); + } catch (Exception e) { + throw new ValidationException("Json <"+json+"> is not valid against the schema provided", e); + } + + validate(jsonNode, jsonSchema); + } + + /** + * Validate a json object (in map format) against a json schema + * + * @param map Map to be transformed in json and validated against the schema + * @param jsonSchema The schema used to validate: either the schema itself or a path to file in the classpath: "classpath:/path/to/schema.json" + * @throws ValidationException if validation fails + */ + public static void validate(Map map, String jsonSchema) throws ValidationException { + validate(map, getJsonSchema(jsonSchema)); + } + + /** + * Validate a json object (in map format) against a json schema + * + * @param map Map to be transformed in json and validated against the schema + * @param jsonSchema the schema used to validate json map + * @throws ValidationException if validation fails + */ + public static void validate(Map map, JsonSchema jsonSchema) throws ValidationException { + JsonNode jsonNode; + try { + jsonNode = ValidatorConfig.get().getObjectMapper().valueToTree(map); + } catch (Exception e) { + throw new ValidationException("Map <"+map+"> cannot be converted to json for validation", e); + } + + validate(jsonNode, jsonSchema); + } + + /** + * Validate a json object (in JsonNode format) against a json schema.
+ * Perform the actual validation. + * + * @param jsonNode Json to be validated against the schema + * @param jsonSchema The schema used to validate: either the schema itself or a path to file in the classpath: "classpath:/path/to/schema.json" + * @throws ValidationException if validation fails + */ + public static void validate(JsonNode jsonNode, String jsonSchema) throws ValidationException { + validate(jsonNode, getJsonSchema(jsonSchema)); + } + + /** + * Validate a json object (in JsonNode format) against a json schema.
+ * Perform the actual validation. + * + * @param jsonNode json to be validated against the schema + * @param jsonSchema the schema to validate json node + * @throws ValidationException if validation fails + */ + public static void validate(JsonNode jsonNode, JsonSchema jsonSchema) throws ValidationException { + Set validationMessages = jsonSchema.validate(jsonNode); + if (!validationMessages.isEmpty()) { + String message; + try { + message = ValidatorConfig.get().getObjectMapper().writeValueAsString(new ValidationErrors(validationMessages)); + } catch (JsonProcessingException e) { + message = validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.joining(", ")); + } + throw new ValidationException(message); + } + } + + /** + * Retrieve {@link JsonSchema} from string (either the schema itself, either from the classpath).
+ * No validation of the schema will be performed (equivalent to
getJsonSchema(schema, false)

+ * Store it in memory to avoid reloading it.
+ * + * @param schema either the schema itself of a "classpath:/path/to/schema.json" + * @return the loaded json schema + */ + public static JsonSchema getJsonSchema(String schema) { + return getJsonSchema(schema, false); + } + + /** + * Retrieve {@link JsonSchema} from string (either the schema itself, either from the classpath).
+ * Optional: validate the schema against the version specifications.
+ * Store it in memory to avoid reloading it.
+ * + * @param schema either the schema itself of a "classpath:/path/to/schema.json" + * @param validateSchema specify if the schema itself must be validated against specifications + * @return the loaded json schema + */ + public static JsonSchema getJsonSchema(String schema, boolean validateSchema) { + JsonSchema jsonSchema = schemas.get(schema); + + if (jsonSchema == null) { + if (schema.startsWith(CLASSPATH)) { + String filePath = schema.substring(CLASSPATH.length()); + InputStream schemaStream = ValidationAspect.class.getResourceAsStream(filePath); + if (schemaStream == null) { + throw new IllegalArgumentException("'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); + } + jsonSchema = ValidatorConfig.get().getFactory().getSchema(schemaStream); + } else { + jsonSchema = ValidatorConfig.get().getFactory().getSchema(schema); + } + + if (validateSchema) { + String version = ValidatorConfig.get().getSchemaVersion().toString(); + try { + validate(jsonSchema.getSchemaNode(), + getJsonSchema("classpath:/schemas/meta_schema_" + version)); + } catch (ValidationException ve) { + throw new IllegalArgumentException("The schema " + schema + " is not valid, it does not respect the specification " + version, ve); + } + } + + schemas.put(schema, jsonSchema); + } + + return jsonSchema; + } + + /** + * + */ + public static class ValidationErrors { + + private final Set validationErrors; + + public ValidationErrors(Set validationErrors) { + this.validationErrors = validationErrors; + } + + public Set getValidationErrors() { + return validationErrors; + } + } +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidatorConfig.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidatorConfig.java new file mode 100644 index 000000000..3ca8194b0 --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidatorConfig.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import io.burt.jmespath.JmesPath; +import io.burt.jmespath.RuntimeConfiguration; +import io.burt.jmespath.function.BaseFunction; +import io.burt.jmespath.function.FunctionRegistry; +import io.burt.jmespath.jackson.JacksonRuntime; +import software.amazon.lambda.powertools.validation.jmespath.Base64Function; +import software.amazon.lambda.powertools.validation.jmespath.Base64GZipFunction; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; + +/** + * Use this if you need to customize some part of the JSON Schema validation + * (eg. specification version, Jackson ObjectMapper, or adding functions to JMESPath) + */ +public class ValidatorConfig { + private ValidatorConfig() { + } + + private static class ConfigHolder { + private final static ValidatorConfig instance = new ValidatorConfig(); + } + + public static ValidatorConfig get() { + return ConfigHolder.instance; + } + + private static final ThreadLocal om = ThreadLocal.withInitial(() -> { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + return objectMapper; + }); + + private SpecVersion.VersionFlag jsonSchemaVersion = SpecVersion.VersionFlag.V7; + private JsonSchemaFactory factory = JsonSchemaFactory.getInstance(jsonSchemaVersion); + + private final FunctionRegistry defaultFunctions = FunctionRegistry.defaultRegistry(); + private final FunctionRegistry customFunctions = defaultFunctions.extend( + new Base64Function(), + new Base64GZipFunction()); + private final RuntimeConfiguration configuration = new RuntimeConfiguration.Builder() + .withFunctionRegistry(customFunctions) + .build(); + private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); + + /** + * Set the version of the json schema specifications (default is V7) + * + * @param version May be V4, V6, V7 or V201909 + */ + public void setSchemaVersion(SpecVersion.VersionFlag version) { + if (version != jsonSchemaVersion) { + jsonSchemaVersion = version; + factory = JsonSchemaFactory.getInstance(version); + } + } + + public SpecVersion.VersionFlag getSchemaVersion() { + return jsonSchemaVersion; + } + + /** + * Add a custom {@link io.burt.jmespath.function.Function} to JMESPath + * {@link Base64Function} and {@link Base64GZipFunction} are already built-in. + * + * @param function the function to add + * @param Must extends {@link BaseFunction} + */ + public void addFunction(T function) { + configuration.functionRegistry().extend(function); + jmesPath = new JacksonRuntime(configuration, getObjectMapper()); + } + + /** + * Return the Json Schema Factory, used to load schemas + * + * @return the Json Schema Factory + */ + public JsonSchemaFactory getFactory() { + return factory; + } + + /** + * Return the JmesPath used to select sub node of Json + * + * @return the {@link JmesPath} + */ + public JmesPath getJmesPath() { + return jmesPath; + } + + /** + * Return an Object Mapper. Use this to customize (de)serialization config. + * + * @return the {@link ObjectMapper} to serialize / deserialize JSON + */ + public ObjectMapper getObjectMapper() { + return om.get(); + } +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java new file mode 100644 index 000000000..38c4a14c7 --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.internal; + +import com.amazonaws.services.lambda.runtime.events.*; +import com.networknt.schema.JsonSchema; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import software.amazon.lambda.powertools.validation.Validation; +import software.amazon.lambda.powertools.validation.ValidatorConfig; + +import static com.networknt.schema.SpecVersion.VersionFlag.V201909; +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; +import static software.amazon.lambda.powertools.validation.Validator.getJsonSchema; +import static software.amazon.lambda.powertools.validation.Validator.validate; +import static software.amazon.lambda.powertools.validation.jmespath.Base64Function.decode; +import static software.amazon.lambda.powertools.validation.jmespath.Base64GZipFunction.decompress; + +/** + * Aspect for {@link Validation} annotation + */ +@Aspect +public class ValidationAspect { + @SuppressWarnings({"EmptyMethod"}) + @Pointcut("@annotation(validation)") + public void callAt(Validation validation) { + } + + @Around(value = "callAt(validation) && execution(@Validation * *.*(..))", argNames = "pjp,validation") + public Object around(ProceedingJoinPoint pjp, + Validation validation) throws Throwable { + Object[] proceedArgs = pjp.getArgs(); + boolean validationNeeded = false; + + if (validation.schemaVersion() != V201909) { + ValidatorConfig.get().setSchemaVersion(validation.schemaVersion()); + } + + if (isHandlerMethod(pjp) + && placedOnRequestHandler(pjp)) { + validationNeeded = true; + + if (!validation.inboundSchema().isEmpty()) { + JsonSchema inboundJsonSchema = getJsonSchema(validation.inboundSchema(), true); + + Object obj = pjp.getArgs()[0]; + if (obj instanceof APIGatewayProxyRequestEvent) { + APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; + validate(event.getBody(), inboundJsonSchema); + } else if (obj instanceof APIGatewayV2HTTPEvent) { + APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; + validate(event.getBody(), inboundJsonSchema); + } else if (obj instanceof SNSEvent) { + SNSEvent event = (SNSEvent) obj; + event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); + } else if (obj instanceof SQSEvent) { + SQSEvent event = (SQSEvent) obj; + event.getRecords().forEach(record -> validate(record.getBody(), inboundJsonSchema)); + } else if (obj instanceof ScheduledEvent) { + ScheduledEvent event = (ScheduledEvent) obj; + validate(event.getDetail(), inboundJsonSchema); + } else if (obj instanceof ApplicationLoadBalancerRequestEvent) { + ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) obj; + validate(event.getBody(), inboundJsonSchema); + } else if (obj instanceof CloudWatchLogsEvent) { + CloudWatchLogsEvent event = (CloudWatchLogsEvent) obj; + validate(decompress(decode(event.getAwsLogs().getData().getBytes(UTF_8))), inboundJsonSchema); + } else if (obj instanceof CloudFormationCustomResourceEvent) { + CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) obj; + validate(event.getResourceProperties(), inboundJsonSchema); + } else if (obj instanceof KinesisEvent) { + KinesisEvent event = (KinesisEvent) obj; + event.getRecords().forEach(record -> validate(decode(record.getKinesis().getData()), inboundJsonSchema)); + } else if (obj instanceof KinesisFirehoseEvent) { + KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj; + event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + } else if (obj instanceof KafkaEvent) { + KafkaEvent event = (KafkaEvent) obj; + event.getRecords().forEach((s, records) -> records.forEach(record -> validate(record.getValue(), inboundJsonSchema))); + }else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { + KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; + event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { + KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; + event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + } else { + validate(obj, inboundJsonSchema, validation.envelope()); + } + } + } + + Object result = pjp.proceed(proceedArgs); + + if (validationNeeded && !validation.outboundSchema().isEmpty()) { + JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); + + if (result instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2HTTPResponse) { + APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2WebSocketResponse) { + APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof ApplicationLoadBalancerResponseEvent) { + ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { + KinesisAnalyticsInputPreprocessingResponse response = (KinesisAnalyticsInputPreprocessingResponse) result; + response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); + } else { + validate(result, outboundJsonSchema, validation.envelope()); + } + } + + return result; + } +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64Function.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64Function.java new file mode 100644 index 000000000..2a418d086 --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64Function.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.jmespath; + +import io.burt.jmespath.Adapter; +import io.burt.jmespath.JmesPathType; +import io.burt.jmespath.function.ArgumentConstraints; +import io.burt.jmespath.function.BaseFunction; +import io.burt.jmespath.function.FunctionArgument; + +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Function used by JMESPath to decode a Base64 encoded String into a decoded String + */ +public class Base64Function extends BaseFunction { + + public Base64Function() { + super("powertools_base64", ArgumentConstraints.typeOf(JmesPathType.STRING)); + } + + @Override + protected T callFunction(Adapter runtime, List> arguments) { + T value = arguments.get(0).value(); + String encodedString = runtime.toString(value); + + String decodedString = decode(encodedString); + + return runtime.createString(decodedString); + } + + public static String decode(String encodedString) { + return new String(decode(encodedString.getBytes(UTF_8))); + } + + public static String decode(ByteBuffer byteBuffer) { + return UTF_8.decode(byteBuffer).toString(); + } + + public static byte[] decode(byte[] encoded) { + return Base64.getDecoder().decode(encoded); + } +} diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64GZipFunction.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64GZipFunction.java new file mode 100644 index 000000000..e5eb52bd4 --- /dev/null +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/jmespath/Base64GZipFunction.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.jmespath; + +import io.burt.jmespath.Adapter; +import io.burt.jmespath.JmesPathType; +import io.burt.jmespath.function.ArgumentConstraints; +import io.burt.jmespath.function.BaseFunction; +import io.burt.jmespath.function.FunctionArgument; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.validation.jmespath.Base64Function.decode; + +/** + * Function used by JMESPath to decode a Base64 encoded GZipped String into a decoded String + */ +public class Base64GZipFunction extends BaseFunction { + + public Base64GZipFunction() { + super("powertools_base64_gzip", ArgumentConstraints.typeOf(JmesPathType.STRING)); + } + + @Override + protected T callFunction(Adapter runtime, List> arguments) { + T value = arguments.get(0).value(); + String encodedString = runtime.toString(value); + + String decompressString = decompress(decode(encodedString.getBytes(UTF_8))); + + return runtime.createString(decompressString); + } + + public static String decompress(byte[] compressed) { + if ((compressed == null) || (compressed.length == 0)) { + return ""; + } + try { + StringBuilder out = new StringBuilder(); + if (isCompressed(compressed)) { + GZIPInputStream gzipStream = new GZIPInputStream(new ByteArrayInputStream(compressed)); + BufferedReader bf = new BufferedReader(new InputStreamReader(gzipStream, UTF_8)); + + String line; + while ((line = bf.readLine()) != null) { + out.append(line); + } + } else { + out.append(Arrays.toString(compressed)); + } + return out.toString(); + } catch (IOException e) { + return new String(compressed); + } + } + + public static boolean isCompressed(final byte[] compressed) { + return (compressed[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (compressed[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)); + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/applicator b/powertools-validation/src/main/resources/schemas/meta/applicator new file mode 100644 index 000000000..24a1cc4f4 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/applicator @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "$recursiveAnchor": true, + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "additionalItems": { "$recursiveRef": "#" }, + "unevaluatedItems": { "$recursiveRef": "#" }, + "items": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "#/$defs/schemaArray" } + ] + }, + "contains": { "$recursiveRef": "#" }, + "additionalProperties": { "$recursiveRef": "#" }, + "unevaluatedProperties": { "$recursiveRef": "#" }, + "properties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$recursiveRef": "#" + } + }, + "propertyNames": { "$recursiveRef": "#" }, + "if": { "$recursiveRef": "#" }, + "then": { "$recursiveRef": "#" }, + "else": { "$recursiveRef": "#" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$recursiveRef": "#" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$recursiveRef": "#" } + } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/content b/powertools-validation/src/main/resources/schemas/meta/content new file mode 100644 index 000000000..f6752a8ef --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/content @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentSchema": { "$recursiveRef": "#" } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/core b/powertools-validation/src/main/resources/schemas/meta/core new file mode 100644 index 000000000..eb708a560 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/core @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true + }, + "$recursiveAnchor": true, + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveRef": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveAnchor": { + "type": "boolean", + "default": false + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/format b/powertools-validation/src/main/resources/schemas/meta/format new file mode 100644 index 000000000..09bbfdda9 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/format @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/format", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/format": true + }, + "$recursiveAnchor": true, + + "title": "Format vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/meta-data b/powertools-validation/src/main/resources/schemas/meta/meta-data new file mode 100644 index 000000000..da04cff6d --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/meta-data @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/meta-data": true + }, + "$recursiveAnchor": true, + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta/validation b/powertools-validation/src/main/resources/schemas/meta/validation new file mode 100644 index 000000000..9f59677b3 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta/validation @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/validation": true + }, + "$recursiveAnchor": true, + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta_schema_V201909 b/powertools-validation/src/main/resources/schemas/meta_schema_V201909 new file mode 100644 index 000000000..2248a0c80 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta_schema_V201909 @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + } + } + } +} diff --git a/powertools-validation/src/main/resources/schemas/meta_schema_V4 b/powertools-validation/src/main/resources/schemas/meta_schema_V4 new file mode 100644 index 000000000..bcbb84743 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta_schema_V4 @@ -0,0 +1,149 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "$schema": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/powertools-validation/src/main/resources/schemas/meta_schema_V6 b/powertools-validation/src/main/resources/schemas/meta_schema_V6 new file mode 100644 index 000000000..bd3e763bc --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta_schema_V6 @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/powertools-validation/src/main/resources/schemas/meta_schema_V7 b/powertools-validation/src/main/resources/schemas/meta_schema_V7 new file mode 100644 index 000000000..fb92c7f75 --- /dev/null +++ b/powertools-validation/src/main/resources/schemas/meta_schema_V7 @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64FunctionTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64FunctionTest.java new file mode 100644 index 000000000..1fae8917c --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64FunctionTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import io.burt.jmespath.Expression; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Base64FunctionTest { + + @Test + public void testPowertoolsBase64() throws IOException { + JsonNode event = ValidatorConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event.json")); + Expression expression = ValidatorConfig.get().getJmesPath().compile("basket.powertools_base64(hiddenProduct)"); + JsonNode result = expression.search(event); + assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); + assertThat(result.asText()).isEqualTo("{\n" + + " \"id\": 43242,\n" + + " \"name\": \"FooBar XY\",\n" + + " \"price\": 258\n" + + "}"); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64GZipFunctionTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64GZipFunctionTest.java new file mode 100644 index 000000000..ad099516e --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/Base64GZipFunctionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import io.burt.jmespath.Expression; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Base64GZipFunctionTest { + + @Test + public void testPowertoolsGzip() throws IOException { + JsonNode event = ValidatorConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + Expression expression = ValidatorConfig.get().getJmesPath().compile("basket.powertools_base64_gzip(hiddenProduct)"); + JsonNode result = expression.search(event); + assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); + assertThat(result.asText()).isEqualTo("{ \"id\": 43242, \"name\": \"FooBar XY\", \"price\": 258}"); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidatorTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidatorTest.java new file mode 100644 index 000000000..6d5f573b9 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidatorTest.java @@ -0,0 +1,249 @@ +package software.amazon.lambda.powertools.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.SpecVersion; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.validation.model.Basket; +import software.amazon.lambda.powertools.validation.model.MyCustomEvent; +import software.amazon.lambda.powertools.validation.model.Product; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; +import static software.amazon.lambda.powertools.validation.Validator.getJsonSchema; +import static software.amazon.lambda.powertools.validation.Validator.validate; + +public class ValidatorTest { + + private JsonSchema schema = getJsonSchema("classpath:/schema_v7.json"); + + @BeforeEach + public void setup() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); + } + + @Test + public void testLoadSchemaV7OK() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); + JsonSchema jsonSchema = getJsonSchema("classpath:/schema_v7.json", true); + assertThat(jsonSchema).isNotNull(); + assertThat(jsonSchema.getCurrentUri()).asString().isEqualTo("http://example.com/product.json"); + } + + @Test + public void testLoadSchemaV7KO() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); + assertThatThrownBy(() -> getJsonSchema("classpath:/schema_v7_ko.json", true)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The schema classpath:/schema_v7_ko.json is not valid, it does not respect the specification V7"); + } + + @Test + public void testLoadMetaSchema_NoValidation() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V201909); + getJsonSchema("classpath:/schemas/meta_schema_V201909", false); + } + + @Test + public void testLoadMetaSchemaV2019() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V201909); + JsonSchema jsonSchema = getJsonSchema("classpath:/schemas/meta_schema_V201909", true); + assertThat(jsonSchema).isNotNull(); + } + + @Test + public void testLoadMetaSchemaV7() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); + JsonSchema jsonSchema = getJsonSchema("classpath:/schemas/meta_schema_V7", true); + assertThat(jsonSchema).isNotNull(); + } + + @Test + public void testLoadMetaSchemaV6() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V6); + JsonSchema jsonSchema = getJsonSchema("classpath:/schemas/meta_schema_V6", true); + assertThat(jsonSchema).isNotNull(); + } + + @Test + public void testLoadMetaSchemaV4() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V4); + JsonSchema jsonSchema = getJsonSchema("classpath:/schemas/meta_schema_V4", true); + assertThat(jsonSchema).isNotNull(); + } + + @Test + public void testLoadSchemaV4OK() { + ValidatorConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V4); + JsonSchema jsonSchema = getJsonSchema("classpath:/schema_v4.json", true); + assertThat(jsonSchema).isNotNull(); + } + + @Test + public void testLoadSchemaNotFound() { + assertThatThrownBy(() -> getJsonSchema("classpath:/dev/null")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'classpath:/dev/null' is invalid, verify '/dev/null' is in your classpath"); + } + + @Test + public void testValidateJsonNodeOK() throws IOException { + JsonNode node = ValidatorConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ok.json")); + + validate(node, schema); + } + + @Test + public void testValidateJsonNodeKO() throws IOException { + JsonNode node = ValidatorConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ko.json")); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(node, schema)); + } + + @Test + public void testValidateMapOK() { + Map map = new HashMap<>(); + map.put("id", 43242); + map.put("name", "FooBar XY"); + map.put("price", 258); + + validate(map, schema); + } + + @Test + public void testValidateMapKO() { + Map map = new HashMap<>(); + map.put("id", 43242); + map.put("name", "FooBar XY"); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(map, schema)); + } + + @Test + public void testValidateStringOK() { + String json = "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 258\n}"; + + validate(json, schema); + } + + @Test + public void testValidateStringKO() { + String json = "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 0\n}"; + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(json, schema)); + } + + @Test + public void testValidateObjectOK() { + Product product = new Product(42, "FooBar", 42); + validate(product, schema); + } + + @Test + public void testValidateObjectKO() { + Product product = new Product(42, "FooBar", -12); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(product, schema)); + } + + @Test + public void testValidateSubObjectOK() { + Product product = new Product(42, "FooBar", 42); + Product product2 = new Product(420, "FooBarBaz", 420); + Basket basket = new Basket(); + basket.add(product); + basket.add(product2); + MyCustomEvent event = new MyCustomEvent(basket); + validate(event, schema, "basket.products[0]"); + } + + @Test + public void testValidateSubObjectKO() { + Product product = new Product(42, null, 42); + Product product2 = new Product(420, "FooBarBaz", 420); + Basket basket = new Basket(); + basket.add(product); + basket.add(product2); + MyCustomEvent event = new MyCustomEvent(basket); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.products[0]")); + } + + @Test + public void testValidateSubObjectListOK() { + Product product = new Product(42, "BarBazFoo", 42); + Product product2 = new Product(420, "FooBarBaz", 23); + Basket basket = new Basket(); + basket.add(product); + basket.add(product2); + MyCustomEvent event = new MyCustomEvent(basket); + + validate(event, schema, "basket.products[*]"); + } + + @Test + public void testValidateSubObjectListKO() { + Product product = new Product(42, "BarBazFoo", 42); + Product product2 = new Product(420, "FooBarBaz", -23); + Basket basket = new Basket(); + basket.add(product); + basket.add(product2); + MyCustomEvent event = new MyCustomEvent(basket); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.products[*]")); + } + + @Test + public void testValidateSubObjectNotFound() { + Product product = new Product(42, "BarBazFoo", 42); + Basket basket = new Basket(); + basket.add(product); + MyCustomEvent event = new MyCustomEvent(basket); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.product")); + } + + @Test + public void testValidateSubObjectNotListNorObject() { + Product product = new Product(42, "Bar", 42); + Product product2 = new Product(420, "FooBarBaz", -23); + Basket basket = new Basket(); + basket.add(product); + basket.add(product2); + MyCustomEvent event = new MyCustomEvent(basket); + + assertThatThrownBy(() -> validate(event, schema, "basket.products[0].id")) + .isInstanceOf(ValidationException.class) + .hasMessage("Invalid format for 'basket.products[0].id': 'NUMBER'"); + } + + @Test + public void testValidateSubObjectJsonString() { + Basket basket = new Basket(); + basket.setHiddenProduct("ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0="); + MyCustomEvent event = new MyCustomEvent(basket); + + validate(event, schema, "basket.powertools_base64(hiddenProduct)"); + } + + @Test + public void testValidateSubObjectSimpleString() { + Basket basket = new Basket(); + basket.setHiddenProduct("ghostbuster"); + MyCustomEvent event = new MyCustomEvent(basket); + + assertThatThrownBy(() -> validate(event, schema, "basket.hiddenProduct")) + .isInstanceOf(ValidationException.class) + .hasMessage("Invalid format for 'basket.hiddenProduct': 'STRING' and no JSON found in it."); + } + + @Test + public void testValidateSubObjectWithoutEnvelope() { + Product product = new Product(42, "BarBazFoo", 42); + validate(product, schema, null); + } + +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java new file mode 100644 index 000000000..7132fcb9b --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import software.amazon.lambda.powertools.validation.Validation; + +public class KinesisHandler implements RequestHandler { + + @Validation(inboundSchema = "classpath:/schema_v7.json") + @Override + public String handleRequest(KinesisEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java new file mode 100644 index 000000000..07954ddff --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.lambda.powertools.validation.Validation; +import software.amazon.lambda.powertools.validation.model.MyCustomEvent; + +public class MyCustomEventHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json", envelope = "basket.products[*]") + public String handleRequest(MyCustomEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java new file mode 100644 index 000000000..cd6719b0f --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import software.amazon.lambda.powertools.validation.Validation; + +public class SQSHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json") + public String handleRequest(SQSEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java new file mode 100644 index 000000000..59b6cc7b5 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import software.amazon.lambda.powertools.validation.Validation; + + +public class ValidationInboundClasspathHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json") + public String handleRequest(APIGatewayProxyRequestEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java new file mode 100644 index 000000000..e27f31129 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import software.amazon.lambda.powertools.validation.Validation; + + +public class ValidationInboundStringHandler implements RequestHandler { + + private static final String schema = "{\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema\",\n" + + " \"$id\": \"http://example.com/product.json\",\n" + + " \"type\": \"object\",\n" + + " \"title\": \"Product schema\",\n" + + " \"description\": \"JSON schema to validate Products\",\n" + + " \"default\": {},\n" + + " \"examples\": [\n" + + " {\n" + + " \"id\": 43242,\n" + + " \"name\": \"FooBar XY\",\n" + + " \"price\": 258\n" + + " }\n" + + " ],\n" + + " \"required\": [\n" + + " \"id\",\n" + + " \"name\",\n" + + " \"price\"\n" + + " ],\n" + + " \"properties\": {\n" + + " \"id\": {\n" + + " \"$id\": \"#/properties/id\",\n" + + " \"type\": \"integer\",\n" + + " \"title\": \"Id of the product\",\n" + + " \"description\": \"Unique identifier of the product\",\n" + + " \"default\": 0,\n" + + " \"examples\": [\n" + + " 43242\n" + + " ]\n" + + " },\n" + + " \"name\": {\n" + + " \"$id\": \"#/properties/name\",\n" + + " \"type\": \"string\",\n" + + " \"title\": \"Name of the product\",\n" + + " \"description\": \"Explicit name of the product\",\n" + + " \"minLength\": 5,\n" + + " \"default\": \"\",\n" + + " \"examples\": [\n" + + " \"FooBar XY\"\n" + + " ]\n" + + " },\n" + + " \"price\": {\n" + + " \"$id\": \"#/properties/price\",\n" + + " \"type\": \"number\",\n" + + " \"title\": \"Price of the product\",\n" + + " \"description\": \"Positive price of the product\",\n" + + " \"default\": 0,\n" + + " \"exclusiveMinimum\": 0,\n" + + " \"examples\": [\n" + + " 258.99\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"additionalProperties\": true\n" + + "}"; + + @Override + @Validation(inboundSchema = schema) + public String handleRequest(APIGatewayV2HTTPEvent input, Context context) { + return "OK"; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java new file mode 100644 index 000000000..e5be82c95 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.internal; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.lambda.powertools.validation.ValidationException; +import software.amazon.lambda.powertools.validation.ValidatorConfig; +import software.amazon.lambda.powertools.validation.handlers.*; +import software.amazon.lambda.powertools.validation.model.MyCustomEvent; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class ValidationAspectTest { + + @Mock + private Context context; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void validate_inputOK_schemaInClasspath_shouldValidate() { + ValidationInboundClasspathHandler handler = new ValidationInboundClasspathHandler(); + APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); + event.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": 42" + + "}"); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } + + @Test + public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { + ValidationInboundClasspathHandler handler = new ValidationInboundClasspathHandler(); + APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); + event.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": -2" + + "}"); + // price is negative + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + } + + @Test + public void validate_inputOK_schemaInString_shouldValidate() { + ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": 42" + + "}"); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } + + @Test + public void validate_inputKO_schemaInString_shouldThrowValidationException() { + ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"" + + "}"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + } + + @Test + public void validate_SQS() throws IOException { + SQSEvent event = ValidatorConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/sqs.json"), SQSEvent.class); + + SQSHandler handler = new SQSHandler(); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } + + @Test + public void validate_Kinesis() throws IOException { + KinesisEvent event = ValidatorConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/kinesis.json"), KinesisEvent.class); + + KinesisHandler handler = new KinesisHandler(); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } + + @Test + public void validate_CustomObject() throws IOException { + MyCustomEvent event = ValidatorConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/custom_event.json"), MyCustomEvent.class); + + MyCustomEventHandler handler = new MyCustomEventHandler(); + assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java new file mode 100644 index 000000000..548ef4660 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.model; + +import java.util.ArrayList; +import java.util.List; + +public class Basket { + private List products = new ArrayList<>(); + + private String hiddenProduct; + + public List getProducts() { + return products; + } + + public void setProducts(List products) { + this.products = products; + } + + public Basket() { + } + + public void add(Product product) { + products.add(product); + } + + public String getHiddenProduct() { + return hiddenProduct; + } + + public void setHiddenProduct(String hiddenProduct) { + this.hiddenProduct = hiddenProduct; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java new file mode 100644 index 000000000..12f3f99ca --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.model; + +public class MyCustomEvent { + private Basket basket; + + public MyCustomEvent() { + } + + public MyCustomEvent(Basket basket) { + this.basket = basket; + } + + public Basket getBasket() { + return basket; + } + + public void setBasket(Basket basket) { + this.basket = basket; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java new file mode 100644 index 000000000..fde888b76 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. + * Licensed 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 software.amazon.lambda.powertools.validation.model; + +public class Product { + private long id; + + private String name; + + private double price; + + public Product() { + } + + public Product(long id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } +} diff --git a/powertools-validation/src/test/resources/custom_event.json b/powertools-validation/src/test/resources/custom_event.json new file mode 100644 index 000000000..13103c434 --- /dev/null +++ b/powertools-validation/src/test/resources/custom_event.json @@ -0,0 +1,12 @@ +{ + "basket": { + "products" : [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "hiddenProduct": "ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0=" + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/custom_event_gzip.json b/powertools-validation/src/test/resources/custom_event_gzip.json new file mode 100644 index 000000000..d212052d0 --- /dev/null +++ b/powertools-validation/src/test/resources/custom_event_gzip.json @@ -0,0 +1,12 @@ +{ + "basket": { + "products" : [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "hiddenProduct": "H4sIAAAAAAAA/6vmUlBQykxRslIwMTYyMdIBcfMSc1OBAkpu+flOiUUKEZFKYOGCosxkkLiRqQVXLQDnWo6bOAAAAA==" + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/json_ko.json b/powertools-validation/src/test/resources/json_ko.json new file mode 100644 index 000000000..1e566080f --- /dev/null +++ b/powertools-validation/src/test/resources/json_ko.json @@ -0,0 +1,5 @@ +{ + "id": 43242, + "name": "FooBar XY", + "price": 0 +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/json_ok.json b/powertools-validation/src/test/resources/json_ok.json new file mode 100644 index 000000000..427253d66 --- /dev/null +++ b/powertools-validation/src/test/resources/json_ok.json @@ -0,0 +1,5 @@ +{ + "id": 43242, + "name": "FooBar XY", + "price": 258 +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/kinesis.json b/powertools-validation/src/test/resources/kinesis.json new file mode 100644 index 000000000..6d99be7e5 --- /dev/null +++ b/powertools-validation/src/test/resources/kinesis.json @@ -0,0 +1,38 @@ +{ + "records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-04", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6IDQyNSwKICAibmFtZSI6ICJCYXJGb28iLAogICJwcmljZSI6IDQzCn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/schema_v4.json b/powertools-validation/src/test/resources/schema_v4.json new file mode 100644 index 000000000..ae277d476 --- /dev/null +++ b/powertools-validation/src/test/resources/schema_v4.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Product", + "description": "A product from the catalog", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier for a product", + "type": "integer" + }, + "name": { + "description": "Name of the product", + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + } + }, + "required": ["id", "name", "price"] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/schema_v7.json b/powertools-validation/src/test/resources/schema_v7.json new file mode 100644 index 000000000..f38272f2d --- /dev/null +++ b/powertools-validation/src/test/resources/schema_v7.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "id", + "name", + "price" + ], + "properties": { + "id": { + "$id": "#/properties/id", + "type": "integer", + "title": "Id of the product", + "description": "Unique identifier of the product", + "default": 0, + "examples": [ + 43242 + ] + }, + "name": { + "$id": "#/properties/name", + "type": "string", + "title": "Name of the product", + "description": "Explicit name of the product", + "minLength": 5, + "default": "", + "examples": [ + "FooBar XY" + ] + }, + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/schema_v7_ko.json b/powertools-validation/src/test/resources/schema_v7_ko.json new file mode 100644 index 000000000..f54bcb3c7 --- /dev/null +++ b/powertools-validation/src/test/resources/schema_v7_ko.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "id", + "name", + "price" + ], + "properties": { + "id": { + "$id": "#/properties/id", + "type": "integer", + "title": "Id of the product", + "description": "Unique identifier of the product", + "default": 0, + "examples": [ + 43242 + ] + }, + "name": { + "$id": "#/properties/name", + "type": "varchar", + "title": "Name of the product", + "description": "Explicit name of the product", + "minLength": 5, + "default": "", + "examples": [ + "FooBar XY" + ] + }, + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/sqs.json b/powertools-validation/src/test/resources/sqs.json new file mode 100644 index 000000000..9180c5839 --- /dev/null +++ b/powertools-validation/src/test/resources/sqs.json @@ -0,0 +1,22 @@ +{ + "records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db2", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 258\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file