Skip to content
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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/CUSTOM_CDEVENTS.md
afrittoli marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Custom CDEvents
If a tool wants to emit events that are not supported by the CDEvents specification,
they can do so via [custom events](https://github.com/cdevents/spec/tree/main/custom).

Custom events follow the CDEvents format and can be defined via the
`CustomTypeEvent` class, available since v0.4.

Let's consider the following scenario: a tool called "MyRegistry" has a concept of "Quota"
which can be "exceeded" by users of the system. We want to use events to notify when that
happens, but CDEvents does not define any quota related subject.

## Steps involved to create a custom CDEvent

### Add SDK dependency to your project

```xml
<dependency>
<groupId>dev.cdevents</groupId>
<artifactId>cdevents-sdk-java</artifactId>
<version>${cdevents.version}</version>
</dependency>
```
### Create a Custom CDEvent
Custom CDEvent can be created using a class `CustomTypeEvent` packaged with in `cdevents-sdk-java`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads very similarly to the sentence above:

Custom events follow the CDEvents format and can be defined via the
CustomTypeEvent class, available since v0.4.

How about changing the wording here to be something more like

In this example, we will create a custom event for our tool utilizing the new `CustomTypeEvent`


```java
public class QuotaExceededCustomEvent {

public static void main(String[] args) {

CustomTypeEvent cdEvent = new CustomTypeEvent();
// Set the event type in the format dev.cdeventsx.<tool-name>-<subject-name>.<predicate-name>.<major.minor.patch>
cdEvent.setType("dev.cdeventsx.myregistry-quota.exceeded.0.1.0");

// Set the required context fields
cdEvent.setSource(URI.create("http://myregistry/region/staging"));
cdEvent.setSubjectId("quotaRule123");

// Set the subject type in the format <tool-name>-<subject-name>
cdEvent.setSubjectType("myregistry-quota");

// Define a map with the content properties
Map<String, Object> contentQuota = new HashMap<>();
contentQuota.put("user", "heavy_user");
contentQuota.put("limit", "50Tb");
contentQuota.put("current", 90);
contentQuota.put("threshold", 85);
contentQuota.put("level", "WARNING");

// Set the required subject content
cdEvent.setSubjectContentProperty(contentQuota);

// If we host a schema for the overall custom CDEvent, we can add it
// to the event so that the receiver may validate custom fields like
// the event type and subject content
cdEvent.setContextSchemaUri(URI.create("https://myregistry.dev/schemas/cdevents/quota-exceeded/0_1_0"));

// Create event as JSON to print
String eventJson = CDEvents.cdEventAsJson(cdEvent);
System.out.println(eventJson);

// Create event as CloudEvent, this validates event against official spec/custom/schema.json
CloudEvent ceEvent = CDEvents.cdEventAsCloudEvent(cdEvent);
// This ceEvent can be sent using HTTP Protocol Binding
// Refer : https://cloudevents.github.io/sdk-java/http-basic.html
}
}

```
The resulting CDEvents JSON will look like:

````json
{"context":{"version":"0.4.1","id":"587b646c-5dd5-4347-aa70-7a624a05120c","source":"http://myregistry/region/staging","type":"dev.cdeventsx.myregistry-quota.exceeded.0.1.0","timestamp":"2024-07-16T16:00:28Z","schemaUri":"https://myregistry.dev/schemas/cdevents/quota-exceeded/0_1_0","links":[]},"subject":{"id":"quotaRule123","type":"myregistry-quota","content":{"current":90,"level":"WARNING","limit":"50Tb","threshold":85,"user":"heavy_user"}},"customData":{},"customDataContentType":"application/json"}

````

The test code is available at [QuotaExceededCustomEvent.java](../sdk/src/test/java/dev/cdevents/QuotaExceededCustomEvent.java)
13 changes: 13 additions & 0 deletions generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@
</sourcePaths>
<targetPackage>dev.cdevents.models.links</targetPackage>
</configuration>
</execution>
<execution>
<id>generate-custom-event-from-schema</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<sourcePaths>
<sourcePath>${parent.project.dir}/spec/custom/schema.json</sourcePath>
</sourcePaths>
<targetPackage>dev.cdevents.models.custom</targetPackage>
</configuration>
</execution>
<execution>
<id>generate-artifact-deleted-from-schema</id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -53,16 +54,16 @@ public static void main(String[] args) {
String sdkBaseDir = args[1];
String parentBaseDir = args[2];
String targetPackageDir = sdkBaseDir + File.separator + "src/main/java/dev/cdevents/events";

//Create Mustache factory and compile event-template.mustache template
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(generatorBaseDir + File.separator + EVENT_TEMPLATE_MUSTACHE);

File folder = new File(parentBaseDir + File.separator + "spec" + File.separator + "schemas");
if (folder.isDirectory()) {
File[] files = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
if (files != null) {
//Create Mustache factory and compile event-template.mustache template
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(generatorBaseDir + File.separator + EVENT_TEMPLATE_MUSTACHE);

//Generate a class file for each Json schema file using a mustache template

for (File file : files) {
SchemaData schemaData = buildCDEventDataFromJsonSchema(file);
generateClassFileFromSchemaData(mustache, schemaData, targetPackageDir);
Expand All @@ -73,10 +74,15 @@ public static void main(String[] args) {
} else {
log.error("No schema directory found in the specified directory {}", folder.getAbsolutePath());
}
// Generate a class file for CustomTypeEvent using a mustache template
File customSchema = new File(parentBaseDir + File.separator + "spec" + File.separator + "custom" + File.separator + "schema.json");
SchemaData schemaData = buildCDEventDataFromJsonSchema(customSchema);
generateClassFileFromSchemaData(mustache, schemaData, targetPackageDir);
}

private static void generateClassFileFromSchemaData(Mustache mustache, SchemaData schemaData, String targetPackageDir) {
String classFileName = StringUtils.join(new String[]{schemaData.getCapitalizedSubject(), schemaData.getCapitalizedPredicate(), "CDEvent", ".java"});
String classFileName = schemaData.isCustomEvent() ? "CustomTypeEvent.java"
: StringUtils.join(schemaData.getCapitalizedSubject(), schemaData.getCapitalizedPredicate(), "CDEvent", ".java");
File classFile = new File(targetPackageDir, classFileName);
try {
FileWriter fileWriter = new FileWriter(classFile);
Expand All @@ -92,38 +98,41 @@ private static void generateClassFileFromSchemaData(Mustache mustache, SchemaDat

private static SchemaData buildCDEventDataFromJsonSchema(File file) {
SchemaData schemaData = new SchemaData();

log.info("Processing event JsonSchema file: {}", file.getAbsolutePath());
try {
JsonNode rootNode = objectMapper.readTree(file);
JsonNode contextNode = rootNode.get("properties").get("context").get("properties");
JsonNode subjectNode = rootNode.get("properties").get("subject").get("properties");
String schemaURL = rootNode.get("$id").asText();
boolean isCustomEvent = schemaURL.endsWith("custom");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really fragile. So any URL that ends with custom is assumed to be custom? Do we define that anywhere? Basically if we have some other even name "some/spec/that-has-custom" would be skipped is it wasn't a custom event

While concerning, and I dont think it needs to be addressed in this PR, can you please add a comment or a TODO to make it less fragile? This may require work in the spec SIG to discuss how to easily identify custom events based on URL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not looking for custom event schema to check isCustomEvent here, generating a base CustomTypeEvent.java from an official schema.json, which has "$id": "https://cdevents.dev/0.5.0-draft/schema/custom",.
And this class file will be used to create any custom events

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this in the CDEvents WG and agreed that extending the match to schema/custom should be sufficient. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah sounds good to me.


String subjectType = subjectNode.get("type").get("enum").get(0).asText();
String eventType = contextNode.get("type").get("enum").get(0).asText();
log.info("eventType: {} subjectType: {}", eventType, subjectType);
String[] type = eventType.split("\\.");
String subject = type[SUBJECT_INDEX];
String predicate = type[PREDICATE_INDEX];
String capitalizedSubject = StringUtils.capitalize(subject);
String capitalizedPredicate = StringUtils.capitalize(predicate);
String version = type[VERSION_INDEX];

String upperCaseSubject = getUpperCaseSubjectType(subjectType);
//set the Schema JsonNode required values to schemaData
schemaData.setSchemaURL(schemaURL);
schemaData.setBaseURI(schemaURL.substring(0, schemaURL.lastIndexOf("/") + 1));
schemaData.setSubject(subject);
schemaData.setPredicate(predicate);
schemaData.setCapitalizedSubject(capitalizedSubject);
schemaData.setCapitalizedPredicate(capitalizedPredicate);
schemaData.setSchemaFileName(file.getName());
schemaData.setUpperCaseSubject(upperCaseSubject);
schemaData.setVersion(version);

JsonNode subjectContentNode = subjectNode.get("content").get("properties");
updateSubjectContentProperties(schemaData, subjectContentNode);
schemaData.setCustomEvent(isCustomEvent);

if (!isCustomEvent) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We can probably just this up some and inverse the if statement

if (isCustomEvent) {
    return schemaData;
}

String subjectType = subjectNode.get("type").get("enum").get(0).asText();
String eventType = contextNode.get("type").get("enum").get(0).asText();
log.info("eventType: {} subjectType: {}", eventType, subjectType);
String[] type = eventType.split("\\.");
String subject = type[SUBJECT_INDEX];
String predicate = type[PREDICATE_INDEX];
String capitalizedSubject = StringUtils.capitalize(subject);
String capitalizedPredicate = StringUtils.capitalize(predicate);
String version = type[VERSION_INDEX];
String upperCaseSubject = getUpperCaseSubjectType(subjectType);

schemaData.setSubject(subject);
schemaData.setPredicate(predicate);
schemaData.setCapitalizedSubject(capitalizedSubject);
schemaData.setCapitalizedPredicate(capitalizedPredicate);
schemaData.setUpperCaseSubject(upperCaseSubject);
schemaData.setVersion(version);

JsonNode subjectContentNode = subjectNode.get("content").get("properties");
updateSubjectContentProperties(schemaData, subjectContentNode);
}
} catch (IOException e) {
log.error("Exception occurred while building schema data from Json schema {}", e.getMessage());
throw new IllegalStateException("Exception occurred while building schema data from Json schema ", e);
Expand Down
18 changes: 18 additions & 0 deletions generator/src/main/java/dev/cdevents/generator/SchemaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class SchemaData {
private List<ContentObjectField> contentObjectFields;
private List<ContentObject> contentObjects;

private boolean isCustomEvent;

/**
* Default constructor.
*/
Expand Down Expand Up @@ -198,6 +200,22 @@ public void setContentObjects(List<ContentObject> contentObjects) {
this.contentObjects = contentObjects;
}

/**
*
* @return true if Custom event
*/
public boolean isCustomEvent() {
return isCustomEvent;
}

/**
*
* @param customEvent
*/
public void setCustomEvent(boolean customEvent) {
isCustomEvent = customEvent;
}

public static class ContentField {
private String fieldName;
private String capitalizedFieldName;
Expand Down
77 changes: 70 additions & 7 deletions generator/src/main/resources/template/event-template.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,40 @@ package dev.cdevents.events;

import dev.cdevents.constants.CDEventConstants;
import dev.cdevents.models.CDEvent;
{{^isCustomEvent}}
import dev.cdevents.models.{{subject}}.{{predicate}}.*;

{{/isCustomEvent}}
{{#isCustomEvent}}
import dev.cdevents.models.custom.*;
import java.util.Map;
{{/isCustomEvent}}
import java.net.URI;
import java.util.Date;
import java.util.UUID;
import java.util.List;


{{^isCustomEvent}}
public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{capitalizedSubject}}{{predicate}} implements CDEvent {
{{/isCustomEvent}}
{{#isCustomEvent}}
public class CustomTypeEvent extends Schema implements CDEvent {
{{/isCustomEvent}}


{{^isCustomEvent}}
/**
* Constructor to init CDEvent and set the Subject for {@link {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent}.
*/

public {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent() {
{{/isCustomEvent}}
{{#isCustomEvent}}
/**
* Constructor to init CustomTypeEvent.
*/

public CustomTypeEvent() {
{{/isCustomEvent}}
initCDEvent();
}

Expand All @@ -58,10 +76,12 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
context.setTimestamp(new Date());
context.setVersion(CDEventConstants.CDEVENTS_SPEC_VERSION);
getSubject().setContent(new Content());
{{#getContentObjects}}
{{^isCustomEvent}}
{{#getContentObjects}}
getSubject().getContent().set{{capitalizedObjectName}}(new {{capitalizedObjectName}}());
{{/getContentObjects}}
{{/getContentObjects}}
getSubject().setType(Subject.Type.{{upperCaseSubject}});
{{/isCustomEvent}}
}

/**
Expand All @@ -80,7 +100,12 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap

@Override
public String currentCDEventType() {
{{^isCustomEvent}}
return getContext().getType().value();
{{/isCustomEvent}}
{{#isCustomEvent}}
return getContext().getType();
{{/isCustomEvent}}
}


Expand Down Expand Up @@ -112,6 +137,15 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
return "{{schemaFileName}}";
}

/**
*
* @return context schema URI
*/
@Override
public URI contextSchemaUri() {
return getContext().getSchemaUri();
}


/**
* @param source
Expand All @@ -127,16 +161,16 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
* Sets the {@link Context} chainId value
*/

public void setChainId(URI chainId) {
getContext().setChainId(chainId.toString());
public void setChainId(String chainId) {
getContext().setChainId(chainId);
}

/**
* @param schemaUri
* Sets the {@link Context} custom schemaUri value
*/

public void setCustomSchemaUri(URI schemaUri) {
public void setContextSchemaUri(URI schemaUri) {
getContext().setSchemaUri(schemaUri);
}

Expand All @@ -159,6 +193,34 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
getSubject().setSource(subjectSource.toString());
}

{{#isCustomEvent}}
/**
* @param type
* Sets the {@link Context} type value,
* must be in the format dev.cdeventsx.<tool-name>-<subject-name>.<predicate-name>.<major.minor.patch>
*/

public void setType(String type) {
getContext().setType(type);
}

/**
* @param subjectType
* sets the subject type, must be in the format <tool-name>-<subject-name>
*/
public void setSubjectType(String subjectType) {
getSubject().setType(subjectType);
}

/**
* @param contentProperty
* sets the subject content custom properties
*/
public void setSubjectContentProperty(Map<String, Object> contentProperty) {
contentProperty.forEach((key, value) -> getSubject().getContent().setAdditionalProperty(key, value));
}
{{/isCustomEvent}}
{{^isCustomEvent}}
//getContentFields starts

{{#getContentFields}}
Expand All @@ -181,5 +243,6 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
getSubject().getContent().get{{capitalizedObjectName}}().set{{capitalizedFieldName}}({{fieldName}});
}
{{/getContentObjectFields}}
{{/isCustomEvent}}

}
Loading