-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for creating Custom Event Types with schemas validation #83
base: main
Are you sure you want to change the base?
Changes from 17 commits
ddd02fb
0b345f8
5a2f5fc
0d0c1c0
0f591e6
e32f8c5
51b3ffc
88d3bdd
bbd4279
5309a0f
03f986b
9ff4519
b2c37fd
aab785a
67d2b79
0bb99f2
65856c9
220cb9f
cadb9a7
d737489
c8ffb25
6ce4bad
495c0b3
32846cc
dff52e9
2227b6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
import com.networknt.schema.ValidationMessage; | ||
import dev.cdevents.config.CustomObjectMapper; | ||
import dev.cdevents.constants.CDEventConstants; | ||
import static dev.cdevents.constants.CDEventConstants.CUSTOM_SCHEMA_CLASSPATH; | ||
import dev.cdevents.exception.CDEventsException; | ||
import dev.cdevents.models.CDEvent; | ||
import io.cloudevents.CloudEvent; | ||
|
@@ -63,18 +64,8 @@ public static CloudEvent cdEventAsCloudEvent(CDEvent cdEvent) { | |
log.error("CDEvent validation failed against schema URL - {}", cdEvent.schemaURL()); | ||
throw new CDEventsException("CDEvent validation failed against schema URL - " + cdEvent.schemaURL()); | ||
} | ||
String cdEventJson = cdEventAsJson(cdEvent); | ||
log.info("CDEvent with type {} as json - {}", cdEvent.currentCDEventType(), cdEventJson); | ||
try { | ||
CloudEvent ceToSend = new CloudEventBuilder() | ||
.withId(UUID.randomUUID().toString()) | ||
.withSource(new URI(cdEvent.eventSource())) | ||
.withType(cdEvent.currentCDEventType()) | ||
.withDataContentType("application/json") | ||
.withData(cdEventJson.getBytes(StandardCharsets.UTF_8)) | ||
.withTime(OffsetDateTime.now()) | ||
.build(); | ||
return ceToSend; | ||
return buildCloudEvent(cdEvent); | ||
} catch (URISyntaxException e) { | ||
throw new CDEventsException("Exception occurred while building CloudEvent from CDEvent ", e); | ||
} | ||
|
@@ -86,8 +77,10 @@ public static CloudEvent cdEventAsCloudEvent(CDEvent cdEvent) { | |
* @return valid cdEvent | ||
*/ | ||
public static boolean validateCDEvent(CDEvent cdEvent) { | ||
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent); | ||
|
||
Map<String, String> schemaMap = new HashMap<>(); | ||
schemaMap.put(cdEvent.schemaURL(), SCHEMA_CLASSPATH + cdEvent.schemaFileName()); | ||
schemaMap.put(cdEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json"); | ||
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent, schemaMap); | ||
if (!errors.isEmpty()) { | ||
log.error("CDEvent validation failed with errors {}", errors); | ||
return false; | ||
|
@@ -97,17 +90,17 @@ public static boolean validateCDEvent(CDEvent cdEvent) { | |
|
||
/** | ||
* Creates cdEvent from cdEventJson string and validates against schema. | ||
* @param cdEventJson | ||
* @return CDEvent, needs type casting to specific CDEvent class | ||
* @param cdEventJson json string of any CDEvent type | ||
* @return CDEvent needs type casting to specific CDEvent class | ||
*/ | ||
public static CDEvent cdEventFromJson(String cdEventJson) { | ||
if (!validateCDEventJson(cdEventJson)) { | ||
throw new CDEventsException("CDEvent Json validation failed against schema"); | ||
} | ||
String eventType = getUnVersionedEventTypeFromJson(cdEventJson); | ||
CDEventConstants.CDEventTypes cdEventType = getCDEventTypeEnum(eventType); | ||
try { | ||
CDEvent cdEvent = (CDEvent) new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass()); | ||
CDEvent cdEvent = new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we constructing a new object mapper every time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed this method using the existing APIs |
||
if (!validateCDEvent(cdEvent)) { | ||
throw new CDEventsException("CDEvent Json validation failed against schema"); | ||
} | ||
return cdEvent; | ||
} catch (JsonProcessingException e) { | ||
log.error("Exception occurred while creating CDEvent from json {}", cdEventJson); | ||
|
@@ -116,35 +109,114 @@ public static CDEvent cdEventFromJson(String cdEventJson) { | |
} | ||
|
||
/** | ||
* Validates the cdEventJson against the Schema URL. | ||
* @param cdEventJson | ||
* @return true, If cdEventJson is valid | ||
* Creates a CloudEvent from the custom cdEvent. | ||
* @param <T> customCDEvent class | ||
* @param customCDEvent custom CDEvent class object of type <T> | ||
* @param validateContextSchema true If validation needed against context.schemaUri | ||
* @return CloudEvent | ||
*/ | ||
public static boolean validateCDEventJson(String cdEventJson) { | ||
String eventType = getUnVersionedEventTypeFromJson(cdEventJson); | ||
CDEventConstants.CDEventTypes cdEventType = getCDEventTypeEnum(eventType); | ||
public static <T extends CDEvent> CloudEvent customCDEventAsCloudEvent(T customCDEvent, boolean validateContextSchema) { | ||
if (!validateCustomCDEvent(customCDEvent, validateContextSchema)) { | ||
throw new CDEventsException("Custom CDEvent validation failed."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not say why something has failed in the exception? We should probably create an issue for that if that's the case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the method with in the condition logs the reason for failures before the exception is thrown. |
||
} | ||
try { | ||
CDEvent cdEvent = (CDEvent) new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass()); | ||
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent); | ||
return buildCloudEvent(customCDEvent); | ||
} catch (URISyntaxException e) { | ||
throw new CDEventsException("Exception occurred while building CloudEvent from custom CDEvent ", e); | ||
} | ||
} | ||
|
||
if (!errors.isEmpty()) { | ||
log.error("CDEvent Json validation failed against schema URL {}", cdEvent.schemaURL()); | ||
log.error("CDEvent Json validation failed with errors {}", errors); | ||
return false; | ||
/** | ||
* Creates customCDEvent from Json string and validates against context and official schemas. | ||
* @param customCDEventJson Json string of customCDEvent class type <T> | ||
* @param <T> customCDEvent class | ||
* @param eventClass custom CDEvent class of type <T> | ||
* @param validateContextSchema true If validation needed against context.schemaUri | ||
* @return CDEvent | ||
*/ | ||
public static <T extends CDEvent> T customCDEventFromJson(String customCDEventJson, Class<T> eventClass, boolean validateContextSchema) { | ||
try { | ||
T cdEvent = new ObjectMapper().readValue(customCDEventJson, eventClass); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some comment here with the object mapper. We can just move this to a static field There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed this method now and using it from existing code |
||
if (!validateCustomCDEvent(cdEvent, validateContextSchema)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment above with the reasons something has failed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using an existing method now, and added more logging for the failure reason |
||
throw new CDEventsException("Custom CDEvent validation failed."); | ||
} | ||
return cdEvent; | ||
} catch (JsonProcessingException e) { | ||
throw new CDEventsException("Exception occurred while validating CDEvent json with schema file ", e); | ||
throw new CDEventsException("Exception occurred while processing cdEventJson with the event class " + eventClass.getName(), e); | ||
} | ||
} | ||
|
||
/** | ||
* Validates the custom CDEvent against the official and context schemas. | ||
* @param customCDEvent custom CDEvent to validate | ||
* @param validateContextSchema true to validate custom CDEvent against context schema | ||
* @return valid cdEvent | ||
*/ | ||
public static boolean validateCustomCDEvent(CDEvent customCDEvent, boolean validateContextSchema) { | ||
if (validateContextSchema) { | ||
return validateWithContextSchemaUri(customCDEvent) && validateWithOfficialCustomSchema(customCDEvent); | ||
} else { | ||
return validateWithOfficialCustomSchema(customCDEvent); | ||
} | ||
} | ||
|
||
/** | ||
* Validates the custom CDEvent against the provided context Schema URI. | ||
* @param customCDEvent custom CDEvent to validate | ||
* @return valid cdEvent | ||
*/ | ||
public static boolean validateWithContextSchemaUri(CDEvent customCDEvent) { | ||
if (customCDEvent.customSchemaUri() == null) { | ||
log.error("Context schemaUri does not exist, required for custom schema validation."); | ||
throw new CDEventsException("Context schemaUri does not exist."); | ||
} | ||
log.info("Validate custom CDEvent against context.schemaUri - {}", customCDEvent.customSchemaUri()); | ||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); | ||
JsonSchema jsonSchema = factory.getSchema(customCDEvent.customSchemaUri()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should not promote a model where for every message the SDK reaches out to some internet location to fetch the schema. The way I was thinking of implementing this for the sdk-go is to let user register a custom schema, so that it's loaded once and re-used from memory, and then give users an option to decide what to do if the custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes agree, will do the required changes in a separate PR. Removed this implementation from current PR and created issue to work on #84 |
||
JsonNode jsonNode = objectMapper.convertValue(customCDEvent, ObjectNode.class); | ||
Set<ValidationMessage> errors = jsonSchema.validate(jsonNode); | ||
if (!errors.isEmpty()) { | ||
log.error("Custom CDEvent validation failed against context.schemaUri - {}, with errors {}", customCDEvent.customSchemaUri(), errors); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
private static Set<ValidationMessage> getJsonSchemaValidationMessages(CDEvent cdEvent) { | ||
/** | ||
* Validates the custom CDEvent against the official spec/custom/schema.json. | ||
* @param customCDEvent | ||
* @return valid cdEvent | ||
*/ | ||
public static boolean validateWithOfficialCustomSchema(CDEvent customCDEvent) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
Map<String, String> schemaMap = new HashMap<>(); | ||
schemaMap.put(cdEvent.schemaURL(), SCHEMA_CLASSPATH + cdEvent.schemaFileName()); | ||
schemaMap.put(cdEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json"); | ||
schemaMap.put(customCDEvent.schemaURL(), CUSTOM_SCHEMA_CLASSPATH + "schema.json"); | ||
schemaMap.put(customCDEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json"); | ||
log.info("Validate custom CDEvent against official spec/custom/schema.json"); | ||
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(customCDEvent, schemaMap); | ||
if (!errors.isEmpty()) { | ||
log.error("Custom CDEvent validation failed against official spec/custom/schema.json, with errors {}", errors); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
private static CloudEvent buildCloudEvent(CDEvent cdEvent) throws URISyntaxException { | ||
String cdEventJson = cdEventAsJson(cdEvent); | ||
log.info("CDEvent with type {} as json - {}", cdEvent.currentCDEventType(), cdEventJson); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I wonder if this should be a debug log with the raw json in there. It seems overkill with it for info. How do you feel about changing this to |
||
return new CloudEventBuilder() | ||
.withId(UUID.randomUUID().toString()) | ||
.withSource(new URI(cdEvent.eventSource())) | ||
.withType(cdEvent.currentCDEventType()) | ||
.withDataContentType("application/json") | ||
.withData(cdEventJson.getBytes(StandardCharsets.UTF_8)) | ||
.withTime(OffsetDateTime.now()) | ||
.build(); | ||
} | ||
|
||
private static Set<ValidationMessage> getJsonSchemaValidationMessages(CDEvent cdEvent, Map<String, String> schemaMap) { | ||
JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012, builder -> | ||
// This creates a mapping from $id which starts with https://cdevents.dev/0.4.0/schema to the retrieval URI classpath:schema/ | ||
builder.schemaMappers(schemaMappers -> schemaMappers.mappings(schemaMap)) | ||
// This creates a mapping from $id which starts with https://cdevents.dev/0.4.0/schema to the retrieval URI classpath:schema/ | ||
builder.schemaMappers(schemaMappers -> schemaMappers.mappings(schemaMap)) | ||
); | ||
SchemaValidatorsConfig config = new SchemaValidatorsConfig(); | ||
config.setPathType(PathType.JSON_POINTER); | ||
|
@@ -167,7 +239,6 @@ private static CDEventConstants.CDEventTypes getCDEventTypeEnum(String eventType | |
} | ||
|
||
private static String getUnVersionedEventTypeFromJson(String cdEventJson) { | ||
String unVersionedEventType = ""; | ||
try { | ||
JsonNode rootNode = objectMapper.readTree(cdEventJson); | ||
if (rootNode.get("context") != null && rootNode.get("context").get("type") != null) { | ||
|
@@ -176,17 +247,15 @@ private static String getUnVersionedEventTypeFromJson(String cdEventJson) { | |
String[] type = versionedEventType.split("\\."); | ||
String subject = type[CDEventConstants.EVENT_SUBJECT_INDEX]; | ||
String predicate = type[CDEventConstants.EVENT_PREDICATE_INDEX]; | ||
unVersionedEventType = CDEventConstants.EVENT_PREFIX + subject + "." + predicate + "."; | ||
return CDEventConstants.EVENT_PREFIX + subject + "." + predicate + "."; | ||
} else { | ||
throw new CDEventsException("Invalid CDEvent type found in CDEvent Json " + versionedEventType); | ||
} | ||
} else { | ||
throw new CDEventsException("Unable to find context and type in CDEvent Json"); | ||
} | ||
return unVersionedEventType; | ||
} catch (JsonProcessingException e) { | ||
throw new CDEventsException("Exception occurred while reading CDEvent Json for eventType ", e); | ||
} | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: the field name is
schemaUri
- perhaps the method name should match it for clarity?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changing it to
contextSchemaUri()